@pienter/ui 0.9.0 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,61 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.11.0 - 2026-09-15
6
+
7
+ ### Added
8
+
9
+ - The Vue CMS playground defines its contacts collection once and mounts it as
10
+ both the `/contacts` module index and the related index on a company's
11
+ Contacts tab, demonstrating `Index` placement and prefixed query syncing with
12
+ realistic consumer-owned columns, loaders, cells and navigation.
13
+
14
+ ## 0.10.0 - 2026-09-15
15
+
16
+ ### Changed
17
+
18
+ - Index renders its title, description and actions through the new
19
+ `PageHeader`, so the `pui-index__header`, `pui-index__heading`,
20
+ `pui-index__title`, `pui-index__description` and `pui-index__actions`
21
+ classes are gone. They were never documented as public API; a consumer
22
+ style that targeted them moves to `pui-page-header` and its
23
+ `__heading`, `__title`, `__description` and `__actions` elements. Markup,
24
+ spacing and type are unchanged in both placements.
25
+ - Bare anchors get the standard `:focus-visible` ring from the base layer,
26
+ and their underline offset is the `--space-3xs` token instead of a `0.14em`
27
+ literal, so content links inside a consumer page need no app rule for
28
+ colour, offset or focus. Components that style their own anchors (Button,
29
+ Breadcrumb, Sidebar) are unaffected: their rules sit in the components
30
+ layer, which outranks base.
31
+ - Index's page-size Select renders through the new inline field layout
32
+ instead of an Index-owned flex override; no visual change.
33
+
34
+ ### Added
35
+
36
+ - Inline field layout: `layout="inline"` on TextInput, Textarea, Select,
37
+ NumberField, DateInput, Combobox and TagsInput puts the label beside the
38
+ control on one row, with hint and errors wrapping below across both
39
+ columns. It sets `data-layout="inline"` on `.pui-field`, the attribute
40
+ Checkbox and Switch already carry, so one scaffold rule serves both DOM
41
+ orders; label association, `aria-describedby` and status recolour are
42
+ identical to the stacked layout. Checkbox and Switch stay inline-only and
43
+ RadioGroup stacked-only, so neither takes the prop.
44
+ - `pui-index__primary` and `pui-index__subline`: a two-line primary cell for
45
+ an Index `cell:<key>` slot, the title (usually the record link) over a
46
+ muted, smaller subline such as a slug.
47
+ - `pui-badge-list`: put it on any element wrapping several badges and they
48
+ wrap with a small gap. Loaded with Badge.
49
+
50
+ - `PageHeader` at `@pienter/ui/components/PageHeader.vue`: the title,
51
+ description and actions band above a module index or a record page,
52
+ extracted from Index so detail and form pages no longer hand-roll the same
53
+ header. `title` and `description` are plain strings, `level` (`1` | `2` |
54
+ `3`, default `1`) picks the heading element and sizes levels 2 and 3 as
55
+ section headings, `title-id` sets the title's `id` for `aria-labelledby`,
56
+ and `#actions` fills the trailing row. It renders nothing when it has no
57
+ title, description or actions. Index uses it at level 1 in module placement
58
+ and level 2 in related placement.
59
+
5
60
  ## 0.9.0 - 2026-09-15
6
61
 
7
62
  ### Added
package/CONVENTIONS.md CHANGED
@@ -96,18 +96,18 @@ list, add a token to `0-settings/` rather than hardcoding.
96
96
 
97
97
  Variants and runtime states live on `data-*` attributes on the block element, never on class names.
98
98
 
