@pienter/ui 0.3.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 (86) hide show
  1. package/CHANGELOG.md +200 -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/sidebar/Sidebar.vue +326 -84
  19. package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
  20. package/components/navigation/sidebar/context.ts +21 -0
  21. package/components/navigation/sidebar/sidebar.css +346 -78
  22. package/components/navigation/sidebar/types.ts +13 -1
  23. package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
  24. package/components/overlay/command/Command.vue +10 -38
  25. package/components/overlay/command/command.css +1 -1
  26. package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
  27. package/components/overlay/modal/Modal.vue +7 -28
  28. package/components/overlay/popover/Popover.vue +9 -31
  29. package/components/overlay/sheet/Sheet.vue +7 -28
  30. package/components/overlay/tooltip/Tooltip.vue +14 -19
  31. package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
  32. package/composables/useDisclosure.ts +113 -0
  33. package/composables/useEventListener.ts +16 -0
  34. package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
  35. package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
  36. package/package.json +5 -39
  37. package/styles/0-settings/layout.css +18 -0
  38. package/styles/0-settings/motion.css +2 -2
  39. package/styles/0-settings/spacing.css +3 -1
  40. package/utils/navigation/sidebar.ts +97 -0
  41. package/utils/validation/form.ts +6 -9
  42. package/components/action/button/Button.astro +0 -95
  43. package/components/action/button/IconButton.astro +0 -86
  44. package/components/display/avatar/Avatar.astro +0 -17
  45. package/components/display/avatar/AvatarStack.astro +0 -9
  46. package/components/display/badge/Badge.astro +0 -15
  47. package/components/display/empty/Empty.astro +0 -9
  48. package/components/display/icon/Icon.astro +0 -52
  49. package/components/feedback/alert/Alert.astro +0 -52
  50. package/components/feedback/progress/Progress.astro +0 -68
  51. package/components/feedback/skeleton/Skeleton.astro +0 -32
  52. package/components/feedback/spinner/Spinner.astro +0 -25
  53. package/components/feedback/toast/Toast.astro +0 -50
  54. package/components/form/checkbox/Checkbox.astro +0 -79
  55. package/components/form/date-input/DateInput.astro +0 -105
  56. package/components/form/form/Form.astro +0 -106
  57. package/components/form/input-otp/InputOTP.astro +0 -147
  58. package/components/form/label/Label.astro +0 -13
  59. package/components/form/number-field/NumberField.astro +0 -142
  60. package/components/form/radio-group/RadioGroup.astro +0 -105
  61. package/components/form/select/Select.astro +0 -105
  62. package/components/form/slider/Slider.astro +0 -205
  63. package/components/form/switch/Switch.astro +0 -75
  64. package/components/form/tags-input/TagsInput.astro +0 -153
  65. package/components/form/text-input/TextInput.astro +0 -84
  66. package/components/form/textarea/Textarea.astro +0 -86
  67. package/components/layout/card/Card.astro +0 -13
  68. package/components/layout/separator/Separator.astro +0 -31
  69. package/components/layout/table/Table.astro +0 -116
  70. package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
  71. package/components/navigation/navbar/Navbar.astro +0 -62
  72. package/components/navigation/pagination/PaginationFooter.astro +0 -24
  73. package/components/navigation/sidebar/Sidebar.astro +0 -132
  74. package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
  75. package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
  76. package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
  77. package/components/overlay/modal/Modal.astro +0 -66
  78. package/utils/controllers/disclosure.ts +0 -117
  79. package/utils/controllers/form.ts +0 -524
  80. package/utils/controllers/index.ts +0 -39
  81. package/utils/controllers/number-field.ts +0 -103
  82. package/utils/controllers/otp.ts +0 -252
  83. package/utils/controllers/sidebar.ts +0 -610
  84. package/utils/controllers/slider.ts +0 -336
  85. package/utils/controllers/tags-input.ts +0 -255
  86. package/utils/controllers/toast.ts +0 -426
