diff --git a/dev/hiccup/src/dev/hiccup.clj b/dev/hiccup/src/dev/hiccup.clj index 0f09da1..d76efbf 100644 --- a/dev/hiccup/src/dev/hiccup.clj +++ b/dev/hiccup/src/dev/hiccup.clj @@ -19,12 +19,15 @@ [ui.sidebar :as sidebar] [ui.icon :as icon])) +;; ── Helpers ───────────────────────────────────────────────────────── + (defn section [title & children] [: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] (into [:div {:style "display: flex; flex-direction: column; gap: 1rem;"}] children)]) -;; ── Button ────────────────────────────────────────────────────────── +;; ── Component Demos ───────────────────────────────────────────────── + (def button-variants [:primary :secondary :ghost :danger]) (def button-sizes [:sm :md :lg]) @@ -45,7 +48,6 @@ (button/button {:variant :link} "Link button") (button/button {:variant :link :href "https://example.com"} "Link with href")])) -;; ── Alert ─────────────────────────────────────────────────────────── (defn alert-demo [] (section "Alert" (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 {:title "Neutral"} "A neutral alert with no variant."))) -;; ── Badge ─────────────────────────────────────────────────────────── (defn badge-demo [] (section "Badge" [: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 :danger} "Danger")])) -;; ── Card ──────────────────────────────────────────────────────────── (defn card-demo [] (section "Card" (card/card {} @@ -75,7 +75,6 @@ (button/button {:variant :secondary :size :sm} "Cancel") (button/button {:variant :primary :size :sm} "Save"))))) -;; ── Accordion ─────────────────────────────────────────────────────── (defn accordion-demo [] (section "Accordion" [: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 "Is it accessible?"} "Yes, follows ARIA best practices.")])) -;; ── Table ─────────────────────────────────────────────────────────── (defn table-demo [] (section "Table" (table/table {:headers ["Name" "Email" "Role" "Status"] @@ -91,7 +89,6 @@ ["Bob Smith" "bob@example.com" "Editor" "Active"] ["Carol White" "carol@example.com" "Viewer" "Pending"]]}))) -;; ── Dialog ────────────────────────────────────────────────────────── (defn dialog-demo [] (section "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()"}} "Confirm"))))) -;; ── Spinner ───────────────────────────────────────────────────────── (defn spinner-demo [] (section "Spinner" [:div {:style "display: flex; gap: 1.5rem; align-items: center;"} @@ -117,7 +113,6 @@ (spinner/spinner {}) (spinner/spinner {:size :lg})])) -;; ── Skeleton ──────────────────────────────────────────────────────── (defn skeleton-demo [] (section "Skeleton" [:div {:style "max-width: 400px;"} @@ -130,7 +125,6 @@ (skeleton/skeleton {:variant :line}) (skeleton/skeleton {:variant :line})]]])) -;; ── Progress ──────────────────────────────────────────────────────── (defn progress-demo [] (section "Progress" (progress/progress {:value 25}) @@ -138,7 +132,6 @@ (progress/progress {:value 75 :variant :warning}) (progress/progress {:value 90 :variant :danger}))) -;; ── Switch ────────────────────────────────────────────────────────── (defn switch-demo [] (section "Switch" [: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 on" :checked true :disabled true})])) -;; ── Tooltip ───────────────────────────────────────────────────────── (defn tooltip-demo [] (section "Tooltip" [:div {:style "display: flex; gap: 1.5rem; padding-top: 2rem;"} @@ -158,7 +150,6 @@ (tooltip/tooltip {:text "View profile"} [:a {:href "#" :style "color: var(--accent);"} "Profile"])])) -;; ── Breadcrumb ────────────────────────────────────────────────────── (defn breadcrumb-demo [] (section "Breadcrumb" (breadcrumb/breadcrumb @@ -167,13 +158,11 @@ {:label "Oat Docs" :href "#"} {:label "Components"}]}))) -;; ── Pagination ────────────────────────────────────────────────────── (defn pagination-demo [] (section "Pagination" (pagination/pagination {:current 3 :total 5 :href-fn (fn [p] (str "#page-" p))}))) -;; ── Form ──────────────────────────────────────────────────────── (defn form-demo [] (section "Form" [:form {:style "max-width: 480px;"} @@ -207,126 +196,222 @@ (form/form-field {:label "Volume"} (form/form-range {:min 0 :max 100 :value 50})) (button/button {:variant :primary :attrs {:type "submit"}} "Submit")] - ;; Input group [:div {:style "max-width: 480px; margin-top: 1.5rem;"} [:h4 {:style "margin-bottom: 0.75rem;"} "Input group"] (form/form-group {} (form/form-group-addon {} "https://") (form/form-input {:placeholder "subdomain"}) (button/button {:variant :primary :size :sm} "Go"))] - ;; Validation error [:div {:style "max-width: 480px; margin-top: 1.5rem;"} [:h4 {:style "margin-bottom: 0.75rem;"} "Validation error"] (form/form-field {:label "Email" :error "Please enter a valid email address."} (form/form-input {:type :email :error true :value "invalid-email"}))])) -;; ── Icon ───────────────────────────────────────────────────────── -(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})])) +;; ── Pages ─────────────────────────────────────────────────────────── -;; ── Sidebar ───────────────────────────────────────────────────────── -(defn sidebar-demo [] - (section "Sidebar" - (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);"}]])))) +(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])]) -;; ── Page ──────────────────────────────────────────────────────────── -(defn page [] - (str - "\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 "body { padding: 2rem; }")]] - [:body - [: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);"} "Hiccup (Backend)"] - [:button {:onclick "document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'" - :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) - (alert-demo) - (badge-demo) - (card-demo) - (accordion-demo) - (table-demo) - (dialog-demo) - (spinner-demo) - (skeleton-demo) - (progress-demo) - (switch-demo) - (tooltip-demo) - (breadcrumb-demo) - (pagination-demo) - (form-demo) - (icon-demo)] - (sidebar-demo)]]))) +(defn components-page [] + [:div + (page-header "Components" "All UI components at a glance.") + (button-demo) + (alert-demo) + (badge-demo) + (card-demo) + (accordion-demo) + (table-demo) + (dialog-demo) + (spinner-demo) + (skeleton-demo) + (progress-demo) + (switch-demo) + (tooltip-demo) + (breadcrumb-demo) + (pagination-demo) + (form-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\".")) + ;; 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 + "\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]}] - (case uri - "/" {:status 200 - :headers {"Content-Type" "text/html; charset=utf-8"} - :body (page)} - "/theme.css" {:status 200 - :headers {"Content-Type" "text/css"} - :body (slurp "dist/theme.css")} - {:status 404 - :headers {"Content-Type" "text/plain"} - :body "Not found"})) + (let [port @!port] + (cond + (= uri "/theme.css") + {:status 200 + :headers {"Content-Type" "text/css"} + :body (slurp "dist/theme.css")} + + (resolve-page uri) + {:status 200 + :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}}] + (reset! !port port) (println (str "Hiccup server running at http://localhost:" port)) (http/run-server handler {:port port})) diff --git a/dev/replicant/public/index.html b/dev/replicant/public/index.html index ddaf44c..03caca1 100644 --- a/dev/replicant/public/index.html +++ b/dev/replicant/public/index.html @@ -5,7 +5,7 @@ diff --git a/dev/replicant/src/dev/replicant.cljs b/dev/replicant/src/dev/replicant.cljs index d2c4616..b222e4c 100644 --- a/dev/replicant/src/dev/replicant.cljs +++ b/dev/replicant/src/dev/replicant.cljs @@ -18,13 +18,26 @@ [ui.sidebar :as sidebar] [ui.icon :as icon])) +;; ── State ─────────────────────────────────────────────────────────── + +(defonce !page (atom :components)) + +;; ── Helpers ───────────────────────────────────────────────────────── + (defn section [title & children] [: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] (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-sizes [:sm :md :lg]) @@ -46,7 +59,6 @@ (button/button {:variant :link} "Link button") (button/button {:variant :link :href "https://example.com"} "Link with href")])) -;; ── Alert ─────────────────────────────────────────────────────────── (defn alert-demo [] (section "Alert" (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 {:title "Neutral"} "A neutral alert with no variant."))) -;; ── Badge ─────────────────────────────────────────────────────────── (defn badge-demo [] (section "Badge" [: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 :danger} "Danger")])) -;; ── Card ──────────────────────────────────────────────────────────── (defn card-demo [] (section "Card" (card/card {} @@ -76,7 +86,6 @@ (button/button {:variant :secondary :size :sm} "Cancel") (button/button {:variant :primary :size :sm} "Save"))))) -;; ── Accordion ─────────────────────────────────────────────────────── (defn accordion-demo [] (section "Accordion" [: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 "Is it accessible?"} "Yes, follows ARIA best practices.")])) -;; ── Table ─────────────────────────────────────────────────────────── (defn table-demo [] (section "Table" (table/table {:headers ["Name" "Email" "Role" "Status"] @@ -92,7 +100,6 @@ ["Bob Smith" "bob@example.com" "Editor" "Active"] ["Carol White" "carol@example.com" "Viewer" "Pending"]]}))) -;; ── Dialog ────────────────────────────────────────────────────────── (defn dialog-demo [] (section "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")))} "Confirm"))))) -;; ── Spinner ───────────────────────────────────────────────────────── (defn spinner-demo [] (section "Spinner" [:div {:style {:display "flex" :gap "1.5rem" :align-items "center"}} @@ -120,7 +126,6 @@ (spinner/spinner {}) (spinner/spinner {:size :lg})])) -;; ── Skeleton ──────────────────────────────────────────────────────── (defn skeleton-demo [] (section "Skeleton" [:div {:style {:max-width "400px"}} @@ -133,7 +138,6 @@ (skeleton/skeleton {:variant :line}) (skeleton/skeleton {:variant :line})]]])) -;; ── Progress ──────────────────────────────────────────────────────── (defn progress-demo [] (section "Progress" (progress/progress {:value 25}) @@ -141,7 +145,6 @@ (progress/progress {:value 75 :variant :warning}) (progress/progress {:value 90 :variant :danger}))) -;; ── Switch ────────────────────────────────────────────────────────── (defn switch-demo [] (section "Switch" [: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 on" :checked true :disabled true})])) -;; ── Tooltip ───────────────────────────────────────────────────────── (defn tooltip-demo [] (section "Tooltip" [:div {:style {:display "flex" :gap "1.5rem" :padding-top "2rem"}} @@ -161,7 +163,6 @@ (tooltip/tooltip {:text "View profile"} [:a {:href "#" :style {:color "var(--accent)"}} "Profile"])])) -;; ── Breadcrumb ────────────────────────────────────────────────────── (defn breadcrumb-demo [] (section "Breadcrumb" (breadcrumb/breadcrumb @@ -170,13 +171,11 @@ {:label "Oat Docs" :href "#"} {:label "Components"}]}))) -;; ── Pagination ────────────────────────────────────────────────────── (defn pagination-demo [] (section "Pagination" (pagination/pagination {:current 3 :total 5 :on-click (fn [p] (js/console.log (str "Page: " p)))}))) -;; ── Form ──────────────────────────────────────────────────────────── (defn form-demo [] (section "Form" [:form {:style {:max-width "480px"}} @@ -210,94 +209,22 @@ (form/form-field {:label "Volume"} (form/form-range {:min 0 :max 100 :value 50})) (button/button {:variant :primary :attrs {:type "submit"}} "Submit")] - ;; Input group [:div {:style {:max-width "480px" :margin-top "1.5rem"}} [:h4 {:style {:margin-bottom "0.75rem"}} "Input group"] (form/form-group {} (form/form-group-addon {} "https://") (form/form-input {:placeholder "subdomain"}) (button/button {:variant :primary :size :sm} "Go"))] - ;; Validation error [:div {:style {:max-width "480px" :margin-top "1.5rem"}} [:h4 {:style {:margin-bottom "0.75rem"}} "Validation error"] (form/form-field {:label "Email" :error "Please enter a valid email address."} (form/form-input {:type :email :error true :value "invalid-email"}))])) -;; ── Icon ───────────────────────────────────────────────────────── -(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})])) +;; ── Pages ─────────────────────────────────────────────────────────── -;; ── Sidebar ───────────────────────────────────────────────────────── -(defn sidebar-demo [] - (section "Sidebar" - (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"]] +(defn components-page [] + [:div + (page-header "Components" "All UI components at a glance.") (button-demo) (alert-demo) (badge-demo) @@ -312,13 +239,172 @@ (tooltip-demo) (breadcrumb-demo) (pagination-demo) - (form-demo) - (icon-demo) - (sidebar-demo)]) + (form-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! [] (d/set-dispatch! (fn [_ _])) - (d/render (.getElementById js/document "app") (app))) + (add-watch !page :render (fn [_ _ _ _] (render!))) + (render!)) (defn ^:export reload! [] - (d/render (.getElementById js/document "app") (app))) + (render!)) diff --git a/dev/squint/index.html b/dev/squint/index.html index 9ebaf95..3356ae5 100644 --- a/dev/squint/index.html +++ b/dev/squint/index.html @@ -5,7 +5,7 @@ diff --git a/dev/squint/src/dev/squint.cljs b/dev/squint/src/dev/squint.cljs index 6bf4f7b..2f1d1b2 100644 --- a/dev/squint/src/dev/squint.cljs +++ b/dev/squint/src/dev/squint.cljs @@ -18,6 +18,12 @@ [ui.sidebar :as sidebar] [ui.icon :as icon])) +;; ── State ─────────────────────────────────────────────────────────── + +(def !page (atom "components")) + +;; ── Helpers ───────────────────────────────────────────────────────── + (defn toggle-theme! [_e] (let [el (.-documentElement js/document) current (.. el -dataset -theme)] @@ -30,7 +36,14 @@ "border-bottom" "var(--border-0)" "padding-bottom" "0.5rem"}} title] (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-sizes ["sm" "md" "lg"]) @@ -54,7 +67,6 @@ (button/button {:variant "link"} "Link button") (button/button {:variant "link" :href "https://example.com"} "Link with href")]))) -;; ── Alert ─────────────────────────────────────────────────────────── (defn alert-demo [] (section "Alert" (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 {:title "Neutral"} "A neutral alert with no variant."))) -;; ── Badge ─────────────────────────────────────────────────────────── (defn badge-demo [] (section "Badge" (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 "danger"} "Danger")]))) -;; ── Card ──────────────────────────────────────────────────────────── (defn card-demo [] (section "Card" (card/card {} @@ -84,7 +94,6 @@ (button/button {:variant "secondary" :size "sm"} "Cancel") (button/button {:variant "primary" :size "sm"} "Save"))))) -;; ── Accordion ─────────────────────────────────────────────────────── (defn accordion-demo [] (section "Accordion" [: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 "Is it accessible?"} "Yes, follows ARIA best practices.")])) -;; ── Table ─────────────────────────────────────────────────────────── (defn table-demo [] (section "Table" (table/table {:headers ["Name" "Email" "Role" "Status"] @@ -100,7 +108,6 @@ ["Bob Smith" "bob@example.com" "Editor" "Active"] ["Carol White" "carol@example.com" "Viewer" "Pending"]]}))) -;; ── Dialog ────────────────────────────────────────────────────────── (defn dialog-demo [] (section "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")))} "Confirm"))))) -;; ── Spinner ───────────────────────────────────────────────────────── (defn spinner-demo [] (section "Spinner" [:div {:style {"display" "flex" "gap" "1.5rem" "align-items" "center"}} @@ -128,7 +134,6 @@ (spinner/spinner {}) (spinner/spinner {:size "lg"})])) -;; ── Skeleton ──────────────────────────────────────────────────────── (defn skeleton-demo [] (section "Skeleton" [:div {:style {"max-width" "400px"}} @@ -141,7 +146,6 @@ (skeleton/skeleton {:variant "line"}) (skeleton/skeleton {:variant "line"})]]])) -;; ── Progress ──────────────────────────────────────────────────────── (defn progress-demo [] (section "Progress" (progress/progress {:value 25}) @@ -149,7 +153,6 @@ (progress/progress {:value 75 :variant "warning"}) (progress/progress {:value 90 :variant "danger"}))) -;; ── Switch ────────────────────────────────────────────────────────── (defn switch-demo [] (section "Switch" [: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 on" :checked true :disabled true})])) -;; ── Tooltip ───────────────────────────────────────────────────────── (defn tooltip-demo [] (section "Tooltip" [:div {:style {"display" "flex" "gap" "1.5rem" "padding-top" "2rem"}} @@ -169,7 +171,6 @@ (tooltip/tooltip {:text "View profile"} [:a {:href "#" :style {"color" "var(--accent)"}} "Profile"])])) -;; ── Breadcrumb ────────────────────────────────────────────────────── (defn breadcrumb-demo [] (section "Breadcrumb" (breadcrumb/breadcrumb @@ -178,13 +179,11 @@ {:label "Oat Docs" :href "#"} {:label "Components"}]}))) -;; ── Pagination ────────────────────────────────────────────────────── (defn pagination-demo [] (section "Pagination" (pagination/pagination {:current 3 :total 5 :on-click (fn [p] (js/console.log (str "Page: " p)))}))) -;; ── Form ──────────────────────────────────────────────────────── (defn form-demo [] (section "Form" [:form {:style {"max-width" "480px"}} @@ -218,88 +217,22 @@ (form/form-field {:label "Volume"} (form/form-range {:min 0 :max 100 :value 50})) (button/button {:variant "primary" :attrs {:type "submit"}} "Submit")] - ;; Input group [:div {:style {"max-width" "480px" "margin-top" "1.5rem"}} [:h4 {:style {"margin-bottom" "0.75rem"}} "Input group"] (form/form-group {} (form/form-group-addon {} "https://") (form/form-input {:placeholder "subdomain"}) (button/button {:variant "primary" :size "sm"} "Go"))] - ;; Validation error [:div {:style {"max-width" "480px" "margin-top" "1.5rem"}} [:h4 {:style {"margin-bottom" "0.75rem"}} "Validation error"] (form/form-field {:label "Email" :error "Please enter a valid email address."} (form/form-input {:type "email" :error true :value "invalid-email"}))])) -;; ── Icon ───────────────────────────────────────────────────────── -(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"})])) +;; ── Pages ─────────────────────────────────────────────────────────── -;; ── Sidebar ───────────────────────────────────────────────────────── -(defn sidebar-demo [] - (section "Sidebar" - (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"]] +(defn components-page [] + [:div + (page-header "Components" "All UI components at a glance.") (button-demo) (alert-demo) (badge-demo) @@ -314,14 +247,177 @@ (tooltip-demo) (breadcrumb-demo) (pagination-demo) - (form-demo) - (icon-demo) - (sidebar-demo)]) + (form-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! [] - (eu/render (app) (js/document.getElementById "app"))) + (render!)) (defn reload! [] - (eu/render (app) (js/document.getElementById "app"))) + (render!)) (init!)