@pienter/ui 0.3.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 (86) hide show
  1. package/CHANGELOG.md +200 -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/sidebar/Sidebar.vue +326 -84
  19. package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
  20. package/components/navigation/sidebar/context.ts +21 -0
  21. package/components/navigation/sidebar/sidebar.css +346 -78
  22. package/components/navigation/sidebar/types.ts +13 -1
  23. package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
  24. package/components/overlay/command/Command.vue +10 -38
  25. package/components/overlay/command/command.css +1 -1
  26. package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
  27. package/components/overlay/modal/Modal.vue +7 -28
  28. package/components/overlay/popover/Popover.vue +9 -31
  29. package/components/overlay/sheet/Sheet.vue +7 -28
  30. package/components/overlay/tooltip/Tooltip.vue +14 -19
  31. package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
  32. package/composables/useDisclosure.ts +113 -0
  33. package/composables/useEventListener.ts +16 -0
  34. package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
  35. package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
  36. package/package.json +5 -39
  37. package/styles/0-settings/layout.css +18 -0
  38. package/styles/0-settings/motion.css +2 -2
  39. package/styles/0-settings/spacing.css +3 -1
  40. package/utils/navigation/sidebar.ts +97 -0
  41. package/utils/validation/form.ts +6 -9
  42. package/components/action/button/Button.astro +0 -95
  43. package/components/action/button/IconButton.astro +0 -86
  44. package/components/display/avatar/Avatar.astro +0 -17
  45. package/components/display/avatar/AvatarStack.astro +0 -9
  46. package/components/display/badge/Badge.astro +0 -15
  47. package/components/display/empty/Empty.astro +0 -9
  48. package/components/display/icon/Icon.astro +0 -52
  49. package/components/feedback/alert/Alert.astro +0 -52
  50. package/components/feedback/progress/Progress.astro +0 -68
  51. package/components/feedback/skeleton/Skeleton.astro +0 -32
  52. package/components/feedback/spinner/Spinner.astro +0 -25
  53. package/components/feedback/toast/Toast.astro +0 -50
  54. package/components/form/checkbox/Checkbox.astro +0 -79
  55. package/components/form/date-input/DateInput.astro +0 -105
  56. package/components/form/form/Form.astro +0 -106
  57. package/components/form/input-otp/InputOTP.astro +0 -147
  58. package/components/form/label/Label.astro +0 -13
  59. package/components/form/number-field/NumberField.astro +0 -142
  60. package/components/form/radio-group/RadioGroup.astro +0 -105
  61. package/components/form/select/Select.astro +0 -105
  62. package/components/form/slider/Slider.astro +0 -205
  63. package/components/form/switch/Switch.astro +0 -75
  64. package/components/form/tags-input/TagsInput.astro +0 -153
  65. package/components/form/text-input/TextInput.astro +0 -84
  66. package/components/form/textarea/Textarea.astro +0 -86
  67. package/components/layout/card/Card.astro +0 -13
  68. package/components/layout/separator/Separator.astro +0 -31
  69. package/components/layout/table/Table.astro +0 -116
  70. package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
  71. package/components/navigation/navbar/Navbar.astro +0 -62
  72. package/components/navigation/pagination/PaginationFooter.astro +0 -24
  73. package/components/navigation/sidebar/Sidebar.astro +0 -132
  74. package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
  75. package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
  76. package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
  77. package/components/overlay/modal/Modal.astro +0 -66
  78. package/utils/controllers/disclosure.ts +0 -117
  79. package/utils/controllers/form.ts +0 -524
  80. package/utils/controllers/index.ts +0 -39
  81. package/utils/controllers/number-field.ts +0 -103
  82. package/utils/controllers/otp.ts +0 -252
  83. package/utils/controllers/sidebar.ts +0 -610
  84. package/utils/controllers/slider.ts +0 -336
  85. package/utils/controllers/tags-input.ts +0 -255
  86. package/utils/controllers/toast.ts +0 -426
package/README.md CHANGED
@@ -1,20 +1,13 @@
1
1
  # @pienter/ui
2
2
 
3
- Shared Pienter UI package with framework-specific components, CSS, icons, and browser utilities.
3
+ Shared Pienter UI package with Vue components, CSS, icons, and browser utilities.
4
4
 
5
- This package publishes raw source files. Consuming projects compile the Vue, Astro, TypeScript, and CSS files with their own toolchain.
5
+ This package publishes raw source files. Consuming projects compile the Vue, TypeScript, and CSS files with their own toolchain.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```sh
10
- npm install @pienter/ui
11
- ```
12
-
13
- Install the framework peer you use:
14
-
15
- ```sh
16
- npm install vue
17
- npm install astro
10
+ npm install @pienter/ui vue
18
11
  ```
