@pienter/ui 0.3.0 → 0.7.1

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 (109) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/CONVENTIONS.md +342 -385
  3. package/README.md +33 -18
  4. package/components/display/record-details/RecordDetails.vue +61 -0
  5. package/components/display/record-details/record-details.css +37 -0
  6. package/components/display/record-details/types.ts +8 -0
  7. package/components/feedback/toast/toast.css +1 -1
  8. package/components/form/block-editor/BlockEditor.vue +454 -0
  9. package/components/form/block-editor/block-editor.css +149 -0
  10. package/components/form/block-editor/types.ts +15 -0
  11. package/components/form/combobox/Combobox.vue +42 -72
  12. package/components/form/combobox/combobox.css +1 -1
  13. package/components/form/form/Form.vue +1 -2
  14. package/components/form/label/label.css +1 -1
  15. package/components/form/number-field/NumberField.vue +1 -2
  16. package/components/form/number-field/number-field.css +1 -1
  17. package/components/form/radio-group/RadioGroup.vue +2 -5
  18. package/components/form/record-form/RecordFields.vue +128 -0
  19. package/components/form/record-form/RecordForm.vue +116 -0
  20. package/components/form/record-form/fields.ts +20 -0
  21. package/components/form/record-form/record-form.css +15 -0
  22. package/components/form/record-form/types.ts +28 -0
  23. package/components/form/slider/slider.css +2 -3
  24. package/components/form/tags-input/tags-input.css +1 -2
  25. package/components/form/text-input/text-input.css +2 -0
  26. package/components/form/textarea/textarea.css +1 -1
  27. package/components/layout/accordion/Accordion.vue +6 -27
  28. package/components/layout/collapsible/Collapsible.vue +9 -19
  29. package/components/layout/index/Index.vue +353 -0
  30. package/components/layout/index/index.css +114 -0
  31. package/components/layout/index/useIndex.ts +390 -0
  32. package/components/layout/table/table.css +5 -4
  33. package/components/navigation/breadcrumb/Breadcrumb.vue +24 -5
  34. package/components/navigation/breadcrumb/breadcrumb.css +15 -0
  35. package/components/navigation/sidebar/Sidebar.vue +329 -84
  36. package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
  37. package/components/navigation/sidebar/context.ts +21 -0
  38. package/components/navigation/sidebar/sidebar.css +346 -78
  39. package/components/navigation/sidebar/types.ts +13 -1
  40. package/components/navigation/tabs/Tabs.vue +6 -0
  41. package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
  42. package/components/overlay/command/Command.vue +10 -38
  43. package/components/overlay/command/command.css +1 -1
  44. package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
  45. package/components/overlay/modal/Modal.vue +7 -28
  46. package/components/overlay/popover/Popover.vue +9 -31
  47. package/components/overlay/sheet/Sheet.vue +7 -28
  48. package/components/overlay/tooltip/Tooltip.vue +14 -19
  49. package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
  50. package/composables/useDisclosure.ts +113 -0
  51. package/composables/useEventListener.ts +16 -0
  52. package/composables/useMenu.ts +212 -0
  53. package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
  54. package/package.json +16 -40
  55. package/styles/0-settings/colors.css +10 -0
  56. package/styles/0-settings/layout.css +18 -0
  57. package/styles/0-settings/motion.css +2 -2
  58. package/styles/0-settings/spacing.css +3 -1
  59. package/utils/a11y/focus.ts +9 -3
  60. package/utils/cms/index.ts +283 -0
  61. package/utils/cms/schema.json +126 -0
  62. package/utils/navigation/sidebar.ts +97 -0
  63. package/utils/validation/form.ts +6 -9
  64. package/components/action/button/Button.astro +0 -95
  65. package/components/action/button/IconButton.astro +0 -86
  66. package/components/display/avatar/Avatar.astro +0 -17
  67. package/components/display/avatar/AvatarStack.astro +0 -9
  68. package/components/display/badge/Badge.astro +0 -15
  69. package/components/display/empty/Empty.astro +0 -9
  70. package/components/display/icon/Icon.astro +0 -52
  71. package/components/feedback/alert/Alert.astro +0 -52
  72. package/components/feedback/progress/Progress.astro +0 -68
  73. package/components/feedback/skeleton/Skeleton.astro +0 -32
  74. package/components/feedback/spinner/Spinner.astro +0 -25
  75. package/components/feedback/toast/Toast.astro +0 -50
  76. package/components/form/checkbox/Checkbox.astro +0 -79
  77. package/components/form/date-input/DateInput.astro +0 -105
  78. package/components/form/form/Form.astro +0 -106
  79. package/components/form/input-otp/InputOTP.astro +0 -147
  80. package/components/form/label/Label.astro +0 -13
  81. package/components/form/number-field/NumberField.astro +0 -142
  82. package/components/form/radio-group/RadioGroup.astro +0 -105
  83. package/components/form/select/Select.astro +0 -105
  84. package/components/form/slider/Slider.astro +0 -205
  85. package/components/form/switch/Switch.astro +0 -75
  86. package/components/form/tags-input/TagsInput.astro +0 -153
  87. package/components/form/text-input/TextInput.astro +0 -84
  88. package/components/form/textarea/Textarea.astro +0 -86
  89. package/components/layout/card/Card.astro +0 -13
  90. package/components/layout/separator/Separator.astro +0 -31
  91. package/components/layout/table/Table.astro +0 -116
  92. package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
  93. package/components/navigation/navbar/Navbar.astro +0 -62
  94. package/components/navigation/pagination/PaginationFooter.astro +0 -24
  95. package/components/navigation/sidebar/Sidebar.astro +0 -132
  96. package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
  97. package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
  98. package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
  99. package/components/overlay/modal/Modal.astro +0 -66
  100. package/utils/controllers/disclosure.ts +0 -117
  101. package/utils/controllers/form.ts +0 -524
  102. package/utils/controllers/index.ts +0 -39
  103. package/utils/controllers/menu.ts +0 -255
  104. package/utils/controllers/number-field.ts +0 -103
  105. package/utils/controllers/otp.ts +0 -252
  106. package/utils/controllers/sidebar.ts +0 -610
  107. package/utils/controllers/slider.ts +0 -336
  108. package/utils/controllers/tags-input.ts +0 -255
  109. package/utils/controllers/toast.ts +0 -426
