@pienter/ui 0.14.0 → 0.15.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 +42 -0
- package/CONVENTIONS.md +20 -1
- package/components/form/form/Form.vue +22 -14
- package/components/form/form/form.css +36 -0
- package/components/layout/app-layout/app-layout.css +115 -8
- package/components/layout/index/index.css +31 -0
- package/components/navigation/breadcrumb/breadcrumb.css +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.15.0 - 2026-09-15
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- `AppLayout`'s main region no longer has padding. A page's rows carry their
|
|
10
|
+
own gutter (see the page container under Added), so a sticky band sits
|
|
11
|
+
flush against the region's edges without negative margins. Consumer-visible:
|
|
12
|
+
a page that has not adopted `.pui-page` loses its inset. The seams stay,
|
|
13
|
+
defaulting to `0`; one line restores the previous behaviour:
|
|
14
|
+
`.pui-app-layout__main { --pui-app-layout-main-padding: var(--space-m) var(--space-l) var(--space-xl); --pui-app-layout-main-padding-mobile: var(--space-s); }`.
|
|
15
|
+
- `Form` wraps everything that is not the actions row — the error summary,
|
|
16
|
+
the default slot, the status message — in a new `<div class="pui-form__body">`.
|
|
17
|
+
This changes the DOM for every consumer, page-placed or not; selectors and
|
|
18
|
+
tests that assumed the fields were direct children of the `<form>` need the
|
|
19
|
+
extra level. The body inherits the form's `gap`, so a `.pui-form` override
|
|
20
|
+
still spaces the fields.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `.pui-page`, the page container for `AppLayout`'s default slot: one grid of
|
|
25
|
+
four full-width rows placed by named area — `pui-page__band` (the breadcrumb
|
|
26
|
+
strip), `__header`, `__main` (takes the leftover height) and `__footer`
|
|
27
|
+
(sticky at the region's bottom) — plus an optional `__aside` beside the main
|
|
28
|
+
row: a stretched, size-contained column whose child sticks to the top of the
|
|
29
|
+
scrollport with its own scroll, stacking below the content under 56em. The
|
|
30
|
+
bands span the whole region without knowing the aside exists. A row has one
|
|
31
|
+
occupant: two elements in the same named area overlap.
|
|
32
|
+
- `.pui-gutter`, the one inline padding every page row takes, in the
|
|
33
|
+
`utilities` layer so it replaces a bar's own padding without a specificity
|
|
34
|
+
fight. It reads `--pui-gutter-inline`, `--space-l` on the shell and
|
|
35
|
+
`--space-s` below the 48rem breakpoint.
|
|
36
|
+
- A module `Index` or a `Form` (so a `RecordForm`) that is a direct child of
|
|
37
|
+
`.pui-page` supplies the page's rows itself: its header, body and pagination
|
|
38
|
+
footer, or its body and its actions row as the sticky footer band. Inferred
|
|
39
|
+
from the position through `display: contents`, no prop; a related index and
|
|
40
|
+
any form inside `pui-page__main` are unaffected.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- A long record title in a `Breadcrumb` wraps inside its item instead of
|
|
45
|
+
widening the trail.
|
|
46
|
+
|
|
5
47
|
## 0.14.0 - 2026-09-15
|
|
6
48
|
|
|
7
49
|
### Changed
|
package/CONVENTIONS.md
CHANGED
|
@@ -1431,6 +1431,23 @@ resolve against a definite height. Scroll-to-top on navigation,
|
|
|
1431
1431
|
`scroll-padding` for sticky bands and scroll restoration target that region,
|
|
1432
1432
|
not the window; the AppLayout audit lists what a consumer wires.
|
|
1433
1433
|
|
|
1434
|
+
The main region has no padding. A page is a `.pui-page` grid of four
|
|
1435
|
+
full-width rows placed by named area — band (`pui-page__band`, the
|
|
1436
|
+
breadcrumb strip), header (`__header`), main (`__main`, the row that takes
|
|
1437
|
+
the leftover height) and footer (`__footer`, sticky at the region's bottom) —
|
|
1438
|
+
plus an optional `__aside` beside the main row. Every row takes its inline
|
|
1439
|
+
padding from the `.pui-gutter` utility, so the trail, the heading, the table
|
|
1440
|
+
and the pagination summary share one edge and a band is full width because it
|
|
1441
|
+
is a full-width row, not because it escaped an inset. No negative margins, no
|
|
1442
|
+
bleed properties, no component computes a distance. A consumer-written row
|
|
1443
|
+
carries the row class plus `pui-gutter`; a module `Index` or a `Form` that is
|
|
1444
|
+
a direct child of the page supplies its own rows through `display: contents`
|
|
1445
|
+
and needs no prop for it. **A row has one occupant.** Two elements in one
|
|
1446
|
+
named area overlap, silently; a module `Index` already supplies the footer
|
|
1447
|
+
row, so a page with one does not add its own. Anything that is not a row goes
|
|
1448
|
+
inside `pui-page__main`. The AppLayout audit holds the
|
|
1449
|
+
details, the aside included.
|
|
1450
|
+
|
|
1434
1451
|
Index accepts `load(query, { signal })`, `columns` and `queryOptions`. It owns
|
|
1435
1452
|
query state, rows, metadata, loading, failures and retry. Search is debounced;
|
|
1436
1453
|
search/filter/sort/page-size changes reset page to 1. New requests and unmount
|
|
@@ -1449,7 +1466,9 @@ DataTable's own border, and the pagination footer as a band below. `related` is
|
|
|
1449
1466
|
a list inside another record's detail page: one bordered region holding the
|
|
1450
1467
|
`h2` header, toolbar, table and pagination footer, divided by the subtle border
|
|
1451
1468
|
token. The heading level follows the placement; there is no heading-level prop.
|
|
1452
|
-
|
|
1469
|
+
A module index that is a direct child of `.pui-page` supplies the page's
|
|
1470
|
+
header, main and footer rows itself; a related index never does. An index
|
|
1471
|
+
definition — one collection's columns, query options, copy and cell
|
|
1453
1472
|
slots — is a consumer component composing `Index` and taking `placement` and a
|
|
1454
1473
|
parent id as props. The library ships no definition type or helper.
|
|
1455
1474
|
|
|
@@ -6,20 +6,28 @@
|
|
|
6
6
|
:aria-busy="busy ? 'true' : undefined"
|
|
7
7
|
@submit.prevent="handleSubmit"
|
|
8
8
|
>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
<div class="pui-form__body">
|
|
10
|
+
<Alert
|
|
11
|
+
v-if="topErrors.length > 0"
|
|
12
|
+
tone="danger"
|
|
13
|
+
title="There was a problem"
|
|
14
|
+
>
|
|
15
|
+
<ul class="pui-form__errors">
|
|
16
|
+
<li v-for="error in topErrors" :key="error">
|
|
17
|
+
{{ error }}
|
|
18
|
+
</li>
|
|
19
|
+
</ul>
|
|
20
|
+
</Alert>
|
|
21
|
+
|
|
22
|
+
<slot />
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
v-if="statusMessage"
|
|
26
|
+
class="pui-form__status"
|
|
27
|
+
aria-live="polite"
|
|
28
|
+
>
|
|
29
|
+
{{ statusMessage }}
|
|
30
|
+
</div>
|
|
23
31
|
</div>
|
|
24
32
|
|
|
25
33
|
<div v-if="!readonly" class="pui-form__actions">
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
gap: var(--space-m);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
.pui-form__body {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
/* The body takes the form's gap, so a consumer's `.pui-form` override keeps working. */
|
|
12
|
+
gap: inherit;
|
|
13
|
+
min-inline-size: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
.pui-form__errors {
|
|
9
17
|
margin: 0;
|
|
10
18
|
padding-inline-start: var(--space-s);
|
|
@@ -23,4 +31,32 @@
|
|
|
23
31
|
gap: var(--space-s);
|
|
24
32
|
align-items: center;
|
|
25
33
|
}
|
|
34
|
+
|
|
35
|
+
/* A form that is a direct child of a page supplies its main and footer
|
|
36
|
+
rows. The `<form>` stays in the DOM, so every control keeps its owner. */
|
|
37
|
+
.pui-page > .pui-form {
|
|
38
|
+
display: contents;
|
|
39
|
+
|
|
40
|
+
& > .pui-form__body {
|
|
41
|
+
grid-area: pui-page-main;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
& > .pui-form__actions {
|
|
45
|
+
grid-area: pui-page-footer;
|
|
46
|
+
position: sticky;
|
|
47
|
+
inset-block-end: 0;
|
|
48
|
+
z-index: var(--layer-2);
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
justify-content: end;
|
|
51
|
+
padding-block: var(--space-xs);
|
|
52
|
+
background: var(--bg-clr-surface);
|
|
53
|
+
border-block-start: var(--stroke-sm) solid var(--border-clr-subtle);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@layer utilities {
|
|
59
|
+
.pui-page > .pui-form > * {
|
|
60
|
+
padding-inline: var(--pui-gutter-inline, var(--space-l));
|
|
61
|
+
}
|
|
26
62
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
@layer components {
|
|
2
2
|
.pui-app-layout {
|
|
3
|
+
--pui-gutter-inline: var(--space-l);
|
|
4
|
+
|
|
3
5
|
display: grid;
|
|
4
6
|
grid-template-rows: auto minmax(0, 1fr);
|
|
5
7
|
block-size: 100dvh;
|
|
@@ -73,10 +75,7 @@
|
|
|
73
75
|
min-block-size: 0;
|
|
74
76
|
overflow: auto;
|
|
75
77
|
overscroll-behavior-block: contain;
|
|
76
|
-
padding: var(
|
|
77
|
-
--pui-app-layout-main-padding,
|
|
78
|
-
var(--space-m) var(--space-l) var(--space-xl)
|
|
79
|
-
);
|
|
78
|
+
padding: var(--pui-app-layout-main-padding, 0);
|
|
80
79
|
|
|
81
80
|
&:focus:not(:focus-visible) {
|
|
82
81
|
outline: none;
|
|
@@ -94,15 +93,14 @@
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
@media (max-width: 47.999em) {
|
|
96
|
+
--pui-gutter-inline: var(--space-s);
|
|
97
|
+
|
|
97
98
|
& .pui-app-layout__drawer-toggle {
|
|
98
99
|
display: inline-flex;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
& .pui-app-layout__main {
|
|
102
|
-
padding: var(
|
|
103
|
-
--pui-app-layout-main-padding-mobile,
|
|
104
|
-
var(--space-s)
|
|
105
|
-
);
|
|
103
|
+
padding: var(--pui-app-layout-main-padding-mobile, 0);
|
|
106
104
|
}
|
|
107
105
|
}
|
|
108
106
|
|
|
@@ -114,4 +112,113 @@
|
|
|
114
112
|
}
|
|
115
113
|
}
|
|
116
114
|
}
|
|
115
|
+
|
|
116
|
+
.pui-page {
|
|
117
|
+
--pui-page-aside-inline-size: calc(
|
|
118
|
+
var(--space-sidebar-width) + 2 *
|
|
119
|
+
var(--pui-gutter-inline, var(--space-l))
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
display: grid;
|
|
123
|
+
grid-template-areas:
|
|
124
|
+
'pui-page-band'
|
|
125
|
+
'pui-page-header'
|
|
126
|
+
'pui-page-main'
|
|
127
|
+
'pui-page-footer';
|
|
128
|
+
/* 1fr, not minmax(0, 1fr): the auto minimum carries the main row's
|
|
129
|
+
content, so a long page grows past the region instead of capping. */
|
|
130
|
+
grid-template-rows: auto auto 1fr auto;
|
|
131
|
+
grid-template-columns: minmax(0, 1fr);
|
|
132
|
+
gap: var(--space-s) 0;
|
|
133
|
+
min-inline-size: 0;
|
|
134
|
+
|
|
135
|
+
&:has(> .pui-page__aside) {
|
|
136
|
+
grid-template-areas:
|
|
137
|
+
'pui-page-band pui-page-band'
|
|
138
|
+
'pui-page-header pui-page-header'
|
|
139
|
+
'pui-page-main pui-page-aside'
|
|
140
|
+
'pui-page-footer pui-page-footer';
|
|
141
|
+
grid-template-columns:
|
|
142
|
+
minmax(0, 1fr)
|
|
143
|
+
var(--pui-page-aside-inline-size);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@media (max-width: 56em) {
|
|
147
|
+
&:has(> .pui-page__aside) {
|
|
148
|
+
grid-template-areas:
|
|
149
|
+
'pui-page-band'
|
|
150
|
+
'pui-page-header'
|
|
151
|
+
'pui-page-main'
|
|
152
|
+
'pui-page-aside'
|
|
153
|
+
'pui-page-footer';
|
|
154
|
+
grid-template-rows: auto auto 1fr auto auto;
|
|
155
|
+
grid-template-columns: minmax(0, 1fr);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.pui-page__band {
|
|
161
|
+
grid-area: pui-page-band;
|
|
162
|
+
padding-block: var(--space-xs);
|
|
163
|
+
background: var(--bg-clr-surface);
|
|
164
|
+
border-block-end: var(--stroke-sm) solid var(--border-clr-subtle);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.pui-page__header {
|
|
168
|
+
grid-area: pui-page-header;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.pui-page__main {
|
|
172
|
+
grid-area: pui-page-main;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.pui-page__footer {
|
|
176
|
+
grid-area: pui-page-footer;
|
|
177
|
+
position: sticky;
|
|
178
|
+
inset-block-end: 0;
|
|
179
|
+
z-index: var(--layer-2);
|
|
180
|
+
padding-block: var(--space-xs);
|
|
181
|
+
background: var(--bg-clr-surface);
|
|
182
|
+
border-block-start: var(--stroke-sm) solid var(--border-clr-subtle);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.pui-page__aside {
|
|
186
|
+
grid-area: pui-page-aside;
|
|
187
|
+
/* Size containment: the column is as tall as the content row and never
|
|
188
|
+
the other way round, so a long aside cannot make a short page scroll. */
|
|
189
|
+
contain: size;
|
|
190
|
+
border-inline-start: var(--stroke-sm) solid var(--border-clr-subtle);
|
|
191
|
+
|
|
192
|
+
/* The child sticks, not the column; `block-size: 100%` here would
|
|
193
|
+
defeat sticky. `100%` is the content row, which is exact on a short
|
|
194
|
+
page; the viewport term caps a long one at the scrollport. */
|
|
195
|
+
& > * {
|
|
196
|
+
position: sticky;
|
|
197
|
+
inset-block-start: 0;
|
|
198
|
+
max-block-size: min(100%, calc(100dvh - var(--space-2xl)));
|
|
199
|
+
overflow-y: auto;
|
|
200
|
+
overscroll-behavior: contain;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@media (max-width: 56em) {
|
|
204
|
+
contain: none;
|
|
205
|
+
border-inline-start: 0;
|
|
206
|
+
border-block-start: var(--stroke-sm) solid var(--border-clr-subtle);
|
|
207
|
+
padding-block-start: var(--space-s);
|
|
208
|
+
|
|
209
|
+
& > * {
|
|
210
|
+
position: static;
|
|
211
|
+
max-block-size: none;
|
|
212
|
+
overflow: visible;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* A utility outranks a component's own padding, so a bar that ships its own
|
|
219
|
+
`padding-inline` lines up when it becomes a page row. */
|
|
220
|
+
@layer utilities {
|
|
221
|
+
.pui-gutter {
|
|
222
|
+
padding-inline: var(--pui-gutter-inline, var(--space-l));
|
|
223
|
+
}
|
|
117
224
|
}
|
|
@@ -118,4 +118,35 @@
|
|
|
118
118
|
border-end-end-radius: inherit;
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
/* A module index that is a direct child of a page supplies three of its
|
|
123
|
+
rows: `display: contents` dissolves the root so the regions are the
|
|
124
|
+
page's grid items. A related index never does. */
|
|
125
|
+
.pui-page > .pui-index[data-placement='module'] {
|
|
126
|
+
display: contents;
|
|
127
|
+
|
|
128
|
+
& > .pui-page-header {
|
|
129
|
+
grid-area: pui-page-header;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
& > .pui-index__body {
|
|
133
|
+
grid-area: pui-page-main;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
& > .pui-index__footer {
|
|
137
|
+
grid-area: pui-page-footer;
|
|
138
|
+
position: sticky;
|
|
139
|
+
inset-block-end: 0;
|
|
140
|
+
z-index: var(--layer-2);
|
|
141
|
+
border-block-start-color: var(--border-clr-subtle);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Utilities layer, so the gutter replaces PaginationFooter's own inline
|
|
147
|
+
padding without a specificity fight. */
|
|
148
|
+
@layer utilities {
|
|
149
|
+
.pui-page > .pui-index[data-placement='module'] > * {
|
|
150
|
+
padding-inline: var(--pui-gutter-inline, var(--space-l));
|
|
151
|
+
}
|
|
121
152
|
}
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
display: inline-flex;
|
|
31
31
|
align-items: center;
|
|
32
32
|
gap: var(--space-2xs);
|
|
33
|
+
min-inline-size: 0;
|
|
34
|
+
/* `anywhere` shrinks min-content too, so a long record title wraps instead of widening the trail. */
|
|
35
|
+
overflow-wrap: anywhere;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
.pui-breadcrumb__link {
|