@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,77 @@
1
+ @layer components {
2
+ /* ===== pui-toggle (press-button — Phase 4) =====
3
+ *
4
+ * A single press-button that toggles between pressed and unpressed
5
+ * states via `aria-pressed`. Distinct from `pui-switch` (the form-
6
+ * primitive on/off control); Toggle is a button-shaped action whose
7
+ * pressed state surfaces as a visual fill.
8
+ */
9
+
10
+ .pui-toggle {
11
+ appearance: none;
12
+ display: inline-flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ gap: var(--space-3xs);
16
+ border: var(--stroke-sm) solid var(--border-clr-base);
17
+ background: var(--bg-clr-surface);
18
+ color: var(--text-clr-base);
19
+ font: inherit;
20
+ font-weight: var(--fw-semibold);
21
+ border-radius: var(--radius-sm);
22
+ padding: var(--space-2xs) var(--space-s);
23
+ cursor: pointer;
24
+ transition:
25
+ background 0.12s ease,
26
+ color 0.12s ease,
27
+ border-color 0.12s ease;
28
+ }
29
+
30
+ .pui-toggle:hover:not(:disabled) {
31
+ background: var(--bg-clr-surface-2);
32
+ }
33
+
34
+ .pui-toggle:focus-visible {
35
+ outline: 3px solid var(--outline-clr-base);
36
+ outline-offset: 2px;
37
+ }
38
+
39
+ .pui-toggle:disabled {
40
+ opacity: 0.5;
41
+ cursor: not-allowed;
42
+ }
43
+
44
+ /* ===== pressed state (default tone) ===== */
45
+
46
+ .pui-toggle[aria-pressed='true'] {
47
+ background: var(--bg-clr-surface-2);
48
+ color: var(--text-clr-base);
49
+ border-color: var(--border-clr-strong);
50
+ box-shadow: inset 0 1px 2px rgba(15, 21, 48, 0.08);
51
+ }
52
+
53
+ /* ===== brand tone ===== */
54
+
55
+ .pui-toggle[data-tone='brand'][aria-pressed='true'] {
56
+ background: var(--bg-clr-brand);
57
+ color: var(--bg-clr-brand-ink);
58
+ border-color: var(--border-clr-strong);
59
+ }
60
+
61
+ /* ===== sizes ===== */
62
+
63
+ .pui-toggle[data-size='sm'] {
64
+ padding: var(--space-3xs) var(--space-xs);
65
+ font-size: var(--step--1);
66
+ }
67
+
68
+ .pui-toggle[data-size='md'] {
69
+ padding: var(--space-2xs) var(--space-s);
70
+ font-size: var(--step--1);
71
+ }
72
+
73
+ .pui-toggle[data-size='lg'] {
74
+ padding: var(--space-xs) var(--space-m);
75
+ font-size: var(--step-1);
76
+ }
77
+ }
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <div
3
+ class="pui-toggle-group"
4
+ role="group"
5
+ :aria-label="label"
6
+ :data-size="size"
7
+ >
8
+ <button
9
+ v-for="opt in options"
10
+ :key="opt.value"
11
+ class="pui-toggle-group__item"
12
+ type="button"
13
+ :disabled="disabled || opt.disabled"
14
+ :aria-pressed="isPressed(opt.value) ? 'true' : 'false'"
15
+ @click="toggle(opt.value)"
16
+ >
17
+ <Icon v-if="opt.icon" :name="opt.icon as IconName" size="sm" />
18
+ <span>{{ opt.label }}</span>
19
+ </button>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import Icon from '../../display/icon/Icon.vue';
25
+ import type { IconName } from '../../../icons/index.js';
26
+
27
+ const props = withDefaults(
28
+ defineProps<{
29
+ options: {
30
+ value: string;
31
+ label: string;
32
+ disabled?: boolean;
33
+ icon?: string;
34
+ }[];
35
+ modelValue?: string[];
36
+ label?: string;
37
+ size?: 'sm' | 'md' | 'lg';
38
+ disabled?: boolean;
39
+ }>(),
40
+ {
41
+ modelValue: () => [],
42
+ label: undefined,
43
+ size: 'md',
44
+ disabled: false,
45
+ },
46
+ );
47
+
48
+ const emit = defineEmits<{
49
+ 'update:modelValue': [value: string[]];
50
+ }>();
51
+
52
+ function isPressed(value: string): boolean {
53
+ return props.modelValue.includes(value);
54
+ }
55
+
56
+ function toggle(value: string): void {
57
+ const current = props.modelValue;
58
+ const next = current.includes(value)
59
+ ? current.filter((v) => v !== value)
60
+ : [...current, value];
61
+ emit('update:modelValue', next);
62
+ }
63
+ </script>
64
+
65
+ <style>
66
+ @import './toggle-group.css';
67
+ </style>
@@ -0,0 +1,78 @@
1
+ @layer components {
2
+ /* ===== pui-toggle-group (multi-select press-button group — Phase 4) =====
3
+ *
4
+ * A horizontally-arranged group of press-buttons where each item
5
+ * carries its own `aria-pressed` state. Multi-select: any subset of
6
+ * items can be pressed at once. Distinct from Segmented (single-
7
+ * select) — see select.css `.pui-segmented` for that single-select
8
+ * sibling.
9
+ */
10
+
11
+ .pui-toggle-group {
12
+ display: inline-flex;
13
+ align-items: stretch;
14
+ gap: var(--space-3xs);
15
+ background: var(--bg-clr-surface-2);
16
+ border: var(--stroke-sm) solid var(--border-clr-base);
17
+ border-radius: var(--radius-sm);
18
+ padding: 3px;
19
+ }
20
+
21
+ .pui-toggle-group__item {
22
+ appearance: none;
23
+ border: 0;
24
+ background: transparent;
25
+ color: var(--text-clr-muted);
26
+ font: inherit;
27
+ font-size: var(--step--1);
28
+ font-weight: var(--fw-semibold);
29
+ padding: 0.4em 0.85em;
30
+ border-radius: calc(var(--radius-sm) - 1px);
31
+ cursor: pointer;
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: var(--space-3xs);
35
+ transition:
36
+ background 0.12s ease,
37
+ color 0.12s ease;
38
+ }
39
+
40
+ .pui-toggle-group__item:hover:not(:disabled) {
41
+ color: var(--text-clr-base);
42
+ }
43
+
44
+ .pui-toggle-group__item:focus-visible {
45
+ outline: 3px solid var(--outline-clr-base);
46
+ outline-offset: 2px;
47
+ }
48
+
49
+ .pui-toggle-group__item:disabled {
50
+ opacity: 0.5;
51
+ cursor: not-allowed;
52
+ }
53
+
54
+ /* ===== pressed state ===== */
55
+
56
+ .pui-toggle-group__item[aria-pressed='true'] {
57
+ background: var(--bg-clr-surface);
58
+ color: var(--text-clr-base);
59
+ box-shadow: 0 1px 2px rgba(15, 21, 48, 0.08);
60
+ }
61
+
62
+ /* ===== sizes (drive padding + font-size of items) ===== */
63
+
64
+ .pui-toggle-group[data-size='sm'] .pui-toggle-group__item {
65
+ padding: 0.3em 0.6em;
66
+ font-size: var(--step--2);
67
+ }
68
+
69
+ .pui-toggle-group[data-size='md'] .pui-toggle-group__item {
70
+ padding: 0.4em 0.85em;
71
+ font-size: var(--step--1);
72
+ }
73
+
74
+ .pui-toggle-group[data-size='lg'] .pui-toggle-group__item {
75
+ padding: 0.5em 1.1em;
76
+ font-size: var(--step-1);
77
+ }
78
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ import './avatar.css';
3
+
4
+ interface Props {
5
+ size?: 'sm' | 'lg';
6
+ tone?: 'brand' | 'solid';
7
+ src?: string;
8
+ alt?: string;
9
+ initials?: string;
10
+ }
11
+
12
+ const { size, tone, src, alt = '', initials, ...rest } = Astro.props;
13
+ ---
14
+
15
+ <span class="pui-avatar" data-size={size} data-tone={tone} {...rest}>
16
+ {src ? <img src={src} alt={alt} /> : initials ? initials : <slot />}
17
+ </span>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <span class="pui-avatar" :data-size="size" :data-tone="tone">
3
+ <img v-if="src" :src="src" :alt="alt ?? ''" />
4
+ <template v-else-if="initials">{{ initials }}</template>
5
+ <slot v-else />
6
+ </span>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ withDefaults(
11
+ defineProps<{
12
+ size?: 'sm' | 'lg';
13
+ tone?: 'brand' | 'solid';
14
+ src?: string;
15
+ alt?: string;
16
+ initials?: string;
17
+ }>(),
18
+ {
19
+ size: undefined,
20
+ tone: undefined,
21
+ src: undefined,
22
+ alt: undefined,
23
+ initials: undefined,
24
+ },
25
+ );
26
+ </script>
27
+
28
+ <style>
29
+ @import './avatar.css';
30
+ </style>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import './avatar.css';
3
+
4
+ const { ...rest } = Astro.props;
5
+ ---
6
+
7
+ <div class="pui-avatar-stack" {...rest}>
8
+ <slot />
9
+ </div>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="pui-avatar-stack">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts"></script>
8
+
9
+ <style>
10
+ @import './avatar.css';
11
+ </style>
@@ -0,0 +1,58 @@
1
+ @layer components {
2
+ .pui-avatar {
3
+ display: inline-grid;
4
+ place-items: center;
5
+ width: 2.4rem;
6
+ height: 2.4rem;
7
+ border-radius: 50%;
8
+ background: var(--bg-clr-accent);
9
+ color: var(--text-clr-base);
10
+ font-weight: var(--fw-bold);
11
+ font-size: var(--step--1);
12
+ border: var(--stroke-sm) solid var(--border-clr-base);
13
+ overflow: hidden;
14
+ line-height: 1;
15
+ }
16
+
17
+ .pui-avatar img {
18
+ width: 100%;
19
+ height: 100%;
20
+ object-fit: cover;
21
+ display: block;
22
+ }
23
+
24
+ .pui-avatar[data-size='sm'] {
25
+ width: 1.75rem;
26
+ height: 1.75rem;
27
+ font-size: var(--step--2);
28
+ }
29
+
30
+ .pui-avatar[data-size='lg'] {
31
+ width: 3.25rem;
32
+ height: 3.25rem;
33
+ font-size: var(--step-1);
34
+ }
35
+
36
+ .pui-avatar[data-tone='brand'] {
37
+ background: var(--bg-clr-brand);
38
+ color: var(--bg-clr-brand-ink);
39
+ }
40
+
41
+ .pui-avatar[data-tone='solid'] {
42
+ background: var(--bg-clr-dark);
43
+ color: var(--text-clr-inverse);
44
+ }
45
+
46
+ .pui-avatar-stack {
47
+ display: inline-flex;
48
+ }
49
+
50
+ .pui-avatar-stack .pui-avatar {
51
+ margin-inline-start: -0.5rem;
52
+ box-shadow: 0 0 0 2px var(--bg-clr-surface);
53
+ }
54
+
55
+ .pui-avatar-stack .pui-avatar:first-child {
56
+ margin-inline-start: 0;
57
+ }
58
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import './badge.css';
3
+
4
+ interface Props {
5
+ tone?: 'brand' | 'success' | 'warning' | 'danger' | 'solid';
6
+ dot?: boolean;
7
+ }
8
+
9
+ const { tone, dot = false, ...rest } = Astro.props;
10
+ ---
11
+
12
+ <span class="pui-badge" data-tone={tone} {...rest}>
13
+ {dot && <span class="pui-badge__dot" />}
14
+ <slot />
15
+ </span>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <span class="pui-badge" :data-tone="tone">
3
+ <span v-if="dot" class="pui-badge__dot" />
4
+ <slot />
5
+ </span>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ withDefaults(
10
+ defineProps<{
11
+ tone?: 'brand' | 'success' | 'warning' | 'danger' | 'solid';
12
+ dot?: boolean;
13
+ }>(),
14
+ {
15
+ tone: undefined,
16
+ dot: false,
17
+ },
18
+ );
19
+ </script>
20
+
21
+ <style>
22
+ @import './badge.css';
23
+ </style>
@@ -0,0 +1,56 @@
1
+ @layer components {
2
+ .pui-badge {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: var(--control-gap);
6
+ min-height: var(--control-height-sm);
7
+ padding-inline: var(--control-inline-pad-sm);
8
+ font-size: var(--step--2);
9
+ line-height: var(--lh-control);
10
+ font-weight: var(--fw-control);
11
+ border-radius: var(--radius-pill);
12
+ background: var(--bg-clr-surface-2);
13
+ color: var(--text-clr-base);
14
+ border: var(--stroke-sm) solid var(--border-clr-base);
15
+ white-space: nowrap;
16
+
17
+ & .pui-badge__dot {
18
+ width: 0.5em;
19
+ height: 0.5em;
20
+ border-radius: 50%;
21
+ background: currentColor;
22
+ }
23
+
24
+ /* ===== tones ===== */
25
+
26
+ &[data-tone='brand'] {
27
+ background: var(--bg-clr-brand-soft);
28
+ color: var(--text-clr-brand);
29
+ border-color: transparent;
30
+ }
31
+
32
+ &[data-tone='success'] {
33
+ background: var(--bg-clr-success-soft);
34
+ color: var(--text-clr-success);
35
+ border-color: transparent;
36
+ }
37
+
38
+ &[data-tone='warning'] {
39
+ background: var(--bg-clr-warning-soft);
40
+ color: var(--text-clr-warning);
41
+ border-color: transparent;
42
+ }
43
+
44
+ &[data-tone='danger'] {
45
+ background: var(--bg-clr-danger-soft);
46
+ color: var(--text-clr-danger);
47
+ border-color: transparent;
48
+ }
49
+
50
+ &[data-tone='solid'] {
51
+ background: var(--bg-clr-dark);
52
+ color: var(--text-clr-inverse);
53
+ border-color: transparent;
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ import './empty.css';
3
+
4
+ const { ...rest } = Astro.props;
5
+ ---
6
+
7
+ <div class="pui-empty" {...rest}>
8
+ <slot />
9
+ </div>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="pui-empty">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts"></script>
8
+
9
+ <style>
10
+ @import './empty.css';
11
+ </style>
@@ -0,0 +1,39 @@
1
+ @layer components {
2
+ .pui-empty {
3
+ display: grid;
4
+ place-items: center;
5
+ text-align: center;
6
+ padding: var(--space-xl) var(--space-m);
7
+ border: 1.5px dashed var(--border-clr-strong);
8
+ border-radius: var(--radius-md);
9
+ background: var(--bg-clr-surface);
10
+ gap: var(--space-xs);
11
+ }
12
+ .pui-empty__icon {
13
+ width: 3rem;
14
+ height: 3rem;
15
+ border-radius: var(--radius-sm);
16
+ border: var(--stroke-md) solid var(--border-clr-strong);
17
+ background: var(--bg-clr-accent);
18
+ display: grid;
19
+ place-items: center;
20
+ font-size: var(--step-1);
21
+ }
22
+ .pui-empty__title {
23
+ font-family: var(--ff-display);
24
+ font-weight: var(--fw-display);
25
+ letter-spacing: var(--ls-display);
26
+ font-size: var(--step-1);
27
+ margin: 0;
28
+ }
29
+ .pui-empty__body {
30
+ margin: 0;
31
+ color: var(--text-clr-muted);
32
+ max-width: 38ch;
33
+ }
34
+ .pui-empty__actions {
35
+ display: flex;
36
+ gap: var(--space-2xs);
37
+ margin-block-start: var(--space-2xs);
38
+ }
39
+ }
@@ -0,0 +1,52 @@
1
+ ---
2
+ import './icon.css';
3
+ import { icons, type IconName } from '../../../icons/index.js';
4
+
5
+ interface Props {
6
+ /** Shipped icon to render. Omit and pass a custom <svg> via the default slot. */
7
+ name?: IconName;
8
+ size?: 'sm' | 'md' | 'lg';
9
+ /** Continuous animation applied to the glyph (e.g. a loading spinner). */
10
+ animation?: 'spin';
11
+ label?: string;
12
+ }
13
+
14
+ const { name, size = 'md', animation, label, ...rest } = Astro.props;
15
+ const hasSlot = Astro.slots.has('default');
16
+
17
+ // slot wins over name; flag the redundant combination in development
18
+ if (import.meta.env.DEV && name && hasSlot) {
19
+ console.warn(
20
+ '[pienter-ui] Icon received both a `name` and slot content; the slot is used and `name` is ignored.',
21
+ );
22
+ }
23
+
24
+ const svg = name ? icons[name] : '';
25
+ ---
26
+
27
+ {
28
+ hasSlot ? (
29
+ <span
30
+ class="pui-icon"
31
+ data-size={size}
32
+ data-animation={animation}
33
+ aria-hidden={!label}
34
+ aria-label={label}
35
+ role={label ? 'img' : undefined}
36
+ {...rest}
37
+ >
38
+ <slot />
39
+ </span>
40
+ ) : (
41
+ <span
42
+ class="pui-icon"
43
+ data-size={size}
44
+ data-animation={animation}
45
+ aria-hidden={!label}
46
+ aria-label={label}
47
+ role={label ? 'img' : undefined}
48
+ {...rest}
49
+ set:html={svg}
50
+ />
51
+ )
52
+ }
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <span v-if="hasSlot" v-bind="iconAttrs"><slot /></span>
3
+ <!--
4
+ `svg` is a build-time constant read from the first-party icon registry
5
+ (icons/index.ts) keyed by the typed `IconName` prop; it is never user- or
6
+ network-supplied, so there is no XSS surface. v-html is required to inline
7
+ the raw SVG markup.
8
+ -->
9
+ <!-- eslint-disable-next-line vue/no-v-html -->
10
+ <span v-else v-bind="iconAttrs" v-html="svg" />
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed, useSlots } from 'vue';
15
+ import { icons, type IconName } from '../../../icons/index.js';
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ /** Shipped icon to render. Omit and pass a custom <svg> via the default slot. */
20
+ name?: IconName;
21
+ size?: 'sm' | 'md' | 'lg';
22
+ /** Continuous animation applied to the glyph (e.g. a loading spinner). */
23
+ animation?: 'spin';
24
+ label?: string;
25
+ }>(),
26
+ {
27
+ name: undefined,
28
+ size: 'md',
29
+ animation: undefined,
30
+ label: undefined,
31
+ },
32
+ );
33
+
34
+ const slots = useSlots();
35
+ const hasSlot = computed(() => !!slots.default);
36
+
37
+ // slot wins over name; flag the redundant combination in development
38
+ if (props.name && slots.default) {
39
+ console.warn(
40
+ '[pienter-ui] Icon received both a `name` and slot content; the slot is used and `name` is ignored.',
41
+ );
42
+ }
43
+
44
+ const svg = computed(() => (props.name ? icons[props.name] : ''));
45
+ const iconAttrs = computed(() => ({
46
+ class: 'pui-icon',
47
+ 'data-size': props.size,
48
+ 'data-animation': props.animation,
49
+ 'aria-hidden': !props.label,
50
+ 'aria-label': props.label,
51
+ role: props.label ? 'img' : undefined,
52
+ }));
53
+ </script>
54
+
55
+ <style>
56
+ @import './icon.css';
57
+ </style>
@@ -0,0 +1,47 @@
1
+ @layer components {
2
+ .pui-icon {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ flex-shrink: 0;
7
+ line-height: 1;
8
+
9
+ & svg {
10
+ display: block;
11
+ }
12
+
13
+ /* ===== sizes ===== */
14
+
15
+ &[data-size='sm'] svg {
16
+ width: 1rem;
17
+ height: 1rem;
18
+ }
19
+
20
+ &[data-size='md'] svg {
21
+ width: 1.25rem;
22
+ height: 1.25rem;
23
+ }
24
+
25
+ &[data-size='lg'] svg {
26
+ width: 1.5rem;
27
+ height: 1.5rem;
28
+ }
29
+
30
+ /* ===== animations ===== */
31
+
32
+ /* spins the square wrapper, so the pivot is the glyph's own centre */
33
+ &[data-animation='spin'] {
34
+ animation: pui-icon-spin var(--duration-slow) linear infinite;
35
+
36
+ @media (prefers-reduced-motion: reduce) {
37
+ animation: none;
38
+ }
39
+ }
40
+ }
41
+
42
+ @keyframes pui-icon-spin {
43
+ to {
44
+ transform: rotate(360deg);
45
+ }
46
+ }
47
+ }