@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,106 +0,0 @@
1
- ---
2
- import './form.css';
3
- import { generateId } from '../../../utils/a11y/id.js';
4
-
5
- interface Props {
6
- /** Stable form ID — used by the consumer's `mountForm` script. */
7
- id?: string;
8
- submitLabel?: string;
9
- busy?: boolean;
10
- /** Top-level errors rendered in the error summary. */
11
- errors?: string[];
12
- /** Success/status message rendered in the polite live region. */
13
- statusMessage?: string;
14
- }
15
-
16
- const {
17
- id,
18
- submitLabel = 'Submit',
19
- busy = false,
20
- errors = [],
21
- statusMessage,
22
- } = Astro.props;
23
-
24
- const formId = id ?? generateId('form');
25
- ---
26
-
27
- <form
28
- class="pui-form"
29
- id={formId}
30
- data-pui-form
31
- novalidate
32
- aria-busy={busy ? 'true' : undefined}
33
- >
34
- {
35
- errors.length > 0 && (
36
- <div class="pui-form__error-summary" role="alert" aria-live="assertive">
37
- <p class="pui-form__error-summary-title">There was a problem</p>
38
- <ul class="pui-form__error-summary-list">
39
- {errors.map((error) => (
40
- <li>{error}</li>
41
- ))}
42
- </ul>
43
- </div>
44
- )
45
- }
46
-
47
- <slot />
48
-
49
- {
50
- statusMessage && (
51
- <div class="pui-form__status" aria-live="polite">
52
- {statusMessage}
53
- </div>
54
- )
55
- }
56
-
57
- <div class="pui-form__actions">
58
- <slot name="actions">
59
- <button
60
- class="pui-btn"
61
- data-variant="primary"
62
- type="submit"
63
- disabled={busy}
64
- >
65
- {submitLabel}
66
- </button>
67
- </slot>
68
- </div>
69
- </form>
70
-
71
- {
72
- /*
73
- Form.astro renders chrome only — it deliberately does NOT auto-mount
74
- `mountForm`. The controller takes `validators` and an async `onSubmit`
75
- handler, both of which are functions that can't be passed as Astro
76
- props. Consumers wire the controller themselves from a `<script>`
77
- block:
78
-
79
- ---
80
- import Form from '@pienter/ui/components/Form.astro';
81
- import TextInput from '@pienter/ui/components/TextInput.astro';
82
- ---
83
- <Form id="signup" submitLabel="Create account">
84
- <TextInput label="Email" name="email" type="email" />
85
- </Form>
86
- <script>
87
- import { mountForm } from '@pienter/ui/utils/controllers/form.js';
88
- import { isRequired, isEmail } from '@pienter/ui/utils/validation/rules.js';
89
- const el = document.getElementById('signup');
90
- if (el instanceof HTMLFormElement) {
91
- mountForm(el, {
92
- validators: { email: [isRequired(), isEmail()] },
93
- onSubmit: async (data) => {
94
- const res = await fetch('/api/signup', { method: 'POST', body: data });
95
- if (res.ok) return { ok: true, statusMessage: 'Account created' };
96
- const body = await res.json();
97
- return { ok: false, fieldErrors: body.fieldErrors, errors: body.errors };
98
- },
99
- });
100
- }
101
- </script>
102
-
103
- This explicit mount pattern is the locked Astro Form contract per the
104
- Form audit entry in CONVENTIONS.md.
105
- */
106
- }
@@ -1,147 +0,0 @@
1
- ---
2
- import './input-otp.css';
3
- import { generateId } from '../../../utils/a11y/id.js';
4
-
5
- interface Props {
6
- id?: string;
7
- label: string;
8
- name?: string;
9
- /**
10
- * Initial OTP string. Length matches `length` (Astro pads with empty
11
- * characters or trims as needed before rendering slots).
12
- */
13
- value?: string;
14
- /** Number of slots (default 6). */
15
- length?: number;
16
- /**
17
- * Per-character validation pattern as a string (regex source). Default
18
- * is digits-only `^[0-9]$`. Pass an alphanumeric pattern as a regex
19
- * source string when the OTP code includes letters. The string is
20
- * stored on a `data-pattern` attribute so the controller can rehydrate
21
- * the regex post-mount.
22
- */
23
- pattern?: string;
24
- disabled?: boolean;
25
- hint?: string;
26
- errors?: string[];
27
- status?: 'error' | 'success';
28
- }
29
-
30
- const {
31
- id,
32
- label,
33
- name,
34
- value = '',
35
- length = 6,
36
- pattern,
37
- disabled = false,
38
- hint,
39
- errors = [],
40
- status,
41
- ...rest
42
- } = Astro.props;
43
-
44
- const firstSlotId = id ?? generateId('input');
45
- const hintId = `${firstSlotId}-hint`;
46
- const errorsId = `${firstSlotId}-errors`;
47
-
48
- const hasErrors = errors.length > 0;
49
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
50
-
51
- const describedByIds: string[] = [];
52
- if (hint) describedByIds.push(hintId);
53
- if (hasErrors) describedByIds.push(errorsId);
54
- const describedBy =
55
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
56
-
57
- // Pad / trim seed value to the slot count.
58
- const slots = Array.from({ length }, (_, i) => value[i] ?? '');
59
- ---
60
-
61
- <div class="pui-field" data-status={computedStatus}>
62
- <label class="pui-field__label" for={firstSlotId}>
63
- {label}
64
- </label>
65
- <div
66
- class="pui-input-otp"
67
- data-pui-input-otp
68
- data-disabled={disabled ? 'true' : undefined}
69
- data-pattern={pattern}
70
- role="group"
71
- >
72
- {
73
- slots.map((char, i) => (
74
- <input
75
- class="pui-input-otp__slot pui-input"
76
- type="text"
77
- inputmode="numeric"
78
- autocomplete="one-time-code"
79
- maxlength="1"
80
- id={i === 0 ? firstSlotId : `${firstSlotId}-slot-${i + 1}`}
81
- aria-label={`Digit ${i + 1} of ${length}`}
82
- value={char}
83
- disabled={disabled}
84
- aria-invalid={i === 0 && hasErrors ? 'true' : undefined}
85
- aria-describedby={i === 0 ? describedBy : undefined}
86
- {...(i === 0 ? rest : {})}
87
- />
88
- ))
89
- }
90
- {
91
- name && (
92
- <input
93
- class="pui-input-otp__hidden"
94
- type="hidden"
95
- name={name}
96
- value={value}
97
- />
98
- )
99
- }
100
- </div>
101
- {
102
- hint && (
103
- <p id={hintId} class="pui-field__hint">
104
- {hint}
105
- </p>
106
- )
107
- }
108
- {
109
- hasErrors && (
110
- <ul id={errorsId} class="pui-field__hint" role="alert">
111
- {errors.map((error) => (
112
- <li>{error}</li>
113
- ))}
114
- </ul>
115
- )
116
- }
117
- </div>
118
-
119
- <script>
120
- import { mountInputOTP } from '@pienter/ui/utils/controllers/otp.js';
121
-
122
- // Hoisted module script — Astro de-dupes across instances.
123
- function mountAll(): void {
124
- const wrappers = document.querySelectorAll<HTMLElement>(
125
- '[data-pui-input-otp]',
126
- );
127
- wrappers.forEach((wrapper) => {
128
- if (wrapper.dataset.vInputOtpMounted === 'true') return;
129
- let pattern: RegExp | undefined;
130
- if (wrapper.dataset.pattern) {
131
- try {
132
- pattern = new RegExp(wrapper.dataset.pattern);
133
- } catch {
134
- pattern = undefined;
135
- }
136
- }
137
- mountInputOTP(wrapper, { pattern });
138
- wrapper.dataset.vInputOtpMounted = 'true';
139
- });
140
- }
141
-
142
- if (document.readyState === 'loading') {
143
- document.addEventListener('DOMContentLoaded', mountAll, { once: true });
144
- } else {
145
- mountAll();
146
- }
147
- </script>
@@ -1,13 +0,0 @@
1
- ---
2
- import './label.css';
3
-
4
- interface Props {
5
- for?: string;
6
- }
7
-
8
- const { for: htmlFor, ...rest } = Astro.props;
9
- ---
10
-
11
- <label class="pui-field__label" for={htmlFor} {...rest}>
12
- <slot />
13
- </label>
@@ -1,142 +0,0 @@
1
- ---
2
- import './number-field.css';
3
- import { generateId } from '../../../utils/a11y/id.js';
4
-
5
- interface Props {
6
- id?: string;
7
- label: string;
8
- name?: string;
9
- /**
10
- * Initial numeric value. Astro renders the value once at SSR time;
11
- * dynamic updates are handled by the consumer's own scripts (see
12
- * the consumer-wires-their-own-script pattern in CONVENTIONS) or
13
- * by the increment/decrement buttons via `mountNumberField`.
14
- */
15
- value?: number | string;
16
- min?: number;
17
- max?: number;
18
- step?: number | string;
19
- placeholder?: string;
20
- required?: boolean;
21
- disabled?: boolean;
22
- hint?: string;
23
- errors?: string[];
24
- status?: 'error' | 'success';
25
- }
26
-
27
- const {
28
- id,
29
- label,
30
- name,
31
- value,
32
- min,
33
- max,
34
- step,
35
- placeholder,
36
- required = false,
37
- disabled = false,
38
- hint,
39
- errors = [],
40
- status,
41
- ...rest
42
- } = Astro.props;
43
-
44
- const inputId = id ?? generateId('input');
45
- const hintId = `${inputId}-hint`;
46
- const errorsId = `${inputId}-errors`;
47
-
48
- const hasErrors = errors.length > 0;
49
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
50
-
51
- const describedByIds: string[] = [];
52
- if (hint) describedByIds.push(hintId);
53
- if (hasErrors) describedByIds.push(errorsId);
54
- const describedBy =
55
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
56
- ---
57
-
58
- <div class="pui-field" data-status={computedStatus}>
59
- <label class="pui-field__label" for={inputId}>
60
- {label}
61
- </label>
62
- <div class="pui-number-field" data-pui-number-field>
63
- <input
64
- class="pui-number-field__input pui-input"
65
- type="number"
66
- id={inputId}
67
- name={name}
68
- min={min}
69
- max={max}
70
- step={step}
71
- placeholder={placeholder}
72
- required={required}
73
- disabled={disabled}
74
- value={value}
75
- aria-invalid={hasErrors ? 'true' : undefined}
76
- aria-describedby={describedBy}
77
- {...rest}
78
- />
79
- <div class="pui-number-field__steppers">
80
- <button
81
- type="button"
82
- class="pui-number-field__step"
83
- data-direction="increment"
84
- aria-label="Increase"
85
- disabled={disabled}
86
- >
87
- <span aria-hidden="true">+</span>
88
- </button>
89
- <button
90
- type="button"
91
- class="pui-number-field__step"
92
- data-direction="decrement"
93
- aria-label="Decrease"
94
- disabled={disabled}
95
- >
96
- <span aria-hidden="true">&#x2212;</span>
97
- </button>
98
- </div>
99
- </div>
100
- {
101
- hint && (
102
- <p id={hintId} class="pui-field__hint">
103
- {hint}
104
- </p>
105
- )
106
- }
107
- {
108
- hasErrors && (
109
- <ul id={errorsId} class="pui-field__hint" role="alert">
110
- {errors.map((error) => (
111
- <li>{error}</li>
112
- ))}
113
- </ul>
114
- )
115
- }
116
- </div>
117
-
118
- <script>
119
- import { mountNumberField } from '@pienter/ui/utils/controllers/number-field.js';
120
-
121
- // Hoisted module script — Astro de-dupes this across every NumberField
122
- // instance on the page. Mount the controller on each [data-pui-number-field]
123
- // wrapper present at DOM-ready. See CONVENTIONS § hoisted-vs-inline rule.
124
- function mountAll(): void {
125
- const wrappers = document.querySelectorAll<HTMLElement>(
126
- '[data-pui-number-field]',
127
- );
128
- wrappers.forEach((wrapper) => {
129
- // Idempotency guard: skip wrappers that have already been mounted
130
- // (e.g. on Astro view transitions / partial re-renders).
131
- if (wrapper.dataset.vNumberFieldMounted === 'true') return;
132
- mountNumberField(wrapper);
133
- wrapper.dataset.vNumberFieldMounted = 'true';
134
- });
135
- }
136
-
137
- if (document.readyState === 'loading') {
138
- document.addEventListener('DOMContentLoaded', mountAll, { once: true });
139
- } else {
140
- mountAll();
141
- }
142
- </script>
@@ -1,105 +0,0 @@
1
- ---
2
- import './radio-group.css';
3
- import { generateId } from '../../../utils/a11y/id.js';
4
- import type { RadioOption } from './types.js';
5
-
6
- interface Props {
7
- id?: string;
8
- label: string;
9
- name: string;
10
- options: RadioOption[];
11
- value?: string;
12
- disabled?: boolean;
13
- orientation?: 'vertical' | 'horizontal';
14
- hint?: string;
15
- errors?: string[];
16
- status?: 'error' | 'success';
17
- }
18
-
19
- const {
20
- id,
21
- label,
22
- name,
23
- options,
24
- value,
25
- disabled = false,
26
- orientation = 'vertical',
27
- hint,
28
- errors = [],
29
- status,
30
- ...rest
31
- } = Astro.props;
32
-
33
- const fieldId = id ?? generateId('radiogroup');
34
- const hintId = `${fieldId}-hint`;
35
- const errorsId = `${fieldId}-errors`;
36
-
37
- const hasErrors = errors.length > 0;
38
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
39
-
40
- const describedByIds: string[] = [];
41
- if (hint) describedByIds.push(hintId);
42
- if (hasErrors) describedByIds.push(errorsId);
43
- const describedBy =
44
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
45
- ---
46
-
47
- <fieldset
48
- id={fieldId}
49
- class="pui-field"
50
- data-status={computedStatus}
51
- role="radiogroup"
52
- aria-describedby={describedBy}
53
- aria-invalid={hasErrors ? 'true' : undefined}
54
- {...rest}
55
- >
56
- {
57
- /* aria-invalid lives on the <fieldset> (group-level signal) AND, when the
58
- Astro mountForm controller is wired, on the first radio. The redundancy
59
- is intentional — see CONVENTIONS.md "RadioGroup audit → ARIA
60
- expectations" for the rationale. Do NOT remove either binding without
61
- coordinating with the form controller. */
62
- }
63
- <legend class="pui-field__label">{label}</legend>
64
- {
65
- hint && (
66
- <p id={hintId} class="pui-field__hint">
67
- {hint}
68
- </p>
69
- )
70
- }
71
- <div class="pui-radio-group" data-orientation={orientation}>
72
- {
73
- options.map((option) => {
74
- const isChecked = value === option.value;
75
- const isDisabled = option.disabled || disabled;
76
- return (
77
- <label
78
- class="pui-radio-group__item"
79
- data-disabled={isDisabled ? 'true' : undefined}
80
- >
81
- <input
82
- class="pui-radio"
83
- type="radio"
84
- name={name}
85
- value={option.value}
86
- checked={isChecked}
87
- disabled={isDisabled}
88
- aria-checked={isChecked ? 'true' : 'false'}
89
- />
90
- <span class="pui-radio-group__label">{option.label}</span>
91
- </label>
92
- );
93
- })
94
- }
95
- </div>
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
- </fieldset>
@@ -1,105 +0,0 @@
1
- ---
2
- import './select.css';
3
- import Icon from '../../display/icon/Icon.astro';
4
- import { generateId } from '../../../utils/a11y/id.js';
5
-
6
- interface Props {
7
- id?: string;
8
- label?: string;
9
- name?: string;
10
- value?: string;
11
- disabled?: boolean;
12
- options: { value: string; label: string }[];
13
- placeholder?: string;
14
- hint?: string;
15
- errors?: string[];
16
- status?: 'error' | 'success';
17
- }
18
-
19
- const {
20
- id,
21
- label,
22
- name,
23
- value,
24
- disabled = false,
25
- options,
26
- placeholder,
27
- hint,
28
- errors = [],
29
- status,
30
- ...rest
31
- } = Astro.props;
32
-
33
- const inputId = id ?? generateId('select');
34
- const hintId = `${inputId}-hint`;
35
- const errorsId = `${inputId}-errors`;
36
-
37
- const hasErrors = errors.length > 0;
38
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
39
-
40
- const describedByIds: string[] = [];
41
- if (hint) describedByIds.push(hintId);
42
- if (hasErrors) describedByIds.push(errorsId);
43
- const describedBy =
44
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
45
- ---
46
-
47
- <div class="pui-field" data-status={computedStatus}>
48
- {
49
- label && (
50
- <label class="pui-field__label" for={inputId}>
51
- {label}
52
- </label>
53
- )
54
- }
55
- <div class="pui-select">
56
- <select
57
- class="pui-select__control"
58
- id={inputId}
59
- name={name}
60
- disabled={disabled}
61
- aria-invalid={hasErrors ? 'true' : undefined}
62
- aria-describedby={describedBy}
63
- {...rest}
64
- >
65
- {
66
- placeholder && (
67
- <option
68
- value=""
69
- disabled
70
- hidden
71
- selected={value === undefined || value === ''}
72
- >
73
- {placeholder}
74
- </option>
75
- )
76
- }
77
- {
78
- options.map((opt) => (
79
- <option value={opt.value} selected={value === opt.value}>
80
- {opt.label}
81
- </option>
82
- ))
83
- }
84
- </select>
85
- <span class="pui-select__caret" aria-hidden="true">
86
- <Icon name="chevron-down" size="sm" />
87
- </span>
88
- </div>
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>