@llui/components 0.0.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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/components/accordion.d.ts +115 -0
  4. package/dist/components/accordion.d.ts.map +1 -0
  5. package/dist/components/accordion.js +138 -0
  6. package/dist/components/alert-dialog.d.ts +45 -0
  7. package/dist/components/alert-dialog.d.ts.map +1 -0
  8. package/dist/components/alert-dialog.js +12 -0
  9. package/dist/components/angle-slider.d.ts +121 -0
  10. package/dist/components/angle-slider.d.ts.map +1 -0
  11. package/dist/components/angle-slider.js +145 -0
  12. package/dist/components/async-list.d.ts +104 -0
  13. package/dist/components/async-list.d.ts.map +1 -0
  14. package/dist/components/async-list.js +117 -0
  15. package/dist/components/avatar.d.ts +58 -0
  16. package/dist/components/avatar.d.ts.map +1 -0
  17. package/dist/components/avatar.js +43 -0
  18. package/dist/components/carousel.d.ts +128 -0
  19. package/dist/components/carousel.d.ts.map +1 -0
  20. package/dist/components/carousel.js +131 -0
  21. package/dist/components/cascade-select.d.ts +95 -0
  22. package/dist/components/cascade-select.d.ts.map +1 -0
  23. package/dist/components/cascade-select.js +100 -0
  24. package/dist/components/checkbox.d.ts +74 -0
  25. package/dist/components/checkbox.d.ts.map +1 -0
  26. package/dist/components/checkbox.js +73 -0
  27. package/dist/components/clipboard.d.ts +72 -0
  28. package/dist/components/clipboard.d.ts.map +1 -0
  29. package/dist/components/clipboard.js +73 -0
  30. package/dist/components/collapsible.d.ts +64 -0
  31. package/dist/components/collapsible.d.ts.map +1 -0
  32. package/dist/components/collapsible.js +51 -0
  33. package/dist/components/color-picker.d.ts +125 -0
  34. package/dist/components/color-picker.d.ts.map +1 -0
  35. package/dist/components/color-picker.js +169 -0
  36. package/dist/components/combobox.d.ts +163 -0
  37. package/dist/components/combobox.d.ts.map +1 -0
  38. package/dist/components/combobox.js +345 -0
  39. package/dist/components/context-menu.d.ts +105 -0
  40. package/dist/components/context-menu.d.ts.map +1 -0
  41. package/dist/components/context-menu.js +177 -0
  42. package/dist/components/date-input.d.ts +117 -0
  43. package/dist/components/date-input.d.ts.map +1 -0
  44. package/dist/components/date-input.js +149 -0
  45. package/dist/components/date-picker.d.ts +142 -0
  46. package/dist/components/date-picker.d.ts.map +1 -0
  47. package/dist/components/date-picker.js +294 -0
  48. package/dist/components/dialog.d.ts +152 -0
  49. package/dist/components/dialog.d.ts.map +1 -0
  50. package/dist/components/dialog.js +140 -0
  51. package/dist/components/drawer.d.ts +106 -0
  52. package/dist/components/drawer.d.ts.map +1 -0
  53. package/dist/components/drawer.js +136 -0
  54. package/dist/components/editable.d.ts +92 -0
  55. package/dist/components/editable.d.ts.map +1 -0
  56. package/dist/components/editable.js +112 -0
  57. package/dist/components/file-upload.d.ts +251 -0
  58. package/dist/components/file-upload.d.ts.map +1 -0
  59. package/dist/components/file-upload.js +324 -0
  60. package/dist/components/floating-panel.d.ts +171 -0
  61. package/dist/components/floating-panel.d.ts.map +1 -0
  62. package/dist/components/floating-panel.js +198 -0
  63. package/dist/components/hover-card.d.ts +85 -0
  64. package/dist/components/hover-card.d.ts.map +1 -0
  65. package/dist/components/hover-card.js +128 -0
  66. package/dist/components/image-cropper.d.ts +129 -0
  67. package/dist/components/image-cropper.d.ts.map +1 -0
  68. package/dist/components/image-cropper.js +208 -0
  69. package/dist/components/index.d.ts +109 -0
  70. package/dist/components/index.d.ts.map +1 -0
  71. package/dist/components/index.js +54 -0
  72. package/dist/components/listbox.d.ts +98 -0
  73. package/dist/components/listbox.d.ts.map +1 -0
  74. package/dist/components/listbox.js +174 -0
  75. package/dist/components/marquee.d.ts +84 -0
  76. package/dist/components/marquee.d.ts.map +1 -0
  77. package/dist/components/marquee.js +73 -0
  78. package/dist/components/menu.d.ts +131 -0
  79. package/dist/components/menu.d.ts.map +1 -0
  80. package/dist/components/menu.js +262 -0
  81. package/dist/components/navigation-menu.d.ts +111 -0
  82. package/dist/components/navigation-menu.d.ts.map +1 -0
  83. package/dist/components/navigation-menu.js +102 -0
  84. package/dist/components/number-input.d.ts +106 -0
  85. package/dist/components/number-input.d.ts.map +1 -0
  86. package/dist/components/number-input.js +178 -0
  87. package/dist/components/pagination.d.ts +113 -0
  88. package/dist/components/pagination.d.ts.map +1 -0
  89. package/dist/components/pagination.js +135 -0
  90. package/dist/components/password-input.d.ts +64 -0
  91. package/dist/components/password-input.d.ts.map +1 -0
  92. package/dist/components/password-input.js +52 -0
  93. package/dist/components/pin-input.d.ts +89 -0
  94. package/dist/components/pin-input.d.ts.map +1 -0
  95. package/dist/components/pin-input.js +139 -0
  96. package/dist/components/popover.d.ts +116 -0
  97. package/dist/components/popover.d.ts.map +1 -0
  98. package/dist/components/popover.js +146 -0
  99. package/dist/components/presence.d.ts +71 -0
  100. package/dist/components/presence.d.ts.map +1 -0
  101. package/dist/components/presence.js +57 -0
  102. package/dist/components/progress.d.ts +74 -0
  103. package/dist/components/progress.d.ts.map +1 -0
  104. package/dist/components/progress.js +80 -0
  105. package/dist/components/qr-code.d.ts +114 -0
  106. package/dist/components/qr-code.d.ts.map +1 -0
  107. package/dist/components/qr-code.js +108 -0
  108. package/dist/components/radio-group.d.ts +89 -0
  109. package/dist/components/radio-group.d.ts.map +1 -0
  110. package/dist/components/radio-group.js +161 -0
  111. package/dist/components/rating-group.d.ts +88 -0
  112. package/dist/components/rating-group.d.ts.map +1 -0
  113. package/dist/components/rating-group.js +122 -0
  114. package/dist/components/scroll-area.d.ts +124 -0
  115. package/dist/components/scroll-area.d.ts.map +1 -0
  116. package/dist/components/scroll-area.js +152 -0
  117. package/dist/components/select.d.ts +161 -0
  118. package/dist/components/select.d.ts.map +1 -0
  119. package/dist/components/select.js +333 -0
  120. package/dist/components/signature-pad.d.ts +138 -0
  121. package/dist/components/signature-pad.d.ts.map +1 -0
  122. package/dist/components/signature-pad.js +142 -0
  123. package/dist/components/slider.d.ts +117 -0
  124. package/dist/components/slider.d.ts.map +1 -0
  125. package/dist/components/slider.js +210 -0
  126. package/dist/components/splitter.d.ts +87 -0
  127. package/dist/components/splitter.d.ts.map +1 -0
  128. package/dist/components/splitter.js +119 -0
  129. package/dist/components/steps.d.ts +104 -0
  130. package/dist/components/steps.d.ts.map +1 -0
  131. package/dist/components/steps.js +133 -0
  132. package/dist/components/switch.d.ts +66 -0
  133. package/dist/components/switch.d.ts.map +1 -0
  134. package/dist/components/switch.js +59 -0
  135. package/dist/components/tabs.d.ts +146 -0
  136. package/dist/components/tabs.d.ts.map +1 -0
  137. package/dist/components/tabs.js +244 -0
  138. package/dist/components/tags-input.d.ts +118 -0
  139. package/dist/components/tags-input.d.ts.map +1 -0
  140. package/dist/components/tags-input.js +168 -0
  141. package/dist/components/time-picker.d.ts +121 -0
  142. package/dist/components/time-picker.d.ts.map +1 -0
  143. package/dist/components/time-picker.js +147 -0
  144. package/dist/components/timer.d.ts +131 -0
  145. package/dist/components/timer.d.ts.map +1 -0
  146. package/dist/components/timer.js +117 -0
  147. package/dist/components/toast.d.ts +119 -0
  148. package/dist/components/toast.d.ts.map +1 -0
  149. package/dist/components/toast.js +102 -0
  150. package/dist/components/toc.d.ts +119 -0
  151. package/dist/components/toc.d.ts.map +1 -0
  152. package/dist/components/toc.js +107 -0
  153. package/dist/components/toggle-group.d.ts +80 -0
  154. package/dist/components/toggle-group.d.ts.map +1 -0
  155. package/dist/components/toggle-group.js +93 -0
  156. package/dist/components/toggle.d.ts +47 -0
  157. package/dist/components/toggle.d.ts.map +1 -0
  158. package/dist/components/toggle.js +41 -0
  159. package/dist/components/tooltip.d.ts +92 -0
  160. package/dist/components/tooltip.d.ts.map +1 -0
  161. package/dist/components/tooltip.js +147 -0
  162. package/dist/components/tour.d.ts +145 -0
  163. package/dist/components/tour.d.ts.map +1 -0
  164. package/dist/components/tour.js +133 -0
  165. package/dist/components/tree-view.d.ts +216 -0
  166. package/dist/components/tree-view.d.ts.map +1 -0
  167. package/dist/components/tree-view.js +293 -0
  168. package/dist/index.d.ts +3 -0
  169. package/dist/index.d.ts.map +1 -0
  170. package/dist/index.js +4 -0
  171. package/dist/patterns/confirm-dialog.d.ts +92 -0
  172. package/dist/patterns/confirm-dialog.d.ts.map +1 -0
  173. package/dist/patterns/confirm-dialog.js +92 -0
  174. package/dist/patterns/index.d.ts +3 -0
  175. package/dist/patterns/index.d.ts.map +1 -0
  176. package/dist/patterns/index.js +1 -0
  177. package/dist/utils/anatomy.d.ts +40 -0
  178. package/dist/utils/anatomy.d.ts.map +1 -0
  179. package/dist/utils/anatomy.js +41 -0
  180. package/dist/utils/aria-hidden.d.ts +12 -0
  181. package/dist/utils/aria-hidden.d.ts.map +1 -0
  182. package/dist/utils/aria-hidden.js +72 -0
  183. package/dist/utils/dismissable.d.ts +25 -0
  184. package/dist/utils/dismissable.d.ts.map +1 -0
  185. package/dist/utils/dismissable.js +65 -0
  186. package/dist/utils/dom.d.ts +8 -0
  187. package/dist/utils/dom.d.ts.map +1 -0
  188. package/dist/utils/dom.js +21 -0
  189. package/dist/utils/floating.d.ts +44 -0
  190. package/dist/utils/floating.d.ts.map +1 -0
  191. package/dist/utils/floating.js +44 -0
  192. package/dist/utils/focus-trap.d.ts +18 -0
  193. package/dist/utils/focus-trap.d.ts.map +1 -0
  194. package/dist/utils/focus-trap.js +85 -0
  195. package/dist/utils/focusables.d.ts +6 -0
  196. package/dist/utils/focusables.d.ts.map +1 -0
  197. package/dist/utils/focusables.js +65 -0
  198. package/dist/utils/index.d.ts +18 -0
  199. package/dist/utils/index.d.ts.map +1 -0
  200. package/dist/utils/index.js +10 -0
  201. package/dist/utils/interact-outside.d.ts +26 -0
  202. package/dist/utils/interact-outside.d.ts.map +1 -0
  203. package/dist/utils/interact-outside.js +46 -0
  204. package/dist/utils/remove-scroll.d.ts +8 -0
  205. package/dist/utils/remove-scroll.d.ts.map +1 -0
  206. package/dist/utils/remove-scroll.js +37 -0
  207. package/dist/utils/tree-collection.d.ts +61 -0
  208. package/dist/utils/tree-collection.d.ts.map +1 -0
  209. package/dist/utils/tree-collection.js +137 -0
  210. package/dist/utils/typeahead.d.ts +49 -0
  211. package/dist/utils/typeahead.d.ts.map +1 -0
  212. package/dist/utils/typeahead.js +81 -0
  213. package/package.json +282 -0
