@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 +55 -0
- package/CONVENTIONS.md +23 -21
- package/components/display/badge/badge.css +7 -0
- package/components/form/checkbox/checkbox.css +6 -0
- package/components/form/combobox/Combobox.vue +9 -1
- package/components/form/date-input/DateInput.vue +8 -1
- package/components/form/number-field/NumberField.vue +8 -1
- package/components/form/select/Select.vue +8 -1
- package/components/form/switch/switch.css +6 -0
- package/components/form/tags-input/TagsInput.vue +8 -1
- package/components/form/text-input/TextInput.vue +8 -1
- package/components/form/textarea/Textarea.vue +8 -1
- package/components/layout/index/Index.vue +10 -18
- package/components/layout/index/index.css +9 -45
- package/components/layout/page-header/PageHeader.vue +49 -0
- package/components/layout/page-header/page-header.css +44 -0
- package/package.json +2 -1
- package/styles/2-base/base.css +6 -1
- package/styles/4-components/form-field.css +3 -7
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
|
|
100
|
-
| ------------------ |
|
|
101
|
-
| `data-variant` | Visual variant (mutually exclusive within a component)
|
|
102
|
-
| `data-size` | Sizing scale (mutually exclusive)
|
|
103
|
-
| `data-animation` | Continuous decorative animation on the block
|
|
104
|
-
| `data-tone` | Semantic tone for non-variant components (Alert, Badge, Toast)
|
|
105
|
-
| `data-block` | Block-level layout flag (boolean)
|
|
106
|
-
| `data-state` | Runtime state controlled by code or a behavior composable
|
|
107
|
-
| `data-status` | Derived semantic status for form primitives (validation outcome)
|
|
108
|
-
| `data-layout` | Form-primitive scaffold layout selector — lives on `.pui-field`;
|
|
109
|
-
| `data-orientation` | Layout direction for grouped controls (RadioGroup items, future SegmentedControl, etc.) — lives on the group container
|
|
110
|
-
| `data-placement` | Where a CMS surface is mounted, which decides its chrome and heading level — lives on `.pui-index`
|
|
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
|
-
- [ ]
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
<PageHeader
|
|
8
|
+
:title="title"
|
|
9
|
+
:description="description"
|
|
10
|
+
:level="placement === 'related' ? 2 : 1"
|
|
11
|
+
:title-id="titleId"
|
|
10
12
|
>
|
|
11
|
-
<
|
|
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
|
-
</
|
|
26
|
-
</
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
gap: var(--space-
|
|
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
|
-
|
|
87
|
-
|
|
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-
|
|
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.
|
|
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",
|
package/styles/2-base/base.css
CHANGED
|
@@ -33,10 +33,15 @@
|
|
|
33
33
|
|
|
34
34
|
a {
|
|
35
35
|
color: var(--text-clr-brand);
|
|
36
|
-
text-underline-offset:
|
|
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
|
|
31
|
-
*
|
|
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
|
}
|