@@ -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>
@@ -1,205 +0,0 @@
1
- ---
2
- import './slider.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 value. Pass a `number` for single-thumb mode, a tuple for
11
- * range. Astro renders the thumb DOM accordingly; the controller
12
- * picks the matching mode at mount time.
13
- */
14
- value?: number | [number, number];
15
- min?: number;
16
- max?: number;
17
- step?: number;
18
- disabled?: boolean;
19
- hint?: string;
20
- errors?: string[];
21
- status?: 'error' | 'success';
22
- }
23
-
24
- const {
25
- id,
26
- label,
27
- name,
28
- value = 0,
29
- min = 0,
30
- max = 100,
31
- step = 1,
32
- disabled = false,
33
- hint,
34
- errors = [],
35
- status,
36
- ...rest
37
- } = Astro.props;
38
-
39
- const baseId = id ?? generateId('input');
40
- const hintId = `${baseId}-hint`;
41
- const errorsId = `${baseId}-errors`;
42
-
43
- const hasErrors = errors.length > 0;
44
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
45
-
46
- const describedByIds: string[] = [];
47
- if (hint) describedByIds.push(hintId);
48
- if (hasErrors) describedByIds.push(errorsId);
49
- const describedBy =
50
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
51
-
52
- const isRange = Array.isArray(value);
53
- const values: number[] = isRange ? [value[0], value[1]] : [value as number];
54
-
55
- function pct(v: number): number {
56
- if (max === min) return 0;
57
- return ((v - min) / (max - min)) * 100;
58
- }
59
-
60
- const fillStart = isRange && values.length === 2 ? `${pct(values[0])}%` : '0%';
61
- const fillEnd =
62
- isRange && values.length === 2 ? `${pct(values[1])}%` : `${pct(values[0])}%`;
63
-
64
- const trackStyle = `--pui-slider-fill-start: ${fillStart}; --pui-slider-fill-end: ${fillEnd};`;
65
- ---
66
-
67
- <div class="pui-field" data-status={computedStatus}>
68
- <label class="pui-field__label" for={`${baseId}-thumb-0`}>
69
- {label}
70
- </label>
71
- <div class="pui-slider-shell">
72
- <div
73
- class="pui-slider"
74
- data-pui-slider
75
- data-min={String(min)}
76
- data-max={String(max)}
77
- data-step={String(step)}
78
- data-disabled={disabled ? 'true' : undefined}
79
- data-range={isRange ? 'true' : undefined}
80
- data-initial={JSON.stringify(values)}
81
- style={trackStyle}
82
- {...rest}
83
- >
84
- <div class="pui-slider__rail" aria-hidden="true"></div>
85
- <div class="pui-slider__fill" aria-hidden="true"></div>
86
- {
87
- values.map((v, idx) => (
88
- <button
89
- id={`${baseId}-thumb-${idx}`}
90
- type="button"
91
- class="pui-slider__thumb"
92
- role="slider"
93
- aria-valuemin={String(min)}
94
- aria-valuemax={String(max)}
95
- aria-valuenow={String(v)}
96
- aria-valuetext={
97
- isRange
98
- ? idx === 0
99
- ? `Lower bound: ${v}`
100
- : `Upper bound: ${v}`
101
- : String(v)
102
- }
103
- aria-label={
104
- isRange
105
- ? idx === 0
106
- ? `${label} (lower bound)`
107
- : `${label} (upper bound)`
108
- : label
109
- }
110
- aria-disabled={disabled ? 'true' : undefined}
111
- aria-describedby={idx === 0 ? describedBy : undefined}
112
- tabindex={disabled ? -1 : 0}
113
- style={`inset-inline-start: ${pct(v)}%`}
114
- />
115
- ))
116
- }
117
- </div>
118
- {
119
- name && !isRange && (
120
- <input
121
- class="pui-slider__hidden"
122
- type="hidden"
123
- name={name}
124
- value={String(values[0])}
125
- />
126
- )
127
- }
128
- {
129
- name && isRange && (
130
- <Fragment>
131
- <input
132
- class="pui-slider__hidden"
133
- data-bound="lower"
134
- type="hidden"
135
- name={`${name}_lower`}
136
- value={String(values[0])}
137
- />
138
- <input
139
- class="pui-slider__hidden"
140
- data-bound="upper"
141
- type="hidden"
142
- name={`${name}_upper`}
143
- value={String(values[1])}
144
- />
145
- </Fragment>
146
- )
147
- }
148
- </div>
149
- {
150
- hint && (
151
- <p id={hintId} class="pui-field__hint">
152
- {hint}
153
- </p>
154
- )
155
- }
156
- {
157
- hasErrors && (
158
- <ul id={errorsId} class="pui-field__hint" role="alert">
159
- {errors.map((error) => (
160
- <li>{error}</li>
161
- ))}
162
- </ul>
163
- )
164
- }
165
- </div>
166
-
167
- <script>
168
- import { mountSlider } from '@pienter/ui/utils/controllers/slider.js';
169
-
170
- function mountAll(): void {
171
- const tracks = document.querySelectorAll<HTMLElement>('[data-pui-slider]');
172
- tracks.forEach((track) => {
173
- if (track.dataset.vSliderMounted === 'true') return;
174
- let initial: number | [number, number] | undefined;
175
- if (track.dataset.initial) {
176
- try {
177
- const parsed: unknown = JSON.parse(track.dataset.initial);
178
- if (
179
- Array.isArray(parsed) &&
180
- parsed.length === 2 &&
181
- parsed.every((v) => typeof v === 'number')
182
- ) {
183
- initial = [parsed[0] as number, parsed[1] as number];
184
- } else if (
185
- Array.isArray(parsed) &&
186
- parsed.length === 1 &&
187
- typeof parsed[0] === 'number'
188
- ) {
189
- initial = parsed[0];
190
- }
191
- } catch {
192
- initial = undefined;
193
- }
194
- }
195
- mountSlider(track, { initialValue: initial });
196
- track.dataset.vSliderMounted = 'true';
197
- });
198
- }
199
-
200
- if (document.readyState === 'loading') {
201
- document.addEventListener('DOMContentLoaded', mountAll, { once: true });
202
- } else {
203
- mountAll();
204
- }
205
- </script>
@@ -1,75 +0,0 @@
1
- ---
2
- import './switch.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
- disabled?: boolean;
11
- hint?: string;
12
- errors?: string[];
13
- status?: 'error' | 'success';
14
- }
15
-
16
- const {
17
- id,
18
- label,
19
- name,
20
- checked = false,
21
- disabled = false,
22
- hint,
23
- errors = [],
24
- status,
25
- ...rest
26
- } = Astro.props;
27
-
28
- const inputId = id ?? generateId('switch');
29
- const hintId = `${inputId}-hint`;
30
- const errorsId = `${inputId}-errors`;
31
-
32
- const hasErrors = errors.length > 0;
33
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
34
-
35
- const describedByIds: string[] = [];
36
- if (hint) describedByIds.push(hintId);
37
- if (hasErrors) describedByIds.push(errorsId);
38
- const describedBy =
39
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
40
- ---
41
-
42
- <div class="pui-field" data-layout="inline" data-status={computedStatus}>
43
- <input
44
- class="pui-switch"
45
- type="checkbox"
46
- role="switch"
47
- id={inputId}
48
- name={name}
49
- checked={checked}
50
- disabled={disabled}
51
- aria-checked={checked ? 'true' : 'false'}
52
- aria-invalid={hasErrors ? 'true' : undefined}
53
- aria-describedby={describedBy}
54
- {...rest}
55
- />
56
- <label class="pui-field__label" for={inputId}>
57
- {label}
58
- </label>
59
- {
60
- hint && (
61
- <p id={hintId} class="pui-field__hint">
62
- {hint}
63
- </p>
64
- )
65
- }
66
- {
67
- hasErrors && (
68
- <ul id={errorsId} class="pui-field__hint" role="alert">
69
- {errors.map((error) => (
70
- <li>{error}</li>
71
- ))}
72
- </ul>
73
- )
74
- }
75
- </div>
@@ -1,153 +0,0 @@
1
- ---
2
- import './tags-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
- * Initial tags array. Astro renders the pill DOM and seeds the
11
- * controller via `data-initial-tags` so post-mount `mountTagsInput`
12
- * stays in sync without re-reading attributes.
13
- */
14
- value?: string[];
15
- placeholder?: string;
16
- disabled?: boolean;
17
- hint?: string;
18
- errors?: string[];
19
- status?: 'error' | 'success';
20
- maxTags?: number;
21
- separators?: string[];
22
- }
23
-
24
- const {
25
- id,
26
- label,
27
- name,
28
- value = [],
29
- placeholder,
30
- disabled = false,
31
- hint,
32
- errors = [],
33
- status,
34
- maxTags,
35
- separators,
36
- ...rest
37
- } = Astro.props;
38
-
39
- const inputId = id ?? generateId('input');
40
- const hintId = `${inputId}-hint`;
41
- const errorsId = `${inputId}-errors`;
42
-
43
- const hasErrors = errors.length > 0;
44
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
45
-
46
- const describedByIds: string[] = [];
47
- if (hint) describedByIds.push(hintId);
48
- if (hasErrors) describedByIds.push(errorsId);
49
- const describedBy =
50
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
51
-
52
- const maxReached = maxTags !== undefined && value.length >= maxTags;
53
- ---
54
-
55
- <div class="pui-field" data-status={computedStatus}>
56
- <label class="pui-field__label" for={inputId}>
57
- {label}
58
- </label>
59
- <div
60
- class="pui-tags-input"
61
- data-pui-tags-input
62
- data-initial-tags={JSON.stringify(value)}
63
- data-max-tags={maxTags !== undefined ? String(maxTags) : undefined}
64
- data-separators={separators ? JSON.stringify(separators) : undefined}
65
- data-disabled={disabled ? 'true' : undefined}
66
- >
67
- {
68
- value.map((tag) => (
69
- <span class="pui-tags-input__tag" data-tag-value={tag}>
70
- <span class="pui-tags-input__tag-label">{tag}</span>
71
- <button
72
- type="button"
73
- class="pui-tags-input__tag-remove"
74
- aria-label={`Remove tag ${tag}`}
75
- data-tags-remove
76
- disabled={disabled}
77
- >
78
- <span aria-hidden="true">&times;</span>
79
- </button>
80
- </span>
81
- ))
82
- }
83
- <input
84
- class="pui-tags-input__entry"
85
- type="text"
86
- id={inputId}
87
- name={name}
88
- placeholder={placeholder}
89
- disabled={disabled || maxReached}
90
- aria-invalid={hasErrors ? 'true' : undefined}
91
- aria-describedby={describedBy}
92
- {...rest}
93
- />
94
- </div>
95
- {
96
- hint && (
97
- <p id={hintId} class="pui-field__hint">
98
- {hint}
99
- </p>
100
- )
101
- }
102
- {
103
- hasErrors && (
104
- <ul id={errorsId} class="pui-field__hint" role="alert">
105
- {errors.map((error) => (
106
- <li>{error}</li>
107
- ))}
108
- </ul>
109
- )
110
- }
111
- </div>
112
-
113
- <script>
114
- import { mountTagsInput } from '@pienter/ui/utils/controllers/tags-input.js';
115
-
116
- // Hoisted module script — Astro de-dupes across instances. Mount on
117
- // every [data-pui-tags-input] wrapper present at DOM-ready.
118
- function mountAll(): void {
119
- const wrappers = document.querySelectorAll<HTMLElement>(
120
- '[data-pui-tags-input]',
121
- );
122
- wrappers.forEach((wrapper) => {
123
- if (wrapper.dataset.vTagsInputMounted === 'true') return;
124
- const maxTagsRaw = wrapper.dataset.maxTags;
125
- const maxTags =
126
- maxTagsRaw === undefined ? undefined : Number.parseInt(maxTagsRaw, 10);
127
- let separators: string[] | undefined;
128
- if (wrapper.dataset.separators) {
129
- try {
130
- const parsed: unknown = JSON.parse(wrapper.dataset.separators);
131
- if (Array.isArray(parsed)) {
132
- separators = parsed.filter(
133
- (v): v is string => typeof v === 'string',
134
- );
135
- }
136
- } catch {
137
- separators = undefined;
138
- }
139
- }
140
- mountTagsInput(wrapper, {
141
- maxTags: Number.isFinite(maxTags) ? maxTags : undefined,
142
- separators,
143
- });
144
- wrapper.dataset.vTagsInputMounted = 'true';
145
- });
146
- }
147
-
148
- if (document.readyState === 'loading') {
149
- document.addEventListener('DOMContentLoaded', mountAll, { once: true });
150
- } else {
151
- mountAll();
152
- }
153
- </script>
@@ -1,84 +0,0 @@
1
- ---
2
- import './text-input.css';
3
- import { generateId } from '../../../utils/a11y/id.js';
4
-
5
- interface Props {
6
- id?: string;
7
- label: string;
8
- name?: string;
9
- type?: string;
10
- value?: string;
11
- autocomplete?: string;
12
- required?: boolean;
13
- disabled?: boolean;
14
- placeholder?: string;
15
- hint?: string;
16
- errors?: string[];
17
- status?: 'error' | 'success';
18
- }
19
-
20
- const {
21
- id,
22
- label,
23
- name,
24
- type = 'text',
25
- value = '',
26
- autocomplete,
27
- required = false,
28
- disabled = false,
29
- placeholder,
30
- hint,
31
- errors = [],
32
- status,
33
- ...rest
34
- } = Astro.props;
35
-
36
- const inputId = id ?? generateId('input');
37
- const hintId = `${inputId}-hint`;
38
- const errorsId = `${inputId}-errors`;
39
-
40
- const hasErrors = errors.length > 0;
41
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
42
-
43
- const describedByIds: string[] = [];
44
- if (hint) describedByIds.push(hintId);
45
- if (hasErrors) describedByIds.push(errorsId);
46
- const describedBy =
47
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
48
- ---
49
-
50
- <div class="pui-field" data-status={computedStatus}>
51
- <label class="pui-field__label" for={inputId}>
52
- {label}
53
- </label>
54
- <input
55
- class="pui-input"
56
- id={inputId}
57
- type={type}
58
- name={name}
59
- value={value}
60
- autocomplete={autocomplete}
61
- required={required}
62
- disabled={disabled}
63
- placeholder={placeholder}
64
- aria-invalid={hasErrors ? 'true' : undefined}
65
- aria-describedby={describedBy}
66
- {...rest}
67
- />
68
- {
69
- hint && (
70
- <p id={hintId} class="pui-field__hint">
71
- {hint}
72
- </p>
73
- )
74
- }
75
- {
76
- hasErrors && (
77
- <ul id={errorsId} class="pui-field__hint" role="alert">
78
- {errors.map((error) => (
79
- <li>{error}</li>
80
- ))}
81
- </ul>
82
- )
83
- }
84
- </div>
@@ -1,86 +0,0 @@
1
- ---
2
- import './textarea.css';
3
- import { generateId } from '../../../utils/a11y/id.js';
4
-
5
- interface Props {
6
- id?: string;
7
- label: string;
8
- name?: string;
9
- value?: string;
10
- rows?: number;
11
- autocomplete?: string;
12
- required?: boolean;
13
- disabled?: boolean;
14
- placeholder?: string;
15
- hint?: string;
16
- errors?: string[];
17
- status?: 'error' | 'success';
18
- autoResize?: boolean;
19
- }
20
-
21
- const {
22
- id,
23
- label,
24
- name,
25
- value = '',
26
- rows,
27
- autocomplete,
28
- required = false,
29
- disabled = false,
30
- placeholder,
31
- hint,
32
- errors = [],
33
- status,
34
- autoResize = false,
35
- ...rest
36
- } = Astro.props;
37
-
38
- const inputId = id ?? generateId('textarea');
39
- const hintId = `${inputId}-hint`;
40
- const errorsId = `${inputId}-errors`;
41
-
42
- const hasErrors = errors.length > 0;
43
- const computedStatus = status ?? (hasErrors ? 'error' : undefined);
44
-
45
- const describedByIds: string[] = [];
46
- if (hint) describedByIds.push(hintId);
47
- if (hasErrors) describedByIds.push(errorsId);
48
- const describedBy =
49
- describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
50
- ---
51
-
52
- <div class="pui-field" data-status={computedStatus}>
53
- <label class="pui-field__label" for={inputId}>
54
- {label}
55
- </label>
56
- <textarea
57
- class="pui-textarea"
58
- id={inputId}
59
- name={name}
60
- rows={rows}
61
- autocomplete={autocomplete}
62
- required={required}
63
- disabled={disabled}
64
- placeholder={placeholder}
65
- style={autoResize ? 'field-sizing: content' : undefined}
66
- aria-invalid={hasErrors ? 'true' : undefined}
67
- aria-describedby={describedBy}
68
- {...rest}>{value}</textarea
69
- >
70
- {
71
- hint && (
72
- <p id={hintId} class="pui-field__hint">
73
- {hint}
74
- </p>
75
- )
76
- }
77
- {
78
- hasErrors && (
79
- <ul id={errorsId} class="pui-field__hint" role="alert">
80
- {errors.map((error) => (
81
- <li>{error}</li>
82
- ))}
83
- </ul>
84
- )
85
- }
86
- </div>