;; Blog admin panel components (defcomp ~blog-cache-panel (&key clear-url csrf) (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6" (div :class "flex flex-col md:flex-row gap-3 items-start" (form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML" (input :type "hidden" :name "csrf_token" :value csrf) (button :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" :type "submit" "Clear cache")) (div :id "cache-status" :class "py-2")))) (defcomp ~blog-snippets-panel (&key list) (div :class "max-w-4xl mx-auto p-6" (div :class "mb-6 flex justify-between items-center" (h1 :class "text-3xl font-bold" "Snippets")) (div :id "snippets-list" list))) (defcomp ~blog-snippet-visibility-select (&key patch-url hx-headers options cls) (select :name "visibility" :sx-patch patch-url :sx-target "#snippets-list" :sx-swap "innerHTML" :sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1" options)) (defcomp ~blog-snippet-option (&key value selected label) (option :value value :selected selected label)) (defcomp ~blog-snippet-row (&key name owner badge-cls visibility extra) (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "flex-1 min-w-0" (div :class "font-medium truncate" name) (div :class "text-xs text-stone-500" owner)) (span :class (str "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium " badge-cls) visibility) extra)) (defcomp ~blog-snippets-list (&key rows) (div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows))) (defcomp ~blog-menu-items-panel (&key new-url list) (div :class "max-w-4xl mx-auto p-6" (div :class "mb-6 flex justify-end items-center" (button :type "button" :sx-get new-url :sx-target "#menu-item-form" :sx-swap "innerHTML" :class "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" (i :class "fa fa-plus") " Add Menu Item")) (div :id "menu-item-form" :class "mb-6") (div :id "menu-items-list" list))) (defcomp ~blog-menu-item-row (&key img label slug sort-order edit-url delete-url confirm-text hx-headers) (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical")) img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" label) (div :class "text-xs text-stone-500 truncate" slug)) (div :class "text-sm text-stone-500" (str "Order: " sort-order)) (div :class "flex gap-2 flex-shrink-0" (button :type "button" :sx-get edit-url :sx-target "#menu-item-form" :sx-swap "innerHTML" :class "px-3 py-1 text-sm bg-stone-200 hover:bg-stone-300 rounded" (i :class "fa fa-edit") " Edit") (~delete-btn :url delete-url :trigger-target "#menu-items-list" :title "Delete menu item?" :text confirm-text :sx-headers hx-headers)))) (defcomp ~blog-menu-items-list (&key rows) (div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows))) ;; Tag groups admin (defcomp ~blog-tag-groups-create-form (&key create-url csrf) (form :method "post" :action create-url :class "border rounded p-4 bg-white space-y-3" (input :type "hidden" :name "csrf_token" :value csrf) (h3 :class "text-sm font-semibold text-stone-700" "New Group") (div :class "flex flex-col sm:flex-row gap-3" (input :type "text" :name "name" :placeholder "Group name" :required "" :class "flex-1 border rounded px-3 py-2 text-sm") (input :type "text" :name "colour" :placeholder "#colour" :class "w-28 border rounded px-3 py-2 text-sm") (input :type "number" :name "sort_order" :placeholder "Order" :value "0" :class "w-20 border rounded px-3 py-2 text-sm")) (input :type "text" :name "feature_image" :placeholder "Image URL (optional)" :class "w-full border rounded px-3 py-2 text-sm") (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Create"))) (defcomp ~blog-tag-group-icon-image (&key src name) (img :src src :alt name :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0")) (defcomp ~blog-tag-group-icon-color (&key style initial) (div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0" :style style initial)) (defcomp ~blog-tag-group-li (&key icon edit-href name slug sort-order) (li :class "border rounded p-3 bg-white flex items-center gap-3" icon (div :class "flex-1" (a :href edit-href :class "font-medium text-stone-800 hover:underline" name) (span :class "text-xs text-stone-500 ml-2" slug)) (span :class "text-xs text-stone-500" (str "order: " sort-order)))) (defcomp ~blog-tag-groups-list (&key items) (ul :class "space-y-2" items)) (defcomp ~blog-unassigned-tag (&key name) (span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200 rounded" name)) (defcomp ~blog-unassigned-tags (&key heading spans) (div :class "border-t pt-4" (h3 :class "text-sm font-semibold text-stone-700 mb-2" heading) (div :class "flex flex-wrap gap-2" spans))) (defcomp ~blog-tag-groups-main (&key form groups unassigned) (div :class "max-w-2xl mx-auto px-4 py-6 space-y-8" form groups unassigned)) ;; Tag group edit (defcomp ~blog-tag-checkbox (&key tag-id checked img name) (label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer" (input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300") img (span name))) (defcomp ~blog-tag-checkbox-image (&key src) (img :src src :alt "" :class "h-4 w-4 rounded-full object-cover")) (defcomp ~blog-tag-group-edit-form (&key save-url csrf name colour sort-order feature-image tags) (form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4" (input :type "hidden" :name "csrf_token" :value csrf) (div :class "space-y-3" (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Name") (input :type "text" :name "name" :value name :required "" :class "w-full border rounded px-3 py-2 text-sm")) (div :class "flex gap-3" (div :class "flex-1" (label :class "block text-xs font-medium text-stone-600 mb-1" "Colour") (input :type "text" :name "colour" :value colour :placeholder "#hex" :class "w-full border rounded px-3 py-2 text-sm")) (div :class "w-24" (label :class "block text-xs font-medium text-stone-600 mb-1" "Order") (input :type "number" :name "sort_order" :value sort-order :class "w-full border rounded px-3 py-2 text-sm"))) (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Feature Image URL") (input :type "text" :name "feature_image" :value feature-image :placeholder "https://..." :class "w-full border rounded px-3 py-2 text-sm"))) (div (label :class "block text-xs font-medium text-stone-600 mb-2" "Assign Tags") (div :class "grid grid-cols-1 sm:grid-cols-2 gap-1 max-h-64 overflow-y-auto border rounded p-2" tags)) (div :class "flex gap-3" (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save")))) (defcomp ~blog-tag-group-delete-form (&key delete-url csrf) (form :method "post" :action delete-url :class "border-t pt-4" :onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')" (input :type "hidden" :name "csrf_token" :value csrf) (button :type "submit" :class "border rounded px-4 py-2 bg-red-600 text-white text-sm" "Delete Group"))) (defcomp ~blog-tag-group-edit-main (&key edit-form delete-form) (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6" edit-form delete-form)) ;; Preview panel components (defcomp ~blog-preview-panel (&key sections) (div :class "max-w-4xl mx-auto px-4 py-6 space-y-4" (style " .sx-pretty, .json-pretty { font-family: monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; } .sx-list, .json-obj, .json-arr { display: block; } .sx-paren { color: #64748b; } .sx-sym { color: #0369a1; } .sx-kw { color: #7c3aed; } .sx-str { color: #15803d; } .sx-num { color: #c2410c; } .sx-bool { color: #b91c1c; font-weight: 600; } .json-brace, .json-bracket { color: #64748b; } .json-key { color: #7c3aed; } .json-str { color: #15803d; } .json-num { color: #c2410c; } .json-lit { color: #b91c1c; font-weight: 600; } .json-field { display: block; } ") sections)) (defcomp ~blog-preview-section (&key title content) (details :class "border rounded bg-white" (summary :class "cursor-pointer px-4 py-3 font-medium text-sm bg-stone-100 hover:bg-stone-200 select-none" title) (div :class "p-4 overflow-x-auto text-xs" content)))