@pienter/ui 0.7.1 → 0.8.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 (48) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/CONVENTIONS.md +47 -29
  3. package/README.md +36 -0
  4. package/components/display/record-details/record-details.css +2 -2
  5. package/components/feedback/toast/Toast.vue +3 -4
  6. package/components/feedback/toast/ToastHost.vue +165 -0
  7. package/components/feedback/toast/toast.ts +116 -0
  8. package/components/feedback/toast/types.ts +48 -0
  9. package/components/form/block-editor/BlockEditor.vue +2 -1
  10. package/components/form/checkbox/Checkbox.vue +10 -2
  11. package/components/form/combobox/Combobox.vue +11 -5
  12. package/components/form/combobox/combobox.css +1 -1
  13. package/components/form/date-input/DateInput.vue +10 -2
  14. package/components/form/date-input/date-input.css +2 -17
  15. package/components/form/form/Form.vue +10 -9
  16. package/components/form/form/form.css +2 -22
  17. package/components/form/input-otp/InputOTP.vue +1 -1
  18. package/components/form/input-otp/input-otp.css +1 -1
  19. package/components/form/label/label.css +2 -11
  20. package/components/form/number-field/NumberField.vue +10 -2
  21. package/components/form/number-field/number-field.css +3 -3
  22. package/components/form/radio-group/RadioGroup.vue +1 -1
  23. package/components/form/select/Select.vue +13 -4
  24. package/components/form/select/select.css +6 -9
  25. package/components/form/slider/Slider.vue +1 -1
  26. package/components/form/switch/Switch.vue +1 -1
  27. package/components/form/tags-input/TagsInput.vue +17 -2
  28. package/components/form/tags-input/tags-input.css +16 -12
  29. package/components/form/text-input/TextInput.vue +10 -2
  30. package/components/form/text-input/text-input.css +10 -131
  31. package/components/form/textarea/Textarea.vue +10 -2
  32. package/components/form/textarea/textarea.css +3 -19
  33. package/components/layout/app-layout/AppLayout.vue +116 -0
  34. package/components/layout/app-layout/app-layout.css +115 -0
  35. package/components/layout/index/Index.vue +31 -11
  36. package/components/layout/index/index.css +55 -12
  37. package/components/layout/index/useIndex.ts +31 -14
  38. package/components/layout/table/DataTable.vue +14 -1
  39. package/components/layout/table/Table.vue +12 -0
  40. package/components/navigation/breadcrumb/breadcrumb.css +6 -1
  41. package/components/navigation/sidebar/sidebar.css +23 -16
  42. package/components/navigation/tabs/tabs.css +7 -7
  43. package/package.json +6 -1
  44. package/styles/4-components/form-field.css +112 -0
  45. package/styles/4-components/index.css +1 -0
  46. package/styles/main.css +1 -0
  47. package/utils/a11y/index.ts +5 -1
  48. package/utils/a11y/live-region.ts +2 -1
@@ -0,0 +1,48 @@
1
+ import type { Ref } from 'vue';
2
+ import type { IconName } from '../../../icons/index.js';
3
+
4
+ export type ToastTone = 'brand' | 'success' | 'warning' | 'danger';
5
+
6
+ export type ToastPosition =
7
+ | 'bottom-right'
8
+ | 'bottom-center'
9
+ | 'bottom-left'
10
+ | 'top-right'
11
+ | 'top-center'
12
+ | 'top-left';
13
+
14
+ export interface ToastOptions {
15
+ title: string;
16
+ message?: string;
17
+ tone?: ToastTone;
18
+ /** Milliseconds. `0` keeps the toast visible until it is dismissed. */
19
+ duration?: number;
20
+ dismissible?: boolean;
21
+ action?: { label: string; onClick: () => void };
22
+ icon?: IconName;
23
+ id?: string;
24
+ }
25
+
26
+ export interface ToastConfig {
27
+ maxVisible?: number;
28
+ defaultDuration?: number;
29
+ position?: ToastPosition;
30
+ }
31
+
32
+ export interface ToastItem extends Omit<
33
+ ToastOptions,
34
+ 'duration' | 'dismissible' | 'id'
35
+ > {
36
+ id: string;
37
+ duration: number;
38
+ dismissible: boolean;
39
+ }
40
+
41
+ export interface ToastController {
42
+ readonly visible: Readonly<Ref<readonly ToastItem[]>>;
43
+ readonly position: Readonly<Ref<ToastPosition>>;
44
+ show(options: ToastOptions): string;
45
+ dismiss(id: string): void;
46
+ dismissAll(): void;
47
+ configure(config: ToastConfig): void;
48
+ }
@@ -311,7 +311,7 @@ onBeforeUnmount(stopDrag);
311
311
  :data-disabled="disabled ? 'true' : undefined"
