@pienter/ui 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +259 -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/pagination/Pagination.vue +1 -1
  19. package/components/navigation/pagination/PaginationFooter.vue +23 -0
  20. package/components/navigation/pagination/pagination.css +128 -29
  21. package/components/navigation/sidebar/Sidebar.vue +326 -84
  22. package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
  23. package/components/navigation/sidebar/context.ts +21 -0
  24. package/components/navigation/sidebar/sidebar.css +346 -78
  25. package/components/navigation/sidebar/types.ts +13 -1
  26. package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
  27. package/components/overlay/command/Command.vue +10 -38
  28. package/components/overlay/command/command.css +1 -1
  29. package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
  30. package/components/overlay/modal/Modal.vue +7 -28
  31. package/components/overlay/popover/Popover.vue +9 -31
  32. package/components/overlay/sheet/Sheet.vue +7 -28
  33. package/components/overlay/tooltip/Tooltip.vue +14 -19
  34. package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
  35. package/composables/useDisclosure.ts +113 -0
  36. package/composables/useEventListener.ts +16 -0
  37. package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
  38. package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
  39. package/package.json +6 -38
  40. package/styles/0-settings/colors.css +8 -1
  41. package/styles/0-settings/layout.css +18 -0
  42. package/styles/0-settings/motion.css +2 -2
  43. package/styles/0-settings/spacing.css +3 -1
  44. package/utils/navigation/sidebar.ts +97 -0
  45. package/utils/validation/form.ts +6 -9
  46. package/components/action/button/Button.astro +0 -95
  47. package/components/action/button/IconButton.astro +0 -86
  48. package/components/display/avatar/Avatar.astro +0 -17
  49. package/components/display/avatar/AvatarStack.astro +0 -9
  50. package/components/display/badge/Badge.astro +0 -15
  51. package/components/display/empty/Empty.astro +0 -9
  52. package/components/display/icon/Icon.astro +0 -52
  53. package/components/feedback/alert/Alert.astro +0 -52
  54. package/components/feedback/progress/Progress.astro +0 -68
  55. package/components/feedback/skeleton/Skeleton.astro +0 -32
  56. package/components/feedback/spinner/Spinner.astro +0 -25
  57. package/components/feedback/toast/Toast.astro +0 -50
  58. package/components/form/checkbox/Checkbox.astro +0 -79
  59. package/components/form/date-input/DateInput.astro +0 -105
  60. package/components/form/form/Form.astro +0 -106
  61. package/components/form/input-otp/InputOTP.astro +0 -147
  62. package/components/form/label/Label.astro +0 -13
  63. package/components/form/number-field/NumberField.astro +0 -142
  64. package/components/form/radio-group/RadioGroup.astro +0 -105
  65. package/components/form/select/Select.astro +0 -105
  66. package/components/form/slider/Slider.astro +0 -205
  67. package/components/form/switch/Switch.astro +0 -75
  68. package/components/form/tags-input/TagsInput.astro +0 -153
  69. package/components/form/text-input/TextInput.astro +0 -84
  70. package/components/form/textarea/Textarea.astro +0 -86
  71. package/components/layout/card/Card.astro +0 -13
  72. package/components/layout/separator/Separator.astro +0 -31
  73. package/components/layout/table/Table.astro +0 -116
  74. package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
  75. package/components/navigation/navbar/Navbar.astro +0 -62
  76. package/components/navigation/sidebar/Sidebar.astro +0 -132
  77. package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
  78. package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
  79. package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
  80. package/components/overlay/modal/Modal.astro +0 -66
  81. package/utils/controllers/disclosure.ts +0 -117
  82. package/utils/controllers/form.ts +0 -524
  83. package/utils/controllers/index.ts +0 -39
  84. package/utils/controllers/number-field.ts +0 -103
  85. package/utils/controllers/otp.ts +0 -252
  86. package/utils/controllers/sidebar.ts +0 -610
  87. package/utils/controllers/slider.ts +0 -336
  88. package/utils/controllers/tags-input.ts +0 -255
  89. package/utils/controllers/toast.ts +0 -426
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Sidebar primitives, applied reactively by Sidebar.vue. See ADR 0007.
3
+ */
4
+
5
+ /** A group's expanded/flyout state, as the templates render it. */
6
+ export type SubmenuState = 'open' | 'closed';
7
+
8
+ /** Grace period before an unpinned flyout closes on pointer-out. */
9
+ export const SUBMENU_CLOSE_DELAY = 140;
10
+
11
+ /**
12
+ * Hierarchical-href match — locked algorithm. Returns true when
13
+ * `itemHref` is the prefix of `activeHref` honoring path boundaries.
14
+ *
15
+ * - `/` (root) is special-cased to strict equality so it doesn't
16
+ * greedily match every URL.
17
+ * - Substring-only matches are rejected (`/admin/members-old` does
18
+ * NOT match `/admin/members`).
19
+ *
20
+ * See CONVENTIONS.md § Active-href hierarchical-match algorithm.
21
+ */
22
+ export function isHrefActive(
23
+ activeHref: string | null | undefined,
24
+ itemHref: string,
25
+ ): boolean {
26
+ if (!activeHref) return false;
27
+ if (itemHref === '/') return activeHref === '/';
28
+ if (activeHref === itemHref) return true;
29
+ return activeHref.startsWith(itemHref + '/');
30
+ }
31
+
32
+ /** Kebab-case slug used as a group's key when none was supplied. */
33
+ export function deriveGroupKey(label: string): string {
34
+ return label
35
+ .normalize('NFKD')
36
+ .replace(/[̀-ͯ]/g, '')
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, '-')
39
+ .replace(/^-+|-+$/g, '');
40
+ }
41
+
42
+ /** Viewport coordinates for a collapsed-rail flyout. */
43
+ export interface SubmenuPosition {
44
+ left: number;
45
+ top: number;
46
+ height: number;
47
+ }
48
+
49
+ /**
50
+ * Where a collapsed rail's flyout sits: flush with the rail, starting below
51
+ * the brand if there is one, running the column's full height. Physical
52
+ * `left` because RTL is deferred library-wide.
53
+ */
54
+ export function computeSubmenuPosition(
55
+ railRect: DOMRect,
56
+ brandRect: DOMRect | null,
57
+ ): SubmenuPosition {
58
+ const top = brandRect ? brandRect.bottom : railRect.top;
59
+ return {
60
+ left: railRect.right,
61
+ top,
62
+ height: Math.max(0, railRect.bottom - top),
63
+ };
64
+ }
65
+
66
+ /** Below the drawer breakpoint there is no rail, so no flyouts. */
67
+ export function isSubmenuActive(options: {
68
+ policy: 'hidden' | 'panel';
69
+ collapsed: boolean;
70
+ viewportWidth: number;
71
+ drawerBreakpoint: number;
72
+ }): boolean {
73
+ return (
74
+ options.policy === 'panel' &&
75
+ options.collapsed &&
76
+ options.viewportWidth >= options.drawerBreakpoint
77
+ );
78
+ }
79
+
80
+ /** Whether a group's subtree contains the current page. */
81
+ export function groupContainsActive(
82
+ items: readonly SidebarItemLike[],
83
+ activeHref: string | null | undefined,
84
+ ): boolean {
85
+ return items.some((item) => {
86
+ if (item.type === 'link') return isHrefActive(activeHref, item.href);
87
+ if (item.type === 'group')
88
+ return groupContainsActive(item.items, activeHref);
89
+ return false;
90
+ });
91
+ }
92
+
93
+ /** The part of `SidebarItem` these primitives read; avoids a component import. */
94
+ export type SidebarItemLike =
95
+ | { type: 'link'; href: string }
96
+ | { type: 'group'; items: readonly SidebarItemLike[] }
97
+ | { type: 'section' };
@@ -1,13 +1,10 @@
1
1
  import { type Validator, validate } from './rules.js';
