@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.
- package/CHANGELOG.md +259 -0
- package/CONVENTIONS.md +297 -385
- package/README.md +3 -18
- package/components/feedback/toast/toast.css +1 -1
- package/components/form/combobox/Combobox.vue +21 -38
- package/components/form/combobox/combobox.css +1 -1
- package/components/form/form/Form.vue +1 -2
- package/components/form/label/label.css +1 -1
- package/components/form/number-field/NumberField.vue +1 -1
- package/components/form/number-field/number-field.css +1 -1
- package/components/form/radio-group/RadioGroup.vue +2 -5
- package/components/form/slider/slider.css +2 -3
- package/components/form/tags-input/tags-input.css +1 -2
- package/components/form/textarea/textarea.css +1 -1
- package/components/layout/accordion/Accordion.vue +6 -27
- package/components/layout/collapsible/Collapsible.vue +9 -19
- package/components/layout/table/table.css +3 -3
- package/components/navigation/pagination/Pagination.vue +1 -1
- package/components/navigation/pagination/PaginationFooter.vue +23 -0
- package/components/navigation/pagination/pagination.css +128 -29
- package/components/navigation/sidebar/Sidebar.vue +326 -84
- package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
- package/components/navigation/sidebar/context.ts +21 -0
- package/components/navigation/sidebar/sidebar.css +346 -78
- package/components/navigation/sidebar/types.ts +13 -1
- package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
- package/components/overlay/command/Command.vue +10 -38
- package/components/overlay/command/command.css +1 -1
- package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
- package/components/overlay/modal/Modal.vue +7 -28
- package/components/overlay/popover/Popover.vue +9 -31
- package/components/overlay/sheet/Sheet.vue +7 -28
- package/components/overlay/tooltip/Tooltip.vue +14 -19
- package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
- package/composables/useDisclosure.ts +113 -0
- package/composables/useEventListener.ts +16 -0
- package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
- package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
- package/package.json +6 -38
- package/styles/0-settings/colors.css +8 -1
- package/styles/0-settings/layout.css +18 -0
- package/styles/0-settings/motion.css +2 -2
- package/styles/0-settings/spacing.css +3 -1
- package/utils/navigation/sidebar.ts +97 -0
- package/utils/validation/form.ts +6 -9
- package/components/action/button/Button.astro +0 -95
- package/components/action/button/IconButton.astro +0 -86
- package/components/display/avatar/Avatar.astro +0 -17
- package/components/display/avatar/AvatarStack.astro +0 -9
- package/components/display/badge/Badge.astro +0 -15
- package/components/display/empty/Empty.astro +0 -9
- package/components/display/icon/Icon.astro +0 -52
- package/components/feedback/alert/Alert.astro +0 -52
- package/components/feedback/progress/Progress.astro +0 -68
- package/components/feedback/skeleton/Skeleton.astro +0 -32
- package/components/feedback/spinner/Spinner.astro +0 -25
- package/components/feedback/toast/Toast.astro +0 -50
- package/components/form/checkbox/Checkbox.astro +0 -79
- package/components/form/date-input/DateInput.astro +0 -105
- package/components/form/form/Form.astro +0 -106
- package/components/form/input-otp/InputOTP.astro +0 -147
- package/components/form/label/Label.astro +0 -13
- package/components/form/number-field/NumberField.astro +0 -142
- package/components/form/radio-group/RadioGroup.astro +0 -105
- package/components/form/select/Select.astro +0 -105
- package/components/form/slider/Slider.astro +0 -205
- package/components/form/switch/Switch.astro +0 -75
- package/components/form/tags-input/TagsInput.astro +0 -153
- package/components/form/text-input/TextInput.astro +0 -84
- package/components/form/textarea/Textarea.astro +0 -86
- package/components/layout/card/Card.astro +0 -13
- package/components/layout/separator/Separator.astro +0 -31
- package/components/layout/table/Table.astro +0 -116
- package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
- package/components/navigation/navbar/Navbar.astro +0 -62
- package/components/navigation/sidebar/Sidebar.astro +0 -132
- package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
- package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
- package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
- package/components/overlay/modal/Modal.astro +0 -66
- package/utils/controllers/disclosure.ts +0 -117
- package/utils/controllers/form.ts +0 -524
- package/utils/controllers/index.ts +0 -39
- package/utils/controllers/number-field.ts +0 -103
- package/utils/controllers/otp.ts +0 -252
- package/utils/controllers/sidebar.ts +0 -610
- package/utils/controllers/slider.ts +0 -336
- package/utils/controllers/tags-input.ts +0 -255
- package/utils/controllers/toast.ts +0 -426
package/utils/controllers/otp.ts
DELETED
|
@@ -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
|
-
}
|