@pienter/ui 0.2.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 (227) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CONVENTIONS.md +1499 -0
  3. package/LICENSE +21 -0
  4. package/README.md +71 -0
  5. package/components/action/button/Button.astro +95 -0
  6. package/components/action/button/Button.vue +92 -0
  7. package/components/action/button/IconButton.astro +86 -0
  8. package/components/action/button/IconButton.vue +85 -0
  9. package/components/action/button/button.css +194 -0
  10. package/components/action/toggle/Toggle.vue +38 -0
  11. package/components/action/toggle/toggle.css +77 -0
  12. package/components/action/toggle-group/ToggleGroup.vue +67 -0
  13. package/components/action/toggle-group/toggle-group.css +78 -0
  14. package/components/display/avatar/Avatar.astro +17 -0
  15. package/components/display/avatar/Avatar.vue +30 -0
  16. package/components/display/avatar/AvatarStack.astro +9 -0
  17. package/components/display/avatar/AvatarStack.vue +11 -0
  18. package/components/display/avatar/avatar.css +58 -0
  19. package/components/display/badge/Badge.astro +15 -0
  20. package/components/display/badge/Badge.vue +23 -0
  21. package/components/display/badge/badge.css +56 -0
  22. package/components/display/empty/Empty.astro +9 -0
  23. package/components/display/empty/Empty.vue +11 -0
  24. package/components/display/empty/empty.css +39 -0
  25. package/components/display/icon/Icon.astro +52 -0
  26. package/components/display/icon/Icon.vue +57 -0
  27. package/components/display/icon/icon.css +47 -0
  28. package/components/feedback/alert/Alert.astro +52 -0
  29. package/components/feedback/alert/Alert.vue +60 -0
  30. package/components/feedback/alert/alert.css +78 -0
  31. package/components/feedback/progress/Progress.astro +68 -0
  32. package/components/feedback/progress/Progress.vue +82 -0
  33. package/components/feedback/progress/progress.css +68 -0
  34. package/components/feedback/skeleton/Skeleton.astro +32 -0
  35. package/components/feedback/skeleton/Skeleton.vue +39 -0
  36. package/components/feedback/skeleton/skeleton.css +56 -0
  37. package/components/feedback/spinner/Spinner.astro +25 -0
  38. package/components/feedback/spinner/Spinner.vue +36 -0
  39. package/components/feedback/spinner/spinner.css +91 -0
  40. package/components/feedback/toast/Toast.astro +50 -0
  41. package/components/feedback/toast/Toast.vue +74 -0
  42. package/components/feedback/toast/toast.css +128 -0
  43. package/components/form/checkbox/Checkbox.astro +79 -0
  44. package/components/form/checkbox/Checkbox.vue +95 -0
  45. package/components/form/checkbox/checkbox.css +59 -0
  46. package/components/form/combobox/Combobox.vue +508 -0
  47. package/components/form/combobox/combobox.css +110 -0
  48. package/components/form/date-input/DateInput.astro +105 -0
  49. package/components/form/date-input/DateInput.vue +121 -0
  50. package/components/form/date-input/date-input.css +19 -0
  51. package/components/form/form/Form.astro +106 -0
  52. package/components/form/form/Form.vue +181 -0
  53. package/components/form/form/form.css +46 -0
  54. package/components/form/input-otp/InputOTP.astro +147 -0
  55. package/components/form/input-otp/InputOTP.vue +209 -0
  56. package/components/form/input-otp/input-otp.css +52 -0
  57. package/components/form/label/Label.astro +13 -0
  58. package/components/form/label/Label.vue +20 -0
  59. package/components/form/label/label.css +11 -0
  60. package/components/form/number-field/NumberField.astro +142 -0
  61. package/components/form/number-field/NumberField.vue +155 -0
  62. package/components/form/number-field/number-field.css +115 -0
  63. package/components/form/radio-group/RadioGroup.astro +105 -0
  64. package/components/form/radio-group/RadioGroup.vue +110 -0
  65. package/components/form/radio-group/radio-group.css +114 -0
  66. package/components/form/radio-group/types.ts +14 -0
  67. package/components/form/select/Segmented.vue +36 -0
  68. package/components/form/select/Select.astro +105 -0
  69. package/components/form/select/Select.vue +109 -0
  70. package/components/form/select/select.css +96 -0
  71. package/components/form/slider/Slider.astro +205 -0
  72. package/components/form/slider/Slider.vue +321 -0
  73. package/components/form/slider/slider.css +115 -0
  74. package/components/form/switch/Switch.astro +75 -0
  75. package/components/form/switch/Switch.vue +89 -0
  76. package/components/form/switch/switch.css +64 -0
  77. package/components/form/tags-input/TagsInput.astro +153 -0
  78. package/components/form/tags-input/TagsInput.vue +207 -0
  79. package/components/form/tags-input/tags-input.css +128 -0
  80. package/components/form/text-input/TextInput.astro +84 -0
  81. package/components/form/text-input/TextInput.vue +99 -0
  82. package/components/form/text-input/text-input.css +165 -0
  83. package/components/form/textarea/Textarea.astro +86 -0
  84. package/components/form/textarea/Textarea.vue +102 -0
  85. package/components/form/textarea/textarea.css +25 -0
  86. package/components/layout/accordion/Accordion.vue +59 -0
  87. package/components/layout/accordion/accordion.css +87 -0
  88. package/components/layout/card/Card.astro +13 -0
  89. package/components/layout/card/Card.vue +20 -0
  90. package/components/layout/card/card.css +55 -0
  91. package/components/layout/collapsible/Collapsible.vue +77 -0
  92. package/components/layout/collapsible/collapsible.css +76 -0
  93. package/components/layout/separator/Separator.astro +31 -0
  94. package/components/layout/separator/Separator.vue +33 -0
  95. package/components/layout/separator/separator.css +27 -0
  96. package/components/layout/table/DataTable.vue +127 -0
  97. package/components/layout/table/Table.astro +116 -0
  98. package/components/layout/table/Table.vue +146 -0
  99. package/components/layout/table/TableRow.vue +59 -0
  100. package/components/layout/table/table.css +201 -0
  101. package/components/layout/table/types.ts +35 -0
  102. package/components/layout/table/useTable.ts +7 -0
  103. package/components/navigation/breadcrumb/Breadcrumb.astro +36 -0
  104. package/components/navigation/breadcrumb/Breadcrumb.vue +36 -0
  105. package/components/navigation/breadcrumb/breadcrumb.css +37 -0
  106. package/components/navigation/navbar/Navbar.astro +62 -0
  107. package/components/navigation/navbar/Navbar.vue +50 -0
  108. package/components/navigation/navbar/navbar.css +77 -0
  109. package/components/navigation/pagination/Pagination.vue +107 -0
  110. package/components/navigation/pagination/pagination.css +53 -0
  111. package/components/navigation/sidebar/Sidebar.astro +132 -0
  112. package/components/navigation/sidebar/Sidebar.vue +174 -0
  113. package/components/navigation/sidebar/SidebarItemRender.astro +83 -0
  114. package/components/navigation/sidebar/SidebarItemRender.vue +98 -0
  115. package/components/navigation/sidebar/sidebar.css +303 -0
  116. package/components/navigation/sidebar/types.ts +72 -0
  117. package/components/navigation/tabs/Tabs.vue +84 -0
  118. package/components/navigation/tabs/tabs.css +39 -0
  119. package/components/overlay/alert-dialog/AlertDialog.astro +112 -0
  120. package/components/overlay/alert-dialog/AlertDialog.vue +117 -0
  121. package/components/overlay/alert-dialog/alert-dialog.css +57 -0
  122. package/components/overlay/command/Command.vue +356 -0
  123. package/components/overlay/command/command.css +179 -0
  124. package/components/overlay/dropdown-menu/DropdownMenu.vue +143 -0
  125. package/components/overlay/dropdown-menu/dropdown-menu.css +120 -0
  126. package/components/overlay/modal/Modal.astro +66 -0
  127. package/components/overlay/modal/Modal.vue +85 -0
  128. package/components/overlay/modal/modal.css +60 -0
  129. package/components/overlay/popover/Popover.vue +113 -0
  130. package/components/overlay/popover/popover.css +53 -0
  131. package/components/overlay/sheet/Sheet.vue +88 -0
  132. package/components/overlay/sheet/sheet.css +108 -0
  133. package/components/overlay/tooltip/Tooltip.vue +210 -0
  134. package/components/overlay/tooltip/tooltip.css +50 -0
  135. package/composables/useUrlSort.ts +48 -0
  136. package/icons/alert-triangle.ts +1 -0
  137. package/icons/arrow-down.ts +1 -0
  138. package/icons/arrow-up-down.ts +5 -0
  139. package/icons/arrow-up.ts +1 -0
  140. package/icons/bell.ts +1 -0
  141. package/icons/check.ts +1 -0
  142. package/icons/chevron-down.ts +1 -0
  143. package/icons/chevron-left.ts +1 -0
  144. package/icons/chevron-right.ts +1 -0
  145. package/icons/chevron-up-down.ts +5 -0
  146. package/icons/chevron-up.ts +1 -0
  147. package/icons/circle-alert.ts +1 -0
  148. package/icons/circle-check.ts +1 -0
  149. package/icons/clipboard.ts +1 -0
  150. package/icons/download.ts +1 -0
  151. package/icons/edit.ts +1 -0
  152. package/icons/external-link.ts +1 -0
  153. package/icons/eye.ts +1 -0
  154. package/icons/file.ts +1 -0
  155. package/icons/filter.ts +1 -0
  156. package/icons/folder.ts +1 -0
  157. package/icons/image.ts +1 -0
  158. package/icons/inbox.ts +1 -0
  159. package/icons/index.ts +91 -0
  160. package/icons/info.ts +1 -0
  161. package/icons/layers.ts +1 -0
  162. package/icons/link-2.ts +1 -0
  163. package/icons/list.ts +1 -0
  164. package/icons/loader.ts +3 -0
  165. package/icons/menu.ts +1 -0
  166. package/icons/more-horizontal.ts +1 -0
  167. package/icons/more-vertical.ts +1 -0
  168. package/icons/plus-circle.ts +1 -0
  169. package/icons/plus.ts +1 -0
  170. package/icons/save.ts +1 -0
  171. package/icons/search.ts +1 -0
  172. package/icons/send.ts +1 -0
  173. package/icons/settings.ts +1 -0
  174. package/icons/tool.ts +1 -0
  175. package/icons/trash-2.ts +1 -0
  176. package/icons/trash.ts +1 -0
  177. package/icons/upload-cloud.ts +1 -0
  178. package/icons/upload.ts +1 -0
  179. package/icons/x.ts +1 -0
  180. package/package.json +150 -0
  181. package/styles/0-settings/colors.css +241 -0
  182. package/styles/0-settings/index.css +5 -0
  183. package/styles/0-settings/layout.css +52 -0
  184. package/styles/0-settings/motion.css +11 -0
  185. package/styles/0-settings/spacing.css +15 -0
  186. package/styles/0-settings/typography.css +37 -0
  187. package/styles/0-utils/index.css +1 -0
  188. package/styles/1-reset/index.css +1 -0
  189. package/styles/1-reset/reset.css +26 -0
  190. package/styles/2-base/base.css +42 -0
  191. package/styles/2-base/forms.css +23 -0
  192. package/styles/2-base/index.css +2 -0
  193. package/styles/3-layout/container.css +57 -0
  194. package/styles/3-layout/index.css +2 -0
  195. package/styles/3-layout/section.css +17 -0
  196. package/styles/5-utilities/accessibility.css +13 -0
  197. package/styles/5-utilities/index.css +2 -0
  198. package/styles/5-utilities/text.css +5 -0
  199. package/styles/main.css +8 -0
  200. package/styles/styles.d.ts +6 -0
  201. package/utils/a11y/focus.ts +68 -0
  202. package/utils/a11y/id.ts +10 -0
  203. package/utils/a11y/index.ts +9 -0
  204. package/utils/a11y/keyboard.ts +32 -0
  205. package/utils/a11y/live-region.ts +36 -0
  206. package/utils/controllers/dialog.ts +205 -0
  207. package/utils/controllers/disclosure.ts +117 -0
  208. package/utils/controllers/form.ts +524 -0
  209. package/utils/controllers/index.ts +39 -0
  210. package/utils/controllers/menu.ts +255 -0
  211. package/utils/controllers/number-field.ts +103 -0
  212. package/utils/controllers/otp.ts +252 -0
  213. package/utils/controllers/popover.ts +434 -0
  214. package/utils/controllers/sidebar.ts +610 -0
  215. package/utils/controllers/slider.ts +336 -0
  216. package/utils/controllers/tags-input.ts +255 -0
  217. package/utils/controllers/toast.ts +426 -0
  218. package/utils/dom/index.ts +1 -0
  219. package/utils/dom/scroll-lock.ts +48 -0
  220. package/utils/index.ts +3 -0
  221. package/utils/sort/index.ts +3 -0
  222. package/utils/sort/serialize.ts +19 -0
  223. package/utils/sort/state.ts +11 -0
  224. package/utils/sort/types.ts +15 -0
  225. package/utils/validation/form.ts +93 -0
  226. package/utils/validation/index.ts +13 -0
  227. package/utils/validation/rules.ts +31 -0
