@pienter/ui 0.2.0 → 0.5.0

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 (89) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/CONVENTIONS.md +297 -385
  3. package/README.md +3 -18
  4. package/components/feedback/toast/toast.css +1 -1
  5. package/components/form/combobox/Combobox.vue +21 -38
  6. package/components/form/combobox/combobox.css +1 -1
  7. package/components/form/form/Form.vue +1 -2
  8. package/components/form/label/label.css +1 -1
  9. package/components/form/number-field/NumberField.vue +1 -1
  10. package/components/form/number-field/number-field.css +1 -1
  11. package/components/form/radio-group/RadioGroup.vue +2 -5
  12. package/components/form/slider/slider.css +2 -3
  13. package/components/form/tags-input/tags-input.css +1 -2
  14. package/components/form/textarea/textarea.css +1 -1
  15. package/components/layout/accordion/Accordion.vue +6 -27
  16. package/components/layout/collapsible/Collapsible.vue +9 -19
  17. package/components/layout/table/table.css +3 -3
  18. package/components/navigation/pagination/Pagination.vue +1 -1
  19. package/components/navigation/pagination/PaginationFooter.vue +23 -0
  20. package/components/navigation/pagination/pagination.css +128 -29
  21. package/components/navigation/sidebar/Sidebar.vue +326 -84
  22. package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
  23. package/components/navigation/sidebar/context.ts +21 -0
  24. package/components/navigation/sidebar/sidebar.css +346 -78
  25. package/components/navigation/sidebar/types.ts +13 -1
  26. package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
  27. package/components/overlay/command/Command.vue +10 -38
  28. package/components/overlay/command/command.css +1 -1
  29. package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
  30. package/components/overlay/modal/Modal.vue +7 -28
  31. package/components/overlay/popover/Popover.vue +9 -31
  32. package/components/overlay/sheet/Sheet.vue +7 -28
  33. package/components/overlay/tooltip/Tooltip.vue +14 -19
  34. package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
  35. package/composables/useDisclosure.ts +113 -0
  36. package/composables/useEventListener.ts +16 -0
  37. package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
  38. package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
  39. package/package.json +6 -38
  40. package/styles/0-settings/colors.css +8 -1
  41. package/styles/0-settings/layout.css +18 -0
  42. package/styles/0-settings/motion.css +2 -2
  43. package/styles/0-settings/spacing.css +3 -1
  44. package/utils/navigation/sidebar.ts +97 -0
  45. package/utils/validation/form.ts +6 -9
  46. package/components/action/button/Button.astro +0 -95
  47. package/components/action/button/IconButton.astro +0 -86
  48. package/components/display/avatar/Avatar.astro +0 -17
  49. package/components/display/avatar/AvatarStack.astro +0 -9
  50. package/components/display/badge/Badge.astro +0 -15
  51. package/components/display/empty/Empty.astro +0 -9
  52. package/components/display/icon/Icon.astro +0 -52
  53. package/components/feedback/alert/Alert.astro +0 -52
  54. package/components/feedback/progress/Progress.astro +0 -68
  55. package/components/feedback/skeleton/Skeleton.astro +0 -32
  56. package/components/feedback/spinner/Spinner.astro +0 -25
  57. package/components/feedback/toast/Toast.astro +0 -50
  58. package/components/form/checkbox/Checkbox.astro +0 -79
  59. package/components/form/date-input/DateInput.astro +0 -105
  60. package/components/form/form/Form.astro +0 -106
  61. package/components/form/input-otp/InputOTP.astro +0 -147
  62. package/components/form/label/Label.astro +0 -13
  63. package/components/form/number-field/NumberField.astro +0 -142
  64. package/components/form/radio-group/RadioGroup.astro +0 -105
  65. package/components/form/select/Select.astro +0 -105
  66. package/components/form/slider/Slider.astro +0 -205
  67. package/components/form/switch/Switch.astro +0 -75
  68. package/components/form/tags-input/TagsInput.astro +0 -153
  69. package/components/form/text-input/TextInput.astro +0 -84
  70. package/components/form/textarea/Textarea.astro +0 -86
  71. package/components/layout/card/Card.astro +0 -13
  72. package/components/layout/separator/Separator.astro +0 -31
  73. package/components/layout/table/Table.astro +0 -116
  74. package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
  75. package/components/navigation/navbar/Navbar.astro +0 -62
  76. package/components/navigation/sidebar/Sidebar.astro +0 -132
  77. package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
  78. package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
  79. package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
  80. package/components/overlay/modal/Modal.astro +0 -66
  81. package/utils/controllers/disclosure.ts +0 -117
  82. package/utils/controllers/form.ts +0 -524
  83. package/utils/controllers/index.ts +0 -39
  84. package/utils/controllers/number-field.ts +0 -103
  85. package/utils/controllers/otp.ts +0 -252
  86. package/utils/controllers/sidebar.ts +0 -610
  87. package/utils/controllers/slider.ts +0 -336
  88. package/utils/controllers/tags-input.ts +0 -255
  89. package/utils/controllers/toast.ts +0 -426