99
- | Attribute | Purpose | Example values |
100
- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
101
- | `data-variant` | Visual variant (mutually exclusive within a component) | `primary`, `secondary`, `ghost`, `danger`, `link` |
102
- | `data-size` | Sizing scale (mutually exclusive) | `sm`, `md` (Button/IconButton); `sm`/`md`/`lg` (Icon); other components add their own |
103
- | `data-animation` | Continuous decorative animation on the block | `spin` (Icon) |
104
- | `data-tone` | Semantic tone for non-variant components (Alert, Badge, Toast) | `brand`, `success`, `warning`, `danger` |
105
- | `data-block` | Block-level layout flag (boolean) | `'true'` or absent |
106
- | `data-state` | Runtime state controlled by code or a behavior composable | `loading`, `open`, `closed`, `checked`, `indeterminate` |
107
- | `data-status` | Derived semantic status for form primitives (validation outcome) | `error`, `success` |
108
- | `data-layout` | Form-primitive scaffold layout selector — lives on `.pui-field`; flips between stacked label-above (default) and compact inline control-left + label-right | `stacked` (default; attribute may be omitted), `inline` |
109
- | `data-orientation` | Layout direction for grouped controls (RadioGroup items, future SegmentedControl, etc.) — lives on the group container | `vertical` (default for RadioGroup), `horizontal` |
110
- | `data-placement` | Where a CMS surface is mounted, which decides its chrome and heading level — lives on `.pui-index` | `module` (default), `related` |
99
+ | Attribute | Purpose | Example values |
100
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
101
+ | `data-variant` | Visual variant (mutually exclusive within a component) | `primary`, `secondary`, `ghost`, `danger`, `link` |
102
+ | `data-size` | Sizing scale (mutually exclusive) | `sm`, `md` (Button/IconButton); `sm`/`md`/`lg` (Icon); other components add their own |
103
+ | `data-animation` | Continuous decorative animation on the block | `spin` (Icon) |
104
+ | `data-tone` | Semantic tone for non-variant components (Alert, Badge, Toast) | `brand`, `success`, `warning`, `danger` |
105
+ | `data-block` | Block-level layout flag (boolean) | `'true'` or absent |
106
+ | `data-state` | Runtime state controlled by code or a behavior composable | `loading`, `open`, `closed`, `checked`, `indeterminate` |
107
+ | `data-status` | Derived semantic status for form primitives (validation outcome) | `error`, `success` |
108
+ | `data-layout` | Form-primitive scaffold layout selector — lives on `.pui-field`; `inline` puts label and control on one row in DOM order, hint and errors below (see checklist) | `stacked` (default; attribute may be omitted), `inline` |
109
+ | `data-orientation` | Layout direction for grouped controls (RadioGroup items, future SegmentedControl, etc.) — lives on the group container | `vertical` (default for RadioGroup), `horizontal` |
110
+ | `data-placement` | Where a CMS surface is mounted, which decides its chrome and heading level — lives on `.pui-index` | `module` (default), `related` |
111
111
 
112
112
  The example values listed for each attribute are non-exhaustive — components add states as needed (e.g. `expanded`, `selected`, `active` for upcoming Disclosure, Tabs, Sidebar). New values follow the same kebab-case rule and live on the same attribute family.
113
113
 
