diff --git a/.changeset/reativa-components-batch-three.md b/.changeset/reativa-components-batch-three.md new file mode 100644 index 0000000..e70d456 --- /dev/null +++ b/.changeset/reativa-components-batch-three.md @@ -0,0 +1,20 @@ +--- +"@prescriptive/reativa": minor +--- + +Add the final batch of reativa example implementations, reaching **full parity** +with the Xote examples — every spec that has a Xote demo now has a matching +**Reativa** preview, mirroring `website/src/Examples.res`. + +- **components**: `form`, `resizable`, `search`, `comment` +- **pages**: `dashboard`, `landing-page`, `pricing`, `settings`, `sign-in` +- **flows**: `authentication`, `checkout`, `onboarding` + +`search` filters a list live with `View.for_`; `resizable` binds a range input +to a reactive `style` on both panels; `form` submits with `View.On.submit` + +`prevent_default` and reveals a success message via `View.show`; `sign-in` wires +a native checkbox through a reactive `checked` binding; the `authentication` and +`onboarding` flows step through their states from a single `step` signal. +Registered in `Registry.mlx`'s `example_for` / `example_ids`. reativa now covers +26 elements, 42 components, 13 blocks, 5 pages, and 3 flows — the same set Xote +implements. diff --git a/.changeset/reativa-components-batch-two.md b/.changeset/reativa-components-batch-two.md new file mode 100644 index 0000000..e1a0cd7 --- /dev/null +++ b/.changeset/reativa-components-batch-two.md @@ -0,0 +1,23 @@ +--- +"@prescriptive/reativa": minor +--- + +Add the second batch of 20 more `@prescriptive/reativa` example implementations — +the overlay, data, picker, and navigation components — so each spec gets a +**Reativa** preview alongside the Xote one, matching the demos in +`website/src/Examples.res`. + +- **overlays & menus**: `alert-dialog`, `dropdown-menu`, `context-menu`, + `popover`, `hover-card`, `sheet`, `drawer`, `toast` +- **data & pickers**: `table`, `data-table`, `chart`, `carousel`, `combobox`, + `command`, `calendar`, `date-picker` +- **navigation**: `navbar`, `menubar`, `navigation-menu`, `sidebar` + +These lean on `Reativa.View`'s reactive primitives: `View.show` for the +open/closed overlays (dismissed via the existing `Backdrop`), `View.for_` for +the live-filtered lists (`combobox`, `command`, `data-table`), `class_reactive` +for the selected-day / active-dot state (`calendar`, `date-picker`, `carousel`), +and a reactive `style` for the carousel track. `context-menu` and `hover-card` +use `View.On.on` for the `contextmenu` / `mouseenter` / `mouseleave` events. +Registered in `Registry.mlx`'s `example_for` / `example_ids`. This brings +reativa to 26 elements, 38 components, and 13 blocks. diff --git a/packages/reativa/README.md b/packages/reativa/README.md index 2340196..8a58a41 100644 --- a/packages/reativa/README.md +++ b/packages/reativa/README.md @@ -13,13 +13,16 @@ reactive regions (driven by signals) update in place. ## What's implemented -26 elements, 18 components, and 13 blocks: +The full set `@prescriptive/xote` covers — 26 elements, 42 components, 13 blocks, +5 pages, and 3 flows: | Layer | Specs | | ----- | ----- | | element | `aspect-ratio` `avatar` `badge` `button` `checkbox` `icon` `icon-button` `input` `input-otp` `kbd` `label` `legend` `link` `logo` `progress` `radio-group` `scroll-area` `separator` `skeleton` `slider` `spinner` `switch` `textarea` `toggle` `toggle-group` `typography` | -| component | `accordion` `alert` `breadcrumb` `button-group` `card` `collapsible` `dialog` `empty-state` `field` `footer` `input-group` `list` `pagination` `select` `stat` `tabs` `toolbar` `tooltip` | +| component | `accordion` `alert` `alert-dialog` `breadcrumb` `button-group` `calendar` `card` `carousel` `chart` `collapsible` `combobox` `command` `comment` `context-menu` `data-table` `date-picker` `dialog` `drawer` `dropdown-menu` `empty-state` `field` `footer` `form` `hover-card` `input-group` `list` `menubar` `navbar` `navigation-menu` `pagination` `popover` `resizable` `search` `select` `sheet` `sidebar` `stat` `table` `tabs` `toast` `toolbar` `tooltip` | | block | `announcement-bar` `contact-section` `cta-section` `faq` `feature-grid` `hero` `logo-cloud` `newsletter` `page-header` `pricing-table` `stat-grid` `steps` `testimonial` | +| page | `dashboard` `landing-page` `pricing` `settings` `sign-in` | +| flow | `authentication` `checkout` `onboarding` | Each component is a plain function — `Button.make ~variant:\`primary ~children ()` — and composes the others. Enum prop types (`variant`, `size`, …) are generated diff --git a/packages/reativa/src/Registry.mlx b/packages/reativa/src/Registry.mlx index e015890..c9aef66 100644 --- a/packages/reativa/src/Registry.mlx +++ b/packages/reativa/src/Registry.mlx @@ -29,6 +29,17 @@ let el ?(cls = "") ?(attrs = []) ?(events = []) tag children = let attrs = if cls = "" then attrs else View.Attr.className (View.static cls) :: attrs in View.element ~attrs ~events tag children +(* Case-insensitive substring test for the filterable examples (combobox, + command, data-table). OCaml's stdlib has no substring search, so this is the + small equivalent of the Xote demos' [String.toLowerCase->String.includes]. *) +let contains_ci haystack needle = + let h = String.lowercase_ascii haystack and n = String.lowercase_ascii needle in + let lh = String.length h and ln = String.length n in + if ln = 0 then true + else + let rec loop i = i + ln <= lh && (String.sub h i ln = n || loop (i + 1)) in + loop 0 + (* ------------------------------------------------------------------ *) (* elements *) (* ------------------------------------------------------------------ *) @@ -1186,6 +1197,1207 @@ let playground_for spec_id (get : string -> string) = (); ] | _ -> View.empty +(* batch two — overlays & menus *) +(* ------------------------------------------------------------------ *) + +(* A shared row for the menu-style popovers (dropdown-menu, context-menu): a + full-width button that dismisses the menu on click. *) +let menu_item ~on_click label = + View.element + ~attrs: + [ + View.Attr.className + (View.static + "block w-full px-3 py-1.5 text-left text-sm text-neutral-700 hover:bg-neutral-100"); + ] + ~events:[ View.On.click on_click ] + "button" + [ txt label ] + +let alert_dialog_example () = + let open_ = Signal.make false in + el ~cls:"relative flex h-64 w-full max-w-lg items-center justify-center" "div" + [ + Button.make ~variant:`destructive + ~on_click:(fun _ -> Signal.set open_ true) + ~children:[ txt "Delete account" ] (); + View.show + (fun () -> Signal.get open_) + (el ~cls:"absolute inset-0 z-10 flex items-center justify-center" "div" + [ + el ~cls:"absolute inset-0 bg-neutral-900/40" "div" []; + el + ~cls: + "relative z-20 w-80 rounded-lg border border-neutral-200 bg-surface p-5 shadow-2xl" + "div" + [ + el ~cls:"text-lg font-semibold text-neutral-900" "h3" + [ txt "Are you absolutely sure?" ]; + el ~cls:"mt-1 text-sm text-neutral-500" "p" + [ + txt + "This permanently deletes your account and all data. This cannot be undone."; + ]; + el ~cls:"mt-5 flex justify-end gap-2" "div" + [ + Button.make ~variant:`secondary + ~on_click:(fun _ -> Signal.set open_ false) + ~children:[ txt "Cancel" ] (); + Button.make ~variant:`destructive + ~on_click:(fun _ -> Signal.set open_ false) + ~children:[ txt "Delete" ] (); + ]; + ]; + ]) + ] + +let dropdown_menu_example () = + let open_ = Signal.make false in + let close _ = Signal.set open_ false in + el ~cls:"relative inline-block" "div" + [ + Button.make ~variant:`secondary + ~on_click:(fun _ -> Signal.update open_ (fun v -> not v)) + ~children:[ txt "Options"; Icon.make ~name:"chevron-down" ~size:`sm () ] (); + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + Backdrop.make ~on_close:(fun () -> Signal.set open_ false) (); + el + ~cls: + "absolute z-20 mt-1 w-48 rounded-md border border-neutral-200 bg-surface py-1 \ + shadow-lg" + "div" + (List.map (menu_item ~on_click:close) + [ "Profile"; "Billing"; "Settings"; "Sign out" ]); + ]) + ] + +let context_menu_example () = + let open_ = Signal.make false in + let close _ = Signal.set open_ false in + el ~cls:"relative flex h-40 w-full max-w-md items-center justify-center" "div" + [ + View.element + ~attrs: + [ + View.Attr.className + (View.static + "flex h-28 w-full items-center justify-center rounded-lg border border-dashed \ + border-neutral-300 text-sm text-neutral-500"); + ] + ~events: + [ View.On.on "contextmenu" (fun e -> Ui.prevent_default e; Signal.set open_ true) ] + "div" + [ txt "Right-click here" ]; + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + Backdrop.make ~on_close:(fun () -> Signal.set open_ false) (); + el + ~cls: + "absolute left-1/2 top-1/2 z-20 w-44 rounded-md border border-neutral-200 \ + bg-surface py-1 shadow-lg" + "div" + (List.map (menu_item ~on_click:close) [ "Cut"; "Copy"; "Paste"; "Delete" ]); + ]) + ] + +let popover_example () = + let open_ = Signal.make false in + el ~cls:"relative inline-block" "div" + [ + Button.make ~variant:`secondary + ~on_click:(fun _ -> Signal.update open_ (fun v -> not v)) + ~children:[ txt "Open popover" ] (); + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + Backdrop.make ~on_close:(fun () -> Signal.set open_ false) (); + el + ~cls: + "absolute z-20 mt-2 w-64 rounded-lg border border-neutral-200 bg-surface p-4 \ + shadow-xl" + "div" + [ + el ~cls:"text-sm font-medium text-neutral-900" "p" [ txt "Dimensions" ]; + el ~cls:"mt-1 text-xs text-neutral-500" "p" + [ txt "Set the width and height of the layer." ]; + el ~cls:"mt-3 space-y-2" "div" + [ Input.make ~placeholder:"Width" (); Input.make ~placeholder:"Height" () ]; + ]; + ]) + ] + +let hover_card_example () = + let open_ = Signal.make false in + el ~cls:"flex h-40 items-center justify-center" "div" + [ + View.element + ~attrs:[ View.Attr.className (View.static "relative inline-block") ] + ~events: + [ + View.On.on "mouseenter" (fun _ -> Signal.set open_ true); + View.On.on "mouseleave" (fun _ -> Signal.set open_ false); + ] + "span" + [ + el ~cls:"font-medium text-neutral-900 underline decoration-neutral-300 underline-offset-4" + ~attrs:[ View.Attr.href (View.static "#") ] "a" [ txt "@ada" ]; + View.show + (fun () -> Signal.get open_) + (el + ~cls: + "absolute left-0 top-7 z-20 w-64 rounded-lg border border-neutral-200 bg-surface \ + p-4 shadow-xl" + "div" + [ + el ~cls:"flex items-center gap-3" "div" + [ + Avatar.make ~initials:"AL" (); + el "div" + [ + el ~cls:"text-sm font-semibold text-neutral-900" "p" [ txt "Ada Lovelace" ]; + el ~cls:"text-xs text-neutral-500" "p" [ txt "First programmer" ]; + ]; + ]; + el ~cls:"mt-3 text-xs text-neutral-600" "p" + [ txt "Wrote the first algorithm intended for a machine." ]; + ]); + ] + ] + +let sheet_example () = + let open_ = Signal.make false in + el ~cls:"relative flex h-64 w-full items-center justify-center overflow-hidden rounded-lg" "div" + [ + Button.make ~variant:`primary + ~on_click:(fun _ -> Signal.set open_ true) + ~children:[ txt "Open sheet" ] (); + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + View.element + ~attrs:[ View.Attr.className (View.static "absolute inset-0 z-10 bg-neutral-900/40") ] + ~events:[ View.On.click (fun _ -> Signal.set open_ false) ] + "div" []; + el + ~cls: + "absolute right-0 top-0 z-20 flex h-full w-72 flex-col border-l border-neutral-200 \ + bg-surface p-5 shadow-2xl" + "div" + [ + el ~cls:"text-lg font-semibold text-neutral-900" "h3" [ txt "Filters" ]; + el ~cls:"mt-1 text-sm text-neutral-500" "p" + [ txt "Refine the results shown in the list." ]; + el ~cls:"mt-auto flex justify-end" "div" + [ + Button.make ~variant:`primary + ~on_click:(fun _ -> Signal.set open_ false) + ~children:[ txt "Apply" ] (); + ]; + ]; + ]) + ] + +let drawer_example () = + let open_ = Signal.make false in + el ~cls:"relative flex h-64 w-full items-center justify-center overflow-hidden rounded-lg" "div" + [ + Button.make ~variant:`primary + ~on_click:(fun _ -> Signal.set open_ true) + ~children:[ txt "Open drawer" ] (); + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + View.element + ~attrs:[ View.Attr.className (View.static "absolute inset-0 z-10 bg-neutral-900/40") ] + ~events:[ View.On.click (fun _ -> Signal.set open_ false) ] + "div" []; + el + ~cls: + "absolute bottom-0 left-0 z-20 w-full rounded-t-2xl border-t border-neutral-200 \ + bg-surface p-5 shadow-2xl" + "div" + [ + el ~cls:"mx-auto mb-4 h-1 w-10 rounded-full bg-neutral-300" "div" []; + el ~cls:"text-lg font-semibold text-neutral-900" "h3" [ txt "Share" ]; + el ~cls:"mt-1 text-sm text-neutral-500" "p" + [ txt "Swipe down or tap the backdrop to dismiss." ]; + el ~cls:"mt-4 flex gap-2" "div" + [ + Button.make ~variant:`secondary + ~on_click:(fun _ -> Signal.set open_ false) + ~children:[ txt "Copy link" ] (); + Button.make ~variant:`primary + ~on_click:(fun _ -> Signal.set open_ false) + ~children:[ txt "Send" ] (); + ]; + ]; + ]) + ] + +let toast_example () = + let show = Signal.make false in + el ~cls:"relative h-40 w-full max-w-md" "div" + [ + Button.make ~variant:`primary + ~on_click:(fun _ -> + Signal.set show true; + Ui.set_timeout (fun () -> Signal.set show false) 2600) + ~children:[ txt "Show toast" ] (); + View.show + (fun () -> Signal.get show) + (el + ~cls: + "absolute bottom-2 right-2 flex w-72 items-start gap-3 rounded-lg border \ + border-neutral-200 bg-surface p-3 shadow-xl" + "div" + [ + el ~cls:"mt-0.5 text-status-success" "span" + [ Icon.make ~name:"check-circle" ~size:`sm () ]; + el ~cls:"flex-1" "div" + [ + el ~cls:"text-sm font-medium text-neutral-900" "p" [ txt "Changes saved" ]; + el ~cls:"text-xs text-neutral-500" "p" [ txt "Your profile has been updated." ]; + ]; + View.element + ~attrs: + [ + View.Attr.className + (View.static "text-sm text-neutral-500 hover:text-neutral-900"); + ] + ~events:[ View.On.click (fun _ -> Signal.set show false) ] + "button" + [ txt "Undo" ]; + ]) + ] + +(* ------------------------------------------------------------------ *) +(* batch two — data & pickers *) +(* ------------------------------------------------------------------ *) + +let table_example () = + let rows = + [ + ("INV-001", "Paid", "$250.00"); + ("INV-002", "Pending", "$150.00"); + ("INV-003", "Unpaid", "$350.00"); + ] + in + let th cls label = el ~cls "th" label in + el ~cls:"w-96 overflow-hidden rounded-lg border border-neutral-200" "div" + [ + el ~cls:"w-full text-sm" "table" + [ + el ~cls:"bg-neutral-50 text-left text-xs uppercase tracking-wide text-neutral-500" "thead" + [ + el "tr" + [ + th "px-4 py-2 font-medium" [ txt "Invoice" ]; + th "px-4 py-2 font-medium" [ txt "Status" ]; + th "px-4 py-2 text-right font-medium" [ txt "Amount" ]; + ]; + ]; + el ~cls:"divide-y divide-neutral-100" "tbody" + (List.map + (fun (inv, status, amount) -> + el ~cls:"text-neutral-700" "tr" + [ + el ~cls:"px-4 py-2 font-medium text-neutral-900" "td" [ txt inv ]; + el ~cls:"px-4 py-2" "td" [ Badge.make ~variant:`soft ~children:[ txt status ] () ]; + el ~cls:"px-4 py-2 text-right tabular-nums" "td" [ txt amount ]; + ]) + rows); + ]; + ] + +let data_table_example () = + let source = + [ + ("Ada Lovelace", "Owner", "Active"); + ("Grace Hopper", "Admin", "Active"); + ("Alan Turing", "Member", "Invited"); + ("Katherine Johnson", "Member", "Active"); + ] + in + let query = Signal.make "" in + let asc = Signal.make true in + let rows () = + let filtered = List.filter (fun (name, _, _) -> contains_ci name (Signal.get query)) source in + let sorted = List.stable_sort (fun (a, _, _) (b, _, _) -> String.compare a b) filtered in + if Signal.get asc then sorted else List.rev sorted + in + let th label = el ~cls:"px-4 py-2 font-medium" "th" [ txt label ] in + el ~cls:"w-[28rem] space-y-3" "div" + [ + el ~cls:"flex items-center justify-between" "div" + [ + Input.make ~type_:"search" ~extra_class:"max-w-48" ~placeholder:"Filter people\xe2\x80\xa6" + ~on_input:(fun e -> Signal.set query (Ui.input_value e)) + (); + Button.make ~variant:`secondary + ~on_click:(fun _ -> Signal.update asc (fun v -> not v)) + ~children: + [ + txt "Sort name"; + View.show + ~fallback:(Icon.make ~name:"arrow-down" ~size:`sm ()) + (fun () -> Signal.get asc) + (Icon.make ~name:"arrow-up" ~size:`sm ()); + ] + (); + ]; + el ~cls:"overflow-hidden rounded-lg border border-neutral-200" "div" + [ + el ~cls:"w-full text-sm" "table" + [ + el ~cls:"bg-neutral-50 text-left text-xs uppercase tracking-wide text-neutral-500" + "thead" [ el "tr" [ th "Name"; th "Role"; th "Status" ] ]; + el ~cls:"divide-y divide-neutral-100" "tbody" + [ + View.for_ rows (fun (name, role, status) -> + el ~cls:"text-neutral-700" "tr" + [ + el ~cls:"px-4 py-2 font-medium text-neutral-900" "td" [ txt name ]; + el ~cls:"px-4 py-2" "td" [ txt role ]; + el ~cls:"px-4 py-2" "td" + [ Badge.make ~variant:`soft ~children:[ txt status ] () ]; + ]); + ]; + ]; + ]; + ] + +let chart_example () = + let bars = + [ + ("Mon", 40); ("Tue", 72); ("Wed", 55); ("Thu", 88); ("Fri", 63); ("Sat", 30); ("Sun", 20); + ] + in + el ~cls:"w-96" "div" + [ + el ~cls:"flex h-40 items-end gap-2 border-b border-l border-neutral-200 p-2" "div" + (List.map + (fun (_l, v) -> + el ~cls:"flex flex-1 items-end justify-center" "div" + [ + el ~cls:"w-full rounded-t bg-neutral-900" + ~attrs: + [ View.Attr.make "style" (View.static ("height:" ^ string_of_int v ^ "%")) ] + "div" []; + ]) + bars); + el ~cls:"mt-1 flex gap-2 px-2" "div" + (List.map + (fun (l, _v) -> el ~cls:"flex-1 text-center text-xs text-neutral-400" "div" [ txt l ]) + bars); + ] + +let carousel_example () = + let idx = Signal.make 0 in + let slides = [ "Slide 1"; "Slide 2"; "Slide 3" ] in + let total = List.length slides in + let track () = "transform:translateX(-" ^ string_of_int (Signal.get idx * 100) ^ "%)" in + let arrow name pos step = + View.element + ~attrs: + [ + View.Attr.className + (View.static + (pos + ^ " absolute top-1/2 flex size-8 -translate-y-1/2 items-center justify-center \ + rounded-full border border-neutral-200 bg-surface/90 hover:bg-surface")); + ] + ~events:[ View.On.click (fun _ -> Signal.update idx step) ] + "button" + [ Icon.make ~name ~size:`sm () ] + in + el ~cls:"w-80" "div" + [ + el ~cls:"relative overflow-hidden rounded-lg border border-neutral-200" "div" + [ + el ~cls:"flex transition-transform duration-300" + ~attrs:[ View.Attr.make "style" (View.dynamic track) ] "div" + (List.map + (fun s -> + el + ~cls: + "flex aspect-video w-80 shrink-0 items-center justify-center bg-neutral-100 \ + text-lg font-medium text-neutral-500" + "div" [ txt s ]) + slides); + arrow "chevron-left" "left-2" (fun i -> if i > 0 then i - 1 else 0); + arrow "chevron-right" "right-2" (fun i -> if i < total - 1 then i + 1 else i); + ]; + el ~cls:"mt-3 flex justify-center gap-1.5" "div" + (List.map + (fun i -> + let cls () = + "size-2 rounded-full transition-colors " + ^ (if Signal.get idx = i then "bg-neutral-900" else "bg-neutral-300") + in + View.element ~attrs:[ View.Attr.class_reactive cls ] "span" []) + (List.init total (fun i -> i))); + ] + +let combobox_example () = + let all = [ "Next.js"; "SvelteKit"; "Remix"; "Astro"; "Nuxt"; "SolidStart"; "Qwik City" ] in + let query = Signal.make "" in + let open_ = Signal.make false in + let value = Signal.make "" in + let filtered () = List.filter (fun o -> contains_ci o (Signal.get query)) all in + let display () = if Signal.get value = "" then "Select framework\xe2\x80\xa6" else Signal.get value in + el ~cls:"relative w-64" "div" + [ + View.element + ~attrs: + [ + View.Attr.className + (View.static (Ui.input_base ^ " flex items-center justify-between text-left")); + ] + ~events:[ View.On.click (fun _ -> Signal.update open_ (fun v -> not v)) ] + "button" + [ + View.dyn_text display; + el ~cls:"text-neutral-400" "span" [ Icon.make ~name:"chevron-down" ~size:`sm () ]; + ]; + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + Backdrop.make ~on_close:(fun () -> Signal.set open_ false) (); + el + ~cls: + "absolute z-20 mt-1 w-full rounded-md border border-neutral-200 bg-surface p-1 \ + shadow-lg" + "div" + [ + Input.make ~extra_class:"mb-1" ~placeholder:"Search\xe2\x80\xa6" + ~on_input:(fun e -> Signal.set query (Ui.input_value e)) + (); + el ~cls:"max-h-40 overflow-y-auto" "ul" + [ + View.for_ filtered (fun o -> + el "li" + [ + View.element + ~attrs: + [ + View.Attr.className + (View.static + "block w-full px-3 py-1.5 text-left text-sm \ + text-neutral-700 hover:bg-neutral-100"); + ] + ~events: + [ + View.On.click (fun _ -> + Signal.set value o; + Signal.set open_ false); + ] + "button" + [ txt o ]; + ]); + ]; + ]; + ]) + ] + +let command_example () = + let all = [ "New file"; "New folder"; "Search"; "Go to settings"; "Toggle theme"; "Sign out" ] in + let query = Signal.make "" in + let filtered () = List.filter (fun o -> contains_ci o (Signal.get query)) all in + el ~cls:"w-80 overflow-hidden rounded-lg border border-neutral-200 shadow-sm" "div" + [ + View.element + ~attrs: + [ + View.Attr.className + (View.static "w-full border-b border-neutral-200 px-4 py-3 text-sm focus:outline-none"); + View.Attr.placeholder (View.static "Type a command\xe2\x80\xa6"); + ] + ~events:[ View.On.input (fun e -> Signal.set query (Ui.input_value e)) ] + "input" []; + el ~cls:"max-h-52 overflow-y-auto p-1" "ul" + [ + View.for_ filtered (fun o -> + el "li" + [ + el + ~cls: + "flex w-full items-center gap-2 rounded px-3 py-2 text-left text-sm \ + text-neutral-700 hover:bg-neutral-100" + "button" + [ + el ~cls:"text-neutral-400" "span" + [ Icon.make ~name:"chevron-right" ~size:`sm () ]; + txt o; + ]; + ]); + ]; + ] + +let calendar_example () = + let selected = Signal.make 14 in + let days = List.init 30 (fun i -> i + 1) in + let dow = [ "S"; "M"; "T"; "W"; "T"; "F"; "S" ] in + el ~cls:"w-72 rounded-lg border border-neutral-200 p-3" "div" + [ + el ~cls:"mb-2 flex items-center justify-between px-1" "div" + [ + el ~cls:"text-sm font-medium text-neutral-900" "span" [ txt "July 2026" ]; + el ~cls:"flex items-center gap-2 text-neutral-400" "span" + [ + Icon.make ~name:"chevron-left" ~size:`sm (); + Icon.make ~name:"chevron-right" ~size:`sm (); + ]; + ]; + el ~cls:"grid grid-cols-7 gap-1 text-center" "div" + (List.map (fun d -> el ~cls:"py-1 text-xs text-neutral-400" "span" [ txt d ]) dow + @ List.map + (fun d -> + let cls () = + "flex size-8 items-center justify-center rounded-full text-sm transition-colors " + ^ + if Signal.get selected = d then "bg-neutral-900 text-neutral-0" + else "text-neutral-700 hover:bg-neutral-100" + in + View.element + ~attrs:[ View.Attr.class_reactive cls ] + ~events:[ View.On.click (fun _ -> Signal.set selected d) ] + "button" + [ txt (string_of_int d) ]) + days); + ] + +let date_picker_example () = + let open_ = Signal.make false in + let day = Signal.make 14 in + let label () = "July " ^ string_of_int (Signal.get day) ^ ", 2026" in + let days = List.init 30 (fun i -> i + 1) in + el ~cls:"relative w-64" "div" + [ + View.element + ~attrs: + [ + View.Attr.className + (View.static (Ui.input_base ^ " flex items-center justify-between text-left")); + ] + ~events:[ View.On.click (fun _ -> Signal.update open_ (fun v -> not v)) ] + "button" + [ + View.dyn_text label; + el ~cls:"text-neutral-400" "span" [ Icon.make ~name:"calendar" ~size:`sm () ]; + ]; + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + Backdrop.make ~on_close:(fun () -> Signal.set open_ false) (); + el + ~cls: + "absolute z-20 mt-1 w-64 rounded-lg border border-neutral-200 bg-surface p-3 \ + shadow-xl" + "div" + [ + el ~cls:"grid grid-cols-7 gap-1 text-center" "div" + (List.map + (fun d -> + let cls () = + "flex size-8 items-center justify-center rounded-full text-sm " + ^ + if Signal.get day = d then "bg-neutral-900 text-neutral-0" + else "text-neutral-700 hover:bg-neutral-100" + in + View.element + ~attrs:[ View.Attr.class_reactive cls ] + ~events: + [ + View.On.click (fun _ -> + Signal.set day d; + Signal.set open_ false); + ] + "button" + [ txt (string_of_int d) ]) + days); + ]; + ]) + ] + +(* ------------------------------------------------------------------ *) +(* batch two — navigation *) +(* ------------------------------------------------------------------ *) + +let navbar_example () = + let nav_link label = + el ~cls:"hover:text-neutral-900" ~attrs:[ View.Attr.href (View.static "#") ] "a" [ txt label ] + in + el ~cls:"w-full max-w-2xl rounded-lg border border-neutral-200 bg-surface" "div" + [ + el ~cls:"flex items-center justify-between px-4 py-3" "div" + [ + el ~cls:"flex items-center gap-6" "div" + [ + el ~cls:"font-semibold text-neutral-900" "span" [ txt "Acme" ]; + el ~cls:"hidden gap-4 text-sm text-neutral-600 sm:flex" "nav" + [ nav_link "Product"; nav_link "Pricing"; nav_link "Docs" ]; + ]; + el ~cls:"flex items-center gap-2" "div" + [ + Button.make ~variant:`ghost ~children:[ txt "Sign in" ] (); + Button.make ~variant:`primary ~children:[ txt "Get started" ] (); + ]; + ]; + ] + +let menubar_example () = + el + ~cls: + "flex w-full max-w-lg items-center gap-1 rounded-lg border border-neutral-200 bg-surface p-1 \ + text-sm" + "div" + (List.map + (fun m -> + el ~cls:"rounded px-3 py-1.5 text-neutral-700 hover:bg-neutral-100" "button" [ txt m ]) + [ "File"; "Edit"; "View"; "Help" ]) + +let navigation_menu_example () = + let open_ = Signal.make false in + let link label = + el ~cls:"rounded px-3 py-1.5 text-neutral-700 hover:bg-neutral-100" + ~attrs:[ View.Attr.href (View.static "#") ] "a" [ txt label ] + in + el ~cls:"relative w-full max-w-lg" "div" + [ + el ~cls:"flex items-center gap-1 rounded-lg border border-neutral-200 bg-surface p-1 text-sm" + "div" + [ + View.element + ~attrs: + [ + View.Attr.className + (View.static + "inline-flex items-center gap-1 rounded px-3 py-1.5 text-neutral-700 \ + hover:bg-neutral-100"); + ] + ~events:[ View.On.click (fun _ -> Signal.update open_ (fun v -> not v)) ] + "button" + [ txt "Products"; Icon.make ~name:"chevron-down" ~size:`sm () ]; + link "Pricing"; + link "Company"; + ]; + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + Backdrop.make ~on_close:(fun () -> Signal.set open_ false) (); + el + ~cls: + "absolute z-20 mt-1 grid w-96 grid-cols-2 gap-1 rounded-lg border \ + border-neutral-200 bg-surface p-2 shadow-xl" + "div" + (List.map + (fun (t, d) -> + el ~cls:"rounded-md p-3 hover:bg-neutral-50" + ~attrs:[ View.Attr.href (View.static "#") ] "a" + [ + el ~cls:"text-sm font-medium text-neutral-900" "p" [ txt t ]; + el ~cls:"text-xs text-neutral-500" "p" [ txt d ]; + ]) + [ + ("Analytics", "Understand your traffic"); + ("Automations", "Build no-code workflows"); + ("Reports", "Share insights"); + ("Integrations", "Connect your stack"); + ]); + ]) + ] + +let sidebar_example () = + let item ?(active = false) label = + let cls = + if active then "block rounded-md bg-neutral-900 px-2 py-1.5 text-neutral-0" + else "block rounded-md px-2 py-1.5 text-neutral-700 hover:bg-neutral-100" + in + el ~cls ~attrs:[ View.Attr.href (View.static "#") ] "a" [ txt label ] + in + el ~cls:"flex h-56 w-56 flex-col rounded-lg border border-neutral-200 bg-neutral-50 p-2" "div" + [ + el ~cls:"px-2 py-1.5 text-sm font-semibold text-neutral-900" "div" [ txt "Workspace" ]; + el ~cls:"mt-1 space-y-0.5 text-sm" "nav" + [ item ~active:true "Dashboard"; item "Projects"; item "Reports"; item "Settings" ]; + ] + +(* ------------------------------------------------------------------ *) +(* batch three — remaining components, pages & flows *) +(* ------------------------------------------------------------------ *) + +let resizable_example () = + let split = Signal.make 55 in + let left_style () = "width:" ^ string_of_int (Signal.get split) ^ "%" in + let right_style () = "width:" ^ string_of_int (100 - Signal.get split) ^ "%" in + el ~cls:"w-96 space-y-3" "div" + [ + el ~cls:"flex h-40 overflow-hidden rounded-lg border border-neutral-200" "div" + [ + el ~cls:"flex items-center justify-center bg-neutral-50 text-sm text-neutral-500" + ~attrs:[ View.Attr.make "style" (View.dynamic left_style) ] "div" [ txt "Panel A" ]; + el ~cls:"w-1.5 cursor-col-resize bg-neutral-200" "div" []; + el ~cls:"flex items-center justify-center bg-surface text-sm text-neutral-500" + ~attrs:[ View.Attr.make "style" (View.dynamic right_style) ] "div" [ txt "Panel B" ]; + ]; + View.element + ~attrs: + [ + View.Attr.type_ (View.static "range"); + View.Attr.make "min" (View.static "20"); + View.Attr.make "max" (View.static "80"); + View.Attr.value_reactive (fun () -> string_of_int (Signal.get split)); + View.Attr.className (View.static "w-full accent-neutral-900"); + ] + ~events: + [ + View.On.input (fun e -> + match int_of_string_opt (Ui.input_value e) with + | Some n -> Signal.set split n + | None -> Signal.set split 50); + ] + "input" []; + ] + +let search_example () = + let query = Signal.make "" in + let open_ = Signal.make false in + let all = [ "Dashboard"; "Settings"; "Billing"; "Team members"; "API keys" ] in + let filtered () = List.filter (fun o -> contains_ci o (Signal.get query)) all in + el ~cls:"relative w-72" "div" + [ + el ~cls:"flex items-center gap-2 rounded-md border border-neutral-300 bg-surface pl-3 pr-1" "div" + [ + el ~cls:"text-neutral-400" "span" [ Icon.make ~name:"search" ~size:`sm () ]; + View.element + ~attrs: + [ + View.Attr.className + (View.static "flex-1 bg-transparent py-2 text-sm focus:outline-none"); + View.Attr.placeholder (View.static "Search\xe2\x80\xa6"); + ] + ~events: + [ + View.On.on "focus" (fun _ -> Signal.set open_ true); + View.On.input (fun e -> + Signal.set query (Ui.input_value e); + Signal.set open_ true); + ] + "input" []; + View.show + (fun () -> Signal.get query <> "") + (IconButton.make ~label:"Clear search" + ~on_click:(fun _ -> Signal.set query "") + ~children:[ txt "\xc3\x97" ] ()); + ]; + View.show + (fun () -> Signal.get open_) + (View.fragment + [ + Backdrop.make ~on_close:(fun () -> Signal.set open_ false) (); + el + ~cls: + "absolute z-20 mt-1 w-full rounded-md border border-neutral-200 bg-surface py-1 \ + shadow-lg" + "ul" + [ + View.for_ filtered (fun o -> + el "li" + [ + View.element + ~attrs: + [ + View.Attr.className + (View.static + "block w-full px-3 py-1.5 text-left text-sm text-neutral-700 \ + hover:bg-neutral-100"); + ] + ~events: + [ + View.On.click (fun _ -> + Signal.set query o; + Signal.set open_ false); + ] + "button" + [ txt o ]; + ]); + ]; + ]) + ] + +let comment_example () = + let liked = Signal.make false in + let like_cls () = + "inline-flex items-center gap-1 " + ^ (if Signal.get liked then "text-status-danger" else "hover:text-neutral-900") + in + el ~cls:"w-full max-w-lg" "div" + [ + el ~cls:"flex gap-3" "div" + [ + Avatar.make ~initials:"AT" ~size:"size-9 text-xs" (); + el ~cls:"flex-1" "div" + [ + el ~cls:"flex items-center gap-2" "div" + [ + el ~cls:"text-sm font-medium text-neutral-900" "span" [ txt "Alan Turing" ]; + Badge.make ~variant:`soft ~children:[ txt "Author" ] (); + el ~cls:"text-xs text-neutral-400" "span" [ txt "2h ago" ]; + ]; + el ~cls:"mt-1 text-sm text-neutral-700" "p" + [ + txt + "Great write-up \xe2\x80\x94 the section on reuse really clicked for me. Shipping \ + this to my team."; + ]; + el ~cls:"mt-2 flex gap-3 text-xs text-neutral-500" "div" + [ + View.element + ~attrs:[ View.Attr.class_reactive like_cls ] + ~events:[ View.On.click (fun _ -> Signal.update liked (fun v -> not v)) ] + "button" + [ + Icon.make ~name:"heart" ~size:`sm (); + View.show ~fallback:(txt "Like") (fun () -> Signal.get liked) (txt "Liked"); + ]; + el ~cls:"hover:text-neutral-900" "button" [ txt "Reply" ]; + ]; + ]; + ]; + ] + +let form_example () = + let sent = Signal.make false in + View.element + ~attrs:[ View.Attr.className (View.static "w-80 space-y-4") ] + ~events:[ View.On.submit (fun e -> Ui.prevent_default e; Signal.set sent true) ] + "form" + [ + Field.make ~label:"Name" ~for_:"frm-name" + ~children:[ Input.make ~id:"frm-name" ~placeholder:"Ada Lovelace" ~required:true () ] (); + Field.make ~label:"Message" ~for_:"frm-msg" + ~children: + [ + View.element + ~attrs: + [ + View.Attr.id (View.static "frm-msg"); + View.Attr.make "rows" (View.static "3"); + View.Attr.className (View.static (Ui.input_base ^ " resize-none")); + View.Attr.placeholder (View.static "Say hello\xe2\x80\xa6"); + ] + "textarea" []; + ] + (); + Button.make ~type_:`submit ~variant:`primary ~extra_class:"w-full" + ~children:[ txt "Send message" ] (); + View.show + (fun () -> Signal.get sent) + (el ~cls:"rounded-md bg-neutral-100 px-3 py-2 text-center text-sm text-neutral-700" "p" + [ txt "Thanks \xe2\x80\x94 we'll be in touch." ]); + ] + +let landing_page_example () = + el ~cls:"w-full max-w-2xl overflow-hidden rounded-lg border border-neutral-200 bg-surface" "div" + [ + el ~cls:"flex items-center justify-between border-b border-neutral-200 px-5 py-3" "div" + [ + el ~cls:"font-semibold text-neutral-900" "span" [ txt "Acme" ]; + Button.make ~variant:`primary ~children:[ txt "Start free" ] (); + ]; + el ~cls:"px-8 py-12 text-center" "div" + [ + Badge.make ~variant:`outline ~children:[ txt "New \xc2\xb7 v2 is here" ] (); + el ~cls:"mx-auto mt-4 max-w-md text-3xl font-bold tracking-tight text-neutral-900" "h2" + [ txt "Ship your product faster" ]; + el ~cls:"mx-auto mt-3 max-w-sm text-sm text-neutral-500" "p" + [ + txt + "One primary action, front and center \xe2\x80\x94 everything on the page builds \ + toward it."; + ]; + el ~cls:"mt-6 flex justify-center gap-3" "div" + [ + Button.make ~variant:`primary ~children:[ txt "Get started" ] (); + Button.make ~variant:`secondary ~children:[ txt "Book a demo" ] (); + ]; + ]; + ] + +let dashboard_example () = + let stat label value delta = + el ~cls:(Ui.card ^ " p-4") "div" + [ + el ~cls:"text-xs text-neutral-500" "p" [ txt label ]; + el ~cls:"mt-1 text-2xl font-bold text-neutral-900" "p" [ txt value ]; + el ~cls:"mt-1 text-xs text-neutral-400" "p" [ txt delta ]; + ] + in + let bars = [ 50; 72; 40; 88; 63; 45; 70 ] in + el ~cls:"w-full max-w-2xl space-y-4" "div" + [ + el ~cls:"grid grid-cols-3 gap-4" "div" + [ + stat "Revenue" "$48.2k" "+12% MoM"; + stat "Active users" "3,190" "+4% MoM"; + stat "Churn" "1.8%" "\xe2\x88\x920.3% MoM"; + ]; + el ~cls:(Ui.card ^ " p-4") "div" + [ + el ~cls:"mb-3 flex items-center justify-between" "div" + [ + el ~cls:"text-sm font-medium text-neutral-900" "p" [ txt "Signups this week" ]; + Badge.make ~variant:`soft ~children:[ txt "Live" ] (); + ]; + el ~cls:"flex h-24 items-end gap-2" "div" + (List.map + (fun v -> + el ~cls:"flex-1 rounded-t bg-neutral-900" + ~attrs: + [ View.Attr.make "style" (View.static ("height:" ^ string_of_int v ^ "%")) ] + "div" []) + bars); + ]; + ] + +let settings_example () = + let notifications = Signal.make true in + let marketing = Signal.make false in + let row text sig_ = + el ~cls:"flex items-center justify-between" "div" + [ el ~cls:"text-sm text-neutral-800" "span" [ txt text ]; Switch.make ~checked:sig_ () ] + in + el ~cls:"w-full max-w-lg space-y-5 rounded-lg border border-neutral-200 bg-surface p-6" "div" + [ + el "div" + [ + el ~cls:"text-sm font-semibold text-neutral-900" "h3" [ txt "Profile" ]; + el ~cls:"text-xs text-neutral-500" "p" [ txt "Update your account details." ]; + ]; + Field.make ~label:"Display name" ~for_:"set-name" + ~children:[ Input.make ~id:"set-name" ~value:"Ada Lovelace" () ] (); + Separator.make (); + el ~cls:"space-y-3" "div" + [ row "Product notifications" notifications; row "Marketing emails" marketing ]; + el ~cls:"flex justify-end gap-2" "div" + [ + Button.make ~variant:`ghost ~children:[ txt "Cancel" ] (); + Button.make ~variant:`primary ~children:[ txt "Save changes" ] (); + ]; + ] + +let sign_in_example () = + let remember = Signal.make true in + el ~cls:"w-full max-w-sm space-y-5 rounded-lg border border-neutral-200 bg-surface p-6" "div" + [ + el ~cls:"text-center" "div" + [ + el + ~cls: + "mx-auto mb-2 flex size-9 items-center justify-center rounded-md bg-neutral-900 \ + text-sm font-bold text-neutral-0" + "div" [ txt "U" ]; + el ~cls:"text-lg font-semibold text-neutral-900" "h3" [ txt "Welcome back" ]; + el ~cls:"text-sm text-neutral-500" "p" [ txt "Sign in to your account" ]; + ]; + Field.make ~label:"Email" ~for_:"si-email" + ~children:[ Input.make ~id:"si-email" ~type_:"email" ~placeholder:"you@example.com" () ] + (); + Field.make ~label:"Password" ~for_:"si-pass" + ~children: + [ + Input.make ~id:"si-pass" ~type_:"password" + ~placeholder: + "\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2" + (); + ] + (); + el ~cls:"flex items-center justify-between text-sm" "div" + [ + el ~cls:"flex items-center gap-2" "div" + [ + View.element + ~attrs: + [ + View.Attr.type_ (View.static "checkbox"); + View.Attr.checked (View.dynamic (fun () -> Signal.get remember)); + View.Attr.className (View.static "size-4 accent-neutral-900"); + ] + ~events:[ View.On.change (fun e -> Signal.set remember (Ui.checked e)) ] + "input" []; + el ~cls:"text-neutral-700" "span" [ txt "Remember me" ]; + ]; + Link_.make ~href:"#" ~children:[ txt "Forgot?" ] (); + ]; + Button.make ~variant:`primary ~extra_class:"w-full" ~children:[ txt "Sign in" ] (); + el ~cls:"text-center text-sm text-neutral-500" "p" + [ txt "No account? "; Link_.make ~href:"#" ~children:[ txt "Create one" ] () ]; + ] + +let pricing_example () = + let plan name price feats recommended = + let card_cls = + if recommended then Ui.card ^ " p-5 ring-2 ring-neutral-900" else Ui.card ^ " p-5" + in + el ~cls:card_cls "div" + [ + el ~cls:"flex items-center justify-between" "div" + [ + el ~cls:"text-sm font-semibold text-neutral-900" "p" [ txt name ]; + (if recommended then Badge.make ~variant:`solid ~children:[ txt "Popular" ] () + else View.empty); + ]; + el ~cls:"mt-2" "p" + [ + el ~cls:"text-3xl font-bold text-neutral-900" "span" [ txt price ]; + el ~cls:"text-sm text-neutral-500" "span" [ txt "/mo" ]; + ]; + el ~cls:"mt-3 space-y-1 text-sm text-neutral-600" "ul" + (List.map + (fun f -> + el ~cls:"flex items-center gap-2" "li" + [ + Icon.make ~name:"check" ~size:`sm ~extra_class:"text-status-success" (); + txt f; + ]) + feats); + el ~cls:"mt-4" "div" + [ + Button.make + ~variant:(if recommended then `primary else `secondary) + ~extra_class:"w-full" ~children:[ txt "Choose" ] (); + ]; + ] + in + el ~cls:"grid w-full max-w-2xl grid-cols-3 gap-4" "div" + [ + plan "Starter" "$0" [ "1 project"; "Community support" ] false; + plan "Pro" "$12" [ "Unlimited projects"; "Priority support"; "Analytics" ] true; + plan "Team" "$29" [ "Everything in Pro"; "SSO"; "Audit log" ] false; + ] + +let authentication_example () = + let step = Signal.make 1 in + let step_label () = "Step " ^ string_of_int (Signal.get step) ^ " of 2" in + let on_step1 () = Signal.get step = 1 in + el ~cls:"w-full max-w-sm rounded-lg border border-neutral-200 bg-surface p-6" "div" + [ + el ~cls:"text-xs text-neutral-400" "p" [ View.dyn_text step_label ]; + View.show + ~fallback: + (el ~cls:"mt-2 space-y-4" "div" + [ + el ~cls:"text-lg font-semibold text-neutral-900" "h3" [ txt "Enter your code" ]; + el ~cls:"text-sm text-neutral-500" "p" + [ txt "We sent a 6-digit code to your email." ]; + Field.make ~label:"Verification code" ~for_:"auth-code" + ~children:[ Input.make ~id:"auth-code" ~placeholder:"123456" () ] (); + Button.make ~variant:`primary ~extra_class:"w-full" + ~on_click:(fun _ -> Signal.set step 1) ~children:[ txt "Verify" ] (); + ]) + on_step1 + (el ~cls:"mt-2 space-y-4" "div" + [ + el ~cls:"text-lg font-semibold text-neutral-900" "h3" [ txt "Sign in" ]; + Field.make ~label:"Email" ~for_:"auth-email" + ~children: + [ Input.make ~id:"auth-email" ~type_:"email" ~placeholder:"you@example.com" () ] + (); + Field.make ~label:"Password" ~for_:"auth-pass" + ~children: + [ + Input.make ~id:"auth-pass" ~type_:"password" + ~placeholder: + "\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2" + (); + ] + (); + Button.make ~variant:`primary ~extra_class:"w-full" + ~on_click:(fun _ -> Signal.set step 2) ~children:[ txt "Continue" ] (); + ]); + ] + +let onboarding_example () = + let step = Signal.make 1 in + let total = 3 in + let pct () = "height:100%;width:" ^ string_of_int (Signal.get step * 100 / total) ^ "%" in + let title () = + match Signal.get step with + | 1 -> "Welcome to Acme" + | 2 -> "Set up your workspace" + | _ -> "Invite your team" + in + let body () = + match Signal.get step with + | 1 -> "Let's get you set up in a couple of steps." + | 2 -> "Name your workspace and pick a theme." + | _ -> "Add teammates to collaborate. You can skip this." + in + let next_label () = if Signal.get step = total then "Finish" else "Next" in + el ~cls:"w-full max-w-md rounded-lg border border-neutral-200 bg-surface p-6" "div" + [ + el ~cls:"h-1.5 w-full overflow-hidden rounded-full bg-neutral-200" "div" + [ + el ~cls:"rounded-full bg-neutral-900 transition-all" + ~attrs:[ View.Attr.make "style" (View.dynamic pct) ] "div" []; + ]; + el ~cls:"mt-4 text-lg font-semibold text-neutral-900" "h3" [ View.dyn_text title ]; + el ~cls:"mt-1 text-sm text-neutral-500" "p" [ View.dyn_text body ]; + el ~cls:"mt-5 flex justify-between" "div" + [ + Button.make ~variant:`ghost + ~on_click:(fun _ -> Signal.update step (fun s -> if s > 1 then s - 1 else 1)) + ~children:[ txt "Back" ] (); + Button.make ~variant:`primary + ~on_click:(fun _ -> Signal.update step (fun s -> if s < total then s + 1 else 1)) + ~children:[ View.dyn_text next_label ] (); + ]; + ] + +let checkout_example () = + let items = [ ("Pro plan (annual)", "$120.00"); ("Add-on: Analytics", "$24.00") ] in + el ~cls:"grid w-full max-w-2xl gap-4 sm:grid-cols-2" "div" + [ + el ~cls:"space-y-4 rounded-lg border border-neutral-200 bg-surface p-5" "div" + [ + el ~cls:"text-sm font-semibold text-neutral-900" "h3" [ txt "Payment details" ]; + Field.make ~label:"Card number" ~for_:"co-card" + ~children:[ Input.make ~id:"co-card" ~placeholder:"4242 4242 4242 4242" () ] (); + el ~cls:"grid grid-cols-2 gap-3" "div" + [ + Field.make ~label:"Expiry" ~for_:"co-exp" + ~children:[ Input.make ~id:"co-exp" ~placeholder:"MM/YY" () ] (); + Field.make ~label:"CVC" ~for_:"co-cvc" + ~children:[ Input.make ~id:"co-cvc" ~placeholder:"123" () ] (); + ]; + Button.make ~variant:`primary ~extra_class:"w-full" + ~children:[ txt "Pay $144.00" ] (); + ]; + el ~cls:"space-y-3 rounded-lg border border-neutral-200 bg-neutral-50 p-5" "div" + [ + el ~cls:"text-sm font-semibold text-neutral-900" "h3" [ txt "Order summary" ]; + el ~cls:"space-y-2 text-sm" "ul" + (List.map + (fun (name, price) -> + el ~cls:"flex justify-between text-neutral-700" "li" + [ txt name; el ~cls:"tabular-nums" "span" [ txt price ] ]) + items); + Separator.make (); + el ~cls:"flex justify-between text-sm font-semibold text-neutral-900" "div" + [ txt "Total"; el ~cls:"tabular-nums" "span" [ txt "$144.00" ] ]; + ]; + ] (* ------------------------------------------------------------------ *) (* registry + mount entry (the JS surface consumed by the website) *) @@ -1242,6 +2454,38 @@ let example_for = function | "pricing-table" -> pricing_table_example () | "faq" -> faq_example () | "cta-section" -> cta_section_example () + | "alert-dialog" -> alert_dialog_example () + | "dropdown-menu" -> dropdown_menu_example () + | "context-menu" -> context_menu_example () + | "popover" -> popover_example () + | "hover-card" -> hover_card_example () + | "sheet" -> sheet_example () + | "drawer" -> drawer_example () + | "toast" -> toast_example () + | "table" -> table_example () + | "data-table" -> data_table_example () + | "chart" -> chart_example () + | "carousel" -> carousel_example () + | "combobox" -> combobox_example () + | "command" -> command_example () + | "calendar" -> calendar_example () + | "date-picker" -> date_picker_example () + | "navbar" -> navbar_example () + | "menubar" -> menubar_example () + | "navigation-menu" -> navigation_menu_example () + | "sidebar" -> sidebar_example () + | "resizable" -> resizable_example () + | "search" -> search_example () + | "comment" -> comment_example () + | "form" -> form_example () + | "landing-page" -> landing_page_example () + | "dashboard" -> dashboard_example () + | "settings" -> settings_example () + | "sign-in" -> sign_in_example () + | "pricing" -> pricing_example () + | "authentication" -> authentication_example () + | "onboarding" -> onboarding_example () + | "checkout" -> checkout_example () | "announcement-bar" -> announcement_bar_example () | "contact-section" -> contact_section_example () | "logo-cloud" -> logo_cloud_example () @@ -1296,6 +2540,13 @@ let example_ids = "label"; "typography"; "logo"; "legend"; "card"; "breadcrumb"; "button-group"; "input-group"; "empty-state"; "stat"; "list"; "pagination"; "toolbar"; "footer"; "hero"; "feature-grid"; "testimonial"; "pricing-table"; "faq"; "cta-section"; + (* batch two *) + "alert-dialog"; "dropdown-menu"; "context-menu"; "popover"; "hover-card"; "sheet"; "drawer"; + "toast"; "table"; "data-table"; "chart"; "carousel"; "combobox"; "command"; "calendar"; + "date-picker"; "navbar"; "menubar"; "navigation-menu"; "sidebar"; + (* batch three — remaining components, pages & flows *) + "resizable"; "search"; "comment"; "form"; "landing-page"; "dashboard"; "settings"; "sign-in"; + "pricing"; "authentication"; "onboarding"; "checkout"; |] (* The spec ids [playground_for] renders — the website only offers a reativa