@lumiose/ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -2
  3. package/dist/accordion/accordion.css +20 -0
  4. package/dist/accordion/accordion.script.ts +66 -0
  5. package/dist/accordion/accordion.tsx +288 -0
  6. package/dist/animation/animation.ts +34 -0
  7. package/dist/autocomplete/autocomplete.tsx +239 -0
  8. package/dist/avatar/avatar.css +54 -0
  9. package/dist/avatar/avatar.script.ts +96 -0
  10. package/dist/avatar/avatar.tsx +110 -0
  11. package/dist/button/button.script.ts +35 -0
  12. package/dist/button/button.tsx +40 -0
  13. package/dist/checkbox/checkbox.script.ts +225 -0
  14. package/dist/checkbox/checkbox.tsx +156 -0
  15. package/dist/checkbox/context.ts +17 -0
  16. package/dist/checkbox-group/checkbox-group.tsx +111 -0
  17. package/dist/collapsible/collapsible.css +12 -0
  18. package/dist/collapsible/collapsible.script.ts +7 -0
  19. package/dist/collapsible/collapsible.tsx +153 -0
  20. package/dist/collapsible/panel.ts +115 -0
  21. package/dist/combobox/combobox.css +37 -0
  22. package/dist/combobox/combobox.script.ts +974 -0
  23. package/dist/combobox/combobox.tsx +556 -0
  24. package/dist/combobox/context.ts +46 -0
  25. package/dist/context-menu/context-menu.css +16 -0
  26. package/dist/context-menu/context-menu.script.ts +172 -0
  27. package/dist/context-menu/context-menu.tsx +178 -0
  28. package/dist/dialog/dialog.css +35 -0
  29. package/dist/dialog/dialog.script.ts +269 -0
  30. package/dist/dialog/dialog.tsx +214 -0
  31. package/dist/drawer/drawer.css +38 -0
  32. package/dist/drawer/drawer.script.ts +207 -0
  33. package/dist/drawer/drawer.tsx +113 -0
  34. package/dist/field/context.ts +156 -0
  35. package/dist/field/field.css +16 -0
  36. package/dist/field/field.script.ts +422 -0
  37. package/dist/field/field.tsx +465 -0
  38. package/dist/fieldset/fieldset.tsx +41 -0
  39. package/dist/form/form.tsx +50 -0
  40. package/dist/form/native.ts +21 -0
  41. package/dist/form/reset.ts +53 -0
  42. package/dist/index.ts +261 -0
  43. package/dist/input/input.tsx +67 -0
  44. package/dist/layer/layer.ts +191 -0
  45. package/dist/locale/digits.ts +21 -0
  46. package/dist/menu/menu.css +22 -0
  47. package/dist/menu/menu.script.ts +591 -0
  48. package/dist/menu/menu.tsx +520 -0
  49. package/dist/menubar/menubar.css +16 -0
  50. package/dist/menubar/menubar.script.ts +195 -0
  51. package/dist/menubar/menubar.tsx +67 -0
  52. package/dist/meter/meter.tsx +134 -0
  53. package/dist/navigation-menu/navigation-menu.css +14 -0
  54. package/dist/navigation-menu/navigation-menu.script.ts +466 -0
  55. package/dist/navigation-menu/navigation-menu.tsx +266 -0
  56. package/dist/number-field/format.ts +70 -0
  57. package/dist/number-field/number-field.css +38 -0
  58. package/dist/number-field/number-field.script.ts +490 -0
  59. package/dist/number-field/number-field.tsx +403 -0
  60. package/dist/otp-field/otp-field.css +45 -0
  61. package/dist/otp-field/otp-field.script.ts +330 -0
  62. package/dist/otp-field/otp-field.tsx +320 -0
  63. package/dist/placement/place.ts +148 -0
  64. package/dist/placement/placement.css +155 -0
  65. package/dist/placement/placement.ts +60 -0
  66. package/dist/popover/hover.ts +178 -0
  67. package/dist/popover/popover.css +20 -0
  68. package/dist/popover/popover.script.ts +256 -0
  69. package/dist/popover/popover.tsx +317 -0
  70. package/dist/preview-card/preview-card.css +7 -0
  71. package/dist/preview-card/preview-card.script.ts +136 -0
  72. package/dist/preview-card/preview-card.tsx +128 -0
  73. package/dist/progress/progress.tsx +155 -0
  74. package/dist/progress/value.ts +23 -0
  75. package/dist/radio-group/radio-group.tsx +156 -0
  76. package/dist/scroll-area/scroll-area.css +72 -0
  77. package/dist/scroll-area/scroll-area.script.ts +281 -0
  78. package/dist/scroll-area/scroll-area.tsx +118 -0
  79. package/dist/select/select.css +25 -0
  80. package/dist/select/select.tsx +171 -0
  81. package/dist/separator/separator.css +12 -0
  82. package/dist/separator/separator.tsx +38 -0
  83. package/dist/slider/format.ts +33 -0
  84. package/dist/slider/slider.css +93 -0
  85. package/dist/slider/slider.script.ts +355 -0
  86. package/dist/slider/slider.tsx +374 -0
  87. package/dist/styles.css +30 -0
  88. package/dist/switch/switch.tsx +92 -0
  89. package/dist/tabs/tabs.css +54 -0
  90. package/dist/tabs/tabs.script.ts +225 -0
  91. package/dist/tabs/tabs.tsx +272 -0
  92. package/dist/themes/default.css +2111 -0
  93. package/dist/toast/command.ts +3 -0
  94. package/dist/toast/toast.css +68 -0
  95. package/dist/toast/toast.script.ts +516 -0
  96. package/dist/toast/toast.tsx +432 -0
  97. package/dist/toggle/context.ts +16 -0
  98. package/dist/toggle/toggle.tsx +99 -0
  99. package/dist/toggle-group/toggle-group.script.ts +166 -0
  100. package/dist/toggle-group/toggle-group.tsx +117 -0
  101. package/dist/toolbar/context.ts +18 -0
  102. package/dist/toolbar/toolbar.css +12 -0
  103. package/dist/toolbar/toolbar.script.ts +123 -0
  104. package/dist/toolbar/toolbar.tsx +180 -0
  105. package/dist/tooltip/tooltip.css +8 -0
  106. package/dist/tooltip/tooltip.script.ts +222 -0
  107. package/dist/tooltip/tooltip.tsx +149 -0
  108. package/docs/README.md +174 -0
  109. package/docs/accordion.md +208 -0
  110. package/docs/autocomplete.md +161 -0
  111. package/docs/avatar.md +198 -0
  112. package/docs/button.md +98 -0
  113. package/docs/checkbox-group.md +112 -0
  114. package/docs/checkbox.md +133 -0
  115. package/docs/collapsible.md +165 -0
  116. package/docs/combobox.md +369 -0
  117. package/docs/context-menu.md +197 -0
  118. package/docs/dialog.md +256 -0
  119. package/docs/drawer.md +204 -0
  120. package/docs/field.md +295 -0
  121. package/docs/fieldset.md +92 -0
  122. package/docs/form.md +131 -0
  123. package/docs/input.md +87 -0
  124. package/docs/installation.md +65 -0
  125. package/docs/llms-full.txt +6923 -0
  126. package/docs/llms.txt +53 -0
  127. package/docs/menu.md +365 -0
  128. package/docs/menubar.md +186 -0
  129. package/docs/meter.md +103 -0
  130. package/docs/navigation-menu.md +257 -0
  131. package/docs/number-field.md +181 -0
  132. package/docs/otp-field.md +160 -0
  133. package/docs/popover.md +271 -0
  134. package/docs/preview-card.md +130 -0
  135. package/docs/progress.md +115 -0
  136. package/docs/radio-group.md +127 -0
  137. package/docs/scroll-area.md +147 -0
  138. package/docs/select.md +236 -0
  139. package/docs/separator.md +116 -0
  140. package/docs/slider.md +184 -0
  141. package/docs/switch.md +98 -0
  142. package/docs/tabs.md +254 -0
  143. package/docs/theming.md +132 -0
  144. package/docs/toast.md +311 -0
  145. package/docs/toggle-group.md +119 -0
  146. package/docs/toggle.md +94 -0
  147. package/docs/toolbar.md +138 -0
  148. package/docs/tooltip.md +128 -0
  149. package/package.json +102 -3
