@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
@@ -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>