@@ -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>
@@ -1,79 +0,0 @@
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>
@@ -1,105 +0,0 @@
1
- ---
2
- import './date-input.css';
3
- import { generateId } from '../../../utils/a11y/id.js';
4
-
5
- interface Props {
6
- id?: string;
7
- label: string;
8
- name?: string;
9
- /**
10
- * Native input type. `month` and `week` are intentionally out of
11
- * scope for v1 because their browser support is uneven (notably
12
- * Safari) — see CONVENTIONS DateInput audit entry.
13
- */
14
- type?: 'date' | 'time' | 'datetime-local';
15
- /**
16
- * Current ISO-formatted value. The exact shape depends on `type`:
17
- * - `date` → `YYYY-MM-DD`
18
- * - `time` → `HH:MM` (or `HH:MM:SS` when `step` enables seconds)
19
- * - `datetime-local` → `YYYY-MM-DDTHH:MM`
20
- */
21
- value?: string;
22
- /** Lower bound, in the same ISO shape as `value`. Forwarded to native `min`. */
23
- min?: string;
24
- /** Upper bound, in the same ISO shape as `value`. Forwarded to native `max`. */
25
- max?: string;
26
- /**
27
- * Native step granularity. Useful for `type="time"`:
28
- * `step="60"` (default) gives minute precision; `step="1"`
29
- * exposes seconds in the picker.
30
- */
31
- step?: number | string;
32
- required?: boolean;
33
- disabled?: boolean;
34
- hint?: string;
35
- errors?: string[];
36
- status?: 'error' | 'success';
37
- }
38
-
39
- const {
40
- id,
41
- label,
42
- name,
43
- type = 'date',
44
- value = '',
45
- min,
46
- max,
47
- step,
48
- required = false,
49
- disabled = false,
50
- hint,
51
- errors = [],
52
- status,
53
- ...rest
54
- } = Astro.props;
55
-
56
- const inputId = id ?? generateId('input');
57
- const hintId = `${inputId}-hint`;
58
- const errorsId = `${inputId}-errors`;
59
-
60
- const hasErrors = errors.length > 0;
61
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
62
-
63
- const describedByIds: string[] = [];
64
- if (hint) describedByIds.push(hintId);
65
- if (hasErrors) describedByIds.push(errorsId);
66
- const describedBy =
67
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
68
- ---
69
-
70
- <div class="pui-field" data-status={computedStatus}>
71
- <label class="pui-field__label" for={inputId}>
72
- {label}
73
- </label>
74
- <input
75
- class="pui-input"
76
- id={inputId}
77
- type={type}
78
- name={name}
79
- value={value}
80
- min={min}
81
- max={max}
82
- step={step}
83
- required={required}
84
- disabled={disabled}
85
- aria-invalid={hasErrors ? 'true' : undefined}
86
- aria-describedby={describedBy}
87
- {...rest}
88
- />
89
- {
90
- hint && (
91
- <p id={hintId} class="pui-field__hint">
92
- {hint}
93
- </p>
94
- )
95
- }
96
- {
97
- hasErrors && (
98
- <ul id={errorsId} class="pui-field__hint" role="alert">
99
- {errors.map((error) => (
100
- <li>{error}</li>
101
- ))}
102
- </ul>
103
- )
104
- }
105
- </div>