@@ -1,252 +0,0 @@
1
- /**
2
- * Configuration for `mountInputOTP`. The controller wires N
3
- * `<input class="v-input-otp__slot">` single-character inputs inside
4
- * an InputOTP wrapper for the canonical one-time-code UX:
5
- * - Type a character matching `pattern` → fill, advance focus.
6
- * - Backspace on a filled slot → clear it (focus stays).
7
- * - Backspace on an empty slot → focus + clear the previous slot.
8
- * - Arrow Left / Right → move focus between slots.
9
- * - Paste a multi-character string → distribute across slots starting
10
- * from the focused position; characters that fail the pattern are
11
- * silently skipped.
12
- *
13
- * `onChange` receives the concatenated OTP string after every change.
14
- * Empty slots are represented as the empty string in the concatenation
15
- * (so a partial code like `"12 "` reflects two filled slots and two
16
- * empty), keeping the contract simple for consumers building reactive
17
- * UIs that need to know when the code is complete (`value.length ===
18
- * length` AND `!value.includes(' ')` — the controller emits a
19
- * literal-empty for unfilled slots, NOT a space; see implementation).
20
- */
21
- export interface InputOTPConfig {
22
- /**
23
- * Optional regex used to validate each typed / pasted character.
24
- * Defaults to digits-only (`/^[0-9]$/`). Pass an alphanumeric or
25
- * uppercase-letter pattern when the OTP code is not a PIN.
26
- */
27
- pattern?: RegExp;
28
- /**
29
- * Callback fired on every value change with the concatenated OTP
30
- * string. Length always equals the slot count; unfilled slots are
31
- * the empty character so consumers can distinguish "fully entered"
32
- * from "partial".
33
- */
34
- onChange?: (value: string) => void;
35
- }
36
-
37
- /**
38
- * Mount the InputOTP controller on `wrapperEl`, which MUST be the
39
- * `<div class="v-input-otp">` wrapper containing N
40
- * `<input class="v-input-otp__slot">` children. The wrapper is the
41
- * only DOM the controller modifies (slot focus / value); the surrounding
42
- * `.v-field` scaffold (label, hint, errors) is owned by the framework
43
- * component.
44
- *
45
- * Lifecycle (non-stateful per CONVENTIONS — teardown-only signature):
46
- * 1. On mount: locate all slot inputs via `.v-input-otp__slot`.
47
- * 2. Wire `input`, `keydown`, `paste`, and `focus` listeners on each
48
- * slot. Behaviours:
49
- * - **input**: validate against `pattern`. Reject (clear the slot)
50
- * if invalid; advance focus to the next empty slot if valid and
51
- * not the last.
52
- * - **keydown** Backspace: if the current slot is empty, focus +
53
- * clear the previous; if filled, clear it (stay).
54
- * - **keydown** ArrowLeft / ArrowRight: navigate between slots.
55
- * - **paste**: distribute pasted characters across slots starting
56
- * from the focused index. Pattern-failing characters are skipped
57
- * so a paste of "123-456" with a digit pattern yields "123456".
58
- * Focus lands on the slot after the last filled one (or the last
59
- * slot if all were filled).
60
- * 3. Every value change dispatches `input` and `change` events
61
- * (bubbling) on the wrapper element, plus calls `onChange` with
62
- * the joined OTP string. The hidden `<input name>` paired by the
63
- * framework component receives the same value via `dispatchEvent`
64
- * on its element when the wrapper emits.
65
- * 4. `teardown()`: removes all listeners. The slot DOM is left in
66
- * place; the framework owns markup cleanup.
67
- *
68
- * Mounting on a wrapper without slot inputs is a graceful no-op.
69
- *
70
- * Per CONVENTIONS, the InputOTP wrapper does NOT use `aria-hidden`
71
- * or `role="group"` on the slot row — each slot is a native input
72
- * with its own `aria-label="Digit N of M"`. The form-primitive
73
- * `.v-field` label still applies to the group as a whole; per-slot
74
- * labels are screen-reader specific.
75
- */
76
- export function mountInputOTP(
77
- wrapperEl: HTMLElement,
78
- config: InputOTPConfig = {},
79
- ): () => void {
80
- const slots = Array.from(
81
- wrapperEl.querySelectorAll<HTMLInputElement>('input.v-input-otp__slot'),
82
- );
83
- if (slots.length === 0) {
84
- // Graceful no-op: wrapper is missing slot inputs.
85
- return () => {};
86
- }
87
-
88
- const pattern = config.pattern ?? /^[0-9]$/;
89
- const onChange = config.onChange;
90
-
91
- // The hidden input the framework component renders for FormData
92
- // serialization (named via `:name`). Optional — if absent, the
93
- // controller still works for client-only state.
94
- const hiddenInput = wrapperEl.querySelector<HTMLInputElement>(
95
- 'input.v-input-otp__hidden',
96
- );
97
-
98
- function readValue(): string {
99
- return slots
100
- .map((s) => (s.value.length > 0 ? s.value[0] : ''))
101
- .join('');
102
- }
103
-
104
- function notifyChange(): void {
105
- const value = readValue();
106
- if (hiddenInput) {
107
- hiddenInput.value = value;
108
- hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
109
- hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
110
- }
111
- onChange?.(value);
112
- }
113
-
114
- function focusSlot(index: number): void {
115
- const target = slots[index];
116
- if (target) {
117
- target.focus();
118
- // Select the slot's content so a typed digit overwrites cleanly.
119
- target.select();
120
- }
121
- }
122
-
123
- function findThisIndex(target: EventTarget | null): number {
124
- if (!(target instanceof HTMLInputElement)) return -1;
125
- return slots.indexOf(target);
126
- }
127
-
128
- function handleInput(event: Event): void {
129
- const idx = findThisIndex(event.target);
130
- if (idx === -1) return;
131
- const slot = slots[idx];
132
- if (!slot) return;
133
- const value = slot.value;
134
-
135
- // Native paste / IME may deliver multi-character input — defer to
136
- // the paste handler for that case; here, normalize single-char.
137
- if (value.length === 0) {
138
- notifyChange();
139
- return;
140
- }
141
-
142
- if (value.length > 1) {
143
- // Multi-char input (paste fallback path on browsers that fire
144
- // `input` instead of `paste`): keep only the last character if
145
- // it matches the pattern; the explicit `paste` handler covers
146
- // the typical Cmd+V flow.
147
- const last = value[value.length - 1] ?? '';
148
- slot.value = pattern.test(last) ? last : '';
149
- } else if (!pattern.test(value)) {
150
- // Reject invalid character.
151
- slot.value = '';
152
- notifyChange();
153
- return;
154
- }
155
-
156
- notifyChange();
157
-
158
- // Advance focus to the next empty slot (or the next slot if all
159
- // remaining are filled — matches the Reka UI / shadcn UX).
160
- if (slot.value.length === 1 && idx < slots.length - 1) {
161
- focusSlot(idx + 1);
162
- }
163
- }
164
-
165
- function handleKeydown(event: KeyboardEvent): void {
166
- const idx = findThisIndex(event.target);
167
- if (idx === -1) return;
168
- const slot = slots[idx];
169
- if (!slot) return;
170
-
171
- if (event.key === 'Backspace') {
172
- if (slot.value.length === 0 && idx > 0) {
173
- event.preventDefault();
174
- const prev = slots[idx - 1];
175
- if (prev) {
176
- prev.value = '';
177
- notifyChange();
178
- focusSlot(idx - 1);
179
- }
180
- } else if (slot.value.length > 0) {
181
- // Clear current slot but stay on it — the next Backspace then
182
- // moves to the previous (canonical OTP UX).
183
- event.preventDefault();
184
- slot.value = '';
185
- notifyChange();
186
- }
187
- return;
188
- }
189
-
190
- if (event.key === 'ArrowLeft' && idx > 0) {
191
- event.preventDefault();
192
- focusSlot(idx - 1);
193
- return;
194
- }
195
-
196
- if (event.key === 'ArrowRight' && idx < slots.length - 1) {
197
- event.preventDefault();
198
- focusSlot(idx + 1);
199
- return;
200
- }
201
- }
202
-
203
- function handlePaste(event: ClipboardEvent): void {
204
- const idx = findThisIndex(event.target);
205
- if (idx === -1) return;
206
- const data = event.clipboardData?.getData('text') ?? '';
207
- if (!data) return;
208
- event.preventDefault();
209
-
210
- // Filter to pattern-matching characters; distribute starting at
211
- // the focused slot.
212
- const valid = Array.from(data).filter((ch) => pattern.test(ch));
213
- if (valid.length === 0) return;
214
-
215
- let cursor = idx;
216
- for (const ch of valid) {
217
- if (cursor >= slots.length) break;
218
- const slot = slots[cursor];
219
- if (slot) slot.value = ch;
220
- cursor += 1;
221
- }
222
- notifyChange();
223
-
224
- // Land focus on the slot after the last filled one (or the last
225
- // slot if the paste filled to the end).
226
- const landingIdx = Math.min(cursor, slots.length - 1);
227
- focusSlot(landingIdx);
228
- }
229
-
230
- function handleFocus(event: FocusEvent): void {
231
- if (!(event.target instanceof HTMLInputElement)) return;
232
- // Select existing content so a typed digit overwrites it cleanly,
233
- // matching Reka UI's "click any slot to retype" UX.
234
- event.target.select();
235
- }
236
-
237
- slots.forEach((slot) => {
238
- slot.addEventListener('input', handleInput);
239
- slot.addEventListener('keydown', handleKeydown);
240
- slot.addEventListener('paste', handlePaste);
241
- slot.addEventListener('focus', handleFocus);
242
- });
243
-
244
- return function teardown(): void {
245
- slots.forEach((slot) => {
246
- slot.removeEventListener('input', handleInput);
247
- slot.removeEventListener('keydown', handleKeydown);
248
- slot.removeEventListener('paste', handlePaste);
249
- slot.removeEventListener('focus', handleFocus);
250
- });
251
- };
252
- }