@@ -0,0 +1,153 @@
1
+ // Collapsible (decision-62, TASK-370): a native `<details>`, so its trigger (the `<summary>`)
2
+ // opens and closes the panel in server HTML with no JavaScript, with the browser's own keyboard
3
+ // handling and expanded state, and find-in-page opens it. The element is the only state: the
4
+ // trigger, find-in-page and the `open` prop all end in its `toggle` event, the single source of
5
+ // `onOpenChange`. The client script (./panel.ts) adds Base UI's animation hooks and the disabled
6
+ // trigger. Ported from Timeless's approach (https://timeless.build).
7
+ import { createContext, effect, ref, untrack, useContext, useId } from "lumiose";
8
+ import collapsibleScript from "./collapsible.script.ts?client";
9
+
10
+ /**
11
+ * The `<details>` props Root passes on (`class`, `style`, `name`, `data-*`, `aria-*`, events),
12
+ * without the brand of intrinsic elements' props, so that `bind:open` applies to Root.
13
+ */
14
+ type DetailsProps = {
15
+ [
16
+ K in keyof JSX.IntrinsicElements["details"] as K extends string ? K : never
17
+ ]: JSX.IntrinsicElements["details"][K];
18
+ };
19
+
20
+ /**
21
+ * `Collapsible.Root` renders the `<details>`, with the element props it is given.
22
+ * `bind:open={signal}` stands in for `open` and `onOpenChange`.
23
+ */
24
+ export interface CollapsibleRootProps extends Omit<DetailsProps, "open" | "children"> {
25
+ /**
26
+ * Opens (`true`) or closes (`false`) the panel when it changes. Pair it with `onOpenChange` to
27
+ * keep a signal in sync; leave both out and the element keeps its own state. Server HTML starts
28
+ * as it says.
29
+ */
30
+ open?: boolean;
31
+ /** Whether the panel starts open, when there is no `open`. Default `false`. */
32
+ defaultOpen?: boolean;
33
+ /** Called after the panel opens or closes, whatever opened or closed it. */
34
+ onOpenChange?: (open: boolean) => void;
35
+ /** The trigger takes no focus and does not toggle the panel. */
36
+ disabled?: boolean;
37
+ /** The other parts: the `Collapsible.Trigger` first, then the `Collapsible.Panel`. */
38
+ children?: JSX.Element;
39
+ }
40
+
41
+ /** The `<summary>` that opens and closes the panel: the first child of `Collapsible.Root`. */
42
+ export type CollapsibleTriggerProps = JSX.IntrinsicElements["summary"];
43
+ /** The `<div>` that the trigger shows and hides. */
44
+ export type CollapsiblePanelProps = JSX.IntrinsicElements["div"];
45
+
46
+ interface CollapsibleContextValue {
47
+ triggerId: string;
48
+ panelId: string;
49
+ disabled: () => boolean;
50
+ }
51
+
52
+ const CollapsibleContext = createContext<CollapsibleContextValue>();
53
+
54
+ function useCollapsible(): CollapsibleContextValue {
55
+ const context = useContext(CollapsibleContext);
56
+ if (!context) throw new Error("Collapsible parts must be inside <Collapsible.Root>.");
57
+ return context;
58
+ }
59
+
60
+ function Root({
61
+ open,
62
+ defaultOpen,
63
+ onOpenChange,
64
+ disabled,
65
+ children,
66
+ ref: forwarded,
67
+ ...rest
68
+ }: CollapsibleRootProps) {
69
+ // The panel's animation hooks and the disabled trigger; the page loads it once (decision-63).
70
+ collapsibleScript();
71
+ const id = useId();
72
+ const details = ref<HTMLDetailsElement>();
73
+ const context: CollapsibleContextValue = {
74
+ triggerId: `${id}-trigger`,
75
+ panelId: `${id}-panel`,
76
+ disabled: () => disabled === true,
77
+ };
78
+
79
+ /** The state last reported: a `<details>` created open fires `toggle` too (TASK-441 M40). */
80
+ let shown = untrack(() => open ?? defaultOpen) === true;
81
+
82
+ // The `open` prop drives the element; its `toggle` event reports back, so a signal written by
83
+ // `onOpenChange` sees the same value again and nothing loops.
84
+ effect(() => {
85
+ const element = details();
86
+ const wanted = open;
87
+ if (element && wanted !== undefined && element.open !== wanted) element.open = wanted;
88
+ });
89
+
90
+ return (
91
+ <CollapsibleContext value={context}>
92
+ <details
93
+ {...rest}
94
+ ref={(element: HTMLDetailsElement) => {
95
+ details(element);
96
+ forwarded?.(element);
97
+ }}
98
+ open={shown}
99
+ on:toggle={(event: ToggleEvent) => {
100
+ const now = event.newState === "open";
101
+ if (now === shown) return;
102
+ shown = now;
103
+ onOpenChange?.(now);
104
+ }}
105
+ >
106
+ {children}
107
+ </details>
108
+ </CollapsibleContext>
109
+ );
110
+ }
111
+
112
+ // The parts spread the caller's props first, so they override the defaults, and set the wiring
113
+ // last, so the ids that connect them stay consistent.
114
+
115
+ function Trigger({ children, ...rest }: CollapsibleTriggerProps) {
116
+ const collapsible = useCollapsible();
117
+ // A caller's `id` names it; the parts after it point at that (TASK-441 M39).
118
+ if (rest.id) collapsible.triggerId = rest.id;
119
+ return (
120
+ <summary
121
+ {...rest}
122
+ id={collapsible.triggerId}
123
+ aria-controls={collapsible.panelId}
124
+ aria-disabled={collapsible.disabled() ? "true" : undefined}
125
+ tabindex={collapsible.disabled() ? -1 : rest.tabindex}
126
+ >
127
+ {children}
128
+ </summary>
129
+ );
130
+ }
131
+
132
+ function Panel({ children, ...rest }: CollapsiblePanelProps) {
133
+ const collapsible = useCollapsible();
134
+ return (
135
+ <div {...rest} id={collapsible.panelId}>
136
+ {children}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * A panel that its trigger shows and hides: a native `<details>`, so it works in server HTML with
143
+ * no JavaScript, and find-in-page opens it. Unstyled: style the parts through `class`, `[open]` on
144
+ * the root, and the panel's `--lu-collapsible-panel-height`, `[data-starting-style]` and
145
+ * `[data-ending-style]` for an animation.
146
+ *
147
+ * @example
148
+ * <Collapsible.Root>
149
+ * <Collapsible.Trigger>Recovery keys</Collapsible.Trigger>
150
+ * <Collapsible.Panel>…</Collapsible.Panel>
151
+ * </Collapsible.Root>
152
+ */
153
+ export const Collapsible = { Root, Trigger, Panel };
@@ -0,0 +1,115 @@
1
+ // The panels of Collapsible and Accordion, for their client scripts (doc-9 §9, TASK-370). A panel
2
+ // is the content of a `<details>`: the browser opens and closes it, whatever asks (its summary,
3
+ // find-in-page, an exclusive `name` group, the `open` prop), and this module only adds what the
4
+ // element lacks next to Base UI:
5
+ //
6
+ // - The open and close animation's measurements, as Base UI exposes them: `--lu-collapsible-panel-
7
+ // height` and `-width` (`--lu-accordion-panel-…` in an accordion) on the panel, its content's size
8
+ // in pixels while it opens and closes and `auto` once open; `data-starting-style` on the panel
9
+ // for the frame an opening starts from, `data-ending-style` while it closes. A stylesheet then
10
+ // transitions the height between 0 and the variable (the default theme does). A panel rendered
11
+ // open does not animate: nothing changed. A change while one runs starts from where it is.
12
+ // - Disabled triggers (`aria-disabled="true"` on the summary) do not toggle.
13
+ //
14
+ // It watches the `open` attribute of every `<details>` on the page, through one
15
+ // MutationObserver: its callback runs before the next frame, so the opening transition starts from
16
+ // the starting style. Closing keeps the content visible only as long as a stylesheet
17
+ // transitions `::details-content`'s `content-visibility` (`allow-discrete`), as the theme does.
18
+ // It must not import `lumiose`: server-rendered pages load it without the runtime.
19
+ import { afterAnimations } from "../animation/animation.ts";
20
+
21
+ /** The pending end of a panel's animation: a newer change replaces it. */
22
+ const pending = new WeakMap<HTMLElement, symbol>();
23
+
24
+ /** The panel of a Collapsible or an accordion item: the child its summary controls. */
25
+ function panelOf(details: HTMLDetailsElement): HTMLElement | undefined {
26
+ const summary = details.querySelector(":scope > summary");
27
+ const id = summary?.getAttribute("aria-controls");
28
+ if (!id) return undefined;
29
+ for (const child of details.children) {
30
+ if (child.id === id && child instanceof HTMLElement) return child;
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ /** The variables' prefix: an accordion item is a `<details>` right inside `<lu-accordion>`. */
36
+ const prefix = (details: HTMLDetailsElement) =>
37
+ details.parentElement?.localName === "lu-accordion" ? "--lu-accordion-panel" : "--lu-collapsible-panel";
38
+
39
+ function measure(panel: HTMLElement, name: string) {
40
+ panel.style.setProperty(`${name}-height`, `${panel.scrollHeight}px`);
41
+ panel.style.setProperty(`${name}-width`, `${panel.scrollWidth}px`);
42
+ }
43
+
44
+ /** Runs `done` once the panel's animations finish, unless another change came first. */
45
+ function settle(panel: HTMLElement, done: () => void) {
46
+ const token = Symbol();
47
+ pending.set(panel, token);
48
+ void afterAnimations(panel).then(() => {
49
+ if (pending.get(panel) === token) done();
50
+ });
51
+ }
52
+
53
+ function opened(details: HTMLDetailsElement, panel: HTMLElement) {
54
+ const name = prefix(details);
55
+ panel.removeAttribute("data-ending-style");
56
+ // From 0, or from where a closing got to, to the content's height: the starting style is
57
+ // computed first, so the transition has a value to start from.
58
+ const fresh = !panel.style.getPropertyValue(`${name}-height`).endsWith("px");
59
+ if (fresh) panel.setAttribute("data-starting-style", "");
60
+ measure(panel, name);
61
+ getComputedStyle(panel).getPropertyValue("block-size");
62
+ panel.removeAttribute("data-starting-style");
63
+ settle(panel, () => {
64
+ panel.style.setProperty(`${name}-height`, "auto");
65
+ panel.style.setProperty(`${name}-width`, "auto");
66
+ });
67
+ }
68
+
69
+ function closed(details: HTMLDetailsElement, panel: HTMLElement) {
70
+ const name = prefix(details);
71
+ panel.removeAttribute("data-starting-style");
72
+ // From the content's height (not `auto`, which does not transition) to 0.
73
+ measure(panel, name);
74
+ getComputedStyle(panel).getPropertyValue("block-size");
75
+ panel.setAttribute("data-ending-style", "");
76
+ settle(panel, () => {
77
+ panel.removeAttribute("data-ending-style");
78
+ // The next opening starts from 0 again.
79
+ panel.style.setProperty(`${name}-height`, "auto");
80
+ panel.style.setProperty(`${name}-width`, "auto");
81
+ });
82
+ }
83
+
84
+ let watching = false;
85
+
86
+ /** Starts watching the page's panels, once. */
87
+ export function watchPanels(): void {
88
+ if (watching) return;
89
+ watching = true;
90
+ new MutationObserver((records) => {
91
+ for (const record of records) {
92
+ const details = record.target;
93
+ if (!(details instanceof HTMLDetailsElement)) continue;
94
+ const panel = panelOf(details);
95
+ // The attribute was set again without a change.
96
+ if (!panel || (record.oldValue !== null) === details.open) continue;
97
+ if (details.open) opened(details, panel);
98
+ else closed(details, panel);
99
+ }
100
+ }).observe(document.documentElement, {
101
+ subtree: true,
102
+ attributes: true,
103
+ attributeFilter: ["open"],
104
+ attributeOldValue: true,
105
+ });
106
+ // A disabled trigger: a click (Enter and Space click a summary too) does not toggle.
107
+ document.addEventListener(
108
+ "click",
109
+ (event) => {
110
+ const summary = (event.target as Element | null)?.closest?.("summary");
111
+ if (summary?.getAttribute("aria-disabled") === "true") event.preventDefault();
112
+ },
113
+ true,
114
+ );
115
+ }
@@ -0,0 +1,37 @@
1
+ /*
2
+ * The CSS Combobox needs to work, and nothing else (decision-62): `lu-combobox` adds no box, the
3
+ * popup is placed next to the input by the shared placement rules (its `data-side` and
4
+ * `data-align`, `--lu-side-offset` and `--lu-align-offset`, with flips), and the options, groups
5
+ * and empty state the script hides stay hidden whatever `display` a theme gives them. Where CSS
6
+ * anchor positioning is missing, the client script places the popup. It sits in the `lu.core`
7
+ * layer, so any app CSS wins.
8
+ */
9
+ @layer lu.core, lu.theme;
10
+ @import "../placement/placement.css";
11
+
12
+ @layer lu.core {
13
+ lu-combobox {
14
+ display: contents;
15
+ }
16
+ lu-combobox [hidden] {
17
+ display: none !important;
18
+ }
19
+ /*
20
+ * A popup around the list stacks its parts in a column, so when the popup shrinks to the room
21
+ * beside the input the list shrinks with it and scrolls, keeping the highlight in view
22
+ * (TASK-428).
23
+ */
24
+ lu-combobox [popover]:popover-open:has(> [role="listbox"]) {
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+ lu-combobox [popover] > [role="listbox"] {
29
+ flex: 0 1 auto;
30
+ min-block-size: 0;
31
+ overflow-y: auto;
32
+ }
33
+ /* The Trigger and Clear buttons do nothing until the script runs (TASK-438, M24). */
34
+ lu-combobox:not(:defined) [command] {
35
+ visibility: hidden;
36
+ }
37
+ }