@@ -0,0 +1,133 @@
1
+ export function init(opts = {}) {
2
+ return {
3
+ current: opts.current ?? 0,
4
+ completed: opts.completed ?? [],
5
+ errors: [],
6
+ steps: opts.steps ?? [],
7
+ linear: opts.linear ?? true,
8
+ disabled: opts.disabled ?? false,
9
+ };
10
+ }
11
+ function canGoTo(state, step) {
12
+ if (step < 0 || step >= state.steps.length)
13
+ return false;
14
+ if (!state.linear)
15
+ return true;
16
+ // Linear: only previous, current, or next-if-current-completed
17
+ if (step <= state.current)
18
+ return true;
19
+ if (step === state.current + 1 && state.completed.includes(state.current))
20
+ return true;
21
+ return false;
22
+ }
23
+ export function update(state, msg) {
24
+ if (state.disabled)
25
+ return [state, []];
26
+ switch (msg.type) {
27
+ case 'goTo':
28
+ if (!canGoTo(state, msg.step))
29
+ return [state, []];
30
+ return [{ ...state, current: msg.step }, []];
31
+ case 'next': {
32
+ const next = state.current + 1;
33
+ if (next >= state.steps.length)
34
+ return [state, []];
35
+ // Completing current step moves forward
36
+ const completed = state.completed.includes(state.current)
37
+ ? state.completed
38
+ : [...state.completed, state.current];
39
+ return [{ ...state, current: next, completed }, []];
40
+ }
41
+ case 'prev': {
42
+ if (state.current === 0)
43
+ return [state, []];
44
+ return [{ ...state, current: state.current - 1 }, []];
45
+ }
46
+ case 'complete': {
47
+ if (state.completed.includes(msg.step))
48
+ return [state, []];
49
+ return [
50
+ {
51
+ ...state,
52
+ completed: [...state.completed, msg.step],
53
+ errors: state.errors.filter((e) => e !== msg.step),
54
+ },
55
+ [],
56
+ ];
57
+ }
58
+ case 'markError':
59
+ if (state.errors.includes(msg.step))
60
+ return [state, []];
61
+ return [{ ...state, errors: [...state.errors, msg.step] }, []];
62
+ case 'clearError':
63
+ return [{ ...state, errors: state.errors.filter((e) => e !== msg.step) }, []];
64
+ case 'reset':
65
+ return [{ ...state, current: 0, completed: [], errors: [] }, []];
66
+ }
67
+ }
68
+ export function stepStatus(state, step) {
69
+ if (state.errors.includes(step))
70
+ return 'error';
71
+ if (step === state.current)
72
+ return 'current';
73
+ if (state.completed.includes(step))
74
+ return 'completed';
75
+ return 'pending';
76
+ }
77
+ export function connect(get, send, opts = {}) {
78
+ const label = opts.label ?? 'Progress';
79
+ return {
80
+ root: {
81
+ role: 'group',
82
+ 'aria-label': label,
83
+ 'data-scope': 'steps',
84
+ 'data-part': 'root',
85
+ 'data-disabled': (s) => (get(s).disabled ? '' : undefined),
86
+ },
87
+ nextTrigger: {
88
+ type: 'button',
89
+ disabled: (s) => {
90
+ const st = get(s);
91
+ return st.disabled || st.current >= st.steps.length - 1;
92
+ },
93
+ 'data-scope': 'steps',
94
+ 'data-part': 'next-trigger',
95
+ onClick: () => send({ type: 'next' }),
96
+ },
97
+ prevTrigger: {
98
+ type: 'button',
99
+ disabled: (s) => {
100
+ const st = get(s);
101
+ return st.disabled || st.current === 0;
102
+ },
103
+ 'data-scope': 'steps',
104
+ 'data-part': 'prev-trigger',
105
+ onClick: () => send({ type: 'prev' }),
106
+ },
107
+ item: (index) => ({
108
+ item: {
109
+ 'data-scope': 'steps',
110
+ 'data-part': 'item',
111
+ 'data-status': (s) => stepStatus(get(s), index),
112
+ 'data-index': String(index),
113
+ 'aria-current': (s) => (get(s).current === index ? 'step' : undefined),
114
+ },
115
+ trigger: {
116
+ type: 'button',
117
+ 'aria-label': `Step ${index + 1}`,
118
+ disabled: (s) => !canGoTo(get(s), index),
119
+ 'data-scope': 'steps',
120
+ 'data-part': 'trigger',
121
+ 'data-status': (s) => stepStatus(get(s), index),
122
+ onClick: () => send({ type: 'goTo', step: index }),
123
+ },
124
+ separator: {
125
+ 'data-scope': 'steps',
126
+ 'data-part': 'separator',
127
+ 'data-status': (s) => stepStatus(get(s), index),
128
+ 'aria-hidden': 'true',
129
+ },
130
+ }),
131
+ };
132
+ }
133
+ export const steps = { init, update, connect, stepStatus };
@@ -0,0 +1,66 @@
1
+ import type { Send } from '@llui/dom';
2
+ /**
3
+ * Switch — two-state on/off control. Semantically like a checkbox but
4
+ * visually a toggle track + thumb. Uses `role="switch"` for ARIA.
5
+ */
6
+ export interface SwitchState {
7
+ checked: boolean;
8
+ disabled: boolean;
9
+ }
10
+ export type SwitchMsg = {
11
+ type: 'toggle';
12
+ } | {
13
+ type: 'setChecked';
14
+ checked: boolean;
15
+ } | {
16
+ type: 'setDisabled';
17
+ disabled: boolean;
18
+ };
19
+ export interface SwitchInit {
20
+ checked?: boolean;
21
+ disabled?: boolean;
22
+ }
23
+ export declare function init(opts?: SwitchInit): SwitchState;
24
+ export declare function update(state: SwitchState, msg: SwitchMsg): [SwitchState, never[]];
25
+ export interface SwitchParts<S> {
26
+ root: {
27
+ role: 'switch';
28
+ 'aria-checked': (s: S) => boolean;
29
+ 'aria-disabled': (s: S) => 'true' | undefined;
30
+ 'data-state': (s: S) => 'checked' | 'unchecked';
31
+ 'data-disabled': (s: S) => '' | undefined;
32
+ 'data-scope': 'switch';
33
+ 'data-part': 'root';
34
+ tabIndex: (s: S) => number;
35
+ onClick: (e: MouseEvent) => void;
36
+ onKeyDown: (e: KeyboardEvent) => void;
37
+ };
38
+ track: {
39
+ 'data-state': (s: S) => 'checked' | 'unchecked';
40
+ 'data-scope': 'switch';
41
+ 'data-part': 'track';
42
+ };
43
+ thumb: {
44
+ 'data-state': (s: S) => 'checked' | 'unchecked';
45
+ 'data-scope': 'switch';
46
+ 'data-part': 'thumb';
47
+ };
48
+ hiddenInput: {
49
+ type: 'checkbox';
50
+ role: 'switch';
51
+ 'aria-hidden': 'true';
52
+ tabIndex: -1;
53
+ style: string;
54
+ checked: (s: S) => boolean;
55
+ disabled: (s: S) => boolean;
56
+ 'data-scope': 'switch';
57
+ 'data-part': 'hidden-input';
58
+ };
59
+ }
60
+ export declare function connect<S>(get: (s: S) => SwitchState, send: Send<SwitchMsg>): SwitchParts<S>;
61
+ export declare const switchMachine: {
62
+ init: typeof init;
63
+ update: typeof update;
64
+ connect: typeof connect;
65
+ };
66
+ //# sourceMappingURL=switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../src/components/switch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC;;;GAGG;AAEH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAA;AAE9C,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,IAAI,CAAC,IAAI,GAAE,UAAe,GAAG,WAAW,CAEvD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,GAAG,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,CAUjF;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,IAAI,EAAE;QACJ,IAAI,EAAE,QAAQ,CAAA;QACd,cAAc,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;QACjC,eAAe,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAA;QAC7C,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,SAAS,GAAG,WAAW,CAAA;QAC/C,eAAe,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,SAAS,CAAA;QACzC,YAAY,EAAE,QAAQ,CAAA;QACtB,WAAW,EAAE,MAAM,CAAA;QACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;QAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;QAChC,SAAS,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;KACtC,CAAA;IACD,KAAK,EAAE;QACL,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,SAAS,GAAG,WAAW,CAAA;QAC/C,YAAY,EAAE,QAAQ,CAAA;QACtB,WAAW,EAAE,OAAO,CAAA;KACrB,CAAA;IACD,KAAK,EAAE;QACL,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,SAAS,GAAG,WAAW,CAAA;QAC/C,YAAY,EAAE,QAAQ,CAAA;QACtB,WAAW,EAAE,OAAO,CAAA;KACrB,CAAA;IACD,WAAW,EAAE;QACX,IAAI,EAAE,UAAU,CAAA;QAChB,IAAI,EAAE,QAAQ,CAAA;QACd,aAAa,EAAE,MAAM,CAAA;QACrB,QAAQ,EAAE,CAAC,CAAC,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;QAC1B,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;QAC3B,YAAY,EAAE,QAAQ,CAAA;QACtB,WAAW,EAAE,cAAc,CAAA;KAC5B,CAAA;CACF;AAKD,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAyC5F;AAED,eAAO,MAAM,aAAa;;;;CAA4B,CAAA"}
@@ -0,0 +1,59 @@
1
+ export function init(opts = {}) {
2
+ return { checked: opts.checked ?? false, disabled: opts.disabled ?? false };
3
+ }
4
+ export function update(state, msg) {
5
+ switch (msg.type) {
6
+ case 'toggle':
7
+ if (state.disabled)
8
+ return [state, []];
9
+ return [{ ...state, checked: !state.checked }, []];
10
+ case 'setChecked':
11
+ return [{ ...state, checked: msg.checked }, []];
12
+ case 'setDisabled':
13
+ return [{ ...state, disabled: msg.disabled }, []];
14
+ }
15
+ }
16
+ const HIDDEN_STYLE = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;';
17
+ export function connect(get, send) {
18
+ return {
19
+ root: {
20
+ role: 'switch',
21
+ 'aria-checked': (s) => get(s).checked,
22
+ 'aria-disabled': (s) => (get(s).disabled ? 'true' : undefined),
23
+ 'data-state': (s) => (get(s).checked ? 'checked' : 'unchecked'),
24
+ 'data-disabled': (s) => (get(s).disabled ? '' : undefined),
25
+ 'data-scope': 'switch',
26
+ 'data-part': 'root',
27
+ tabIndex: (s) => (get(s).disabled ? -1 : 0),
28
+ onClick: () => send({ type: 'toggle' }),
29
+ onKeyDown: (e) => {
30
+ if (e.key === ' ' || e.key === 'Enter') {
31
+ e.preventDefault();
32
+ send({ type: 'toggle' });
33
+ }
34
+ },
35
+ },
36
+ track: {
37
+ 'data-state': (s) => (get(s).checked ? 'checked' : 'unchecked'),
38
+ 'data-scope': 'switch',
39
+ 'data-part': 'track',
40
+ },
41
+ thumb: {
42
+ 'data-state': (s) => (get(s).checked ? 'checked' : 'unchecked'),
43
+ 'data-scope': 'switch',
44
+ 'data-part': 'thumb',
45
+ },
46
+ hiddenInput: {
47
+ type: 'checkbox',
48
+ role: 'switch',
49
+ 'aria-hidden': 'true',
50
+ tabIndex: -1,
51
+ style: HIDDEN_STYLE,
52
+ checked: (s) => get(s).checked,
53
+ disabled: (s) => get(s).disabled,
54
+ 'data-scope': 'switch',
55
+ 'data-part': 'hidden-input',
56
+ },
57
+ };
58
+ }
59
+ export const switchMachine = { init, update, connect };
@@ -0,0 +1,146 @@
1
+ import type { Send } from '@llui/dom';
2
+ /**
3
+ * Tabs — tabbed interface with keyboard navigation. Each tab has a value
4
+ * (string) that identifies both the trigger and the associated panel.
5
+ *
6
+ * Two activation modes:
7
+ * - `'automatic'` (default): focusing a trigger also activates it.
8
+ * - `'manual'`: arrow keys move focus without activating; Enter/Space activates.
9
+ */
10
+ export type Orientation = 'horizontal' | 'vertical';
11
+ export type Activation = 'automatic' | 'manual';
12
+ export interface TabsState {
13
+ value: string;
14
+ items: string[];
15
+ disabledItems: string[];
16
+ orientation: Orientation;
17
+ activation: Activation;
18
+ /** The currently focused (but not necessarily active) tab. For manual mode. */
19
+ focused: string | null;
20
+ /** Whether Arrow navigation wraps at the ends of the tab list. Default: true. */
21
+ loopFocus: boolean;
22
+ /** Whether clicking the active tab deselects it (empty value). Default: false. */
23
+ deselectable: boolean;
24
+ }
25
+ export type TabsMsg = {
26
+ type: 'setValue';
27
+ value: string;
28
+ } | {
29
+ type: 'setItems';
30
+ items: string[];
31
+ disabled?: string[];
32
+ } | {
33
+ type: 'focusTab';
34
+ value: string;
35
+ } | {
36
+ type: 'focusNext';
37
+ from: string;
38
+ } | {
39
+ type: 'focusPrev';
40
+ from: string;
41
+ } | {
42
+ type: 'focusFirst';
43
+ } | {
44
+ type: 'focusLast';
45
+ } | {
46
+ type: 'activateFocused';
47
+ };
48
+ export interface TabsInit {
49
+ value?: string;
50
+ items?: string[];
51
+ disabledItems?: string[];
52
+ orientation?: Orientation;
53
+ activation?: Activation;
54
+ loopFocus?: boolean;
55
+ deselectable?: boolean;
56
+ }
57
+ export declare function init(opts?: TabsInit): TabsState;
58
+ export declare function update(state: TabsState, msg: TabsMsg): [TabsState, never[]];
59
+ export interface TabsItemParts<S> {
60
+ trigger: {
61
+ type: 'button';
62
+ role: 'tab';
63
+ 'aria-selected': (s: S) => boolean;
64
+ 'aria-controls': string;
65
+ 'aria-disabled': (s: S) => 'true' | undefined;
66
+ id: string;
67
+ 'data-state': (s: S) => 'active' | 'inactive';
68
+ 'data-disabled': (s: S) => '' | undefined;
69
+ 'data-scope': 'tabs';
70
+ 'data-part': 'trigger';
71
+ 'data-value': string;
72
+ tabIndex: (s: S) => number;
73
+ onClick: (e: MouseEvent) => void;
74
+ onKeyDown: (e: KeyboardEvent) => void;
75
+ onFocus: (e: FocusEvent) => void;
76
+ };
77
+ panel: {
78
+ role: 'tabpanel';
79
+ id: string;
80
+ 'aria-labelledby': string;
81
+ tabIndex: 0;
82
+ hidden: (s: S) => boolean;
83
+ 'data-state': (s: S) => 'active' | 'inactive';
84
+ 'data-scope': 'tabs';
85
+ 'data-part': 'panel';
86
+ 'data-value': string;
87
+ };
88
+ }
89
+ export interface TabsParts<S> {
90
+ root: {
91
+ 'data-scope': 'tabs';
92
+ 'data-part': 'root';
93
+ 'data-orientation': (s: S) => Orientation;
94
+ };
95
+ /**
96
+ * A movable underline/highlight element. Position tracks the active
97
+ * trigger via CSS custom properties written by `watchTabIndicator()`:
98
+ * `--indicator-left`, `--indicator-top`, `--indicator-width`,
99
+ * `--indicator-height` — all in pixels.
100
+ * The consumer styles the indicator using these properties (e.g.
101
+ * `transform: translateX(var(--indicator-left))`).
102
+ */
103
+ indicator: {
104
+ 'data-scope': 'tabs';
105
+ 'data-part': 'indicator';
106
+ 'data-orientation': (s: S) => Orientation;
107
+ };
108
+ list: {
109
+ role: 'tablist';
110
+ 'aria-orientation': (s: S) => Orientation;
111
+ 'data-scope': 'tabs';
112
+ 'data-part': 'list';
113
+ };
114
+ item: (value: string) => TabsItemParts<S>;
115
+ }
116
+ export interface ConnectOptions {
117
+ id: string;
118
+ /**
119
+ * Called whenever a tab is clicked/activated. Useful for anchor-style
120
+ * navigation where the tab's value is a URL path and you want to push
121
+ * to the history or router.
122
+ */
123
+ onNavigate?: (value: string) => void;
124
+ }
125
+ export declare function connect<S>(get: (s: S) => TabsState, send: Send<TabsMsg>, opts: ConnectOptions): TabsParts<S>;
126
+ /**
127
+ * Track the active tab trigger and update CSS custom properties on the
128
+ * indicator element so it can be animated into position. Call from
129
+ * `onMount` with the tabs root element; the returned function removes
130
+ * the observers.
131
+ *
132
+ * Sets `--indicator-left`, `--indicator-top`, `--indicator-width`,
133
+ * `--indicator-height` on the indicator element every time the active
134
+ * trigger changes or the list resizes. Style the indicator with:
135
+ * transform: translate(var(--indicator-left), var(--indicator-top));
136
+ * width: var(--indicator-width);
137
+ * height: var(--indicator-height);
138
+ */
139
+ export declare function watchTabIndicator(root: HTMLElement): () => void;
140
+ export declare const tabs: {
141
+ init: typeof init;
142
+ update: typeof update;
143
+ connect: typeof connect;
144
+ watchTabIndicator: typeof watchTabIndicator;
145
+ };
146
+ //# sourceMappingURL=tabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../src/components/tabs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,244 @@
1
+ export function init(opts = {}) {
2
+ const items = opts.items ?? [];
3
+ return {
4
+ value: opts.value ?? items[0] ?? '',
5
+ items,
6
+ disabledItems: opts.disabledItems ?? [],
7
+ orientation: opts.orientation ?? 'horizontal',
8
+ activation: opts.activation ?? 'automatic',
9
+ focused: null,
10
+ loopFocus: opts.loopFocus ?? true,
11
+ deselectable: opts.deselectable ?? false,
12
+ };
13
+ }
14
+ function firstEnabled(items, disabled) {
15
+ for (const v of items)
16
+ if (!disabled.includes(v))
17
+ return v;
18
+ return null;
19
+ }
20
+ function lastEnabled(items, disabled) {
21
+ for (let i = items.length - 1; i >= 0; i--) {
22
+ const v = items[i];
23
+ if (!disabled.includes(v))
24
+ return v;
25
+ }
26
+ return null;
27
+ }
28
+ function nextEnabled(items, disabled, from, delta, loop) {
29
+ if (items.length === 0)
30
+ return null;
31
+ const idx = items.indexOf(from);
32
+ if (idx === -1)
33
+ return firstEnabled(items, disabled);
34
+ const n = items.length;
35
+ for (let i = 1; i <= n; i++) {
36
+ const rawIdx = idx + delta * i;
37
+ if (!loop && (rawIdx < 0 || rawIdx >= n))
38
+ return null;
39
+ const next = items[(rawIdx + n * n) % n];
40
+ if (!disabled.includes(next))
41
+ return next;
42
+ }
43
+ return null;
44
+ }
45
+ export function update(state, msg) {
46
+ switch (msg.type) {
47
+ case 'setValue':
48
+ if (state.disabledItems.includes(msg.value))
49
+ return [state, []];
50
+ return [{ ...state, value: msg.value }, []];
51
+ case 'setItems': {
52
+ const disabled = msg.disabled ?? state.disabledItems;
53
+ // Ensure value still points to an existing enabled item
54
+ let value = state.value;
55
+ if (!msg.items.includes(value) || disabled.includes(value)) {
56
+ value = firstEnabled(msg.items, disabled) ?? '';
57
+ }
58
+ return [{ ...state, items: msg.items, disabledItems: disabled, value }, []];
59
+ }
60
+ case 'focusTab': {
61
+ if (state.disabledItems.includes(msg.value))
62
+ return [state, []];
63
+ const next = { ...state, focused: msg.value };
64
+ if (state.activation === 'automatic') {
65
+ // Deselectable: clicking the already-active tab clears the value.
66
+ next.value = state.deselectable && state.value === msg.value ? '' : msg.value;
67
+ }
68
+ return [next, []];
69
+ }
70
+ case 'focusNext': {
71
+ const to = nextEnabled(state.items, state.disabledItems, msg.from, 1, state.loopFocus);
72
+ if (to === null)
73
+ return [state, []];
74
+ const next = { ...state, focused: to };
75
+ if (state.activation === 'automatic')
76
+ next.value = to;
77
+ return [next, []];
78
+ }
79
+ case 'focusPrev': {
80
+ const to = nextEnabled(state.items, state.disabledItems, msg.from, -1, state.loopFocus);
81
+ if (to === null)
82
+ return [state, []];
83
+ const next = { ...state, focused: to };
84
+ if (state.activation === 'automatic')
85
+ next.value = to;
86
+ return [next, []];
87
+ }
88
+ case 'focusFirst': {
89
+ const to = firstEnabled(state.items, state.disabledItems);
90
+ if (to === null)
91
+ return [state, []];
92
+ const next = { ...state, focused: to };
93
+ if (state.activation === 'automatic')
94
+ next.value = to;
95
+ return [next, []];
96
+ }
97
+ case 'focusLast': {
98
+ const to = lastEnabled(state.items, state.disabledItems);
99
+ if (to === null)
100
+ return [state, []];
101
+ const next = { ...state, focused: to };
102
+ if (state.activation === 'automatic')
103
+ next.value = to;
104
+ return [next, []];
105
+ }
106
+ case 'activateFocused': {
107
+ if (state.focused === null)
108
+ return [state, []];
109
+ return [{ ...state, value: state.focused }, []];
110
+ }
111
+ }
112
+ }
113
+ export function connect(get, send, opts) {
114
+ const base = opts.id;
115
+ const triggerId = (v) => `${base}:trigger:${v}`;
116
+ const panelId = (v) => `${base}:panel:${v}`;
117
+ return {
118
+ root: {
119
+ 'data-scope': 'tabs',
120
+ 'data-part': 'root',
121
+ 'data-orientation': (s) => get(s).orientation,
122
+ },
123
+ list: {
124
+ role: 'tablist',
125
+ 'aria-orientation': (s) => get(s).orientation,
126
+ 'data-scope': 'tabs',
127
+ 'data-part': 'list',
128
+ },
129
+ indicator: {
130
+ 'data-scope': 'tabs',
131
+ 'data-part': 'indicator',
132
+ 'data-orientation': (s) => get(s).orientation,
133
+ },
134
+ item: (value) => ({
135
+ trigger: {
136
+ type: 'button',
137
+ role: 'tab',
138
+ 'aria-selected': (s) => get(s).value === value,
139
+ 'aria-controls': panelId(value),
140
+ 'aria-disabled': (s) => (get(s).disabledItems.includes(value) ? 'true' : undefined),
141
+ id: triggerId(value),
142
+ 'data-state': (s) => (get(s).value === value ? 'active' : 'inactive'),
143
+ 'data-disabled': (s) => (get(s).disabledItems.includes(value) ? '' : undefined),
144
+ 'data-scope': 'tabs',
145
+ 'data-part': 'trigger',
146
+ 'data-value': value,
147
+ tabIndex: (s) => (get(s).value === value ? 0 : -1),
148
+ onClick: () => {
149
+ send({ type: 'focusTab', value });
150
+ opts.onNavigate?.(value);
151
+ },
152
+ onFocus: () => {
153
+ // `focusTab` handles automatic activation
154
+ send({ type: 'focusTab', value });
155
+ },
156
+ onKeyDown: (e) => {
157
+ // Read orientation from the ancestor [data-part="list"] so the
158
+ // handler can dispatch the correct arrow keys per WAI-ARIA.
159
+ // Horizontal tabs: ArrowLeft/Right navigate; vertical: Up/Down.
160
+ const target = e.currentTarget;
161
+ const list = target?.closest('[data-scope="tabs"][data-part="list"]');
162
+ const orientation = list?.getAttribute('aria-orientation') ?? 'horizontal';
163
+ const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
164
+ const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
165
+ switch (e.key) {
166
+ case nextKey:
167
+ e.preventDefault();
168
+ send({ type: 'focusNext', from: value });
169
+ return;
170
+ case prevKey:
171
+ e.preventDefault();
172
+ send({ type: 'focusPrev', from: value });
173
+ return;
174
+ case 'Home':
175
+ e.preventDefault();
176
+ send({ type: 'focusFirst' });
177
+ return;
178
+ case 'End':
179
+ e.preventDefault();
180
+ send({ type: 'focusLast' });
181
+ return;
182
+ case 'Enter':
183
+ case ' ':
184
+ e.preventDefault();
185
+ send({ type: 'activateFocused' });
186
+ return;
187
+ }
188
+ },
189
+ },
190
+ panel: {
191
+ role: 'tabpanel',
192
+ id: panelId(value),
193
+ 'aria-labelledby': triggerId(value),
194
+ tabIndex: 0,
195
+ hidden: (s) => get(s).value !== value,
196
+ 'data-state': (s) => (get(s).value === value ? 'active' : 'inactive'),
197
+ 'data-scope': 'tabs',
198
+ 'data-part': 'panel',
199
+ 'data-value': value,
200
+ },
201
+ }),
202
+ };
203
+ }
204
+ /**
205
+ * Track the active tab trigger and update CSS custom properties on the
206
+ * indicator element so it can be animated into position. Call from
207
+ * `onMount` with the tabs root element; the returned function removes
208
+ * the observers.
209
+ *
210
+ * Sets `--indicator-left`, `--indicator-top`, `--indicator-width`,
211
+ * `--indicator-height` on the indicator element every time the active
212
+ * trigger changes or the list resizes. Style the indicator with:
213
+ * transform: translate(var(--indicator-left), var(--indicator-top));
214
+ * width: var(--indicator-width);
215
+ * height: var(--indicator-height);
216
+ */
217
+ export function watchTabIndicator(root) {
218
+ const indicator = root.querySelector('[data-scope="tabs"][data-part="indicator"]');
219
+ const list = root.querySelector('[data-scope="tabs"][data-part="list"]');
220
+ if (!indicator || !list)
221
+ return () => { };
222
+ const sync = () => {
223
+ const active = list.querySelector('[data-scope="tabs"][data-part="trigger"][data-state="active"]');
224
+ if (!active)
225
+ return;
226
+ indicator.style.setProperty('--indicator-left', `${active.offsetLeft}px`);
227
+ indicator.style.setProperty('--indicator-top', `${active.offsetTop}px`);
228
+ indicator.style.setProperty('--indicator-width', `${active.offsetWidth}px`);
229
+ indicator.style.setProperty('--indicator-height', `${active.offsetHeight}px`);
230
+ };
231
+ sync();
232
+ const mo = new MutationObserver(sync);
233
+ mo.observe(list, { attributes: true, attributeFilter: ['data-state'], subtree: true });
234
+ // ResizeObserver may be absent in older environments or jsdom — skip
235
+ // gracefully; layout changes that don't involve a data-state flip just
236
+ // won't reposition the indicator until the next attribute change.
237
+ const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(sync) : null;
238
+ ro?.observe(list);
239
+ return () => {
240
+ mo.disconnect();
241
+ ro?.disconnect();
242
+ };
243
+ }
244
+ export const tabs = { init, update, connect, watchTabIndicator };