312
312
  >
313
313
  <p :id="`${id}-hint`" class="pui-block-editor__hint">
314
- Drag to reorder, or use the move buttons.
314
+ Use the move buttons to reorder, or drag with a pointer.
315
315
  </p>
316
316
  <ol
317
317
  v-if="modelValue.length"
@@ -335,6 +335,7 @@ onBeforeUnmount(stopDrag);
335
335
  <button
336
336
  class="pui-block-editor__drag"
337
337
  type="button"
338
+ tabindex="-1"
338
339
  :disabled="disabled || modelValue.length < 2"
339
340
  :aria-label="`Drag ${title(block, index)}`"
340
341
  :aria-describedby="`${id}-hint`"
@@ -19,13 +19,21 @@
19
19
  )
20
20
  "
21
21
  />
22
- <label class="pui-field__label" :for="inputId">{{ label }}</label>
22
+ <label class="pui-field__label" :for="inputId"
23
+ >{{ label
24
+ }}<span
25
+ v-if="required"
26
+ class="pui-field__required"
27
+ aria-hidden="true"
28
+ >*</span
29
+ ></label
30
+ >
23
31
  <p v-if="hint" :id="hintId" class="pui-field__hint">{{ hint }}</p>
24
32
  <ul
25
33
  v-if="errors.length"
26
34
  :id="errorsId"
27
35
  class="pui-field__hint"
28
- role="alert"
36
+ role="status"
29
37
  >
30
38
  <li v-for="error in errors" :key="error">{{ error }}</li>
31
39
  </ul>
@@ -1,8 +1,14 @@
1
1
  <template>
2
2
  <div ref="fieldRef" class="pui-field" :data-status="computedStatus">
3
- <label v-if="label" class="pui-field__label" :for="inputId">{{
4
- label
5
- }}</label>
3
+ <label v-if="label" class="pui-field__label" :for="inputId"
4
+ >{{ label
5
+ }}<span
6
+ v-if="required"
7
+ class="pui-field__required"
8
+ aria-hidden="true"
9
+ >*</span
10
+ ></label
11
+ >
6
12
  <input
7
13
  v-bind="$attrs"
8
14
  :id="inputId"
@@ -77,7 +83,7 @@
77
83
  v-if="errors.length"
78
84
  :id="errorsId"
79
85
  class="pui-field__hint"
80
- role="alert"
86
+ role="status"
81
87
  >
82
88
  <li v-for="error in errors" :key="error">{{ error }}</li>
83
89
  </ul>