19
12
 
20
13
  ## Usage
@@ -24,14 +17,6 @@ import Button from '@pienter/ui/components/Button.vue';
24
17
  import '@pienter/ui/styles';
25
18
  ```
26
19
 
27
- Astro components are available beside Vue components:
28
-
29
- ```astro
30
- ---
31
- import Button from '@pienter/ui/components/Button.astro';
32
- ---
33
- ```
34
-
35
20
  Components use explicit flat public entrypoints. Intentional shared types are
36
21
  available through module type entrypoints:
37
22
 
@@ -9,7 +9,7 @@
9
9
  display: flex;
10
10
  flex-direction: column;
11
11
  gap: var(--space-2xs);
12
- z-index: 1000;
12
+ z-index: var(--layer-top);
13
13
  pointer-events: none;
14
14
  max-width: calc(100vw - var(--space-s) * 2);
15
15
  }
@@ -85,12 +85,8 @@
85
85
  </template>
86
86
 
87
87
  <script setup lang="ts">
88
- import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue';
89
- import {
90
- mountPopover,
91
- type PopoverControl,
92
- type Placement,
93
- } from '../../../utils/controllers/popover.js';
88
+ import { computed, ref, watch } from 'vue';
89
+ import { usePopover, type Placement } from '../../../composables/usePopover.js';
94
90
  import { generateId } from '../../../utils/a11y/id.js';
95
91
 
96
92
  defineOptions({ inheritAttrs: false });
@@ -218,7 +214,25 @@ const activeIndex = ref<number | null>(null);
218
214
  // label of the currently-selected option.
219
215
  const isTyping = ref(false);
220
216
 
221
- let control: PopoverControl | null = null;
217
+ // Anchor the listbox to the input itself so positioning tracks the
218
+ // input's box (the input is the visual reference, not the wrapper).
219
+ // `modal: false` → `popover="auto"` so the browser handles
220
+ // outside-click / focus-out / Escape light-dismiss. The listbox
221
+ // closes naturally when the user tabs or clicks away.
222
+ const control = usePopover(listboxRef, {
223
+ anchor: inputRef,
224
+ placement: () => props.placement,
225
+ offset: () => props.offset,
226
+ onOpen: () => {
227
+ isOpen.value = true;
228
+ emit('open-change', true);
229
+ },
230
+ onClose: () => {
231
+ isOpen.value = false;
232
+ activeIndex.value = null;
233
+ emit('open-change', false);
234
+ },
235
+ });
222
236
 
223
237
  const hasErrors = computed(() => props.errors.length > 0);
224
238
 
@@ -271,12 +285,10 @@ const activeOptionDomId = computed(() => {
271
285
 
272
286
  function open(): void {
273
287
  if (props.disabled) return;
274
- if (!control) return;
275
288
  control.show();
276
289
  }
277
290
 
278
291
  function close(): void {
279
- if (!control) return;
280
292
  control.hide();
281
293
  }
282
294
 
@@ -463,30 +475,6 @@ function onKeydown(event: KeyboardEvent): void {
463
475
  }
464
476
  }
465
477
 
466
- onMounted(() => {
467
- if (!listboxRef.value || !inputRef.value) return;
468
- control = mountPopover(listboxRef.value, {
469
- // Anchor the listbox to the input itself so positioning
470
- // tracks the input's box (the input is the visual reference,
471
- // not the wrapper).
472
- anchor: inputRef.value,
473
- placement: props.placement,
474
- offset: props.offset,
475
- // `modal: false` → `popover="auto"` so the browser handles
476
- // outside-click / focus-out / Escape light-dismiss. The
477
- // listbox closes naturally when the user tabs or clicks away.
478
- onOpen: () => {
479
- isOpen.value = true;
480
- emit('open-change', true);
481
- },
482
- onClose: () => {
483
- isOpen.value = false;
484
- activeIndex.value = null;
485
- emit('open-change', false);
486
- },
487
- });
488
- });
489
-
490
478
  // Watch model changes from the consumer and reset typing state so
491
479
  // the input mirrors the new selection's label cleanly.
492
480
  watch(
@@ -496,11 +484,6 @@ watch(
496
484
  query.value = '';
497
485
  },
498
486
  );
499
-
500
- onBeforeUnmount(() => {
501
- control?.teardown();
502
- control = null;
503
- });
504
487
  </script>
505
488
 
506
489
  <style>
@@ -8,7 +8,7 @@
8
8
  * disabled state match TextInput / Textarea exactly.
9
9
  *
10
10
  * The listbox is a separate element (a `<ul popover="auto">`) the
11
- * controller positions via `mountPopover`. CSS resets the
11
+ * composable positions via `usePopover`. CSS resets the
12
12
  * user-agent popover defaults (`margin: 0; border: 0; inset: auto`)
13
13
  * and supplies the surface look — the Popover API places the
14
14
  * element in the top layer; inline `left`/`top` from the controller
@@ -131,8 +131,7 @@ function handleSubmit(): void {
131
131
 
132
132
  // Validation passed. Clear any lingering client-side errors from a
133
133
  // prior failed submit so child form-primitives don't keep displaying
134
- // them while the consumer's async submit runs (mirrors the Astro
135
- // controller's `clearAllErrors(el)` between client-pass and onSubmit).
134
+ // them while the consumer's async submit runs.
136
135
  clientFieldErrors.value = {};
137
136
 
138
137
  // Surface FormData to the consumer's submit handler; consumer manages
@@ -5,7 +5,7 @@
5
5
  * use cases where the form-primitive components' built-in label is not
6
6
  * suitable (e.g., labels associated with custom groups or with multiple
7
7
  * controls). Importing this file re-imports the scaffold so a
8
- * standalone Label dragged into an Astro page that does not also use
8
+ * standalone Label on a page that does not also use
9
9
  * TextInput / Textarea / Select still gets the typography rules.
10
10
  */
11
11
  @import '../text-input/text-input.css';
@@ -143,7 +143,7 @@ function handleStep(direction: 'decrement' | 'increment'): void {
143
143
  input.stepDown();
144
144
  }
145
145
  // Dispatch native input/change events so consumer-attached DOM
146
- // listeners observe the same behavior as the Astro controller path.
146
+ // listeners observe stepper clicks the same as typed edits.
147
147
  input.dispatchEvent(new Event('input', { bubbles: true }));
148
148
  input.dispatchEvent(new Event('change', { bubbles: true }));
149
149
  emit('update:modelValue', input.valueAsNumber);
@@ -90,7 +90,7 @@
90
90
  /* Lift the focused button above its neighbours so the outline
91
91
  * isn't clipped by the adjacent button's edge. */
92
92
  position: relative;
93
- z-index: 1;
93
+ z-index: var(--layer-1);
94
94
  }
95
95
 
96
96
  .pui-number-field__step:disabled {
@@ -8,11 +8,8 @@
8
8
  :aria-describedby="describedBy"
9
9
  :aria-invalid="hasErrors ? 'true' : undefined"
10
10
  >
11
- <!-- aria-invalid lives on the <fieldset> (group-level signal) AND, when the
12
- Astro mountForm controller is wired, on the first radio. The redundancy
13
- is intentional — see ./AUDIT.md "ARIA
14
- expectations" for the rationale. Do NOT remove either binding without
15
- coordinating with the form controller. -->
11
+ <!-- aria-invalid lives on the <fieldset>: a group-level signal see
12
+ ./AUDIT.md "ARIA expectations" for the rationale. -->
16
13
  <legend class="pui-field__label">{{ label }}</legend>
17
14
  <p v-if="hint" :id="hintId" class="pui-field__hint">{{ hint }}</p>
18
15
  <div class="pui-radio-group" :data-orientation="orientation">
@@ -5,9 +5,8 @@
5
5
  * horizontal track that hosts a faint background rail, a coloured
6
6
  * fill segment representing the current value (or range), and one
7
7
  * or two `<button role="slider">` thumbs. Each thumb is absolutely
8
- * positioned via `inset-inline-start: <pct>%`; the controller
9
- * (Astro) and Vue's reactive computed both write the same
10
- * percentage so the visual is identical across both tiers.
8
+ * positioned via `inset-inline-start: <pct>%`, written by Vue's
9
+ * reactive computed.
11
10
  *
12
11
  * Layout rationale: rejected the native `<input type="range">`
13
12
  * baseline for two reasons — (a) cross-browser thumb / track
@@ -6,8 +6,7 @@
6
6
  * row that hosts existing tag pills followed by a text input where
7
7
  * the user types the next tag. The whole row reads as one
8
8
  * integrated control: clicking anywhere lands focus on the entry
9
- * input (handled by Vue / via the click handler in
10
- * `mountTagsInput`).
9
+ * input (a click handler in `TagsInput.vue`).
11
10
  *
12
11
  * Layout rationale: rejected the "fixed-height pills + horizontally-
13
12
  * scrolling row" alternative because vertical wrapping reads better
@@ -13,7 +13,7 @@
13
13
 
14
14
  /* Auto-resize variant — opt-in via the `autoResize` prop, which sets
15
15
  * `style="field-sizing: content"` inline on the <textarea>. The
16
- * inline style is the only documented Vue/Astro path, so the
16
+ * inline style is the only documented path, so the
17
17
  * selector matches it directly. Browsers without `field-sizing`
18
18
  * support fall back gracefully to the default fixed-rows/min-height
19
19
  * box. */
@@ -5,8 +5,8 @@
5
5
  </template>
6
6
 
7
7
  <script setup lang="ts">
8
- import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
9
- import { mountDisclosure } from '../../../utils/controllers/disclosure.js';
8
+ import { ref } from 'vue';
9
+ import { useDisclosure } from '../../../composables/useDisclosure.js';
10
10
 
11
11
  const props = withDefaults(
12
12
  defineProps<{
@@ -25,32 +25,11 @@ const props = withDefaults(
25
25
  );
26
26
 
27
27
  const rootRef = ref<HTMLDivElement | null>(null);
28
- let teardown: (() => void) | null = null;
29
28
 
30
- function mount(): void {
31
- if (!rootRef.value) return;
32
- teardown = mountDisclosure(rootRef.value, {
33
- singleOpen: props.singleOpen,
34
- });
35
- }
36
-
37
- onMounted(mount);
38
-
39
- // `singleOpen` is read once at mount time. If a consumer toggles it
40
- // at runtime we re-mount so the new policy takes effect immediately.
41
- // This is rare (the prop is typically static per surface) but cheap.
42
- watch(
43
- () => props.singleOpen,
44
- () => {
45
- teardown?.();
46
- teardown = null;
47
- mount();
48
- },
49
- );
50
-
51
- onBeforeUnmount(() => {
52
- teardown?.();
53
- teardown = null;
29
+ // `singleOpen` is read per toggle, so a runtime change takes effect on
30
+ // the next interaction without re-wiring.
31
+ useDisclosure(rootRef, {
32
+ singleOpen: () => props.singleOpen,
54
33
  });
55
34
  </script>
56
35
 
@@ -17,8 +17,8 @@
17
17
  </template>
18
18
 
19
19
  <script setup lang="ts">
20
- import { onBeforeUnmount, onMounted, ref } from 'vue';
21
- import { mountDisclosure } from '../../../utils/controllers/disclosure.js';
20
+ import { ref } from 'vue';
21
+ import { useDisclosure } from '../../../composables/useDisclosure.js';
22
22
  import Icon from '../../display/icon/Icon.vue';
23
23
 
24
24
  withDefaults(
@@ -51,24 +51,14 @@ const emit = defineEmits<{
51
51
  }>();
52
52
 
53
53
  const rootRef = ref<HTMLDetailsElement | null>(null);
54
- let teardown: (() => void) | null = null;
55
54
 
56
- onMounted(() => {
57
- if (!rootRef.value) return;
58
- // Collapsible is single-disclosure: no `singleOpen` coordination
59
- // needed. The controller's only role here is to provide the
60
- // standard mount/teardown lifecycle and a normalized callback —
61
- // the native `<details>` element does the open/close work itself.
62
- teardown = mountDisclosure(rootRef.value, {
63
- onToggle: (_target, open) => {
64
- emit('update:open', open);
65
- },
66
- });
67
- });
68
-
69
- onBeforeUnmount(() => {
70
- teardown?.();
71
- teardown = null;
55
+ // Collapsible is single-disclosure: no `singleOpen` coordination
56
+ // needed. The composable's only role here is a normalized callback —
57
+ // the native `<details>` element does the open/close work itself.
58
+ useDisclosure(rootRef, {
59
+ onToggle: (_target, open) => {
60
+ emit('update:open', open);
61
+ },
72
62
  });
73
63
  </script>
74
64
 
@@ -93,8 +93,8 @@
93
93
 
94
94
  /* ===== sort affordance ===== */
95
95
 
96
- /* One rule for both tiers — a <button> in Vue, an <a> in Astro — so the resets below
97
- deliberately cover both elements. */
96
+ /* Rendered as a <button>; the resets below also cover <a> so a
97
+ consumer-supplied link header behaves identically. */
98
98
  .pui-table__sort {
99
99
  display: inline-flex;
100
100
  align-items: center;
@@ -167,7 +167,7 @@
167
167
  .pui-table__loading {
168
168
  position: absolute;
169
169
  inset: 0;
170
- z-index: 1;
170
+ z-index: var(--layer-1);
171
171
 
172
172
  /* the tint gives the blur a surface to sit in; without it the frosting
173
173
  reads as a smudge rather than a pane */