@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,52 @@
1
+ ---
2
+ import './alert.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
+ /** Override the tone's default icon with a shipped one. Use the `icon` slot for a custom <svg>. */
12
+ icon?: IconName;
13
+ }
14
+
15
+ const toneIconMap: Record<Tone, IconName> = {
16
+ brand: 'info',
17
+ success: 'circle-check',
18
+ warning: 'alert-triangle',
19
+ danger: 'circle-alert',
20
+ };
21
+
22
+ const { tone, title, icon, ...rest } = Astro.props;
23
+ const resolvedIcon: IconName = icon ?? (tone ? toneIconMap[tone] : 'info');
24
+ const hasIconSlot = Astro.slots.has('icon');
25
+
26
+ // the icon slot wins over the `icon` name; flag the redundant combination in dev
27
+ if (import.meta.env.DEV && icon && hasIconSlot) {
28
+ console.warn(
29
+ '[pienter-ui] Alert received both an `icon` name and an `icon` slot; the slot is used and `icon` is ignored.',
30
+ );
31
+ }
32
+ ---
33
+
34
+ <div class="pui-alert" data-tone={tone} role="alert" {...rest}>
35
+ <span class="pui-alert__icon">
36
+ {
37
+ hasIconSlot ? (
38
+ <Icon size="md">
39
+ <slot name="icon" />
40
+ </Icon>
41
+ ) : (
42
+ <Icon name={resolvedIcon} size="md" />
43
+ )
44
+ }
45
+ </span>
46
+ <div class="pui-alert__content">
47
+ {title && <p class="pui-alert__title">{title}</p>}
48
+ <div class="pui-alert__body">
49
+ <slot />
50
+ </div>
51
+ </div>
52
+ </div>
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div class="pui-alert" :data-tone="tone" role="alert">
3
+ <span class="pui-alert__icon">
4
+ <Icon v-if="$slots.icon" size="md"><slot name="icon" /></Icon>
5
+ <Icon v-else :name="resolvedIcon" size="md" />
6
+ </span>
7
+ <div class="pui-alert__content">
8
+ <p v-if="title" class="pui-alert__title">{{ title }}</p>
9
+ <div class="pui-alert__body">
10
+ <slot />
11
+ </div>
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { computed, useSlots } from 'vue';
18
+ import Icon from '../../display/icon/Icon.vue';
19
+ import type { IconName } from '../../../icons/index.js';
20
+
21
+ type Tone = 'brand' | 'success' | 'warning' | 'danger';
22
+
23
+ const toneIconMap: Record<Tone, IconName> = {
24
+ brand: 'info',
25
+ success: 'circle-check',
26
+ warning: 'alert-triangle',
27
+ danger: 'circle-alert',
28
+ };
29
+
30
+ const props = withDefaults(
31
+ defineProps<{
32
+ tone?: Tone;
33
+ title?: string;
34
+ /** Override the tone's default icon with a shipped one. Use the `icon` slot for a custom <svg>. */
35
+ icon?: IconName;
36
+ }>(),
37
+ {
38
+ tone: undefined,
39
+ title: undefined,
40
+ icon: undefined,
41
+ },
42
+ );
43
+
44
+ const slots = useSlots();
45
+
46
+ // the icon slot wins over the `icon` name; flag the redundant combination in dev
47
+ if (props.icon && slots.icon) {
48
+ console.warn(
49
+ '[pienter-ui] Alert received both an `icon` name and an `icon` slot; the slot is used and `icon` is ignored.',
50
+ );
51
+ }
52
+
53
+ const resolvedIcon = computed<IconName>(
54
+ () => props.icon ?? (props.tone ? toneIconMap[props.tone] : 'info'),
55
+ );
56
+ </script>
57
+
58
+ <style>
59
+ @import './alert.css';
60
+ </style>
@@ -0,0 +1,78 @@
1
+ @layer components {
2
+ .pui-alert {
3
+ display: grid;
4
+ grid-template-columns: auto 1fr;
5
+ gap: var(--space-xs);
6
+ padding: var(--space-xs) var(--space-s);
7
+ border-radius: var(--radius-sm);
8
+ border: var(--stroke-sm) solid var(--border-clr-base);
9
+ /* tone-coloured left rule carries the emphasis (a non-colour cue too) */
10
+ border-left: var(--stroke-lg) solid var(--border-clr-strong);
11
+ background: var(--bg-clr-surface-2);
12
+
13
+ .pui-alert__icon {
14
+ display: grid;
15
+ place-items: center;
16
+ color: var(--text-clr-muted);
17
+ }
18
+
19
+ .pui-alert__content {
20
+ display: grid;
21
+ gap: 2px;
22
+ }
23
+
24
+ .pui-alert__title {
25
+ margin: 0;
26
+ font-weight: var(--fw-bold);
27
+ font-size: var(--step--1);
28
+ }
29
+
30
+ .pui-alert__body {
31
+ margin: 0;
32
+ color: var(--text-clr-muted);
33
+ font-size: var(--step--1);
34
+ }
35
+
36
+ /* ===== tones ===== */
37
+
38
+ &[data-tone='brand'] {
39
+ background: var(--bg-clr-brand-soft);
40
+ border-color: var(--border-clr-base);
41
+ border-left-color: var(--border-clr-brand);
42
+
43
+ .pui-alert__icon {
44
+ color: var(--text-clr-brand);
45
+ }
46
+ }
47
+
48
+ &[data-tone='success'] {
49
+ background: var(--bg-clr-success-soft);
50
+ border-color: var(--border-clr-base);
51
+ border-left-color: var(--border-clr-success);
52
+
53
+ .pui-alert__icon {
54
+ color: var(--text-clr-success);
55
+ }
56
+ }
57
+
58
+ &[data-tone='warning'] {
59
+ background: var(--bg-clr-warning-soft);
60
+ border-color: var(--border-clr-base);
61
+ border-left-color: var(--border-clr-warning);
62
+
63
+ .pui-alert__icon {
64
+ color: var(--text-clr-warning);
65
+ }
66
+ }
67
+
68
+ &[data-tone='danger'] {
69
+ background: var(--bg-clr-danger-soft);
70
+ border-color: var(--border-clr-base);
71
+ border-left-color: var(--border-clr-danger);
72
+
73
+ .pui-alert__icon {
74
+ color: var(--text-clr-danger);
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,68 @@
1
+ ---
2
+ import './progress.css';
3
+
4
+ interface Props {
5
+ /**
6
+ * Current value of the progress bar. When `null` or `undefined`, the
7
+ * component renders the indeterminate ("loading") state per WAI-ARIA
8
+ * convention — `aria-valuenow` is OMITTED from the wrapper so assistive
9
+ * technology announces an indeterminate progressbar.
10
+ */
11
+ value?: number | null;
12
+ /** Maximum value of the range. */
13
+ max?: number;
14
+ /** Minimum value of the range. */
15
+ min?: number;
16
+ /**
17
+ * Accessible name for the progressbar — emitted as `aria-label`. Omit
18
+ * when an external `aria-labelledby` is supplied via `...rest`.
19
+ */
20
+ label?: string;
21
+ /** Tone variant — drives indicator color via `data-tone`. */
22
+ tone?: 'brand' | 'success' | 'warning' | 'danger';
23
+ }
24
+
25
+ const {
26
+ value = null,
27
+ max = 100,
28
+ min = 0,
29
+ label,
30
+ tone = 'brand',
31
+ ...rest
32
+ } = Astro.props;
33
+
34
+ const isIndeterminate = value === null || value === undefined;
35
+
36
+ const clampedValue = isIndeterminate
37
+ ? min
38
+ : Math.min(Math.max(value as number, min), max);
39
+
40
+ const span = max - min;
41
+ const percent =
42
+ isIndeterminate || span <= 0 ? 0 : ((clampedValue - min) / span) * 100;
43
+
44
+ const state: 'loading' | 'in-progress' | 'complete' = isIndeterminate
45
+ ? 'loading'
46
+ : clampedValue === max
47
+ ? 'complete'
48
+ : 'in-progress';
49
+
50
+ const indicatorStyle: Record<string, string> = {};
51
+ if (!isIndeterminate) {
52
+ indicatorStyle.width = `${percent}%`;
53
+ }
54
+ ---
55
+
56
+ <div
57
+ class="pui-progress"
58
+ role="progressbar"
59
+ aria-valuenow={isIndeterminate ? undefined : clampedValue}
60
+ aria-valuemin={min}
61
+ aria-valuemax={max}
62
+ aria-label={label}
63
+ data-state={state}
64
+ data-tone={tone}
65
+ {...rest}
66
+ >
67
+ <div class="pui-progress__indicator" style={indicatorStyle}></div>
68
+ </div>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div
3
+ class="pui-progress"
4
+ role="progressbar"
5
+ :aria-valuenow="isIndeterminate ? undefined : clampedValue"
6
+ :aria-valuemin="min"
7
+ :aria-valuemax="max"
8
+ :aria-label="label"
9
+ :data-state="state"
10
+ :data-tone="tone"
11
+ >
12
+ <div class="pui-progress__indicator" :style="indicatorStyle" />
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { computed } from 'vue';
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ /**
22
+ * Current value of the progress bar. When `null` or `undefined`, the
23
+ * component renders the indeterminate ("loading") state per WAI-ARIA
24
+ * convention — `aria-valuenow` is OMITTED from the wrapper so assistive
25
+ * technology announces an indeterminate progressbar.
26
+ */
27
+ value?: number | null;
28
+ /** Maximum value of the range. */
29
+ max?: number;
30
+ /** Minimum value of the range. */
31
+ min?: number;
32
+ /**
33
+ * Accessible name for the progressbar — emitted as `aria-label`. Omit
34
+ * when an external `aria-labelledby` is supplied via `$attrs`.
35
+ */
36
+ label?: string;
37
+ /** Tone variant — drives indicator color via `data-tone`. */
38
+ tone?: 'brand' | 'success' | 'warning' | 'danger';
39
+ }>(),
40
+ {
41
+ value: null,
42
+ max: 100,
43
+ min: 0,
44
+ label: undefined,
45
+ tone: 'brand',
46
+ },
47
+ );
48
+
49
+ const isIndeterminate = computed(
50
+ () => props.value === null || props.value === undefined,
51
+ );
52
+
53
+ const clampedValue = computed(() => {
54
+ if (isIndeterminate.value) return props.min;
55
+ const v = props.value as number;
56
+ if (v < props.min) return props.min;
57
+ if (v > props.max) return props.max;
58
+ return v;
59
+ });
60
+
61
+ const percent = computed(() => {
62
+ if (isIndeterminate.value) return 0;
63
+ const span = props.max - props.min;
64
+ if (span <= 0) return 0;
65
+ return ((clampedValue.value - props.min) / span) * 100;
66
+ });
67
+
68
+ const state = computed<'loading' | 'in-progress' | 'complete'>(() => {
69
+ if (isIndeterminate.value) return 'loading';
70
+ if (clampedValue.value === props.max) return 'complete';
71
+ return 'in-progress';
72
+ });
73
+
74
+ const indicatorStyle = computed(() => {
75
+ if (isIndeterminate.value) return {};
76
+ return { width: `${percent.value}%` };
77
+ });
78
+ </script>
79
+
80
+ <style>
81
+ @import './progress.css';
82
+ </style>
@@ -0,0 +1,68 @@
1
+ @layer components {
2
+ .pui-progress {
3
+ display: block;
4
+ width: 100%;
5
+ height: 0.5rem;
6
+ background-color: var(--bg-clr-surface-2);
7
+ border-radius: var(--radius-pill);
8
+ overflow: hidden;
9
+ /* Establish containing block for the indeterminate animation so the
10
+ * indicator's transform is clipped to the track. */
11
+ position: relative;
12
+ }
13
+
14
+ .pui-progress__indicator {
15
+ display: block;
16
+ height: 100%;
17
+ width: 0%;
18
+ background-color: var(--bg-clr-brand);
19
+ border-radius: inherit;
20
+ /* Smooth width transition for determinate updates. */
21
+ transition: width 200ms ease-out;
22
+ }
23
+
24
+ /* Tone variants — color the indicator. */
25
+ .pui-progress[data-tone='success'] .pui-progress__indicator {
26
+ background-color: var(--bg-clr-success);
27
+ }
28
+
29
+ .pui-progress[data-tone='warning'] .pui-progress__indicator {
30
+ background-color: var(--bg-clr-warning);
31
+ }
32
+
33
+ .pui-progress[data-tone='danger'] .pui-progress__indicator {
34
+ background-color: var(--bg-clr-danger);
35
+ }
36
+
37
+ /* Indeterminate (loading) state — no determinate `aria-valuenow`. The
38
+ * indicator becomes a moving bar that scrolls across the track. */
39
+ .pui-progress[data-state='loading'] .pui-progress__indicator {
40
+ width: 40%;
41
+ /* Drop the determinate width transition; animation owns motion. */
42
+ transition: none;
43
+ animation: pui-progress-indeterminate 1.4s ease-in-out infinite;
44
+ }
45
+
46
+ @keyframes pui-progress-indeterminate {
47
+ 0% {
48
+ transform: translateX(-100%);
49
+ }
50
+ 100% {
51
+ transform: translateX(250%);
52
+ }
53
+ }
54
+
55
+ @media (prefers-reduced-motion: reduce) {
56
+ .pui-progress__indicator {
57
+ transition: none;
58
+ }
59
+
60
+ .pui-progress[data-state='loading'] .pui-progress__indicator {
61
+ /* Replace the moving-bar shimmer with a static, full-width fill
62
+ * so the loading state remains visible without motion. */
63
+ animation: none;
64
+ width: 100%;
65
+ opacity: 0.55;
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,32 @@
1
+ ---
2
+ import './skeleton.css';
3
+
4
+ interface Props {
5
+ variant?: 'text' | 'rect' | 'circle';
6
+ width?: string;
7
+ height?: string;
8
+ rounded?: boolean;
9
+ }
10
+
11
+ const {
12
+ variant = 'rect',
13
+ width,
14
+ height,
15
+ rounded = false,
16
+ ...rest
17
+ } = Astro.props;
18
+
19
+ const inlineStyle: Record<string, string> = {};
20
+ if (width) inlineStyle.width = width;
21
+ if (height) inlineStyle.height = height;
22
+ ---
23
+
24
+ <div
25
+ class="pui-skeleton"
26
+ data-variant={variant}
27
+ data-rounded={rounded ? '' : undefined}
28
+ style={inlineStyle}
29
+ aria-hidden="true"
30
+ {...rest}
31
+ >
32
+ </div>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div
3
+ class="pui-skeleton"
4
+ :data-variant="variant"
5
+ :data-rounded="rounded ? '' : null"
6
+ :style="inlineStyle"
7
+ aria-hidden="true"
8
+ />
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { computed } from 'vue';
13
+
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ variant?: 'text' | 'rect' | 'circle';
17
+ width?: string;
18
+ height?: string;
19
+ rounded?: boolean;
20
+ }>(),
21
+ {
22
+ variant: 'rect',
23
+ width: undefined,
24
+ height: undefined,
25
+ rounded: false,
26
+ },
27
+ );
28
+
29
+ const inlineStyle = computed(() => {
30
+ const style: Record<string, string> = {};
31
+ if (props.width) style.width = props.width;
32
+ if (props.height) style.height = props.height;
33
+ return style;
34
+ });
35
+ </script>
36
+
37
+ <style>
38
+ @import './skeleton.css';
39
+ </style>
@@ -0,0 +1,56 @@
1
+ @layer components {
2
+ .pui-skeleton {
3
+ display: block;
4
+ background-color: var(--bg-clr-surface-2);
5
+ background-image: linear-gradient(
6
+ 90deg,
7
+ transparent 0%,
8
+ hsl(from var(--bg-clr-base) h s l / 0.55) 50%,
9
+ transparent 100%
10
+ );
11
+ background-size: 200% 100%;
12
+ background-repeat: no-repeat;
13
+ background-position: -150% 0;
14
+ animation: pui-skeleton-shimmer 1.4s ease-in-out infinite;
15
+ border-radius: var(--radius-sm);
16
+ }
17
+
18
+ .pui-skeleton[data-variant='rect'] {
19
+ width: 100%;
20
+ height: 8rem;
21
+ }
22
+
23
+ .pui-skeleton[data-variant='rect'][data-rounded] {
24
+ border-radius: var(--radius-md);
25
+ }
26
+
27
+ .pui-skeleton[data-variant='text'] {
28
+ display: inline-block;
29
+ width: 100%;
30
+ height: 1em;
31
+ border-radius: var(--radius-xs);
32
+ vertical-align: middle;
33
+ }
34
+
35
+ .pui-skeleton[data-variant='circle'] {
36
+ width: 2.4rem;
37
+ height: 2.4rem;
38
+ border-radius: 50%;
39
+ }
40
+
41
+ @keyframes pui-skeleton-shimmer {
42
+ 0% {
43
+ background-position: -150% 0;
44
+ }
45
+ 100% {
46
+ background-position: 250% 0;
47
+ }
48
+ }
49
+
50
+ @media (prefers-reduced-motion: reduce) {
51
+ .pui-skeleton {
52
+ animation: none;
53
+ background-image: none;
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,25 @@
1
+ ---
2
+ // Spinner (Astro) — same contract as Spinner.vue; composes Icon so the spin stays in icon.css.
3
+ import './spinner.css';
4
+ import Icon from '../../display/icon/Icon.astro';
5
+
6
+ interface Props {
7
+ size?: 'sm' | 'md' | 'lg' | 'xl';
8
+ /** Accessible name; supply it when the spinner is the only signal. See Spinner.vue. */
9
+ label?: string;
10
+ }
11
+
12
+ const { size = 'md', label, ...rest } = Astro.props;
13
+ ---
14
+
15
+ <span
16
+ class="pui-spinner"
17
+ data-size={size}
18
+ role={label ? 'status' : undefined}
19
+ aria-hidden={label ? undefined : 'true'}
20
+ {...rest}
21
+ >
22
+ <Icon name="loader" animation="spin" />
23
+ {/* Real text, not an aria-label — see Spinner.vue. */}
24
+ {label && <span class="pui-spinner__label">{label}</span>}
25
+ </span>
@@ -0,0 +1,36 @@
1
+ <!--
2
+ Spinner — the indeterminate "working" indicator. Composes Icon, so the glyph, the spin
3
+ and the reduced-motion escape stay defined once in icon.css; adds a larger size scale.
4
+ -->
5
+ <template>
6
+ <span
7
+ class="pui-spinner"
8
+ :data-size="size"
9
+ :role="label ? 'status' : undefined"
10
+ :aria-hidden="label ? undefined : true"
11
+ >
12
+ <Icon name="loader" animation="spin" />
13
+ <!-- Real text, not an `aria-label`: a live region whose only child is `aria-hidden` announces nothing. -->
14
+ <span v-if="label" class="pui-spinner__label">{{ label }}</span>
15
+ </span>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import Icon from '../../display/icon/Icon.vue';
20
+
21
+ withDefaults(
22
+ defineProps<{
23
+ size?: 'sm' | 'md' | 'lg' | 'xl';
24
+ /**
25
+ * Accessible name, making the spinner a `role="status"` region. Omit it when
26
+ * something around it already carries the state (`aria-busy`, a "Saving…" label).
27
+ */
28
+ label?: string;
29
+ }>(),
30
+ { size: 'md', label: undefined },
31
+ );
32
+ </script>
33
+
34
+ <style>
35
+ @import './spinner.css';
36
+ </style>
@@ -0,0 +1,91 @@
1
+ @layer components {
2
+ .pui-spinner {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ flex-shrink: 0;
7
+ line-height: 1;
8
+
9
+ /* ===== sizes ===== */
10
+
11
+ /* Own scale because Spinner outgrows Icon's (which stops at lg); set per size
12
+ because the rule reading the property is itself [data-size]-qualified. */
13
+
14
+ &[data-size='sm'] {
15
+ --pui-spinner-size: 1rem;
16
+ }
17
+
18
+ &[data-size='md'] {
19
+ --pui-spinner-size: 1.5rem;
20
+ }
21
+
22
+ &[data-size='lg'] {
23
+ --pui-spinner-size: 2rem;
24
+ }
25
+
26
+ &[data-size='xl'] {
27
+ --pui-spinner-size: 2.75rem;
28
+ }
29
+
30
+ /* [data-size]-qualified to outrank icon.css's `[data-size] svg` in the same layer. */
31
+ &[data-size] .pui-icon svg {
32
+ width: var(--pui-spinner-size, 1.5rem);
33
+ height: var(--pui-spinner-size, 1.5rem);
34
+ }
35
+
36
+ /* Live-region text: in the DOM and out of sight, so the region has content to
37
+ announce. Inlined rather than borrowed from the utilities layer. */
38
+ & .pui-spinner__label {
39
+ position: absolute;
40
+ width: 1px;
41
+ height: 1px;
42
+ overflow: hidden;
43
+ clip-path: inset(50%);
44
+ white-space: nowrap;
45
+ }
46
+
47
+ /* ===== motion ===== */
48
+
49
+ /* One step per spoke (360deg / 8): each tick lands a spoke where its neighbour was,
50
+ so the ramp appears to advance one position. [data-size]-qualified as above. */
51
+ &[data-size] .pui-icon {
52
+ animation-timing-function: steps(8);
53
+ }
54
+
55
+ /* ===== the chasing tail ===== */
56
+
57
+ /* Coupled to the rotation: the wrapper spins clockwise, so brightness must increase
58
+ clockwise over `loader`'s clockwise spokes, or the tail leads — see AUDIT.md. */
59
+ & svg path:nth-child(1) {
60
+ opacity: 0.13;
61
+ }
62
+
63
+ & svg path:nth-child(2) {
64
+ opacity: 0.22;
65
+ }
66
+
67
+ & svg path:nth-child(3) {
68
+ opacity: 0.33;
69
+ }
70
+
71
+ & svg path:nth-child(4) {
72
+ opacity: 0.45;
73
+ }
74
+
75
+ & svg path:nth-child(5) {
76
+ opacity: 0.58;
77
+ }
78
+
79
+ & svg path:nth-child(6) {
80
+ opacity: 0.72;
81
+ }
82
+
83
+ & svg path:nth-child(7) {
84
+ opacity: 0.86;
85
+ }
86
+
87
+ & svg path:nth-child(8) {
88
+ opacity: 1;
89
+ }
90
+ }
91
+ }