@@ -144,7 +150,7 @@ const props = withDefaults(
144
150
  hint?: string;
145
151
  /**
146
152
  * Validation errors. Renders as a list under the input with
147
- * `role="alert"` and toggles `aria-invalid="true"` plus the
153
+ * `role="status"` and toggles `aria-invalid="true"` plus the
148
154
  * `data-status="error"` styling on `.pui-field`.
149
155
  */
150
156
  errors?: string[];
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Combobox is built on top of the form-primitive scaffold (`.pui-field`
5
5
  * with `.pui-field__label` / `.pui-field__hint` / status-driven recolour)
6
- * owned by `text-input.css`. The input itself uses the shared
6
+ * owned by `styles/4-components/form-field.css`. The input itself uses the shared
7
7
  * `.pui-input` class so its border, padding, focus ring, and
8
8
  * disabled state match TextInput / Textarea exactly.
9
9
  *
@@ -1,6 +1,14 @@
1
1
  <template>
2
2
  <div class="pui-field" :data-status="computedStatus">
3
- <label class="pui-field__label" :for="inputId">{{ label }}</label>
3
+ <label class="pui-field__label" :for="inputId"
4
+ >{{ label
5
+ }}<span
6
+ v-if="required"
7
+ class="pui-field__required"
8
+ aria-hidden="true"
9
+ >*</span
10
+ ></label
11
+ >
4
12
  <input
5
13
  v-bind="$attrs"
6
14
  :id="inputId"
@@ -28,7 +36,7 @@
28
36
  v-if="errors.length"
29
37
  :id="errorsId"
30
38
  class="pui-field__hint"
31
- role="alert"
39
+ role="status"
32
40
  >
33
41
  <li v-for="error in errors" :key="error">{{ error }}</li>
34
42
  </ul>
@@ -1,19 +1,4 @@
1
1
  @layer components {
2
- /* DateInput reuses the shared `.pui-input` rules (sizing, padding,
3
- * border, focus ring, disabled, status-driven recolour) that live
4
- * alongside `.pui-textarea` in `components/form/text-input/text-input.css`
5
- * because the visual shell is identical to TextInput — only the
6
- * native input `type` differs (`date` / `time` / `datetime-local`
7
- * vs `text`). The browser owns the picker UI; we just style the
8
- * input chrome consistently with the rest of the form-primitive
9
- * scaffold.
10
- *
11
- * This file is intentionally a near-empty placeholder following the
12
- * Textarea pattern — every form primitive keeps its own CSS file
13
- * for layered consistency, even when there are no component-specific
14
- * rules. When the fifth-plus form primitive ships and the shared
15
- * `.pui-input` rules outgrow `text-input.css`, they'll move into a
16
- * dedicated `form-field.css` per the deferred-extraction note in
17
- * CONVENTIONS.
18
- */
2
+ /* DateInput reuses `.pui-input` from `styles/4-components/form-field.css`;
3
+ * only the native `type` differs from TextInput. */
19
4
  }
@@ -6,17 +6,15 @@
6
6
  :aria-busy="busy ? 'true' : undefined"
7
7
  @submit.prevent="handleSubmit"
8
8
  >
9
- <div
9
+ <Alert
10
10
  v-if="topErrors.length > 0"
11
- class="pui-form__error-summary"
12
- role="alert"
13
- aria-live="assertive"
11
+ tone="danger"
12
+ title="There was a problem"
14
13
  >
15
- <p class="pui-form__error-summary-title">There was a problem</p>
16
- <ul class="pui-form__error-summary-list">
14
+ <ul class="pui-form__errors">
17
15
  <li v-for="error in topErrors" :key="error">{{ error }}</li>
18
16
  </ul>
19
- </div>
17
+ </Alert>
20
18
 
21
19
  <slot />
22
20
 
@@ -40,9 +38,10 @@
40
38
  </template>
41
39
 
42
40
  <script setup lang="ts">
43
- import { computed, ref, watch } from 'vue';
41
+ import { computed, onMounted, ref, watch } from 'vue';
42
+ import Alert from '../../feedback/alert/Alert.vue';
44
43
  import { focusFirstInvalid } from '../../../utils/a11y/focus.js';
45
- import { announce } from '../../../utils/a11y/live-region.js';
44
+ import { announce, ensureLiveRegion } from '../../../utils/a11y/live-region.js';
46
45
  import { type Validator } from '../../../utils/validation/rules.js';
47
46
  import {
48
47
  validateFormData,
@@ -81,6 +80,8 @@ const emit = defineEmits<{
81
80
 
82
81
  const formRef = ref<HTMLFormElement | null>(null);
83
82
 
83
+ onMounted(ensureLiveRegion);
84
+
84
85
  // Internal client-validation errors. Merged with server-supplied
85
86
  // `fieldErrors` for display via the form-primitive children's own
86
87
  // `errors` prop pattern (consumer wires them up by name); the summary
@@ -5,29 +5,9 @@
5
5
  gap: var(--space-m);
6
6
  }
7
7
 
8
- .pui-form__error-summary {
9
- background: var(--bg-clr-danger-soft);
10
- border: var(--stroke-sm) solid var(--border-clr-danger);
11
- border-radius: var(--radius-sm);
12
- padding: var(--space-s) var(--space-m);
13
- }
14
-
15
- .pui-form__error-summary-title {
16
- font-weight: var(--fw-semibold);
17
- color: var(--text-clr-danger);
18
- margin-bottom: var(--space-xs);
19
- }
20
-
21
- .pui-form__error-summary-list {
22
- list-style: disc inside;
23
- padding: 0;
8
+ .pui-form__errors {
24
9
  margin: 0;
25
- color: var(--text-clr-danger);
26
- font-size: var(--step--1);
27
- }
28
-
29
- .pui-form__error-summary-list li {
30
- margin-block: var(--space-2xs);
10
+ padding-inline-start: var(--space-s);
31
11
  }
32
12
 
33
13
  .pui-form__status {
@@ -34,7 +34,7 @@
34
34
  v-if="errors.length"
35
35
  :id="errorsId"
36
36
  class="pui-field__hint"
37
- role="alert"
37
+ role="status"
38
38
  >
39
39
  <li v-for="error in errors" :key="error">{{ error }}</li>
40
40
  </ul>
@@ -28,7 +28,7 @@
28
28
 
29
29
  .pui-input-otp__slot {
30
30
  /* Inherits sizing, padding, focus-ring, disabled, and status
31
- * recolour from the shared .pui-input rule (text-input.css). The
31
+ * recolour from the shared .pui-input rule (form-field.css). The
32
32
  * overrides here narrow the slot to a square box and centre the
33
33
  * typed character. */
34
34
  width: 2.75rem;
@@ -1,11 +1,2 @@
1
- /*
2
- * Label is a near-empty stylesheet. The actual `.pui-field__label` rules
3
- * live in `components/form/text-input/text-input.css` (the shared form-primitive
4
- * scaffold). Label exposes the same class as a standalone component for
5
- * use cases where the form-primitive components' built-in label is not
6
- * suitable (e.g., labels associated with custom groups or with multiple
7
- * controls). Importing this file re-imports the scaffold so a
8
- * standalone Label on a page that does not also use
9
- * TextInput / Textarea / Select still gets the typography rules.
10
- */
11
- @import '../text-input/text-input.css';
1
+ /* `.pui-field__label` is defined by the scaffold in
2
+ * `styles/4-components/form-field.css`, which the styles entry loads. */
@@ -1,6 +1,14 @@
1
1
  <template>
2
2
  <div class="pui-field" :data-status="computedStatus">
3
- <label class="pui-field__label" :for="inputId">{{ label }}</label>
3
+ <label class="pui-field__label" :for="inputId"
4
+ >{{ label
5
+ }}<span
6
+ v-if="required"
7
+ class="pui-field__required"
8
+ aria-hidden="true"
9
+ >*</span
10
+ ></label
11
+ >
4
12
  <div class="pui-number-field">
5
13
  <input
6
14
  v-bind="$attrs"
@@ -49,7 +57,7 @@
49
57
  v-if="errors.length"
50
58
  :id="errorsId"
51
59
  class="pui-field__hint"
52
- role="alert"
60
+ role="status"
53
61
  >
54
62
  <li v-for="error in errors" :key="error">{{ error }}</li>
55
63
  </ul>
@@ -2,7 +2,7 @@
2
2
  /* ===== pui-number-field (input + flanking step buttons) =====
3
3
  *
4
4
  * NumberField extends the locked Form-primitive scaffold (the outer
5
- * `.pui-field` wrapper from `text-input.css`) with an inline-flex
5
+ * `.pui-field` wrapper from `styles/4-components/form-field.css`) with an inline-flex
6
6
  * row that hosts the native `<input type="number">` on the left and
7
7
  * two stacked step buttons on the right. The native browser spinner
8
8
  * is suppressed via `appearance: textfield` (Webkit / Firefox)
@@ -24,7 +24,7 @@
24
24
 
25
25
  .pui-number-field__input {
26
26
  /* Inherits sizing, padding, focus-ring, disabled, and status
27
- * recolour from the shared .pui-input rule (text-input.css). The
27
+ * recolour from the shared .pui-input rule (form-field.css). The
28
28
  * overrides here only adjust the right-side border radii so the
29
29
  * input segment butts cleanly against the stepper column. */
30
30
  flex: 1;
@@ -102,7 +102,7 @@
102
102
  /* Status states inherited from .pui-field — recolour BOTH the input
103
103
  * and the step buttons so the whole control reads as errored /
104
104
  * succeeded. Mirrors the .pui-input / .pui-textarea status recolour in
105
- * text-input.css. */
105
+ * form-field.css. */
106
106
  .pui-field[data-status='error'] .pui-number-field__input,
107
107
  .pui-field[data-status='error'] .pui-number-field__step {
108
108
  border-color: var(--border-clr-danger);
@@ -40,7 +40,7 @@
40
40
  v-if="errors.length"
41
41
  :id="errorsId"
42
42
  class="pui-field__hint"
43
- role="alert"
43
+ role="status"
44
44
  >
45
45
  <li v-for="error in errors" :key="error">{{ error }}</li>
46
46
  </ul>
@@ -1,14 +1,21 @@
1
1
  <template>
2
2
  <div class="pui-field" :data-status="computedStatus">
3
- <label v-if="label" class="pui-field__label" :for="inputId">{{
4
- label
5
- }}</label>
3
+ <label v-if="label" class="pui-field__label" :for="inputId"
4
+ >{{ label
5
+ }}<span
6
+ v-if="required"
7
+ class="pui-field__required"
8
+ aria-hidden="true"
9
+ >*</span
10
+ ></label
11
+ >
6
12
  <div class="pui-select">
7
13
  <select
8
14
  v-bind="$attrs"
9
15
  :id="inputId"
10
16
  class="pui-select__control"
11
17
  :name="name"
18
+ :required="required"
12
19
  :disabled="disabled"
13
20
  :value="modelValue"
14
21
  :aria-invalid="hasErrors ? 'true' : undefined"
@@ -40,7 +47,7 @@
40
47
  v-if="errors.length"
41
48
  :id="errorsId"
42
49
  class="pui-field__hint"
43
- role="alert"
50
+ role="status"
44
51
  >
45
52
  <li v-for="error in errors" :key="error">{{ error }}</li>
46
53
  </ul>
@@ -60,6 +67,7 @@ const props = withDefaults(
60
67
  label?: string;
61
68
  modelValue?: string;
62
69
  name?: string;
70
+ required?: boolean;
63
71
  disabled?: boolean;
64
72
  options: { value: string; label: string }[];
65
73
  placeholder?: string;
@@ -72,6 +80,7 @@ const props = withDefaults(
72
80
  label: undefined,
73
81
  modelValue: undefined,
74
82
  name: undefined,
83
+ required: false,
75
84
  disabled: false,
76
85
  placeholder: undefined,
77
86
  hint: undefined,
@@ -2,10 +2,9 @@
2
2
  /* ===== pui-select (form-primitive control) =====
3
3
  *
4
4
  * Wrapped by `.pui-field` (the shared form-primitive scaffold owned by
5
- * text-input.css). The status-driven recolour (`error` / `success` border)
5
+ * `styles/4-components/form-field.css`). The status-driven recolour (`error` / `success` border)
6
6
  * is applied by the parent `.pui-field[data-status]` selector below, mirroring
7
- * `.pui-input` in text-input.css. Focus ring uses the same box-shadow pattern
8
- * as `.pui-input` for visual consistency.
7
+ * `.pui-input` in form-field.css.
9
8
  */
10
9
 
11
10
  .pui-select {
@@ -20,19 +19,17 @@
20
19
  padding: 0.65em 2.4em 0.65em 0.85em;
21
20
  font: inherit;
22
21
  background: var(--bg-clr-surface);
23
- border: var(--stroke-sm) solid var(--border-clr-base);
22
+ border: var(--stroke-sm) solid var(--border-clr-input);
24
23
  border-radius: var(--radius-sm);
25
24
  cursor: pointer;
26
25
  color: var(--text-clr-base);
27
- transition:
28
- border-color 0.12s ease,
29
- box-shadow 0.12s ease;
26
+ transition: border-color var(--duration-fast) var(--ease-base);
30
27
  }
31
28
 
32
29
  .pui-select__control:focus-visible {
33
- outline: none;
34
30
  border-color: var(--border-clr-brand);
35
- box-shadow: 0 0 0 3px hsl(from var(--outline-clr-base) h s l / 0.25);
31
+ outline: var(--outline-width) solid var(--outline-clr-base);
32
+ outline-offset: var(--outline-offset);
36
33
  }
37
34
 
38
35
  .pui-select__control:disabled {
@@ -56,7 +56,7 @@
56
56
  v-if="errors.length"
57
57
  :id="errorsId"
58
58
  class="pui-field__hint"
59
- role="alert"
59
+ role="status"
60
60
  >
61
61
  <li v-for="error in errors" :key="error">{{ error }}</li>
62
62
  </ul>
@@ -25,7 +25,7 @@
25
25
  v-if="errors.length"
26
26
  :id="errorsId"
27
27
  class="pui-field__hint"
28
- role="alert"
28
+ role="status"
29
29
  >
30
30
  <li v-for="error in errors" :key="error">{{ error }}</li>
31
31
  </ul>
@@ -1,6 +1,14 @@
1
1
  <template>
2
2
  <div class="pui-field" :data-status="computedStatus">
3
- <label class="pui-field__label" :for="inputId">{{ label }}</label>
3
+ <label class="pui-field__label" :for="inputId"
4
+ >{{ label
5
+ }}<span
6
+ v-if="required"
7
+ class="pui-field__required"
8
+ aria-hidden="true"
9
+ >*</span
10
+ ></label
11
+ >
4
12
  <!--
5
13
  onWrapperClick only forwards a click on the empty chrome of the field
6
14
  to the real <input> below (a larger pointer target). That input is a
@@ -42,6 +50,7 @@
42
50
  :name="name"
43
51
  :placeholder="placeholder"
44
52
  :disabled="disabled || maxReached"
53
+ :aria-required="required ? 'true' : undefined"
45
54
  :aria-invalid="hasErrors ? 'true' : undefined"
46
55
  :aria-describedby="describedBy"
47
56
  @keydown="onKeydown"
@@ -53,7 +62,7 @@
53
62
  v-if="errors.length"
54
63
  :id="errorsId"
55
64
  class="pui-field__hint"
56
- role="alert"
65
+ role="status"
57
66
  >
58
67
  <li v-for="error in errors" :key="error">{{ error }}</li>
59
68
  </ul>
@@ -63,6 +72,7 @@
63
72
  <script setup lang="ts">
64
73
  import { computed, ref } from 'vue';
65
74
  import { generateId } from '../../../utils/a11y/id.js';
75
+ import { announce } from '../../../utils/a11y/live-region.js';
66
76
 
67
77
  defineOptions({ inheritAttrs: false });
68
78
 
@@ -74,6 +84,8 @@ const props = withDefaults(
74
84
  modelValue: string[];
75
85
  name?: string;
76
86
  placeholder?: string;
87
+ /** Marks the field; set as `aria-required` on the entry input since the value is the tag list, not the typed text. */
88
+ required?: boolean;
77
89
  disabled?: boolean;
78
90
  hint?: string;
79
91
  errors?: string[];
@@ -93,6 +105,7 @@ const props = withDefaults(
93
105
  id: undefined,
94
106
  name: undefined,
95
107
  placeholder: undefined,
108
+ required: false,
96
109
  disabled: false,
97
110
  hint: undefined,
98
111
  errors: () => [],
@@ -153,8 +166,10 @@ function addTag(raw: string): boolean {
153
166
  function removeTag(index: number): void {
154
167
  if (props.disabled) return;
155
168
  if (index < 0 || index >= props.modelValue.length) return;
169
+ const tag = props.modelValue[index];
156
170
  const next = props.modelValue.filter((_, i) => i !== index);
157
171
  emit('update:modelValue', next);
172
+ announce(`Removed ${tag}`);
158
173
  // Return focus to the entry input so the keyboard user keeps typing.
159
174
  entryRef.value?.focus();
160
175
  }
@@ -2,7 +2,7 @@
2
2
  /* ===== pui-tags-input (flex-wrapping pills + entry input) =====
3
3
  *
4
4
  * TagsInput extends the locked Form-primitive scaffold (the outer
5
- * `.pui-field` wrapper from `text-input.css`) with a flex-wrapping
5
+ * `.pui-field` wrapper from `styles/4-components/form-field.css`) with a flex-wrapping
6
6
  * row that hosts existing tag pills followed by a text input where
7
7
  * the user types the next tag. The whole row reads as one
8
8
  * integrated control: clicking anywhere lands focus on the entry
@@ -23,19 +23,22 @@
23
23
  width: 100%;
24
24
  padding: 0.4em 0.55em;
25
25
  background: var(--bg-clr-surface);
26
- border: var(--stroke-sm) solid var(--border-clr-base);
26
+ border: var(--stroke-sm) solid var(--border-clr-input);
27
27
  border-radius: var(--radius-sm);
28
28
  color: var(--text-clr-base);
29
- transition:
30
- border-color 0.12s ease,
31
- box-shadow 0.12s ease;
29
+ transition: border-color var(--duration-fast) var(--ease-base);
32
30
  cursor: text;
33
31
  }
34
32
 
35
33
  .pui-tags-input:focus-within {
36
- outline: none;
37
34
  border-color: var(--border-clr-brand);
38
- box-shadow: 0 0 0 3px hsl(from var(--outline-clr-base) h s l / 0.25);
35
+ }
36
+
37
+ /* The entry is borderless, so the wrapper wears its ring; a focused
38
+ remove button keeps its own. */
39
+ .pui-tags-input:has(.pui-tags-input__entry:focus-visible) {
40
+ outline: var(--outline-width) solid var(--outline-clr-base);
41
+ outline-offset: var(--outline-offset);
39
42
  }
40
43
 
41
44
  .pui-tags-input[data-disabled='true'] {
@@ -59,7 +62,8 @@
59
62
  display: inline-flex;
60
63
  align-items: center;
61
64
  gap: var(--space-3xs);
62
- padding: 0.15em 0.5em;
65
+ padding-block: 0;
66
+ padding-inline: var(--space-2xs) var(--space-3xs);
63
67
  background: var(--bg-clr-surface-2);
64
68
  border: var(--stroke-sm) solid var(--border-clr-base);
65
69
  border-radius: var(--radius-pill, 999px);
@@ -84,8 +88,8 @@
84
88
  cursor: pointer;
85
89
  font: inherit;
86
90
  line-height: 1;
87
- width: 1.25em;
88
- height: 1.25em;
91
+ min-inline-size: var(--space-m);
92
+ min-block-size: var(--space-m);
89
93
  display: grid;
90
94
  place-items: center;
91
95
  border-radius: var(--radius-pill, 999px);
@@ -96,8 +100,8 @@
96
100
  }
97
101
 
98
102
  .pui-tags-input__tag-remove:focus-visible {
99
- outline: 2px solid var(--outline-clr-base);
100
- outline-offset: 1px;
103
+ outline: var(--outline-width) solid var(--outline-clr-base);
104
+ outline-offset: var(--outline-offset);
101
105
  }
102
106
 
103
107
  .pui-tags-input__tag-remove:disabled {
@@ -1,6 +1,14 @@
1
1
  <template>
2
2
  <div class="pui-field" :data-status="computedStatus">
3
- <label class="pui-field__label" :for="inputId">{{ label }}</label>
3
+ <label class="pui-field__label" :for="inputId"
4
+ >{{ label
5
+ }}<span
6
+ v-if="required"
7
+ class="pui-field__required"
8
+ aria-hidden="true"
9
+ >*</span
10
+ ></label
11
+ >
4
12
  <input
5
13
  v-bind="$attrs"
6
14
  :id="inputId"
@@ -27,7 +35,7 @@
27
35
  v-if="errors.length"
28
36
  :id="errorsId"
29
37
  class="pui-field__hint"
30
- role="alert"
38
+ role="status"
31
39
  >
32
40
  <li v-for="error in errors" :key="error">{{ error }}</li>
33
41
  </ul>