@@ -0,0 +1,50 @@
1
+ ---
2
+ import './toast.css';
3
+ import Icon from '../../display/icon/Icon.astro';
4
+ import type { IconName } from '../../../icons/index.js';
5
+
6
+ type Tone = 'brand' | 'success' | 'warning' | 'danger';
7
+
8
+ interface Props {
9
+ tone?: Tone;
10
+ title: string;
11
+ message?: string;
12
+ dismissible?: boolean;
13
+ icon?: IconName;
14
+ action?: { label: string };
15
+ }
16
+
17
+ const toneIconMap: Record<Tone, IconName> = {
18
+ brand: 'info',
19
+ success: 'circle-check',
20
+ warning: 'alert-triangle',
21
+ danger: 'circle-alert',
22
+ };
23
+
24
+ const { tone, title, message, dismissible = true, icon, action } = Astro.props;
25
+ const resolvedIcon: IconName = icon ?? (tone ? toneIconMap[tone] : 'info');
26
+ ---
27
+
28
+ <div class="pui-toast" data-tone={tone}>
29
+ <span class="pui-toast__icon" aria-hidden="true">
30
+ <Icon name={resolvedIcon} size="sm" />
31
+ </span>
32
+ <div class="pui-toast__body">
33
+ <p class="pui-toast__title">{title}</p>
34
+ {message && <p class="pui-toast__msg">{message}</p>}
35
+ </div>
36
+ {
37
+ action && (
38
+ <button class="pui-toast__action" type="button">
39
+ {action.label}
40
+ </button>
41
+ )
42
+ }
43
+ {
44
+ dismissible && (
45
+ <button class="pui-toast__close" type="button" aria-label="Close">
46
+ <Icon name="x" size="sm" />
47
+ </button>
48
+ )
49
+ }
50
+ </div>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <div class="pui-toast" :data-tone="tone">
3
+ <span class="pui-toast__icon" aria-hidden="true">
4
+ <Icon :name="resolvedIcon" size="sm" />
5
+ </span>
6
+ <div class="pui-toast__body">
7
+ <p class="pui-toast__title">{{ title }}</p>
8
+ <p v-if="message" class="pui-toast__msg">{{ message }}</p>
9
+ </div>
10
+ <button
11
+ v-if="action"
12
+ class="pui-toast__action"
13
+ type="button"
14
+ @click="$emit('action')"
15
+ >
16
+ {{ action.label }}
17
+ </button>
18
+ <button
19
+ v-if="dismissible"
20
+ class="pui-toast__close"
21
+ type="button"
22
+ aria-label="Close"
23
+ @click="$emit('dismiss')"
24
+ >
25
+ <Icon name="x" size="sm" />
26
+ </button>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { computed } from 'vue';
32
+ import Icon from '../../display/icon/Icon.vue';
33
+ import type { IconName } from '../../../icons/index.js';
34
+
35
+ type Tone = 'brand' | 'success' | 'warning' | 'danger';
36
+
37
+ const toneIconMap: Record<Tone, IconName> = {
38
+ brand: 'info',
39
+ success: 'circle-check',
40
+ warning: 'alert-triangle',
41
+ danger: 'circle-alert',
42
+ };
43
+
44
+ const props = withDefaults(
45
+ defineProps<{
46
+ tone?: Tone;
47
+ title: string;
48
+ message?: string;
49
+ dismissible?: boolean;
50
+ icon?: IconName;
51
+ action?: { label: string };
52
+ }>(),
53
+ {
54
+ tone: undefined,
55
+ message: undefined,
56
+ dismissible: true,
57
+ icon: undefined,
58
+ action: undefined,
59
+ },
60
+ );
61
+
62
+ defineEmits<{
63
+ dismiss: [];
64
+ action: [];
65
+ }>();
66
+
67
+ const resolvedIcon = computed<IconName>(
68
+ () => props.icon ?? (props.tone ? toneIconMap[props.tone] : 'info'),
69
+ );
70
+ </script>
71
+
72
+ <style>
73
+ @import './toast.css';
74
+ </style>
@@ -0,0 +1,128 @@
1
+ @layer components {
2
+ /* The global region container — appended once on first toast.show().
3
+ Position fixed bottom-right; flex column with gap so stacked toasts
4
+ space cleanly. v1 only ships bottom-right; the data-position attr
5
+ is reserved for future positions. */
6
+ .pui-toast-region {
7
+ position: fixed;
8
+ inset: auto var(--space-s) var(--space-s) auto;
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--space-2xs);
12
+ z-index: 1000;
13
+ pointer-events: none;
14
+ max-width: calc(100vw - var(--space-s) * 2);
15
+ }
16
+
17
+ /* Reactivate pointer events on the toasts themselves so the
18
+ region's `pointer-events: none` lets clicks pass through the
19
+ gaps between toasts. */
20
+ .pui-toast-region > .pui-toast {
21
+ pointer-events: auto;
22
+ }
23
+
24
+ .pui-toast {
25
+ display: grid;
26
+ grid-template-columns: auto 1fr auto auto;
27
+ align-items: start;
28
+ gap: var(--space-xs);
29
+ padding: var(--space-xs) var(--space-s);
30
+ background: var(--bg-clr-surface);
31
+ border: var(--stroke-md) solid var(--border-clr-strong);
32
+ border-radius: var(--radius-sm);
33
+ box-shadow: var(--shadow-spark);
34
+ max-width: 28rem;
35
+ }
36
+
37
+ .pui-toast__icon {
38
+ width: 1.6rem;
39
+ height: 1.6rem;
40
+ border-radius: 50%;
41
+ background: var(--bg-clr-dark);
42
+ color: var(--text-clr-inverse);
43
+ display: grid;
44
+ place-items: center;
45
+ }
46
+
47
+ .pui-toast__body {
48
+ display: grid;
49
+ gap: 2px;
50
+ }
51
+
52
+ .pui-toast__title {
53
+ font-weight: var(--fw-bold);
54
+ margin: 0;
55
+ }
56
+
57
+ .pui-toast__msg {
58
+ margin: 0;
59
+ color: var(--text-clr-muted);
60
+ font-size: var(--step--1);
61
+ }
62
+
63
+ /* Action button — same baseline as the close button (no chrome) but
64
+ the label is visible. Clicking invokes the action callback and
65
+ dismisses the toast. */
66
+ .pui-toast__action {
67
+ appearance: none;
68
+ border: 0;
69
+ background: transparent;
70
+ color: var(--text-clr-link);
71
+ cursor: pointer;
72
+ padding: 0;
73
+ line-height: 1;
74
+ font: inherit;
75
+ font-weight: var(--fw-bold);
76
+ align-self: center;
77
+ }
78
+
79
+ .pui-toast__action:hover {
80
+ color: var(--text-clr-link-hover);
81
+ text-decoration: underline;
82
+ }
83
+
84
+ .pui-toast__action:focus-visible {
85
+ outline: var(--stroke-md) solid var(--outline-clr-base);
86
+ outline-offset: 2px;
87
+ border-radius: var(--radius-xs);
88
+ }
89
+
90
+ .pui-toast__close {
91
+ appearance: none;
92
+ border: 0;
93
+ background: transparent;
94
+ color: var(--text-clr-muted);
95
+ cursor: pointer;
96
+ padding: 0;
97
+ line-height: 1;
98
+ display: grid;
99
+ place-items: center;
100
+ }
101
+
102
+ .pui-toast__close:hover {
103
+ color: var(--text-clr-base);
104
+ }
105
+
106
+ .pui-toast__close:focus-visible {
107
+ outline: var(--stroke-md) solid var(--outline-clr-base);
108
+ outline-offset: 2px;
109
+ border-radius: var(--radius-xs);
110
+ color: var(--text-clr-base);
111
+ }
112
+
113
+ .pui-toast[data-tone='success'] .pui-toast__icon {
114
+ background: var(--bg-clr-success);
115
+ }
116
+
117
+ .pui-toast[data-tone='warning'] .pui-toast__icon {
118
+ background: var(--bg-clr-warning);
119
+ }
120
+
121
+ .pui-toast[data-tone='danger'] .pui-toast__icon {
122
+ background: var(--bg-clr-danger);
123
+ }
124
+
125
+ .pui-toast[data-tone='brand'] .pui-toast__icon {
126
+ background: var(--bg-clr-brand);
127
+ }
128
+ }
@@ -0,0 +1,79 @@
1
+ ---
2
+ import './checkbox.css';
3
+ import { generateId } from '../../../utils/a11y/id.js';
4
+
5
+ interface Props {
6
+ id?: string;
7
+ label: string;
8
+ name?: string;
9
+ checked?: boolean;
10
+ required?: boolean;
11
+ disabled?: boolean;
12
+ hint?: string;
13
+ errors?: string[];
14
+ status?: 'error' | 'success';
15
+ }
16
+
17
+ const {
18
+ id,
19
+ label,
20
+ name,
21
+ checked = false,
22
+ required = false,
23
+ disabled = false,
24
+ hint,
25
+ errors = [],
26
+ status,
27
+ ...rest
28
+ } = Astro.props;
29
+
30
+ const inputId = id ?? generateId('checkbox');
31
+ const hintId = `${inputId}-hint`;
32
+ const errorsId = `${inputId}-errors`;
33
+
34
+ const hasErrors = errors.length > 0;
35
+ const computedStatus = status ?? (hasErrors ? 'error' : undefined);
36
+
37
+ const describedByIds: string[] = [];
38
+ if (hint) describedByIds.push(hintId);
39
+ if (hasErrors) describedByIds.push(errorsId);
40
+ const describedBy =
41
+ describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
42
+
43
+ const ariaChecked = checked ? 'true' : 'false';
44
+ ---
45
+
46
+ <div class="pui-field" data-layout="inline" data-status={computedStatus}>
47
+ <input
48
+ class="pui-checkbox"
49
+ type="checkbox"
50
+ id={inputId}
51
+ name={name}
52
+ checked={checked}
53
+ required={required}
54
+ disabled={disabled}
55
+ aria-checked={ariaChecked}
56
+ aria-invalid={hasErrors ? 'true' : undefined}
57
+ aria-describedby={describedBy}
58
+ {...rest}
59
+ />
60
+ <label class="pui-field__label" for={inputId}>
61
+ {label}
62
+ </label>
63
+ {
64
+ hint && (
65
+ <p id={hintId} class="pui-field__hint">
66
+ {hint}
67
+ </p>
68
+ )
69
+ }
70
+ {
71
+ hasErrors && (
72
+ <ul id={errorsId} class="pui-field__hint" role="alert">
73
+ {errors.map((error) => (
74
+ <li>{error}</li>
75
+ ))}
76
+ </ul>
77
+ )
78
+ }
79
+ </div>
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <div class="pui-field" data-layout="inline" :data-status="computedStatus">
3
+ <input
4
+ v-bind="$attrs"
5
+ :id="inputId"
6
+ class="pui-checkbox"
7
+ type="checkbox"
8
+ :name="name"
9
+ :checked="modelValue"
10
+ :required="required"
11
+ :disabled="disabled"
12
+ :aria-checked="ariaCheckedValue"
13
+ :aria-invalid="hasErrors ? 'true' : undefined"
14
+ :aria-describedby="describedBy"
15
+ @change="
16
+ emit(
17
+ 'update:modelValue',
18
+ ($event.target as HTMLInputElement).checked,
19
+ )
20
+ "
21
+ />
22
+ <label class="pui-field__label" :for="inputId">{{ label }}</label>
23
+ <p v-if="hint" :id="hintId" class="pui-field__hint">{{ hint }}</p>
24
+ <ul
25
+ v-if="errors.length"
26
+ :id="errorsId"
27
+ class="pui-field__hint"
28
+ role="alert"
29
+ >
30
+ <li v-for="error in errors" :key="error">{{ error }}</li>
31
+ </ul>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { computed } from 'vue';
37
+ import { generateId } from '../../../utils/a11y/id.js';
38
+
39
+ defineOptions({ inheritAttrs: false });
40
+
41
+ const props = withDefaults(
42
+ defineProps<{
43
+ id?: string;
44
+ label: string;
45
+ modelValue?: boolean;
46
+ name?: string;
47
+ required?: boolean;
48
+ disabled?: boolean;
49
+ hint?: string;
50
+ errors?: string[];
51
+ status?: 'error' | 'success';
52
+ }>(),
53
+ {
54
+ id: undefined,
55
+ modelValue: false,
56
+ name: undefined,
57
+ required: false,
58
+ disabled: false,
59
+ hint: undefined,
60
+ errors: () => [],
61
+ status: undefined,
62
+ },
63
+ );
64
+
65
+ const emit = defineEmits<{
66
+ 'update:modelValue': [value: boolean];
67
+ }>();
68
+
69
+ const inputId = props.id ?? generateId('checkbox');
70
+ const hintId = `${inputId}-hint`;
71
+ const errorsId = `${inputId}-errors`;
72
+
73
+ const hasErrors = computed(() => props.errors.length > 0);
74
+
75
+ const computedStatus = computed(() => {
76
+ if (props.status) return props.status;
77
+ if (hasErrors.value) return 'error';
78
+ return undefined;
79
+ });
80
+
81
+ const describedBy = computed(() => {
82
+ const ids: string[] = [];
83
+ if (props.hint) ids.push(hintId);
84
+ if (hasErrors.value) ids.push(errorsId);
85
+ return ids.length > 0 ? ids.join(' ') : undefined;
86
+ });
87
+
88
+ const ariaCheckedValue = computed<'true' | 'false'>(() =>
89
+ props.modelValue ? 'true' : 'false',
90
+ );
91
+ </script>
92
+
93
+ <style>
94
+ @import './checkbox.css';
95
+ </style>
@@ -0,0 +1,59 @@
1
+ @layer components {
2
+ .pui-checkbox {
3
+ appearance: none;
4
+ flex: none;
5
+ margin: 0;
6
+ width: 1.15em;
7
+ height: 1.15em;
8
+ border: var(--stroke-md) solid var(--border-clr-strong);
9
+ border-radius: var(--radius-xs);
10
+ background: var(--bg-clr-surface);
11
+ display: grid;
12
+ place-items: center;
13
+ cursor: pointer;
14
+ transition:
15
+ background 0.12s ease,
16
+ border-color 0.12s ease;
17
+ }
18
+
19
+ .pui-checkbox:checked {
20
+ background: var(--bg-clr-brand);
21
+ border-color: var(--border-clr-brand);
22
+ }
23
+
24
+ /* Checkmark for the binary-checked state. */
25
+ .pui-checkbox:checked::after {
26
+ content: '';
27
+ width: 0.3em;
28
+ height: 0.55em;
29
+ border: solid var(--bg-clr-brand-ink);
30
+ border-width: 0 2px 2px 0;
31
+ transform: rotate(45deg);
32
+ }
33
+
34
+ .pui-checkbox:focus-visible {
35
+ outline: 3px solid var(--outline-clr-base);
36
+ outline-offset: 2px;
37
+ }
38
+
39
+ .pui-checkbox:disabled {
40
+ opacity: 0.5;
41
+ cursor: not-allowed;
42
+ }
43
+
44
+ /* Status-driven recolour from the parent pui-field wrapper. */
45
+ .pui-field[data-status='error'] .pui-checkbox {
46
+ border-color: var(--border-clr-danger);
47
+ }
48
+
49
+ .pui-field[data-status='success'] .pui-checkbox:checked {
50
+ background: var(--bg-clr-success, var(--bg-clr-brand));
51
+ border-color: var(--bg-clr-success, var(--border-clr-brand));
52
+ }
53
+
54
+ /* Mute the label + cursor when the input is disabled. */
55
+ .pui-field:has(.pui-checkbox:disabled) .pui-field__label {
56
+ color: var(--text-clr-muted);
57
+ cursor: not-allowed;
58
+ }
59
+ }