feat: dogfood sidebar as app shell with multi-page nav and target switcher
Restructures all three dev targets (hiccup, replicant, squint) to use the sidebar component as the actual app shell. The sidebar navigates between three pages: - Components — all UI component demos - Icons — categorized gallery with all 50+ icons and size variants - Sidebar — embedded sidebar example with dashboard layout The sidebar includes a Targets section that links between all three dev servers, with ports derived dynamically from the current port so they stay correct when using a custom base port (bb dev-all 5000). Port scheme: replicant=base+1, squint=base+2, hiccup=base+3.
This commit is contained in:
@@ -19,12 +19,15 @@
|
|||||||
[ui.sidebar :as sidebar]
|
[ui.sidebar :as sidebar]
|
||||||
[ui.icon :as icon]))
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
|
;; ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
(defn section [title & children]
|
(defn section [title & children]
|
||||||
[:section {:style "margin-bottom: 2.5rem;"}
|
[:section {:style "margin-bottom: 2.5rem;"}
|
||||||
[:h3 {:style "color: var(--fg-1); margin-bottom: 1rem; border-bottom: var(--border-0); padding-bottom: 0.5rem;"} title]
|
[:h3 {:style "color: var(--fg-1); margin-bottom: 1rem; border-bottom: var(--border-0); padding-bottom: 0.5rem;"} title]
|
||||||
(into [:div {:style "display: flex; flex-direction: column; gap: 1rem;"}] children)])
|
(into [:div {:style "display: flex; flex-direction: column; gap: 1rem;"}] children)])
|
||||||
|
|
||||||
;; ── Button ──────────────────────────────────────────────────────────
|
;; ── Component Demos ─────────────────────────────────────────────────
|
||||||
|
|
||||||
(def button-variants [:primary :secondary :ghost :danger])
|
(def button-variants [:primary :secondary :ghost :danger])
|
||||||
(def button-sizes [:sm :md :lg])
|
(def button-sizes [:sm :md :lg])
|
||||||
|
|
||||||
@@ -45,7 +48,6 @@
|
|||||||
(button/button {:variant :link} "Link button")
|
(button/button {:variant :link} "Link button")
|
||||||
(button/button {:variant :link :href "https://example.com"} "Link with href")]))
|
(button/button {:variant :link :href "https://example.com"} "Link with href")]))
|
||||||
|
|
||||||
;; ── Alert ───────────────────────────────────────────────────────────
|
|
||||||
(defn alert-demo []
|
(defn alert-demo []
|
||||||
(section "Alert"
|
(section "Alert"
|
||||||
(alert/alert {:variant :success :title "Success!"} "Your changes have been saved.")
|
(alert/alert {:variant :success :title "Success!"} "Your changes have been saved.")
|
||||||
@@ -54,7 +56,6 @@
|
|||||||
(alert/alert {:variant :info :title "Info"} "This is an informational alert.")
|
(alert/alert {:variant :info :title "Info"} "This is an informational alert.")
|
||||||
(alert/alert {:title "Neutral"} "A neutral alert with no variant.")))
|
(alert/alert {:title "Neutral"} "A neutral alert with no variant.")))
|
||||||
|
|
||||||
;; ── Badge ───────────────────────────────────────────────────────────
|
|
||||||
(defn badge-demo []
|
(defn badge-demo []
|
||||||
(section "Badge"
|
(section "Badge"
|
||||||
[:div {:style "display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;"}
|
[:div {:style "display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;"}
|
||||||
@@ -65,7 +66,6 @@
|
|||||||
(badge/badge {:variant :warning} "Warning")
|
(badge/badge {:variant :warning} "Warning")
|
||||||
(badge/badge {:variant :danger} "Danger")]))
|
(badge/badge {:variant :danger} "Danger")]))
|
||||||
|
|
||||||
;; ── Card ────────────────────────────────────────────────────────────
|
|
||||||
(defn card-demo []
|
(defn card-demo []
|
||||||
(section "Card"
|
(section "Card"
|
||||||
(card/card {}
|
(card/card {}
|
||||||
@@ -75,7 +75,6 @@
|
|||||||
(button/button {:variant :secondary :size :sm} "Cancel")
|
(button/button {:variant :secondary :size :sm} "Cancel")
|
||||||
(button/button {:variant :primary :size :sm} "Save")))))
|
(button/button {:variant :primary :size :sm} "Save")))))
|
||||||
|
|
||||||
;; ── Accordion ───────────────────────────────────────────────────────
|
|
||||||
(defn accordion-demo []
|
(defn accordion-demo []
|
||||||
(section "Accordion"
|
(section "Accordion"
|
||||||
[:div {:class "accordion-group"}
|
[:div {:class "accordion-group"}
|
||||||
@@ -83,7 +82,6 @@
|
|||||||
(accordion/accordion {:title "How do I use it?" :open true} "Just require the namespace and call functions.")
|
(accordion/accordion {:title "How do I use it?" :open true} "Just require the namespace and call functions.")
|
||||||
(accordion/accordion {:title "Is it accessible?"} "Yes, follows ARIA best practices.")]))
|
(accordion/accordion {:title "Is it accessible?"} "Yes, follows ARIA best practices.")]))
|
||||||
|
|
||||||
;; ── Table ───────────────────────────────────────────────────────────
|
|
||||||
(defn table-demo []
|
(defn table-demo []
|
||||||
(section "Table"
|
(section "Table"
|
||||||
(table/table {:headers ["Name" "Email" "Role" "Status"]
|
(table/table {:headers ["Name" "Email" "Role" "Status"]
|
||||||
@@ -91,7 +89,6 @@
|
|||||||
["Bob Smith" "bob@example.com" "Editor" "Active"]
|
["Bob Smith" "bob@example.com" "Editor" "Active"]
|
||||||
["Carol White" "carol@example.com" "Viewer" "Pending"]]})))
|
["Carol White" "carol@example.com" "Viewer" "Pending"]]})))
|
||||||
|
|
||||||
;; ── Dialog ──────────────────────────────────────────────────────────
|
|
||||||
(defn dialog-demo []
|
(defn dialog-demo []
|
||||||
(section "Dialog"
|
(section "Dialog"
|
||||||
[:p {:style "color: var(--fg-2); font-size: var(--font-sm);"} "Click button to open dialog."]
|
[:p {:style "color: var(--fg-2); font-size: var(--font-sm);"} "Click button to open dialog."]
|
||||||
@@ -109,7 +106,6 @@
|
|||||||
:attrs {:onclick "document.getElementById('demo-dialog').close()"}}
|
:attrs {:onclick "document.getElementById('demo-dialog').close()"}}
|
||||||
"Confirm")))))
|
"Confirm")))))
|
||||||
|
|
||||||
;; ── Spinner ─────────────────────────────────────────────────────────
|
|
||||||
(defn spinner-demo []
|
(defn spinner-demo []
|
||||||
(section "Spinner"
|
(section "Spinner"
|
||||||
[:div {:style "display: flex; gap: 1.5rem; align-items: center;"}
|
[:div {:style "display: flex; gap: 1.5rem; align-items: center;"}
|
||||||
@@ -117,7 +113,6 @@
|
|||||||
(spinner/spinner {})
|
(spinner/spinner {})
|
||||||
(spinner/spinner {:size :lg})]))
|
(spinner/spinner {:size :lg})]))
|
||||||
|
|
||||||
;; ── Skeleton ────────────────────────────────────────────────────────
|
|
||||||
(defn skeleton-demo []
|
(defn skeleton-demo []
|
||||||
(section "Skeleton"
|
(section "Skeleton"
|
||||||
[:div {:style "max-width: 400px;"}
|
[:div {:style "max-width: 400px;"}
|
||||||
@@ -130,7 +125,6 @@
|
|||||||
(skeleton/skeleton {:variant :line})
|
(skeleton/skeleton {:variant :line})
|
||||||
(skeleton/skeleton {:variant :line})]]]))
|
(skeleton/skeleton {:variant :line})]]]))
|
||||||
|
|
||||||
;; ── Progress ────────────────────────────────────────────────────────
|
|
||||||
(defn progress-demo []
|
(defn progress-demo []
|
||||||
(section "Progress"
|
(section "Progress"
|
||||||
(progress/progress {:value 25})
|
(progress/progress {:value 25})
|
||||||
@@ -138,7 +132,6 @@
|
|||||||
(progress/progress {:value 75 :variant :warning})
|
(progress/progress {:value 75 :variant :warning})
|
||||||
(progress/progress {:value 90 :variant :danger})))
|
(progress/progress {:value 90 :variant :danger})))
|
||||||
|
|
||||||
;; ── Switch ──────────────────────────────────────────────────────────
|
|
||||||
(defn switch-demo []
|
(defn switch-demo []
|
||||||
(section "Switch"
|
(section "Switch"
|
||||||
[:div {:style "display: flex; flex-direction: column; gap: 0.75rem;"}
|
[:div {:style "display: flex; flex-direction: column; gap: 0.75rem;"}
|
||||||
@@ -147,7 +140,6 @@
|
|||||||
(switch/switch-toggle {:label "Disabled off" :disabled true})
|
(switch/switch-toggle {:label "Disabled off" :disabled true})
|
||||||
(switch/switch-toggle {:label "Disabled on" :checked true :disabled true})]))
|
(switch/switch-toggle {:label "Disabled on" :checked true :disabled true})]))
|
||||||
|
|
||||||
;; ── Tooltip ─────────────────────────────────────────────────────────
|
|
||||||
(defn tooltip-demo []
|
(defn tooltip-demo []
|
||||||
(section "Tooltip"
|
(section "Tooltip"
|
||||||
[:div {:style "display: flex; gap: 1.5rem; padding-top: 2rem;"}
|
[:div {:style "display: flex; gap: 1.5rem; padding-top: 2rem;"}
|
||||||
@@ -158,7 +150,6 @@
|
|||||||
(tooltip/tooltip {:text "View profile"}
|
(tooltip/tooltip {:text "View profile"}
|
||||||
[:a {:href "#" :style "color: var(--accent);"} "Profile"])]))
|
[:a {:href "#" :style "color: var(--accent);"} "Profile"])]))
|
||||||
|
|
||||||
;; ── Breadcrumb ──────────────────────────────────────────────────────
|
|
||||||
(defn breadcrumb-demo []
|
(defn breadcrumb-demo []
|
||||||
(section "Breadcrumb"
|
(section "Breadcrumb"
|
||||||
(breadcrumb/breadcrumb
|
(breadcrumb/breadcrumb
|
||||||
@@ -167,13 +158,11 @@
|
|||||||
{:label "Oat Docs" :href "#"}
|
{:label "Oat Docs" :href "#"}
|
||||||
{:label "Components"}]})))
|
{:label "Components"}]})))
|
||||||
|
|
||||||
;; ── Pagination ──────────────────────────────────────────────────────
|
|
||||||
(defn pagination-demo []
|
(defn pagination-demo []
|
||||||
(section "Pagination"
|
(section "Pagination"
|
||||||
(pagination/pagination {:current 3 :total 5
|
(pagination/pagination {:current 3 :total 5
|
||||||
:href-fn (fn [p] (str "#page-" p))})))
|
:href-fn (fn [p] (str "#page-" p))})))
|
||||||
|
|
||||||
;; ── Form ────────────────────────────────────────────────────────
|
|
||||||
(defn form-demo []
|
(defn form-demo []
|
||||||
(section "Form"
|
(section "Form"
|
||||||
[:form {:style "max-width: 480px;"}
|
[:form {:style "max-width: 480px;"}
|
||||||
@@ -207,126 +196,222 @@
|
|||||||
(form/form-field {:label "Volume"}
|
(form/form-field {:label "Volume"}
|
||||||
(form/form-range {:min 0 :max 100 :value 50}))
|
(form/form-range {:min 0 :max 100 :value 50}))
|
||||||
(button/button {:variant :primary :attrs {:type "submit"}} "Submit")]
|
(button/button {:variant :primary :attrs {:type "submit"}} "Submit")]
|
||||||
;; Input group
|
|
||||||
[:div {:style "max-width: 480px; margin-top: 1.5rem;"}
|
[:div {:style "max-width: 480px; margin-top: 1.5rem;"}
|
||||||
[:h4 {:style "margin-bottom: 0.75rem;"} "Input group"]
|
[:h4 {:style "margin-bottom: 0.75rem;"} "Input group"]
|
||||||
(form/form-group {}
|
(form/form-group {}
|
||||||
(form/form-group-addon {} "https://")
|
(form/form-group-addon {} "https://")
|
||||||
(form/form-input {:placeholder "subdomain"})
|
(form/form-input {:placeholder "subdomain"})
|
||||||
(button/button {:variant :primary :size :sm} "Go"))]
|
(button/button {:variant :primary :size :sm} "Go"))]
|
||||||
;; Validation error
|
|
||||||
[:div {:style "max-width: 480px; margin-top: 1.5rem;"}
|
[:div {:style "max-width: 480px; margin-top: 1.5rem;"}
|
||||||
[:h4 {:style "margin-bottom: 0.75rem;"} "Validation error"]
|
[:h4 {:style "margin-bottom: 0.75rem;"} "Validation error"]
|
||||||
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
||||||
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
||||||
|
|
||||||
;; ── Icon ─────────────────────────────────────────────────────────
|
;; ── Pages ───────────────────────────────────────────────────────────
|
||||||
(defn icon-demo []
|
|
||||||
(section "Icon"
|
|
||||||
[:div {:style "display: flex; gap: 1.5rem; flex-wrap: wrap; align-items: center;"}
|
|
||||||
(for [n [:home :search :settings :user :mail :bell :calendar :star
|
|
||||||
:file :folder :code :terminal :globe :shield :zap :heart
|
|
||||||
:plus :minus :check :edit :trash :download :upload :copy
|
|
||||||
:eye :lock :bookmark :inbox :database :map-pin]]
|
|
||||||
[:div {:style "display: flex; flex-direction: column; align-items: center; gap: 0.25rem;"}
|
|
||||||
(icon/icon {:icon-name n})
|
|
||||||
[:span {:style "font-size: var(--font-xs); color: var(--fg-2);"} (name n)]])]
|
|
||||||
[:div {:style "display: flex; gap: 1rem; align-items: center; margin-top: 0.5rem;"}
|
|
||||||
[:span {:style "font-size: var(--font-xs); color: var(--fg-2);"} "Sizes:"]
|
|
||||||
(icon/icon {:icon-name :star :size :sm})
|
|
||||||
(icon/icon {:icon-name :star})
|
|
||||||
(icon/icon {:icon-name :star :size :lg})
|
|
||||||
(icon/icon {:icon-name :star :size :xl})]))
|
|
||||||
|
|
||||||
;; ── Sidebar ─────────────────────────────────────────────────────────
|
(defn page-header [title subtitle]
|
||||||
(defn sidebar-demo []
|
[:div {:style "margin-bottom: 2rem;"}
|
||||||
(section "Sidebar"
|
[:h2 {:style "margin: 0 0 0.25rem; color: var(--fg-0);"} title]
|
||||||
(sidebar/sidebar-layout {}
|
(when subtitle
|
||||||
(sidebar/sidebar {}
|
[:p {:style "margin: 0; color: var(--fg-2); font-size: var(--font-sm);"} subtitle])])
|
||||||
(sidebar/sidebar-header {}
|
|
||||||
(sidebar/sidebar-brand {:title "Acme Inc." :subtitle "Enterprise" :icon "A"})
|
|
||||||
(sidebar/sidebar-search {:placeholder "Search..."}))
|
|
||||||
(sidebar/sidebar-content {}
|
|
||||||
(sidebar/sidebar-group {:label "Getting Started"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :download} "Installation")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :folder :active true} "Project Structure")))
|
|
||||||
(sidebar/sidebar-group {:label "Building"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :globe} "Routing")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :database :badge "New"} "Data Fetching")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :layers} "Rendering")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :zap} "Caching")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :eye} "Styling")))
|
|
||||||
(sidebar/sidebar-group {:label "API Reference"}
|
|
||||||
(sidebar/sidebar-collapsible {:title "Components" :open true}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Button")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Card")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Dialog")))
|
|
||||||
(sidebar/sidebar-collapsible {:title "Functions"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "fetch")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "redirect")))))
|
|
||||||
(sidebar/sidebar-footer {}
|
|
||||||
(sidebar/sidebar-user {:user-name "Alice Johnson" :email "alice@example.com"})))
|
|
||||||
(sidebar/sidebar-layout-main {}
|
|
||||||
[:div {:style "padding: 2rem;"}
|
|
||||||
[:h2 {:style "margin: 0 0 1rem; color: var(--fg-0);"} "Dashboard"]
|
|
||||||
[:div {:style "display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;"}
|
|
||||||
[:div {:style "aspect-ratio: 16/9; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]
|
|
||||||
[:div {:style "aspect-ratio: 16/9; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]
|
|
||||||
[:div {:style "aspect-ratio: 16/9; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]]
|
|
||||||
[:div {:style "margin-top: 1rem; min-height: 200px; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]]))))
|
|
||||||
|
|
||||||
;; ── Page ────────────────────────────────────────────────────────────
|
(defn components-page []
|
||||||
(defn page []
|
[:div
|
||||||
(str
|
(page-header "Components" "All UI components at a glance.")
|
||||||
"<!DOCTYPE html>\n"
|
(button-demo)
|
||||||
(h/html
|
(alert-demo)
|
||||||
[:html
|
(badge-demo)
|
||||||
[:head
|
(card-demo)
|
||||||
[:meta {:charset "utf-8"}]
|
(accordion-demo)
|
||||||
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
|
(table-demo)
|
||||||
[:link {:rel "stylesheet" :href "/theme.css"}]
|
(dialog-demo)
|
||||||
[:style (h/raw "body { padding: 2rem; }")]]
|
(spinner-demo)
|
||||||
[:body
|
(skeleton-demo)
|
||||||
[:div {:style "max-width: 800px; margin: 0 auto;"}
|
(progress-demo)
|
||||||
[:div {:style "display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;"}
|
(switch-demo)
|
||||||
[:h2 {:style "margin: 0; color: var(--fg-0);"} "Hiccup (Backend)"]
|
(tooltip-demo)
|
||||||
[:button {:onclick "document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'"
|
(breadcrumb-demo)
|
||||||
:style "padding: 0.5rem 1rem; cursor: pointer; border-radius: var(--radius-md); border: var(--border-0); background: var(--bg-1); color: var(--fg-0);"}
|
(pagination-demo)
|
||||||
"Toggle Dark Mode"]]
|
(form-demo)])
|
||||||
(button-demo)
|
|
||||||
(alert-demo)
|
(def icon-categories
|
||||||
(badge-demo)
|
[["Navigation"
|
||||||
(card-demo)
|
[:home :menu :x
|
||||||
(accordion-demo)
|
:chevron-down :chevron-up :chevron-left :chevron-right
|
||||||
(table-demo)
|
:arrow-down :arrow-up :arrow-left :arrow-right
|
||||||
(dialog-demo)
|
:external-link]]
|
||||||
(spinner-demo)
|
["Actions"
|
||||||
(skeleton-demo)
|
[:search :plus :minus :check :edit :trash
|
||||||
(progress-demo)
|
:download :upload :copy :filter :link :refresh]]
|
||||||
(switch-demo)
|
["Objects"
|
||||||
(tooltip-demo)
|
[:file :folder :image :mail :bell :calendar :clock
|
||||||
(breadcrumb-demo)
|
:bookmark :star :heart :inbox :layers :package]]
|
||||||
(pagination-demo)
|
["UI & System"
|
||||||
(form-demo)
|
[:settings :user :users :log-out :log-in :eye :eye-off
|
||||||
(icon-demo)]
|
:lock :grid :list :layout-dashboard :monitor :moon :sun]]
|
||||||
(sidebar-demo)]])))
|
["Status"
|
||||||
|
[:alert-triangle :alert-circle :info :circle-check :circle-x]]
|
||||||
|
["Dev & Technical"
|
||||||
|
[:code :terminal :database :globe :shield :zap :book-open :map-pin]]])
|
||||||
|
|
||||||
|
(defn icons-page []
|
||||||
|
[:div
|
||||||
|
(page-header "Icons" (str (count icon/icon-names) " icons based on Lucide. All render as inline SVG with stroke=\"currentColor\"."))
|
||||||
|
;; Sizes
|
||||||
|
(section "Sizes"
|
||||||
|
[:div {:style "display: flex; gap: 1.5rem; align-items: end;"}
|
||||||
|
(for [[s label] [[:sm "sm"] [:md "md (default)"] [:lg "lg"] [:xl "xl"]]]
|
||||||
|
[:div {:style "display: flex; flex-direction: column; align-items: center; gap: var(--size-2);"}
|
||||||
|
(icon/icon {:icon-name :star :size s})
|
||||||
|
[:span {:style "font-size: var(--font-xs); color: var(--fg-2);"} label]])])
|
||||||
|
;; Categories
|
||||||
|
(for [[cat-name icons] icon-categories]
|
||||||
|
(section cat-name
|
||||||
|
[:div {:style "display: grid; grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr)); gap: var(--size-4);"}
|
||||||
|
(for [n icons]
|
||||||
|
[:div {:style "display: flex; flex-direction: column; align-items: center; gap: var(--size-2); padding: var(--size-3); border-radius: var(--radius-md); border: var(--border-0);"}
|
||||||
|
(icon/icon {:icon-name n})
|
||||||
|
[:span {:style "font-size: var(--font-xs); color: var(--fg-2); text-align: center; word-break: break-all;"} (name n)]])]))])
|
||||||
|
|
||||||
|
(defn sidebar-page []
|
||||||
|
[:div
|
||||||
|
(page-header "Sidebar" "A composable sidebar with brand, search, grouped navigation, collapsible sections, and user footer.")
|
||||||
|
(section "Example"
|
||||||
|
(sidebar/sidebar-layout {:attrs {:style "border: var(--border-0); border-radius: var(--radius-lg); overflow: hidden; height: 500px;"}}
|
||||||
|
(sidebar/sidebar {:attrs {:style "height: 100%; position: static;"}}
|
||||||
|
(sidebar/sidebar-header {}
|
||||||
|
(sidebar/sidebar-brand {:title "Acme Inc." :subtitle "Enterprise" :icon "A"})
|
||||||
|
(sidebar/sidebar-search {:placeholder "Search..."}))
|
||||||
|
(sidebar/sidebar-content {}
|
||||||
|
(sidebar/sidebar-group {:label "Getting Started"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :download} "Installation")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :folder :active true} "Project Structure")))
|
||||||
|
(sidebar/sidebar-group {:label "Building"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :globe} "Routing")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :database :badge "New"} "Data Fetching")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :layers} "Rendering")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :zap} "Caching")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :eye} "Styling")))
|
||||||
|
(sidebar/sidebar-group {:label "API Reference"}
|
||||||
|
(sidebar/sidebar-collapsible {:title "Components" :open true}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Button")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Card")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Dialog")))
|
||||||
|
(sidebar/sidebar-collapsible {:title "Functions"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "fetch")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "redirect")))))
|
||||||
|
(sidebar/sidebar-footer {}
|
||||||
|
(sidebar/sidebar-user {:user-name "Alice Johnson" :email "alice@example.com"})))
|
||||||
|
(sidebar/sidebar-layout-main {}
|
||||||
|
[:div {:style "padding: 2rem;"}
|
||||||
|
[:h3 {:style "margin: 0 0 1rem; color: var(--fg-0);"} "Dashboard"]
|
||||||
|
[:div {:style "display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;"}
|
||||||
|
[:div {:style "aspect-ratio: 16/9; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]
|
||||||
|
[:div {:style "aspect-ratio: 16/9; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]
|
||||||
|
[:div {:style "aspect-ratio: 16/9; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]]
|
||||||
|
[:div {:style "margin-top: 1rem; min-height: 120px; background: var(--bg-1); border-radius: var(--radius-lg); border: var(--border-0);"}]])))])
|
||||||
|
|
||||||
|
;; ── Navigation Data ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(def nav-items
|
||||||
|
[{:id :components :label "Components" :icon-name :package :href "/"}
|
||||||
|
{:id :icons :label "Icons" :icon-name :image :href "/icons"}
|
||||||
|
{:id :sidebar :label "Sidebar" :icon-name :layout-dashboard :href "/sidebar"}])
|
||||||
|
|
||||||
|
(defn resolve-page [uri]
|
||||||
|
(case uri
|
||||||
|
"/" :components
|
||||||
|
"/icons" :icons
|
||||||
|
"/sidebar" :sidebar
|
||||||
|
nil))
|
||||||
|
|
||||||
|
;; ── App Shell ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn make-targets [own-port]
|
||||||
|
(let [base (- own-port 3)]
|
||||||
|
[{:label "Hiccup" :port (+ base 3) :active true}
|
||||||
|
{:label "Replicant" :port (+ base 1)}
|
||||||
|
{:label "Squint" :port (+ base 2)}]))
|
||||||
|
|
||||||
|
(defn app-sidebar [active-page own-port]
|
||||||
|
(sidebar/sidebar {}
|
||||||
|
(sidebar/sidebar-header {}
|
||||||
|
(sidebar/sidebar-brand {:title "UI Framework" :subtitle "Hiccup" :icon "U"}))
|
||||||
|
(sidebar/sidebar-content {}
|
||||||
|
(sidebar/sidebar-group {:label "Pages"}
|
||||||
|
(apply sidebar/sidebar-menu {}
|
||||||
|
(for [{:keys [id label icon-name href]} nav-items]
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:href href :icon-name icon-name :active (= id active-page)}
|
||||||
|
label))))
|
||||||
|
(sidebar/sidebar-separator)
|
||||||
|
(sidebar/sidebar-group {:label "Targets"}
|
||||||
|
(apply sidebar/sidebar-menu {}
|
||||||
|
(for [{:keys [label port active]} (make-targets own-port)]
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:href (str "http://localhost:" port)
|
||||||
|
:icon-name :monitor
|
||||||
|
:active active}
|
||||||
|
label))))
|
||||||
|
(sidebar/sidebar-separator)
|
||||||
|
(sidebar/sidebar-group {:label "Theme"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:icon-name :sun
|
||||||
|
:attrs {:onclick "document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'"}}
|
||||||
|
"Toggle Dark Mode"))))
|
||||||
|
(sidebar/sidebar-footer {}
|
||||||
|
(sidebar/sidebar-user {:user-name "Dev Mode" :email (str "hiccup · port " own-port) :avatar "bb"}))))
|
||||||
|
|
||||||
|
(defn render-page [uri port]
|
||||||
|
(let [active-page (resolve-page uri)]
|
||||||
|
(str
|
||||||
|
"<!DOCTYPE html>\n"
|
||||||
|
(h/html
|
||||||
|
[:html
|
||||||
|
[:head
|
||||||
|
[:meta {:charset "utf-8"}]
|
||||||
|
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
|
||||||
|
[:link {:rel "stylesheet" :href "/theme.css"}]
|
||||||
|
[:style (h/raw "html, body { margin: 0; padding: 0; }")]]
|
||||||
|
[:body
|
||||||
|
(sidebar/sidebar-layout {}
|
||||||
|
(app-sidebar active-page port)
|
||||||
|
(sidebar/sidebar-layout-main {}
|
||||||
|
[:div {:style "padding: 2rem; max-width: 960px;"}
|
||||||
|
(case active-page
|
||||||
|
:components (components-page)
|
||||||
|
:icons (icons-page)
|
||||||
|
:sidebar (sidebar-page)
|
||||||
|
[:div (page-header "Not Found" "This page doesn't exist.")])]))]]))))
|
||||||
|
|
||||||
|
;; ── Server ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defonce !port (atom 3003))
|
||||||
|
|
||||||
(defn handler [{:keys [uri]}]
|
(defn handler [{:keys [uri]}]
|
||||||
(case uri
|
(let [port @!port]
|
||||||
"/" {:status 200
|
(cond
|
||||||
:headers {"Content-Type" "text/html; charset=utf-8"}
|
(= uri "/theme.css")
|
||||||
:body (page)}
|
{:status 200
|
||||||
"/theme.css" {:status 200
|
:headers {"Content-Type" "text/css"}
|
||||||
:headers {"Content-Type" "text/css"}
|
:body (slurp "dist/theme.css")}
|
||||||
:body (slurp "dist/theme.css")}
|
|
||||||
{:status 404
|
(resolve-page uri)
|
||||||
:headers {"Content-Type" "text/plain"}
|
{:status 200
|
||||||
:body "Not found"}))
|
:headers {"Content-Type" "text/html; charset=utf-8"}
|
||||||
|
:body (render-page uri port)}
|
||||||
|
|
||||||
|
:else
|
||||||
|
{:status 404
|
||||||
|
:headers {"Content-Type" "text/html; charset=utf-8"}
|
||||||
|
:body (render-page uri port)})))
|
||||||
|
|
||||||
(defn start! [{:keys [port] :or {port 3003}}]
|
(defn start! [{:keys [port] :or {port 3003}}]
|
||||||
|
(reset! !port port)
|
||||||
(println (str "Hiccup server running at http://localhost:" port))
|
(println (str "Hiccup server running at http://localhost:" port))
|
||||||
(http/run-server handler {:port port}))
|
(http/run-server handler {:port port}))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="/theme.css" />
|
<link rel="stylesheet" href="/theme.css" />
|
||||||
<style>
|
<style>
|
||||||
body { padding: 2rem; }
|
html, body { margin: 0; padding: 0; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -18,13 +18,26 @@
|
|||||||
[ui.sidebar :as sidebar]
|
[ui.sidebar :as sidebar]
|
||||||
[ui.icon :as icon]))
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
|
;; ── State ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defonce !page (atom :components))
|
||||||
|
|
||||||
|
;; ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
(defn section [title & children]
|
(defn section [title & children]
|
||||||
[:section {:style {:margin-bottom "2.5rem"}}
|
[:section {:style {:margin-bottom "2.5rem"}}
|
||||||
[:h3 {:style {:color "var(--fg-1)" :margin-bottom "1rem"
|
[:h3 {:style {:color "var(--fg-1)" :margin-bottom "1rem"
|
||||||
:border-bottom "var(--border-0)" :padding-bottom "0.5rem"}} title]
|
:border-bottom "var(--border-0)" :padding-bottom "0.5rem"}} title]
|
||||||
(into [:div {:style {:display "flex" :flex-direction "column" :gap "1rem"}}] children)])
|
(into [:div {:style {:display "flex" :flex-direction "column" :gap "1rem"}}] children)])
|
||||||
|
|
||||||
;; ── Button ──────────────────────────────────────────────────────────
|
(defn page-header [title subtitle]
|
||||||
|
[:div {:style {:margin-bottom "2rem"}}
|
||||||
|
[:h2 {:style {:margin "0 0 0.25rem" :color "var(--fg-0)"}} title]
|
||||||
|
(when subtitle
|
||||||
|
[:p {:style {:margin "0" :color "var(--fg-2)" :font-size "var(--font-sm)"}} subtitle])])
|
||||||
|
|
||||||
|
;; ── Component Demos ─────────────────────────────────────────────────
|
||||||
|
|
||||||
(def button-variants [:primary :secondary :ghost :danger])
|
(def button-variants [:primary :secondary :ghost :danger])
|
||||||
(def button-sizes [:sm :md :lg])
|
(def button-sizes [:sm :md :lg])
|
||||||
|
|
||||||
@@ -46,7 +59,6 @@
|
|||||||
(button/button {:variant :link} "Link button")
|
(button/button {:variant :link} "Link button")
|
||||||
(button/button {:variant :link :href "https://example.com"} "Link with href")]))
|
(button/button {:variant :link :href "https://example.com"} "Link with href")]))
|
||||||
|
|
||||||
;; ── Alert ───────────────────────────────────────────────────────────
|
|
||||||
(defn alert-demo []
|
(defn alert-demo []
|
||||||
(section "Alert"
|
(section "Alert"
|
||||||
(alert/alert {:variant :success :title "Success!"} "Your changes have been saved.")
|
(alert/alert {:variant :success :title "Success!"} "Your changes have been saved.")
|
||||||
@@ -55,7 +67,6 @@
|
|||||||
(alert/alert {:variant :info :title "Info"} "This is an informational alert.")
|
(alert/alert {:variant :info :title "Info"} "This is an informational alert.")
|
||||||
(alert/alert {:title "Neutral"} "A neutral alert with no variant.")))
|
(alert/alert {:title "Neutral"} "A neutral alert with no variant.")))
|
||||||
|
|
||||||
;; ── Badge ───────────────────────────────────────────────────────────
|
|
||||||
(defn badge-demo []
|
(defn badge-demo []
|
||||||
(section "Badge"
|
(section "Badge"
|
||||||
[:div {:style {:display "flex" :gap "0.5rem" :flex-wrap "wrap" :align-items "center"}}
|
[:div {:style {:display "flex" :gap "0.5rem" :flex-wrap "wrap" :align-items "center"}}
|
||||||
@@ -66,7 +77,6 @@
|
|||||||
(badge/badge {:variant :warning} "Warning")
|
(badge/badge {:variant :warning} "Warning")
|
||||||
(badge/badge {:variant :danger} "Danger")]))
|
(badge/badge {:variant :danger} "Danger")]))
|
||||||
|
|
||||||
;; ── Card ────────────────────────────────────────────────────────────
|
|
||||||
(defn card-demo []
|
(defn card-demo []
|
||||||
(section "Card"
|
(section "Card"
|
||||||
(card/card {}
|
(card/card {}
|
||||||
@@ -76,7 +86,6 @@
|
|||||||
(button/button {:variant :secondary :size :sm} "Cancel")
|
(button/button {:variant :secondary :size :sm} "Cancel")
|
||||||
(button/button {:variant :primary :size :sm} "Save")))))
|
(button/button {:variant :primary :size :sm} "Save")))))
|
||||||
|
|
||||||
;; ── Accordion ───────────────────────────────────────────────────────
|
|
||||||
(defn accordion-demo []
|
(defn accordion-demo []
|
||||||
(section "Accordion"
|
(section "Accordion"
|
||||||
[:div {:class ["accordion-group"]}
|
[:div {:class ["accordion-group"]}
|
||||||
@@ -84,7 +93,6 @@
|
|||||||
(accordion/accordion {:title "How do I use it?" :open true} "Just require the namespace and call functions.")
|
(accordion/accordion {:title "How do I use it?" :open true} "Just require the namespace and call functions.")
|
||||||
(accordion/accordion {:title "Is it accessible?"} "Yes, follows ARIA best practices.")]))
|
(accordion/accordion {:title "Is it accessible?"} "Yes, follows ARIA best practices.")]))
|
||||||
|
|
||||||
;; ── Table ───────────────────────────────────────────────────────────
|
|
||||||
(defn table-demo []
|
(defn table-demo []
|
||||||
(section "Table"
|
(section "Table"
|
||||||
(table/table {:headers ["Name" "Email" "Role" "Status"]
|
(table/table {:headers ["Name" "Email" "Role" "Status"]
|
||||||
@@ -92,7 +100,6 @@
|
|||||||
["Bob Smith" "bob@example.com" "Editor" "Active"]
|
["Bob Smith" "bob@example.com" "Editor" "Active"]
|
||||||
["Carol White" "carol@example.com" "Viewer" "Pending"]]})))
|
["Carol White" "carol@example.com" "Viewer" "Pending"]]})))
|
||||||
|
|
||||||
;; ── Dialog ──────────────────────────────────────────────────────────
|
|
||||||
(defn dialog-demo []
|
(defn dialog-demo []
|
||||||
(section "Dialog"
|
(section "Dialog"
|
||||||
[:p {:style {:color "var(--fg-2)" :font-size "var(--font-sm)"}} "Click button to open dialog."]
|
[:p {:style {:color "var(--fg-2)" :font-size "var(--font-sm)"}} "Click button to open dialog."]
|
||||||
@@ -112,7 +119,6 @@
|
|||||||
:on-click (fn [_] (.close (.getElementById js/document "demo-dialog")))}
|
:on-click (fn [_] (.close (.getElementById js/document "demo-dialog")))}
|
||||||
"Confirm")))))
|
"Confirm")))))
|
||||||
|
|
||||||
;; ── Spinner ─────────────────────────────────────────────────────────
|
|
||||||
(defn spinner-demo []
|
(defn spinner-demo []
|
||||||
(section "Spinner"
|
(section "Spinner"
|
||||||
[:div {:style {:display "flex" :gap "1.5rem" :align-items "center"}}
|
[:div {:style {:display "flex" :gap "1.5rem" :align-items "center"}}
|
||||||
@@ -120,7 +126,6 @@
|
|||||||
(spinner/spinner {})
|
(spinner/spinner {})
|
||||||
(spinner/spinner {:size :lg})]))
|
(spinner/spinner {:size :lg})]))
|
||||||
|
|
||||||
;; ── Skeleton ────────────────────────────────────────────────────────
|
|
||||||
(defn skeleton-demo []
|
(defn skeleton-demo []
|
||||||
(section "Skeleton"
|
(section "Skeleton"
|
||||||
[:div {:style {:max-width "400px"}}
|
[:div {:style {:max-width "400px"}}
|
||||||
@@ -133,7 +138,6 @@
|
|||||||
(skeleton/skeleton {:variant :line})
|
(skeleton/skeleton {:variant :line})
|
||||||
(skeleton/skeleton {:variant :line})]]]))
|
(skeleton/skeleton {:variant :line})]]]))
|
||||||
|
|
||||||
;; ── Progress ────────────────────────────────────────────────────────
|
|
||||||
(defn progress-demo []
|
(defn progress-demo []
|
||||||
(section "Progress"
|
(section "Progress"
|
||||||
(progress/progress {:value 25})
|
(progress/progress {:value 25})
|
||||||
@@ -141,7 +145,6 @@
|
|||||||
(progress/progress {:value 75 :variant :warning})
|
(progress/progress {:value 75 :variant :warning})
|
||||||
(progress/progress {:value 90 :variant :danger})))
|
(progress/progress {:value 90 :variant :danger})))
|
||||||
|
|
||||||
;; ── Switch ──────────────────────────────────────────────────────────
|
|
||||||
(defn switch-demo []
|
(defn switch-demo []
|
||||||
(section "Switch"
|
(section "Switch"
|
||||||
[:div {:style {:display "flex" :flex-direction "column" :gap "0.75rem"}}
|
[:div {:style {:display "flex" :flex-direction "column" :gap "0.75rem"}}
|
||||||
@@ -150,7 +153,6 @@
|
|||||||
(switch/switch-toggle {:label "Disabled off" :disabled true})
|
(switch/switch-toggle {:label "Disabled off" :disabled true})
|
||||||
(switch/switch-toggle {:label "Disabled on" :checked true :disabled true})]))
|
(switch/switch-toggle {:label "Disabled on" :checked true :disabled true})]))
|
||||||
|
|
||||||
;; ── Tooltip ─────────────────────────────────────────────────────────
|
|
||||||
(defn tooltip-demo []
|
(defn tooltip-demo []
|
||||||
(section "Tooltip"
|
(section "Tooltip"
|
||||||
[:div {:style {:display "flex" :gap "1.5rem" :padding-top "2rem"}}
|
[:div {:style {:display "flex" :gap "1.5rem" :padding-top "2rem"}}
|
||||||
@@ -161,7 +163,6 @@
|
|||||||
(tooltip/tooltip {:text "View profile"}
|
(tooltip/tooltip {:text "View profile"}
|
||||||
[:a {:href "#" :style {:color "var(--accent)"}} "Profile"])]))
|
[:a {:href "#" :style {:color "var(--accent)"}} "Profile"])]))
|
||||||
|
|
||||||
;; ── Breadcrumb ──────────────────────────────────────────────────────
|
|
||||||
(defn breadcrumb-demo []
|
(defn breadcrumb-demo []
|
||||||
(section "Breadcrumb"
|
(section "Breadcrumb"
|
||||||
(breadcrumb/breadcrumb
|
(breadcrumb/breadcrumb
|
||||||
@@ -170,13 +171,11 @@
|
|||||||
{:label "Oat Docs" :href "#"}
|
{:label "Oat Docs" :href "#"}
|
||||||
{:label "Components"}]})))
|
{:label "Components"}]})))
|
||||||
|
|
||||||
;; ── Pagination ──────────────────────────────────────────────────────
|
|
||||||
(defn pagination-demo []
|
(defn pagination-demo []
|
||||||
(section "Pagination"
|
(section "Pagination"
|
||||||
(pagination/pagination {:current 3 :total 5
|
(pagination/pagination {:current 3 :total 5
|
||||||
:on-click (fn [p] (js/console.log (str "Page: " p)))})))
|
:on-click (fn [p] (js/console.log (str "Page: " p)))})))
|
||||||
|
|
||||||
;; ── Form ────────────────────────────────────────────────────────────
|
|
||||||
(defn form-demo []
|
(defn form-demo []
|
||||||
(section "Form"
|
(section "Form"
|
||||||
[:form {:style {:max-width "480px"}}
|
[:form {:style {:max-width "480px"}}
|
||||||
@@ -210,94 +209,22 @@
|
|||||||
(form/form-field {:label "Volume"}
|
(form/form-field {:label "Volume"}
|
||||||
(form/form-range {:min 0 :max 100 :value 50}))
|
(form/form-range {:min 0 :max 100 :value 50}))
|
||||||
(button/button {:variant :primary :attrs {:type "submit"}} "Submit")]
|
(button/button {:variant :primary :attrs {:type "submit"}} "Submit")]
|
||||||
;; Input group
|
|
||||||
[:div {:style {:max-width "480px" :margin-top "1.5rem"}}
|
[:div {:style {:max-width "480px" :margin-top "1.5rem"}}
|
||||||
[:h4 {:style {:margin-bottom "0.75rem"}} "Input group"]
|
[:h4 {:style {:margin-bottom "0.75rem"}} "Input group"]
|
||||||
(form/form-group {}
|
(form/form-group {}
|
||||||
(form/form-group-addon {} "https://")
|
(form/form-group-addon {} "https://")
|
||||||
(form/form-input {:placeholder "subdomain"})
|
(form/form-input {:placeholder "subdomain"})
|
||||||
(button/button {:variant :primary :size :sm} "Go"))]
|
(button/button {:variant :primary :size :sm} "Go"))]
|
||||||
;; Validation error
|
|
||||||
[:div {:style {:max-width "480px" :margin-top "1.5rem"}}
|
[:div {:style {:max-width "480px" :margin-top "1.5rem"}}
|
||||||
[:h4 {:style {:margin-bottom "0.75rem"}} "Validation error"]
|
[:h4 {:style {:margin-bottom "0.75rem"}} "Validation error"]
|
||||||
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
||||||
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
(form/form-input {:type :email :error true :value "invalid-email"}))]))
|
||||||
|
|
||||||
;; ── Icon ─────────────────────────────────────────────────────────
|
;; ── Pages ───────────────────────────────────────────────────────────
|
||||||
(defn icon-demo []
|
|
||||||
(section "Icon"
|
|
||||||
[:div {:style {:display "flex" :gap "1.5rem" :flex-wrap "wrap" :align-items "center"}}
|
|
||||||
(for [n [:home :search :settings :user :mail :bell :calendar :star
|
|
||||||
:file :folder :code :terminal :globe :shield :zap :heart
|
|
||||||
:plus :minus :check :edit :trash :download :upload :copy
|
|
||||||
:eye :lock :bookmark :inbox :database :map-pin]]
|
|
||||||
[:div {:style {:display "flex" :flex-direction "column" :align-items "center" :gap "0.25rem"}}
|
|
||||||
(icon/icon {:icon-name n})
|
|
||||||
[:span {:style {:font-size "var(--font-xs)" :color "var(--fg-2)"}} (name n)]])]
|
|
||||||
[:div {:style {:display "flex" :gap "1rem" :align-items "center" :margin-top "0.5rem"}}
|
|
||||||
[:span {:style {:font-size "var(--font-xs)" :color "var(--fg-2)"}} "Sizes:"]
|
|
||||||
(icon/icon {:icon-name :star :size :sm})
|
|
||||||
(icon/icon {:icon-name :star})
|
|
||||||
(icon/icon {:icon-name :star :size :lg})
|
|
||||||
(icon/icon {:icon-name :star :size :xl})]))
|
|
||||||
|
|
||||||
;; ── Sidebar ─────────────────────────────────────────────────────────
|
(defn components-page []
|
||||||
(defn sidebar-demo []
|
[:div
|
||||||
(section "Sidebar"
|
(page-header "Components" "All UI components at a glance.")
|
||||||
(sidebar/sidebar-layout {}
|
|
||||||
(sidebar/sidebar {}
|
|
||||||
(sidebar/sidebar-header {}
|
|
||||||
(sidebar/sidebar-brand {:title "Acme Inc." :subtitle "Enterprise" :icon "A"})
|
|
||||||
(sidebar/sidebar-search {:placeholder "Search..."}))
|
|
||||||
(sidebar/sidebar-content {}
|
|
||||||
(sidebar/sidebar-group {:label "Getting Started"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :download} "Installation")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :folder :active true} "Project Structure")))
|
|
||||||
(sidebar/sidebar-group {:label "Building"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :globe} "Routing")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :database :badge "New"} "Data Fetching")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :layers} "Rendering")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :zap} "Caching")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name :eye} "Styling")))
|
|
||||||
(sidebar/sidebar-group {:label "API Reference"}
|
|
||||||
(sidebar/sidebar-collapsible {:title "Components" :open true}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Button")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Card")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Dialog")))
|
|
||||||
(sidebar/sidebar-collapsible {:title "Functions"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "fetch")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "redirect")))))
|
|
||||||
(sidebar/sidebar-footer {}
|
|
||||||
(sidebar/sidebar-user {:user-name "Alice Johnson" :email "alice@example.com"})))
|
|
||||||
(sidebar/sidebar-layout-main {}
|
|
||||||
[:div {:style {:padding "2rem"}}
|
|
||||||
[:h2 {:style {:margin "0 0 1rem" :color "var(--fg-0)"}} "Dashboard"]
|
|
||||||
[:div {:style {:display "grid" :grid-template-columns "repeat(3, 1fr)" :gap "1rem"}}
|
|
||||||
[:div {:style {:aspect-ratio "16/9" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]
|
|
||||||
[:div {:style {:aspect-ratio "16/9" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]
|
|
||||||
[:div {:style {:aspect-ratio "16/9" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]]
|
|
||||||
[:div {:style {:margin-top "1rem" :min-height "200px" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]]))))
|
|
||||||
|
|
||||||
;; ── Theme toggle ────────────────────────────────────────────────────
|
|
||||||
(defn toggle-theme! [_e]
|
|
||||||
(let [el (.-documentElement js/document)
|
|
||||||
current (.. el -dataset -theme)]
|
|
||||||
(set! (.. el -dataset -theme)
|
|
||||||
(if (= current "dark") "light" "dark"))))
|
|
||||||
|
|
||||||
;; ── App ─────────────────────────────────────────────────────────────
|
|
||||||
(defn app []
|
|
||||||
[:div {:style {:max-width "800px" :margin "0 auto"}}
|
|
||||||
[:div {:style {:display "flex" :justify-content "space-between" :align-items "center" :margin-bottom "2rem"}}
|
|
||||||
[:h2 {:style {:margin "0" :color "var(--fg-0)"}} "Replicant (CLJS)"]
|
|
||||||
[:button {:on {:click toggle-theme!}
|
|
||||||
:style {:padding "0.5rem 1rem" :cursor "pointer" :border-radius "var(--radius-md)"
|
|
||||||
:border "var(--border-0)" :background "var(--bg-1)" :color "var(--fg-0)"}}
|
|
||||||
"Toggle Dark Mode"]]
|
|
||||||
(button-demo)
|
(button-demo)
|
||||||
(alert-demo)
|
(alert-demo)
|
||||||
(badge-demo)
|
(badge-demo)
|
||||||
@@ -312,13 +239,172 @@
|
|||||||
(tooltip-demo)
|
(tooltip-demo)
|
||||||
(breadcrumb-demo)
|
(breadcrumb-demo)
|
||||||
(pagination-demo)
|
(pagination-demo)
|
||||||
(form-demo)
|
(form-demo)])
|
||||||
(icon-demo)
|
|
||||||
(sidebar-demo)])
|
(def icon-categories
|
||||||
|
[["Navigation"
|
||||||
|
[:home :menu :x
|
||||||
|
:chevron-down :chevron-up :chevron-left :chevron-right
|
||||||
|
:arrow-down :arrow-up :arrow-left :arrow-right
|
||||||
|
:external-link]]
|
||||||
|
["Actions"
|
||||||
|
[:search :plus :minus :check :edit :trash
|
||||||
|
:download :upload :copy :filter :link :refresh]]
|
||||||
|
["Objects"
|
||||||
|
[:file :folder :image :mail :bell :calendar :clock
|
||||||
|
:bookmark :star :heart :inbox :layers :package]]
|
||||||
|
["UI & System"
|
||||||
|
[:settings :user :users :log-out :log-in :eye :eye-off
|
||||||
|
:lock :grid :list :layout-dashboard :monitor :moon :sun]]
|
||||||
|
["Status"
|
||||||
|
[:alert-triangle :alert-circle :info :circle-check :circle-x]]
|
||||||
|
["Dev & Technical"
|
||||||
|
[:code :terminal :database :globe :shield :zap :book-open :map-pin]]])
|
||||||
|
|
||||||
|
(defn icons-page []
|
||||||
|
[:div
|
||||||
|
(page-header "Icons" (str (count icon/icon-names) " icons based on Lucide. All render as inline SVG with stroke=\"currentColor\"."))
|
||||||
|
(section "Sizes"
|
||||||
|
[:div {:style {:display "flex" :gap "1.5rem" :align-items "end"}}
|
||||||
|
(for [[s label] [[:sm "sm"] [:md "md (default)"] [:lg "lg"] [:xl "xl"]]]
|
||||||
|
[:div {:style {:display "flex" :flex-direction "column" :align-items "center" :gap "var(--size-2)"}}
|
||||||
|
(icon/icon {:icon-name :star :size s})
|
||||||
|
[:span {:style {:font-size "var(--font-xs)" :color "var(--fg-2)"}} label]])])
|
||||||
|
(for [[cat-name icons] icon-categories]
|
||||||
|
(section cat-name
|
||||||
|
[:div {:style {:display "grid" :grid-template-columns "repeat(auto-fill, minmax(5rem, 1fr))" :gap "var(--size-4)"}}
|
||||||
|
(for [n icons]
|
||||||
|
[:div {:style {:display "flex" :flex-direction "column" :align-items "center"
|
||||||
|
:gap "var(--size-2)" :padding "var(--size-3)"
|
||||||
|
:border-radius "var(--radius-md)" :border "var(--border-0)"}}
|
||||||
|
(icon/icon {:icon-name n})
|
||||||
|
[:span {:style {:font-size "var(--font-xs)" :color "var(--fg-2)"
|
||||||
|
:text-align "center" :word-break "break-all"}} (name n)]])]))])
|
||||||
|
|
||||||
|
(defn sidebar-page []
|
||||||
|
[:div
|
||||||
|
(page-header "Sidebar" "A composable sidebar with brand, search, grouped navigation, collapsible sections, and user footer.")
|
||||||
|
(section "Example"
|
||||||
|
(sidebar/sidebar-layout {:attrs {:style {:border "var(--border-0)" :border-radius "var(--radius-lg)"
|
||||||
|
:overflow "hidden" :height "500px"}}}
|
||||||
|
(sidebar/sidebar {:attrs {:style {:height "100%" :position "static"}}}
|
||||||
|
(sidebar/sidebar-header {}
|
||||||
|
(sidebar/sidebar-brand {:title "Acme Inc." :subtitle "Enterprise" :icon "A"})
|
||||||
|
(sidebar/sidebar-search {:placeholder "Search..."}))
|
||||||
|
(sidebar/sidebar-content {}
|
||||||
|
(sidebar/sidebar-group {:label "Getting Started"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :download} "Installation")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :folder :active true} "Project Structure")))
|
||||||
|
(sidebar/sidebar-group {:label "Building"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :globe} "Routing")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :database :badge "New"} "Data Fetching")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :layers} "Rendering")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :zap} "Caching")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name :eye} "Styling")))
|
||||||
|
(sidebar/sidebar-group {:label "API Reference"}
|
||||||
|
(sidebar/sidebar-collapsible {:title "Components" :open true}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Button")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Card")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Dialog")))
|
||||||
|
(sidebar/sidebar-collapsible {:title "Functions"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "fetch")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "redirect")))))
|
||||||
|
(sidebar/sidebar-footer {}
|
||||||
|
(sidebar/sidebar-user {:user-name "Alice Johnson" :email "alice@example.com"})))
|
||||||
|
(sidebar/sidebar-layout-main {}
|
||||||
|
[:div {:style {:padding "2rem"}}
|
||||||
|
[:h3 {:style {:margin "0 0 1rem" :color "var(--fg-0)"}} "Dashboard"]
|
||||||
|
[:div {:style {:display "grid" :grid-template-columns "repeat(3, 1fr)" :gap "1rem"}}
|
||||||
|
[:div {:style {:aspect-ratio "16/9" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]
|
||||||
|
[:div {:style {:aspect-ratio "16/9" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]
|
||||||
|
[:div {:style {:aspect-ratio "16/9" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]]
|
||||||
|
[:div {:style {:margin-top "1rem" :min-height "120px" :background "var(--bg-1)" :border-radius "var(--radius-lg)" :border "var(--border-0)"}}]])))])
|
||||||
|
|
||||||
|
;; ── Navigation ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(def nav-items
|
||||||
|
[{:id :components :label "Components" :icon-name :package}
|
||||||
|
{:id :icons :label "Icons" :icon-name :image}
|
||||||
|
{:id :sidebar :label "Sidebar" :icon-name :layout-dashboard}])
|
||||||
|
|
||||||
|
(defn navigate! [page-id]
|
||||||
|
(fn [_e]
|
||||||
|
(reset! !page page-id)))
|
||||||
|
|
||||||
|
(defn toggle-theme! [_e]
|
||||||
|
(let [el (.-documentElement js/document)
|
||||||
|
current (.. el -dataset -theme)]
|
||||||
|
(set! (.. el -dataset -theme)
|
||||||
|
(if (= current "dark") "light" "dark"))))
|
||||||
|
|
||||||
|
;; ── App Shell ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn own-port []
|
||||||
|
(let [p (js/parseInt (.-port js/window.location) 10)]
|
||||||
|
(if (js/isNaN p) 3001 p)))
|
||||||
|
|
||||||
|
(defn make-targets []
|
||||||
|
(let [port (own-port)
|
||||||
|
base (- port 1)]
|
||||||
|
[{:label "Hiccup" :port (+ base 3)}
|
||||||
|
{:label "Replicant" :port (+ base 1) :active true}
|
||||||
|
{:label "Squint" :port (+ base 2)}]))
|
||||||
|
|
||||||
|
(defn app-sidebar [active-page]
|
||||||
|
(sidebar/sidebar {}
|
||||||
|
(sidebar/sidebar-header {}
|
||||||
|
(sidebar/sidebar-brand {:title "UI Framework" :subtitle "Replicant" :icon "U"}))
|
||||||
|
(sidebar/sidebar-content {}
|
||||||
|
(sidebar/sidebar-group {:label "Pages"}
|
||||||
|
(apply sidebar/sidebar-menu {}
|
||||||
|
(for [{:keys [id label icon-name]} nav-items]
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:icon-name icon-name :active (= id active-page)
|
||||||
|
:on-click (navigate! id)}
|
||||||
|
label))))
|
||||||
|
(sidebar/sidebar-separator)
|
||||||
|
(sidebar/sidebar-group {:label "Targets"}
|
||||||
|
(apply sidebar/sidebar-menu {}
|
||||||
|
(for [{:keys [label port active]} (make-targets)]
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:href (str "http://localhost:" port)
|
||||||
|
:icon-name :monitor
|
||||||
|
:active active}
|
||||||
|
label))))
|
||||||
|
(sidebar/sidebar-separator)
|
||||||
|
(sidebar/sidebar-group {:label "Theme"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:icon-name :sun :on-click toggle-theme!}
|
||||||
|
"Toggle Dark Mode"))))
|
||||||
|
(sidebar/sidebar-footer {}
|
||||||
|
(sidebar/sidebar-user {:user-name "Dev Mode" :email (str "replicant · port " (own-port)) :avatar "cl"}))))
|
||||||
|
|
||||||
|
(defn app []
|
||||||
|
(let [active-page @!page]
|
||||||
|
(sidebar/sidebar-layout {}
|
||||||
|
(app-sidebar active-page)
|
||||||
|
(sidebar/sidebar-layout-main {}
|
||||||
|
[:div {:style {:padding "2rem" :max-width "960px"}}
|
||||||
|
(case active-page
|
||||||
|
:components (components-page)
|
||||||
|
:icons (icons-page)
|
||||||
|
:sidebar (sidebar-page)
|
||||||
|
(components-page))]))))
|
||||||
|
|
||||||
|
;; ── Init ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn render! []
|
||||||
|
(d/render (.getElementById js/document "app") (app)))
|
||||||
|
|
||||||
(defn ^:export init! []
|
(defn ^:export init! []
|
||||||
(d/set-dispatch! (fn [_ _]))
|
(d/set-dispatch! (fn [_ _]))
|
||||||
(d/render (.getElementById js/document "app") (app)))
|
(add-watch !page :render (fn [_ _ _ _] (render!)))
|
||||||
|
(render!))
|
||||||
|
|
||||||
(defn ^:export reload! []
|
(defn ^:export reload! []
|
||||||
(d/render (.getElementById js/document "app") (app)))
|
(render!))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="/theme.css" />
|
<link rel="stylesheet" href="/theme.css" />
|
||||||
<style>
|
<style>
|
||||||
body { padding: 2rem; }
|
html, body { margin: 0; padding: 0; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
[ui.sidebar :as sidebar]
|
[ui.sidebar :as sidebar]
|
||||||
[ui.icon :as icon]))
|
[ui.icon :as icon]))
|
||||||
|
|
||||||
|
;; ── State ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(def !page (atom "components"))
|
||||||
|
|
||||||
|
;; ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
(defn toggle-theme! [_e]
|
(defn toggle-theme! [_e]
|
||||||
(let [el (.-documentElement js/document)
|
(let [el (.-documentElement js/document)
|
||||||
current (.. el -dataset -theme)]
|
current (.. el -dataset -theme)]
|
||||||
@@ -30,7 +36,14 @@
|
|||||||
"border-bottom" "var(--border-0)" "padding-bottom" "0.5rem"}} title]
|
"border-bottom" "var(--border-0)" "padding-bottom" "0.5rem"}} title]
|
||||||
(into [:div {:style {"display" "flex" "flex-direction" "column" "gap" "1rem"}}] children)])
|
(into [:div {:style {"display" "flex" "flex-direction" "column" "gap" "1rem"}}] children)])
|
||||||
|
|
||||||
;; ── Button ──────────────────────────────────────────────────────────
|
(defn page-header [title subtitle]
|
||||||
|
[:div {:style {"margin-bottom" "2rem"}}
|
||||||
|
[:h2 {:style {"margin" "0 0 0.25rem" "color" "var(--fg-0)"}} title]
|
||||||
|
(when subtitle
|
||||||
|
[:p {:style {"margin" "0" "color" "var(--fg-2)" "font-size" "var(--font-sm)"}} subtitle])])
|
||||||
|
|
||||||
|
;; ── Component Demos ─────────────────────────────────────────────────
|
||||||
|
|
||||||
(def button-variants ["primary" "secondary" "ghost" "danger"])
|
(def button-variants ["primary" "secondary" "ghost" "danger"])
|
||||||
(def button-sizes ["sm" "md" "lg"])
|
(def button-sizes ["sm" "md" "lg"])
|
||||||
|
|
||||||
@@ -54,7 +67,6 @@
|
|||||||
(button/button {:variant "link"} "Link button")
|
(button/button {:variant "link"} "Link button")
|
||||||
(button/button {:variant "link" :href "https://example.com"} "Link with href")])))
|
(button/button {:variant "link" :href "https://example.com"} "Link with href")])))
|
||||||
|
|
||||||
;; ── Alert ───────────────────────────────────────────────────────────
|
|
||||||
(defn alert-demo []
|
(defn alert-demo []
|
||||||
(section "Alert"
|
(section "Alert"
|
||||||
(alert/alert {:variant "success" :title "Success!"} "Your changes have been saved.")
|
(alert/alert {:variant "success" :title "Success!"} "Your changes have been saved.")
|
||||||
@@ -63,7 +75,6 @@
|
|||||||
(alert/alert {:variant "info" :title "Info"} "This is an informational alert.")
|
(alert/alert {:variant "info" :title "Info"} "This is an informational alert.")
|
||||||
(alert/alert {:title "Neutral"} "A neutral alert with no variant.")))
|
(alert/alert {:title "Neutral"} "A neutral alert with no variant.")))
|
||||||
|
|
||||||
;; ── Badge ───────────────────────────────────────────────────────────
|
|
||||||
(defn badge-demo []
|
(defn badge-demo []
|
||||||
(section "Badge"
|
(section "Badge"
|
||||||
(into [:div {:style {"display" "flex" "gap" "0.5rem" "flex-wrap" "wrap" "align-items" "center"}}]
|
(into [:div {:style {"display" "flex" "gap" "0.5rem" "flex-wrap" "wrap" "align-items" "center"}}]
|
||||||
@@ -74,7 +85,6 @@
|
|||||||
(badge/badge {:variant "warning"} "Warning")
|
(badge/badge {:variant "warning"} "Warning")
|
||||||
(badge/badge {:variant "danger"} "Danger")])))
|
(badge/badge {:variant "danger"} "Danger")])))
|
||||||
|
|
||||||
;; ── Card ────────────────────────────────────────────────────────────
|
|
||||||
(defn card-demo []
|
(defn card-demo []
|
||||||
(section "Card"
|
(section "Card"
|
||||||
(card/card {}
|
(card/card {}
|
||||||
@@ -84,7 +94,6 @@
|
|||||||
(button/button {:variant "secondary" :size "sm"} "Cancel")
|
(button/button {:variant "secondary" :size "sm"} "Cancel")
|
||||||
(button/button {:variant "primary" :size "sm"} "Save")))))
|
(button/button {:variant "primary" :size "sm"} "Save")))))
|
||||||
|
|
||||||
;; ── Accordion ───────────────────────────────────────────────────────
|
|
||||||
(defn accordion-demo []
|
(defn accordion-demo []
|
||||||
(section "Accordion"
|
(section "Accordion"
|
||||||
[:div {:class "accordion-group"}
|
[:div {:class "accordion-group"}
|
||||||
@@ -92,7 +101,6 @@
|
|||||||
(accordion/accordion {:title "How do I use it?" :open true} "Just require the namespace and call functions.")
|
(accordion/accordion {:title "How do I use it?" :open true} "Just require the namespace and call functions.")
|
||||||
(accordion/accordion {:title "Is it accessible?"} "Yes, follows ARIA best practices.")]))
|
(accordion/accordion {:title "Is it accessible?"} "Yes, follows ARIA best practices.")]))
|
||||||
|
|
||||||
;; ── Table ───────────────────────────────────────────────────────────
|
|
||||||
(defn table-demo []
|
(defn table-demo []
|
||||||
(section "Table"
|
(section "Table"
|
||||||
(table/table {:headers ["Name" "Email" "Role" "Status"]
|
(table/table {:headers ["Name" "Email" "Role" "Status"]
|
||||||
@@ -100,7 +108,6 @@
|
|||||||
["Bob Smith" "bob@example.com" "Editor" "Active"]
|
["Bob Smith" "bob@example.com" "Editor" "Active"]
|
||||||
["Carol White" "carol@example.com" "Viewer" "Pending"]]})))
|
["Carol White" "carol@example.com" "Viewer" "Pending"]]})))
|
||||||
|
|
||||||
;; ── Dialog ──────────────────────────────────────────────────────────
|
|
||||||
(defn dialog-demo []
|
(defn dialog-demo []
|
||||||
(section "Dialog"
|
(section "Dialog"
|
||||||
[:p {:style {"color" "var(--fg-2)" "font-size" "var(--font-sm)"}} "Click button to open dialog."]
|
[:p {:style {"color" "var(--fg-2)" "font-size" "var(--font-sm)"}} "Click button to open dialog."]
|
||||||
@@ -120,7 +127,6 @@
|
|||||||
:on-click (fn [_] (.close (js/document.getElementById "demo-dialog")))}
|
:on-click (fn [_] (.close (js/document.getElementById "demo-dialog")))}
|
||||||
"Confirm")))))
|
"Confirm")))))
|
||||||
|
|
||||||
;; ── Spinner ─────────────────────────────────────────────────────────
|
|
||||||
(defn spinner-demo []
|
(defn spinner-demo []
|
||||||
(section "Spinner"
|
(section "Spinner"
|
||||||
[:div {:style {"display" "flex" "gap" "1.5rem" "align-items" "center"}}
|
[:div {:style {"display" "flex" "gap" "1.5rem" "align-items" "center"}}
|
||||||
@@ -128,7 +134,6 @@
|
|||||||
(spinner/spinner {})
|
(spinner/spinner {})
|
||||||
(spinner/spinner {:size "lg"})]))
|
(spinner/spinner {:size "lg"})]))
|
||||||
|
|
||||||
;; ── Skeleton ────────────────────────────────────────────────────────
|
|
||||||
(defn skeleton-demo []
|
(defn skeleton-demo []
|
||||||
(section "Skeleton"
|
(section "Skeleton"
|
||||||
[:div {:style {"max-width" "400px"}}
|
[:div {:style {"max-width" "400px"}}
|
||||||
@@ -141,7 +146,6 @@
|
|||||||
(skeleton/skeleton {:variant "line"})
|
(skeleton/skeleton {:variant "line"})
|
||||||
(skeleton/skeleton {:variant "line"})]]]))
|
(skeleton/skeleton {:variant "line"})]]]))
|
||||||
|
|
||||||
;; ── Progress ────────────────────────────────────────────────────────
|
|
||||||
(defn progress-demo []
|
(defn progress-demo []
|
||||||
(section "Progress"
|
(section "Progress"
|
||||||
(progress/progress {:value 25})
|
(progress/progress {:value 25})
|
||||||
@@ -149,7 +153,6 @@
|
|||||||
(progress/progress {:value 75 :variant "warning"})
|
(progress/progress {:value 75 :variant "warning"})
|
||||||
(progress/progress {:value 90 :variant "danger"})))
|
(progress/progress {:value 90 :variant "danger"})))
|
||||||
|
|
||||||
;; ── Switch ──────────────────────────────────────────────────────────
|
|
||||||
(defn switch-demo []
|
(defn switch-demo []
|
||||||
(section "Switch"
|
(section "Switch"
|
||||||
[:div {:style {"display" "flex" "flex-direction" "column" "gap" "0.75rem"}}
|
[:div {:style {"display" "flex" "flex-direction" "column" "gap" "0.75rem"}}
|
||||||
@@ -158,7 +161,6 @@
|
|||||||
(switch/switch-toggle {:label "Disabled off" :disabled true})
|
(switch/switch-toggle {:label "Disabled off" :disabled true})
|
||||||
(switch/switch-toggle {:label "Disabled on" :checked true :disabled true})]))
|
(switch/switch-toggle {:label "Disabled on" :checked true :disabled true})]))
|
||||||
|
|
||||||
;; ── Tooltip ─────────────────────────────────────────────────────────
|
|
||||||
(defn tooltip-demo []
|
(defn tooltip-demo []
|
||||||
(section "Tooltip"
|
(section "Tooltip"
|
||||||
[:div {:style {"display" "flex" "gap" "1.5rem" "padding-top" "2rem"}}
|
[:div {:style {"display" "flex" "gap" "1.5rem" "padding-top" "2rem"}}
|
||||||
@@ -169,7 +171,6 @@
|
|||||||
(tooltip/tooltip {:text "View profile"}
|
(tooltip/tooltip {:text "View profile"}
|
||||||
[:a {:href "#" :style {"color" "var(--accent)"}} "Profile"])]))
|
[:a {:href "#" :style {"color" "var(--accent)"}} "Profile"])]))
|
||||||
|
|
||||||
;; ── Breadcrumb ──────────────────────────────────────────────────────
|
|
||||||
(defn breadcrumb-demo []
|
(defn breadcrumb-demo []
|
||||||
(section "Breadcrumb"
|
(section "Breadcrumb"
|
||||||
(breadcrumb/breadcrumb
|
(breadcrumb/breadcrumb
|
||||||
@@ -178,13 +179,11 @@
|
|||||||
{:label "Oat Docs" :href "#"}
|
{:label "Oat Docs" :href "#"}
|
||||||
{:label "Components"}]})))
|
{:label "Components"}]})))
|
||||||
|
|
||||||
;; ── Pagination ──────────────────────────────────────────────────────
|
|
||||||
(defn pagination-demo []
|
(defn pagination-demo []
|
||||||
(section "Pagination"
|
(section "Pagination"
|
||||||
(pagination/pagination {:current 3 :total 5
|
(pagination/pagination {:current 3 :total 5
|
||||||
:on-click (fn [p] (js/console.log (str "Page: " p)))})))
|
:on-click (fn [p] (js/console.log (str "Page: " p)))})))
|
||||||
|
|
||||||
;; ── Form ────────────────────────────────────────────────────────
|
|
||||||
(defn form-demo []
|
(defn form-demo []
|
||||||
(section "Form"
|
(section "Form"
|
||||||
[:form {:style {"max-width" "480px"}}
|
[:form {:style {"max-width" "480px"}}
|
||||||
@@ -218,88 +217,22 @@
|
|||||||
(form/form-field {:label "Volume"}
|
(form/form-field {:label "Volume"}
|
||||||
(form/form-range {:min 0 :max 100 :value 50}))
|
(form/form-range {:min 0 :max 100 :value 50}))
|
||||||
(button/button {:variant "primary" :attrs {:type "submit"}} "Submit")]
|
(button/button {:variant "primary" :attrs {:type "submit"}} "Submit")]
|
||||||
;; Input group
|
|
||||||
[:div {:style {"max-width" "480px" "margin-top" "1.5rem"}}
|
[:div {:style {"max-width" "480px" "margin-top" "1.5rem"}}
|
||||||
[:h4 {:style {"margin-bottom" "0.75rem"}} "Input group"]
|
[:h4 {:style {"margin-bottom" "0.75rem"}} "Input group"]
|
||||||
(form/form-group {}
|
(form/form-group {}
|
||||||
(form/form-group-addon {} "https://")
|
(form/form-group-addon {} "https://")
|
||||||
(form/form-input {:placeholder "subdomain"})
|
(form/form-input {:placeholder "subdomain"})
|
||||||
(button/button {:variant "primary" :size "sm"} "Go"))]
|
(button/button {:variant "primary" :size "sm"} "Go"))]
|
||||||
;; Validation error
|
|
||||||
[:div {:style {"max-width" "480px" "margin-top" "1.5rem"}}
|
[:div {:style {"max-width" "480px" "margin-top" "1.5rem"}}
|
||||||
[:h4 {:style {"margin-bottom" "0.75rem"}} "Validation error"]
|
[:h4 {:style {"margin-bottom" "0.75rem"}} "Validation error"]
|
||||||
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
(form/form-field {:label "Email" :error "Please enter a valid email address."}
|
||||||
(form/form-input {:type "email" :error true :value "invalid-email"}))]))
|
(form/form-input {:type "email" :error true :value "invalid-email"}))]))
|
||||||
|
|
||||||
;; ── Icon ─────────────────────────────────────────────────────────
|
;; ── Pages ───────────────────────────────────────────────────────────
|
||||||
(defn icon-demo []
|
|
||||||
(section "Icon"
|
|
||||||
(into [:div {:style {"display" "flex" "gap" "1.5rem" "flex-wrap" "wrap" "align-items" "center"}}]
|
|
||||||
(map (fn [n]
|
|
||||||
[:div {:style {"display" "flex" "flex-direction" "column" "align-items" "center" "gap" "0.25rem"}}
|
|
||||||
(icon/icon {:icon-name n})
|
|
||||||
[:span {:style {"font-size" "var(--font-xs)" "color" "var(--fg-2)"}} n]])
|
|
||||||
["home" "search" "settings" "user" "mail" "bell" "calendar" "star"
|
|
||||||
"file" "folder" "code" "terminal" "globe" "shield" "zap" "heart"
|
|
||||||
"plus" "minus" "check" "edit" "trash" "download" "upload" "copy"
|
|
||||||
"eye" "lock" "bookmark" "inbox" "database" "map-pin"]))
|
|
||||||
[:div {:style {"display" "flex" "gap" "1rem" "align-items" "center" "margin-top" "0.5rem"}}
|
|
||||||
[:span {:style {"font-size" "var(--font-xs)" "color" "var(--fg-2)"}} "Sizes:"]
|
|
||||||
(icon/icon {:icon-name "star" :size "sm"})
|
|
||||||
(icon/icon {:icon-name "star"})
|
|
||||||
(icon/icon {:icon-name "star" :size "lg"})
|
|
||||||
(icon/icon {:icon-name "star" :size "xl"})]))
|
|
||||||
|
|
||||||
;; ── Sidebar ─────────────────────────────────────────────────────────
|
(defn components-page []
|
||||||
(defn sidebar-demo []
|
[:div
|
||||||
(section "Sidebar"
|
(page-header "Components" "All UI components at a glance.")
|
||||||
(sidebar/sidebar-layout {}
|
|
||||||
(sidebar/sidebar {}
|
|
||||||
(sidebar/sidebar-header {}
|
|
||||||
(sidebar/sidebar-brand {:title "Acme Inc." :subtitle "Enterprise" :icon "A"})
|
|
||||||
(sidebar/sidebar-search {:placeholder "Search..."}))
|
|
||||||
(sidebar/sidebar-content {}
|
|
||||||
(sidebar/sidebar-group {:label "Getting Started"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name "download"} "Installation")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name "folder" :active true} "Project Structure")))
|
|
||||||
(sidebar/sidebar-group {:label "Building"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name "globe"} "Routing")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name "database" :badge "New"} "Data Fetching")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name "layers"} "Rendering")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name "zap"} "Caching")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#" :icon-name "eye"} "Styling")))
|
|
||||||
(sidebar/sidebar-group {:label "API Reference"}
|
|
||||||
(sidebar/sidebar-collapsible {:title "Components" :open true}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Button")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Card")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "Dialog")))
|
|
||||||
(sidebar/sidebar-collapsible {:title "Functions"}
|
|
||||||
(sidebar/sidebar-menu {}
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "fetch")
|
|
||||||
(sidebar/sidebar-menu-item {:href "#"} "redirect")))))
|
|
||||||
(sidebar/sidebar-footer {}
|
|
||||||
(sidebar/sidebar-user {:user-name "Alice Johnson" :email "alice@example.com"})))
|
|
||||||
(sidebar/sidebar-layout-main {}
|
|
||||||
[:div {:style {"padding" "2rem"}}
|
|
||||||
[:h2 {:style {"margin" "0 0 1rem" "color" "var(--fg-0)"}} "Dashboard"]
|
|
||||||
[:div {:style {"display" "grid" "grid-template-columns" "repeat(3, 1fr)" "gap" "1rem"}}
|
|
||||||
[:div {:style {"aspect-ratio" "16/9" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]
|
|
||||||
[:div {:style {"aspect-ratio" "16/9" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]
|
|
||||||
[:div {:style {"aspect-ratio" "16/9" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]]
|
|
||||||
[:div {:style {"margin-top" "1rem" "min-height" "200px" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]]))))
|
|
||||||
|
|
||||||
;; ── App ─────────────────────────────────────────────────────────────
|
|
||||||
(defn app []
|
|
||||||
[:div {:style {"max-width" "800px" "margin" "0 auto"}}
|
|
||||||
[:div {:style {"display" "flex" "justify-content" "space-between" "align-items" "center" "margin-bottom" "2rem"}}
|
|
||||||
[:h2 {:style {"margin" "0" "color" "var(--fg-0)"}} "Squint (Eucalypt)"]
|
|
||||||
[:button {:on-click toggle-theme!
|
|
||||||
:style {"padding" "0.5rem 1rem" "cursor" "pointer" "border-radius" "var(--radius-md)"
|
|
||||||
"border" "var(--border-0)" "background" "var(--bg-1)" "color" "var(--fg-0)"}}
|
|
||||||
"Toggle Dark Mode"]]
|
|
||||||
(button-demo)
|
(button-demo)
|
||||||
(alert-demo)
|
(alert-demo)
|
||||||
(badge-demo)
|
(badge-demo)
|
||||||
@@ -314,14 +247,177 @@
|
|||||||
(tooltip-demo)
|
(tooltip-demo)
|
||||||
(breadcrumb-demo)
|
(breadcrumb-demo)
|
||||||
(pagination-demo)
|
(pagination-demo)
|
||||||
(form-demo)
|
(form-demo)])
|
||||||
(icon-demo)
|
|
||||||
(sidebar-demo)])
|
(def icon-categories
|
||||||
|
[["Navigation"
|
||||||
|
["home" "menu" "x"
|
||||||
|
"chevron-down" "chevron-up" "chevron-left" "chevron-right"
|
||||||
|
"arrow-down" "arrow-up" "arrow-left" "arrow-right"
|
||||||
|
"external-link"]]
|
||||||
|
["Actions"
|
||||||
|
["search" "plus" "minus" "check" "edit" "trash"
|
||||||
|
"download" "upload" "copy" "filter" "link" "refresh"]]
|
||||||
|
["Objects"
|
||||||
|
["file" "folder" "image" "mail" "bell" "calendar" "clock"
|
||||||
|
"bookmark" "star" "heart" "inbox" "layers" "package"]]
|
||||||
|
["UI & System"
|
||||||
|
["settings" "user" "users" "log-out" "log-in" "eye" "eye-off"
|
||||||
|
"lock" "grid" "list" "layout-dashboard" "monitor" "moon" "sun"]]
|
||||||
|
["Status"
|
||||||
|
["alert-triangle" "alert-circle" "info" "circle-check" "circle-x"]]
|
||||||
|
["Dev & Technical"
|
||||||
|
["code" "terminal" "database" "globe" "shield" "zap" "book-open" "map-pin"]]])
|
||||||
|
|
||||||
|
(defn- icon-card [n]
|
||||||
|
[:div {:style {"display" "flex" "flex-direction" "column" "align-items" "center"
|
||||||
|
"gap" "var(--size-2)" "padding" "var(--size-3)"
|
||||||
|
"border-radius" "var(--radius-md)" "border" "var(--border-0)"}}
|
||||||
|
(icon/icon {:icon-name n})
|
||||||
|
[:span {:style {"font-size" "var(--font-xs)" "color" "var(--fg-2)"
|
||||||
|
"text-align" "center" "word-break" "break-all"}} n]])
|
||||||
|
|
||||||
|
(defn- icon-category-section [entry]
|
||||||
|
(let [cat-name (first entry)
|
||||||
|
icons (second entry)]
|
||||||
|
(section cat-name
|
||||||
|
(into [:div {:style {"display" "grid" "grid-template-columns" "repeat(auto-fill, minmax(5rem, 1fr))" "gap" "var(--size-4)"}}]
|
||||||
|
(map icon-card icons)))))
|
||||||
|
|
||||||
|
(defn icons-page []
|
||||||
|
[:div
|
||||||
|
(page-header "Icons" (str (count icon/icon-names) " icons based on Lucide. All render as inline SVG with stroke=\"currentColor\"."))
|
||||||
|
(section "Sizes"
|
||||||
|
(into [:div {:style {"display" "flex" "gap" "1.5rem" "align-items" "end"}}]
|
||||||
|
(map (fn [pair]
|
||||||
|
(let [s (first pair)
|
||||||
|
label (second pair)]
|
||||||
|
[:div {:style {"display" "flex" "flex-direction" "column" "align-items" "center" "gap" "var(--size-2)"}}
|
||||||
|
(icon/icon {:icon-name "star" :size s})
|
||||||
|
[:span {:style {"font-size" "var(--font-xs)" "color" "var(--fg-2)"}} label]]))
|
||||||
|
[["sm" "sm"] ["md" "md (default)"] ["lg" "lg"] ["xl" "xl"]])))
|
||||||
|
(into [:div] (map icon-category-section icon-categories))])
|
||||||
|
|
||||||
|
(defn sidebar-page []
|
||||||
|
[:div
|
||||||
|
(page-header "Sidebar" "A composable sidebar with brand, search, grouped navigation, collapsible sections, and user footer.")
|
||||||
|
(section "Example"
|
||||||
|
(sidebar/sidebar-layout {:attrs {:style "border: var(--border-0); border-radius: var(--radius-lg); overflow: hidden; height: 500px;"}}
|
||||||
|
(sidebar/sidebar {:attrs {:style "height: 100%; position: static;"}}
|
||||||
|
(sidebar/sidebar-header {}
|
||||||
|
(sidebar/sidebar-brand {:title "Acme Inc." :subtitle "Enterprise" :icon "A"})
|
||||||
|
(sidebar/sidebar-search {:placeholder "Search..."}))
|
||||||
|
(sidebar/sidebar-content {}
|
||||||
|
(sidebar/sidebar-group {:label "Getting Started"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name "download"} "Installation")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name "folder" :active true} "Project Structure")))
|
||||||
|
(sidebar/sidebar-group {:label "Building"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name "globe"} "Routing")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name "database" :badge "New"} "Data Fetching")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name "layers"} "Rendering")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name "zap"} "Caching")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#" :icon-name "eye"} "Styling")))
|
||||||
|
(sidebar/sidebar-group {:label "API Reference"}
|
||||||
|
(sidebar/sidebar-collapsible {:title "Components" :open true}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Button")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Card")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "Dialog")))
|
||||||
|
(sidebar/sidebar-collapsible {:title "Functions"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "fetch")
|
||||||
|
(sidebar/sidebar-menu-item {:href "#"} "redirect")))))
|
||||||
|
(sidebar/sidebar-footer {}
|
||||||
|
(sidebar/sidebar-user {:user-name "Alice Johnson" :email "alice@example.com"})))
|
||||||
|
(sidebar/sidebar-layout-main {}
|
||||||
|
[:div {:style {"padding" "2rem"}}
|
||||||
|
[:h3 {:style {"margin" "0 0 1rem" "color" "var(--fg-0)"}} "Dashboard"]
|
||||||
|
[:div {:style {"display" "grid" "grid-template-columns" "repeat(3, 1fr)" "gap" "1rem"}}
|
||||||
|
[:div {:style {"aspect-ratio" "16/9" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]
|
||||||
|
[:div {:style {"aspect-ratio" "16/9" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]
|
||||||
|
[:div {:style {"aspect-ratio" "16/9" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]]
|
||||||
|
[:div {:style {"margin-top" "1rem" "min-height" "120px" "background" "var(--bg-1)" "border-radius" "var(--radius-lg)" "border" "var(--border-0)"}}]])))])
|
||||||
|
|
||||||
|
;; ── Navigation ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(def nav-items
|
||||||
|
[{:id "components" :label "Components" :icon-name "package"}
|
||||||
|
{:id "icons" :label "Icons" :icon-name "image"}
|
||||||
|
{:id "sidebar" :label "Sidebar" :icon-name "layout-dashboard"}])
|
||||||
|
|
||||||
|
(defn navigate! [page-id]
|
||||||
|
(fn [_e]
|
||||||
|
(reset! !page page-id)
|
||||||
|
(render!)))
|
||||||
|
|
||||||
|
;; ── App Shell ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn own-port []
|
||||||
|
(let [p (js/parseInt (.-port js/window.location) 10)]
|
||||||
|
(if (js/isNaN p) 3002 p)))
|
||||||
|
|
||||||
|
(defn make-targets []
|
||||||
|
(let [port (own-port)
|
||||||
|
base (- port 2)]
|
||||||
|
[{:label "Hiccup" :port (+ base 3)}
|
||||||
|
{:label "Replicant" :port (+ base 1)}
|
||||||
|
{:label "Squint" :port (+ base 2) :active true}]))
|
||||||
|
|
||||||
|
(defn app-sidebar [active-page]
|
||||||
|
(sidebar/sidebar {}
|
||||||
|
(sidebar/sidebar-header {}
|
||||||
|
(sidebar/sidebar-brand {:title "UI Framework" :subtitle "Squint" :icon "U"}))
|
||||||
|
(sidebar/sidebar-content {}
|
||||||
|
(sidebar/sidebar-group {:label "Pages"}
|
||||||
|
(into (sidebar/sidebar-menu {})
|
||||||
|
(map (fn [{:keys [id label icon-name]}]
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:icon-name icon-name :active (= id active-page)
|
||||||
|
:on-click (navigate! id)}
|
||||||
|
label))
|
||||||
|
nav-items)))
|
||||||
|
(sidebar/sidebar-separator)
|
||||||
|
(sidebar/sidebar-group {:label "Targets"}
|
||||||
|
(into (sidebar/sidebar-menu {})
|
||||||
|
(map (fn [{:keys [label port active]}]
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:href (str "http://localhost:" port)
|
||||||
|
:icon-name "monitor"
|
||||||
|
:active active}
|
||||||
|
label))
|
||||||
|
(make-targets))))
|
||||||
|
(sidebar/sidebar-separator)
|
||||||
|
(sidebar/sidebar-group {:label "Theme"}
|
||||||
|
(sidebar/sidebar-menu {}
|
||||||
|
(sidebar/sidebar-menu-item
|
||||||
|
{:icon-name "sun" :on-click toggle-theme!}
|
||||||
|
"Toggle Dark Mode"))))
|
||||||
|
(sidebar/sidebar-footer {}
|
||||||
|
(sidebar/sidebar-user {:user-name "Dev Mode" :email (str "squint · port " (own-port)) :avatar "sq"}))))
|
||||||
|
|
||||||
|
(defn app []
|
||||||
|
(let [active-page @!page]
|
||||||
|
(sidebar/sidebar-layout {}
|
||||||
|
(app-sidebar active-page)
|
||||||
|
(sidebar/sidebar-layout-main {}
|
||||||
|
[:div {:style {"padding" "2rem" "max-width" "960px"}}
|
||||||
|
(case active-page
|
||||||
|
"components" (components-page)
|
||||||
|
"icons" (icons-page)
|
||||||
|
"sidebar" (sidebar-page)
|
||||||
|
(components-page))]))))
|
||||||
|
|
||||||
|
;; ── Init ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(defn render! []
|
||||||
|
(eu/render (app) (js/document.getElementById "app")))
|
||||||
|
|
||||||
(defn init! []
|
(defn init! []
|
||||||
(eu/render (app) (js/document.getElementById "app")))
|
(render!))
|
||||||
|
|
||||||
(defn reload! []
|
(defn reload! []
|
||||||
(eu/render (app) (js/document.getElementById "app")))
|
(render!))
|
||||||
|
|
||||||
(init!)
|
(init!)
|
||||||
|
|||||||
Reference in New Issue
Block a user