@@ -272,15 +272,17 @@ role="status">` with one `<li>` per error message, only when errors are
272
272
  typed prop through. Order matters: bind `$attrs` first, then explicit
273
273
  props — explicit bindings win, which is what we want.
274
274
 
275
- - [ ] Compact form primitives (Switch, Checkbox, individual Radio items) use
276
- `data-layout="inline"` on the `pui-field` wrapper to flip the layout to
277
- control-left + label-right. Everything else (label association via
278
- `for`/`id`, hint/error rendering, ARIA wiring, `aria-describedby`
279
- joining, status-driven recolour) stays unchanged. The DOM source order
280
- under `data-layout="inline"` is **control first, label second** so the
281
- `<label for>` association is read in left-to-right order, and the
282
- `pui-field__hint` elements remain after the label so a single CSS rule
283
- (`grid-column: 1 / -1`) drops them onto a row below the control + label.
275
+ - [ ] `data-layout="inline"` on the `pui-field` wrapper puts label and
276
+ control on one row in DOM order; the `pui-field__hint` elements stay
277
+ after them so a single CSS rule (`grid-column: 1 / -1`) drops them onto
278
+ the row below. Compact primitives (Switch, Checkbox) always render it,
279
+ with the control **first** in DOM order so the `<label for>`
280
+ association is read left-to-right. Label-first primitives (TextInput,
281
+ Textarea, Select, NumberField, DateInput, Combobox, TagsInput) expose
282
+ it as `layout?: 'stacked' | 'inline'` (default `stacked`, attribute
283
+ omitted). Everything else (label association via `for`/`id`,
284
+ hint/error rendering, ARIA wiring, `aria-describedby` joining,
285
+ status-driven recolour) stays unchanged in both layouts.
284
286
 
285
287
  **Worked example — TextInput**: see `packages/ui/components/form/text-input/`. The
286
288
  wrapper is `<div class="pui-field" data-status>`, the label is
@@ -53,4 +53,11 @@
53
53
  border-color: transparent;
54
54
  }
55
55
  }
56
+
57
+ /* A wrapped group of badges (tags, labels). */
58
+ .pui-badge-list {
59
+ display: flex;
60
+ flex-wrap: wrap;
61
+ gap: var(--space-2xs);
62
+ }
56
63
  }
@@ -51,6 +51,12 @@
51
51
  border-color: var(--bg-clr-success, var(--border-clr-brand));
52
52
  }
53
53
 
54
+ /* The label is the click target for the box, so it reads as one. */
55
+ .pui-field:has(.pui-checkbox) .pui-field__label {
56
+ cursor: pointer;
57
+ user-select: none;
58
+ }
59
+
54
60
  /* Mute the label + cursor when the input is disabled. */
55
61
  .pui-field:has(.pui-checkbox:disabled) .pui-field__label {
56
62
  color: var(--text-clr-muted);
@@ -1,5 +1,10 @@
1
1
  <template>
2
- <div ref="fieldRef" class="pui-field" :data-status="computedStatus">
2
+ <div
3
+ ref="fieldRef"
4
+ class="pui-field"
5
+ :data-layout="layout === 'inline' ? 'inline' : undefined"
6
+ :data-status="computedStatus"
7
+ >
3
8
  <label v-if="label" class="pui-field__label" :for="inputId"
4
9
  >{{ label
5
10
  }}<span
@@ -174,6 +179,8 @@ const props = withDefaults(
174
179
  placement?: Placement;
175
180
  /** Pixel offset between input and listbox; default `4`. */
176
181
  offset?: number;
182
+ /** `inline` puts the label beside the control; hint and errors wrap below. */
183
+ layout?: 'stacked' | 'inline';
177
184
  }>(),
178
185
  {
179
186
  id: undefined,
@@ -191,6 +198,7 @@ const props = withDefaults(
191
198
  emptyMessage: 'No results',
192
199
  placement: 'bottom-start',
193
200
  offset: 4,
201
+ layout: 'stacked',
194
202
  },
195
203
  );
196
204
 
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <div class="pui-field" :data-status="computedStatus">
2
+ <div
3
+ class="pui-field"
4
+ :data-layout="layout === 'inline' ? 'inline' : undefined"
5
+ :data-status="computedStatus"
6
+ >
3
7
  <label class="pui-field__label" :for="inputId"
4
8
  >{{ label
5
9
  }}<span
@@ -83,6 +87,8 @@ const props = withDefaults(
83
87
  hint?: string;
84
88
  errors?: string[];
85
89
  status?: 'error' | 'success';
90
+ /** `inline` puts the label beside the control; hint and errors wrap below. */
91
+ layout?: 'stacked' | 'inline';
86
92
  }>(),
87
93
  {
88
94
  id: undefined,
@@ -96,6 +102,7 @@ const props = withDefaults(
96
102
  hint: undefined,
97
103
  errors: () => [],
98
104
  status: undefined,
105
+ layout: 'stacked',
99
106
  },
100
107
  );
101
108
 
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <div class="pui-field" :data-status="computedStatus">
2
+ <div
3
+ class="pui-field"
4
+ :data-layout="layout === 'inline' ? 'inline' : undefined"
5
+ :data-status="computedStatus"
6
+ >
3
7
  <label class="pui-field__label" :for="inputId"
4
8
  >{{ label
5
9
  }}<span
@@ -90,6 +94,8 @@ const props = withDefaults(
90
94
  hint?: string;
91
95
  errors?: string[];
92
96
  status?: 'error' | 'success';
97
+ /** `inline` puts the label beside the control; hint and errors wrap below. */
98
+ layout?: 'stacked' | 'inline';
93
99
  }>(),
94
100
  {
95
101
  id: undefined,
@@ -104,6 +110,7 @@ const props = withDefaults(
104
110
  hint: undefined,
105
111
  errors: () => [],
106
112
  status: undefined,
113
+ layout: 'stacked',
107
114
  },
108
115
  );
109
116
 
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <div class="pui-field" :data-status="computedStatus">
2
+ <div
3
+ class="pui-field"
4
+ :data-layout="layout === 'inline' ? 'inline' : undefined"
5
+ :data-status="computedStatus"
6
+ >
3
7
  <label v-if="label" class="pui-field__label" :for="inputId"
4
8
  >{{ label
5
9
  }}<span
@@ -74,6 +78,8 @@ const props = withDefaults(
74
78
  hint?: string;
75
79
  errors?: string[];
76
80
  status?: 'error' | 'success';
81
+ /** `inline` puts the label beside the control; hint and errors wrap below. */
82
+ layout?: 'stacked' | 'inline';
77
83
  }>(),
78
84
  {
79
85
  id: undefined,
@@ -86,6 +92,7 @@ const props = withDefaults(
86
92
  hint: undefined,
87
93
  errors: () => [],
88
94
  status: undefined,
95
+ layout: 'stacked',
89
96
  },
90
97
  );
91
98
 
@@ -56,6 +56,12 @@
56
56
  background: var(--bg-clr-success, var(--bg-clr-brand));
57
57
  }
58
58
 
59
+ /* The label is the click target for the switch, so it reads as one. */
60
+ .pui-field:has(.pui-switch) .pui-field__label {
61
+ cursor: pointer;
62
+ user-select: none;
63
+ }
64
+
59
65
  /* Disable the cursor + label colour when the input is disabled. */
60
66
  .pui-field:has(.pui-switch:disabled) .pui-field__label {
61
67
  color: var(--text-clr-muted);
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <div class="pui-field" :data-status="computedStatus">
2
+ <div
3
+ class="pui-field"
4
+ :data-layout="layout === 'inline' ? 'inline' : undefined"
5
+ :data-status="computedStatus"
6
+ >
3
7
  <label class="pui-field__label" :for="inputId"
4
8
  >{{ label
5
9
  }}<span
@@ -100,6 +104,8 @@ const props = withDefaults(
100
104
  * `[',', 'Enter']`.
101
105
  */
102
106
  separators?: string[];
107
+ /** `inline` puts the label beside the control; hint and errors wrap below. */
108
+ layout?: 'stacked' | 'inline';
103
109
  }>(),
104
110
  {
105
111
  id: undefined,
@@ -112,6 +118,7 @@ const props = withDefaults(
112
118
  status: undefined,
113
119
  maxTags: undefined,
114
120
  separators: () => [',', 'Enter'],
121
+ layout: 'stacked',
115
122
  },
116
123
  );
117
124
 
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <div class="pui-field" :data-status="computedStatus">
2
+ <div
3
+ class="pui-field"
4
+ :data-layout="layout === 'inline' ? 'inline' : undefined"
5
+ :data-status="computedStatus"
6
+ >
3
7
  <label class="pui-field__label" :for="inputId"
4
8
  >{{ label
5
9
  }}<span
@@ -62,6 +66,8 @@ const props = withDefaults(
62
66
  hint?: string;
63
67
  errors?: string[];
64
68
  status?: 'error' | 'success';
69
+ /** `inline` puts the label beside the control; hint and errors wrap below. */
70
+ layout?: 'stacked' | 'inline';
65
71
  }>(),
66
72
  {
67
73
  id: undefined,
@@ -74,6 +80,7 @@ const props = withDefaults(
74
80
  hint: undefined,
75
81
  errors: () => [],
76
82
  status: undefined,
83
+ layout: 'stacked',
77
84
  },
78
85
  );
79
86
 
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <div class="pui-field" :data-status="computedStatus">
2
+ <div
3
+ class="pui-field"
4
+ :data-layout="layout === 'inline' ? 'inline' : undefined"
5
+ :data-status="computedStatus"
6
+ >
3
7
  <label class="pui-field__label" :for="inputId"
4
8
  >{{ label
5
9
  }}<span
@@ -64,6 +68,8 @@ const props = withDefaults(
64
68
  errors?: string[];
65
69
  status?: 'error' | 'success';
66
70
  autoResize?: boolean;
71
+ /** `inline` puts the label beside the control; hint and errors wrap below. */
72
+ layout?: 'stacked' | 'inline';
67
73
  }>(),
68
74
  {
69
75
  id: undefined,
@@ -77,6 +83,7 @@ const props = withDefaults(
77
83
  errors: () => [],
78
84
  status: undefined,
79
85
  autoResize: false,
86
+ layout: 'stacked',
80
87
  },
81
88
  );
82
89
 
@@ -4,26 +4,16 @@
4
4
  :data-placement="placement"
5
5
  :data-state="loading ? 'loading' : undefined"
6
6
  >
7
- <header
8
- v-if="title || description || $slots.actions"
9
- class="pui-index__header"
7
+ <PageHeader
8
+ :title="title"
9
+ :description="description"
10
+ :level="placement === 'related' ? 2 : 1"
11
+ :title-id="titleId"
10
12
  >
11
- <div v-if="title || description" class="pui-index__heading">
12
- <component
13
- :is="placement === 'related' ? 'h2' : 'h1'"
14
- v-if="title"
15
- :id="titleId"
16
- class="pui-index__title"
17
- >{{ title }}</component
18
- >
19
- <p v-if="description" class="pui-index__description">
20
- {{ description }}
21
- </p>
22
- </div>
23
- <div v-if="$slots.actions" class="pui-index__actions">
13
+ <template v-if="$slots.actions" #actions>
24
14
  <slot name="actions" :reload="reload" :loading="loading" />
25
- </div>
26
- </header>
15
+ </template>
16
+ </PageHeader>
27
17
 
28
18
  <div class="pui-index__body">
29
19
  <div
@@ -150,6 +140,7 @@
150
140
  <Select
151
141
  :label="pageSizeLabel"
152
142
  :aria-label="pageSizeLabel"
143
+ layout="inline"
153
144
  :model-value="String(query.page_size)"
154
145
  :options="pageSizeOptions"
155
146
  @update:model-value="
@@ -172,6 +163,7 @@
172
163
  <script setup lang="ts" generic="T extends object">
173
164
  import { computed, watch } from 'vue';
174
165
  import DataTable from '../table/DataTable.vue';
166
+ import PageHeader from '../page-header/PageHeader.vue';
175
167
  import type { Column } from '../table/types.js';
176
168
  import TextInput from '../../form/text-input/TextInput.vue';
177
169
  import Select from '../../form/select/Select.vue';
@@ -6,37 +6,6 @@
6
6
  color: var(--text-clr-base);
7
7
  }
8
8
 
9
- .pui-index__header {
10
- display: grid;
11
- grid-template-columns: minmax(0, 1fr) auto;
12
- align-items: center;
13
- justify-content: space-between;
14
- gap: var(--space-s);
15
- padding-block-end: var(--space-s);
16
- border-block-end: var(--stroke-sm) solid var(--border-clr-subtle);
17
- }
18
-
19
- .pui-index__heading {
20
- display: grid;
21
- gap: var(--space-3xs);
22
- min-inline-size: 0;
23
- }
24
-
25
- .pui-index__title {
26
- margin: 0;
27
- font-family: var(--ff-display);
28
- font-size: var(--step-2);
29
- font-weight: var(--fw-display);
30
- letter-spacing: var(--ls-display);
31
- }
32
-
33
- .pui-index__description {
34
- margin: 0;
35
- font-size: var(--step--1);
36
- color: var(--text-clr-muted);
37
- }
38
-
39
- .pui-index__actions,
40
9
  .pui-index__selection {
41
10
  display: flex;
42
11
  flex-wrap: wrap;
@@ -78,14 +47,15 @@
78
47
  --pui-table-cell-padding-block: var(--space-2xs);
79
48
  }
80
49
 
81
- .pui-index__page-size > .pui-field {
82
- display: flex;
83
- align-items: center;
84
- gap: var(--space-xs);
50
+ /* Two-line primary cell: title (usually a link) over a muted subline. */
51
+ .pui-index__primary {
52
+ display: grid;
53
+ gap: var(--space-3xs);
54
+ }
85
55
 
86
- & > .pui-field__label {
87
- white-space: nowrap;
88
- }
56
+ .pui-index__subline {
57
+ color: var(--text-clr-muted);
58
+ font-size: var(--step--2);
89
59
  }
90
60
 
91
61
  .pui-index__pagination {
@@ -123,16 +93,10 @@
123
93
  border: var(--stroke-sm) solid var(--border-clr-subtle);
124
94
  border-radius: var(--radius-md);
125
95
 
126
- & > .pui-index__header {
96
+ & > .pui-page-header {
127
97
  padding: var(--space-s);
128
98
  }
129
99
 
130
- & .pui-index__title {
131
- font-size: var(--step-1);
132
- font-weight: var(--fw-semibold);
133
- letter-spacing: normal;
134
- }
135
-
136
100
  & .pui-index__toolbar {
137
101
  padding: var(--space-s);
138
102
  border-block-end: var(--stroke-sm) solid var(--border-clr-subtle);
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <header
3
+ v-if="title || description || $slots.actions"
4
+ class="pui-page-header"
5
+ >
6
+ <div v-if="title || description" class="pui-page-header__heading">
7
+ <component
8
+ :is="`h${level}`"
9
+ v-if="title"
10
+ :id="titleId"
11
+ class="pui-page-header__title"
12
+ >{{ title }}</component
13
+ >
14
+ <p v-if="description" class="pui-page-header__description">
15
+ {{ description }}
16
+ </p>
17
+ </div>
18
+ <div v-if="$slots.actions" class="pui-page-header__actions">
19
+ <slot name="actions" />
20
+ </div>
21
+ </header>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ withDefaults(
26
+ defineProps<{
27
+ title?: string;
28
+ description?: string;
29
+ /** Heading level of the title: `1` for a page, `2` or `3` for a band inside one. */
30
+ level?: 1 | 2 | 3;
31
+ /** `id` for the title element, so a region can reference it through `aria-labelledby`. */
32
+ titleId?: string;
33
+ }>(),
34
+ {
35
+ title: undefined,
36
+ description: undefined,
37
+ level: 1,
38
+ titleId: undefined,
39
+ },
40
+ );
41
+
42
+ defineSlots<{
43
+ actions?: () => unknown;
44
+ }>();
45
+ </script>
46
+
47
+ <style>
48
+ @import './page-header.css';
49
+ </style>
@@ -0,0 +1,44 @@
1
+ @layer components {
2
+ .pui-page-header {
3
+ display: grid;
4
+ grid-template-columns: minmax(0, 1fr) auto;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ gap: var(--space-s);
8
+ padding-block-end: var(--space-s);
9
+ border-block-end: var(--stroke-sm) solid var(--border-clr-subtle);
10
+ }
11
+
12
+ .pui-page-header__heading {
13
+ display: grid;
14
+ gap: var(--space-3xs);
15
+ min-inline-size: 0;
16
+ }
17
+
18
+ .pui-page-header__title {
19
+ margin: 0;
20
+ font-family: var(--ff-display);
21
+ font-size: var(--step-2);
22
+ font-weight: var(--fw-display);
23
+ letter-spacing: var(--ls-display);
24
+
25
+ &:is(h2, h3) {
26
+ font-size: var(--step-1);
27
+ font-weight: var(--fw-semibold);
28
+ letter-spacing: normal;
29
+ }
30
+ }
31
+
32
+ .pui-page-header__description {
33
+ margin: 0;
34
+ font-size: var(--step--1);
35
+ color: var(--text-clr-muted);
36
+ }
37
+
38
+ .pui-page-header__actions {
39
+ display: flex;
40
+ flex-wrap: wrap;
41
+ align-items: center;
42
+ gap: var(--space-xs);
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pienter/ui",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Shared Pienter UI components, styles, icons, and browser utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -63,6 +63,7 @@
63
63
  "./components/Card.vue": "./components/layout/card/Card.vue",
64
64
  "./components/Collapsible.vue": "./components/layout/collapsible/Collapsible.vue",
65
65
  "./components/AppLayout.vue": "./components/layout/app-layout/AppLayout.vue",
66
+ "./components/PageHeader.vue": "./components/layout/page-header/PageHeader.vue",
66
67
  "./components/Separator.vue": "./components/layout/separator/Separator.vue",
67
68
  "./components/Table.vue": "./components/layout/table/Table.vue",
68
69
  "./components/TableRow.vue": "./components/layout/table/TableRow.vue",
@@ -33,10 +33,15 @@
33
33
 
34
34
  a {
35
35
  color: var(--text-clr-brand);
36
- text-underline-offset: 0.14em;
36
+ text-underline-offset: var(--space-3xs);
37
37
  }
38
38
 
39
39
  a:hover {
40
40
  color: var(--text-clr-brand-hover);
41
41
  }
42
+
43
+ a:focus-visible {
44
+ outline: var(--outline-width) solid var(--outline-clr-base);
45
+ outline-offset: var(--outline-offset);
46
+ }
42
47
  }
@@ -27,19 +27,15 @@
27
27
  float: none;
28
28
  }
29
29
 
30
- /* `data-layout="inline"`: control first in DOM order, label beside it,
31
- * hint and errors below spanning both columns. */
30
+ /* `data-layout="inline"`: label and control share a row in DOM order
31
+ * (control first for Checkbox/Switch, label first elsewhere); hint and
32
+ * errors drop below spanning both columns. */
32
33
  .pui-field[data-layout='inline'] {
33
34
  grid-template-columns: max-content 1fr;
34
35
  column-gap: var(--space-xs);
35
36
  align-items: center;
36
37
  }
37
38
 
38
- .pui-field[data-layout='inline'] .pui-field__label {
39
- cursor: pointer;
40
- user-select: none;
41
- }
42
-
43
39
  .pui-field[data-layout='inline'] .pui-field__hint {
44
40
  grid-column: 1 / -1;
45
41
  }