2
2
 
3
3
  /**
4
- * Placeholder shape for form-submit responses, consumed identically by both
5
- * `Form.vue` (reactive orchestration) and `controllers/form.ts:mountForm`
6
- * (Astro imperative orchestration). The symmetry requirement is locked in
7
- * design D13 / spec "Unified Form Submit Response Handling"; the exact
8
- * shape is **subject to change when ts-sdk publishes the canonical
9
- * response shape**, at which point both implementations are updated
10
- * together.
4
+ * Placeholder shape for form-submit responses, consumed by `Form.vue`'s
5
+ * orchestration. Locked in design D13 / spec "Unified Form Submit Response
6
+ * Handling"; the exact shape is **subject to change when ts-sdk publishes
7
+ * the canonical response shape**.
11
8
  */
12
9
  export interface FormSubmitResponse {
13
10
  /** Whether the submit succeeded. */
@@ -28,8 +25,8 @@ export interface FormSubmitResponse {
28
25
 
29
26
  /**
30
27
  * Run a per-field validator map against a `FormData` instance and return
31
- * a `Record<string, string[]>` of field errors. Pure function; framework
32
- * agnostic. Used by both Vue and Astro Form orchestration.
28
+ * a `Record<string, string[]>` of field errors. Pure function; used by
29
+ * Form.vue's orchestration.
33
30
  *
34
31
  * Field values are read via `formData.get(name)` and coerced to strings.
35
32
  * Fields without an entry in `validators` are not checked. Fields whose
@@ -1,95 +0,0 @@
1
- ---
2
- import './button.css';
3
- import Icon from '../../display/icon/Icon.astro';
4
- import Spinner from '../../feedback/spinner/Spinner.astro';
5
- import type { IconName } from '../../../icons/index.js';
6
-
7
- interface Props {
8
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
9
- size?: 'sm' | 'md';
10
- disabled?: boolean;
11
- loading?: boolean;
12
- type?: 'button' | 'submit' | 'reset';
13
- block?: boolean;
14
- href?: string;
15
- /** Leading icon from the shipped set. Use the `icon` slot for a custom <svg>. */
16
- icon?: IconName;
17
- /** Anchor target, only applied when `href` is set (renders an <a>). */
18
- target?: string;
19
- /** Anchor rel; defaults to a safe value for `target="_blank"`. */
20
- rel?: string;
21
- id?: string;
22
- }
23
-
24
- const {
25
- variant = 'secondary',
26
- size = 'md',
27
- disabled = false,
28
- loading = false,
29
- type = 'button',
30
- block = false,
31
- href,
32
- icon,
33
- target,
34
- rel,
35
- id,
36
- ...rest
37
- } = Astro.props;
38
-
39
- const Tag = href ? 'a' : 'button';
40
- const isInert = disabled || loading;
41
- // Button tier -> Icon size for the leading glyph (the Spinner is sized in button.css)
42
- const iconSize = size === 'sm' ? 'sm' : 'md';
43
- const hasIconSlot = Astro.slots.has('icon');
44
- const hasIcon = !!icon || hasIconSlot;
45
- const resolvedRel = href
46
- ? (rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined))
47
- : undefined;
48
-
49
- // the icon slot wins over the `icon` name; flag the redundant combination in dev
50
- if (import.meta.env.DEV && icon && hasIconSlot) {
51
- console.warn(
52
- '[pienter-ui] Button received both an `icon` name and an `icon` slot; the slot is used and `icon` is ignored.',
53
- );
54
- }
55
- ---
56
-
57
- <Tag
58
- id={id}
59
- class="pui-btn"
60
- type={href ? undefined : type}
61
- href={href}
62
- target={href ? target : undefined}
63
- rel={resolvedRel}
64
- disabled={href ? undefined : isInert}
65
- aria-disabled={href && isInert ? 'true' : undefined}
66
- tabindex={href && isInert ? '-1' : undefined}
67
- aria-busy={loading ? 'true' : undefined}
68
- data-variant={variant}
69
- data-size={size}
70
- data-block={block ? 'true' : undefined}
71
- data-state={loading ? 'loading' : undefined}
72
- {...rest}
73
- >
74
- {
75
- loading && (
76
- <span class="pui-btn__spinner" aria-hidden="true">
77
- <Spinner />
78
- </span>
79
- )
80
- }
81
- {
82
- hasIcon && (
83
- <span class="pui-btn__icon" aria-hidden="true">
84
- {hasIconSlot ? (
85
- <Icon size={iconSize}>
86
- <slot name="icon" />
87
- </Icon>
88
- ) : (
89
- <Icon name={icon} size={iconSize} />
90
- )}
91
- </span>
92
- )
93
- }
94
- <span class="pui-btn__label"><slot /></span>
95
- </Tag>
@@ -1,86 +0,0 @@
1
- ---
2
- import './button.css';
3
- import Icon from '../../display/icon/Icon.astro';
4
- import Spinner from '../../feedback/spinner/Spinner.astro';
5
- import type { IconName } from '../../../icons/index.js';
6
-
7
- interface Props {
8
- /** Accessible name for the icon-only control (required — there is no visible label). */
9
- label: string;
10
- /** Shipped icon to render. Omit and pass your own SVG via the default slot instead. */
11
- name?: IconName;
12
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
13
- size?: 'sm' | 'md';
14
- disabled?: boolean;
15
- loading?: boolean;
16
- type?: 'button' | 'submit' | 'reset';
17
- href?: string;
18
- /** Anchor target, only applied when `href` is set (renders an <a>). */
19
- target?: string;
20
- /** Anchor rel; defaults to a safe value for `target="_blank"`. */
21
- rel?: string;
22
- id?: string;
23
- }
24
-
25
- const {
26
- label,
27
- name,
28
- variant = 'secondary',
29
- size = 'md',
30
- disabled = false,
31
- loading = false,
32
- type = 'button',
33
- href,
34
- target,
35
- rel,
36
- id,
37
- ...rest
38
- } = Astro.props;
39
-
40
- const Tag = href ? 'a' : 'button';
41
- const isInert = disabled || loading;
42
- const iconSize = size === 'sm' ? 'sm' : 'md';
43
- const hasSlot = Astro.slots.has('default');
44
- const resolvedRel = href
45
- ? (rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined))
46
- : undefined;
47
-
48
- // the slot wins over the `name`; flag the redundant combination in dev
49
- if (import.meta.env.DEV && name && hasSlot) {
50
- console.warn(
51
- '[pienter-ui] IconButton received both a `name` and slot content; the slot is used and `name` is ignored.',
52
- );
53
- }
54
- ---
55
-
56
- <Tag
57
- id={id}
58
- class="pui-icon-btn"
59
- type={href ? undefined : type}
60
- href={href}
61
- target={href ? target : undefined}
62
- rel={resolvedRel}
63
- disabled={href ? undefined : isInert}
64
- aria-disabled={href && isInert ? 'true' : undefined}
65
- tabindex={href && isInert ? '-1' : undefined}
66
- aria-busy={loading ? 'true' : undefined}
67
- aria-label={label}
68
- data-variant={variant}
69
- data-size={size}
70
- data-state={loading ? 'loading' : undefined}
71
- {...rest}
72
- >
73
- {
74
- loading ? (
75
- <span class="pui-btn__spinner" aria-hidden="true">
76
- <Spinner />
77
- </span>
78
- ) : hasSlot ? (
79
- <Icon size={iconSize}>
80
- <slot />
81
- </Icon>
82
- ) : (
83
- <Icon name={name} size={iconSize} />
84
- )
85
- }
86
- </Tag>
@@ -1,17 +0,0 @@
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>
@@ -1,9 +0,0 @@
1
- ---
2
- import './avatar.css';
3
-
4
- const { ...rest } = Astro.props;
5
- ---
6
-
7
- <div class="pui-avatar-stack" {...rest}>
8
- <slot />
9
- </div>
@@ -1,15 +0,0 @@
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>
@@ -1,9 +0,0 @@
1
- ---
2
- import './empty.css';
3
-
4
- const { ...rest } = Astro.props;
5
- ---
6
-
7
- <div class="pui-empty" {...rest}>
8
- <slot />
9
- </div>
@@ -1,52 +0,0 @@
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
- }
@@ -1,52 +0,0 @@
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>
@@ -1,68 +0,0 @@
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>
@@ -1,32 +0,0 @@
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>
@@ -1,25 +0,0 @@
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>
@@ -1,50 +0,0 @@
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>