@ptbdynamic/plinth 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +258 -2
- package/package.json +99 -4
- package/packages/css/dist/avatar.css +122 -0
- package/packages/css/dist/badge.css +76 -0
- package/packages/css/dist/button.css +273 -0
- package/packages/css/dist/callout.css +143 -0
- package/packages/css/dist/code-block.css +283 -0
- package/packages/css/dist/confirm-dialog.css +183 -0
- package/packages/css/dist/data-table.css +470 -0
- package/packages/css/dist/dialog.css +113 -0
- package/packages/css/dist/drawer.css +156 -0
- package/packages/css/dist/fonts/geist/LICENSE +93 -0
- package/packages/css/dist/fonts/geist/geist-cyrillic-ext-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist/geist-cyrillic-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist/geist-latin-ext-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist/geist-latin-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist/geist-vietnamese-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist-mono/LICENSE +93 -0
- package/packages/css/dist/fonts/geist-mono/geist-mono-cyrillic-ext-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist-mono/geist-mono-cyrillic-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist-mono/geist-mono-latin-ext-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist-mono/geist-mono-latin-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist-mono/geist-mono-symbols2-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/geist-mono/geist-mono-vietnamese-wght-normal.woff2 +0 -0
- package/packages/css/dist/fonts/index.css +109 -0
- package/packages/css/dist/header.css +110 -0
- package/packages/css/dist/icon.css +51 -0
- package/packages/css/dist/list-card.css +76 -0
- package/packages/css/dist/list-row.css +164 -0
- package/packages/css/dist/menu-sheet.css +84 -0
- package/packages/css/dist/modal.css +367 -0
- package/packages/css/dist/pill-row.css +197 -0
- package/packages/css/dist/plinth.css +3773 -0
- package/packages/css/dist/section-menu.css +225 -0
- package/packages/css/dist/sheet.css +239 -0
- package/packages/css/dist/tokens.css +405 -0
- package/packages/react/dist/PlinthAvatar.d.ts +17 -0
- package/packages/react/dist/PlinthBadge.d.ts +7 -0
- package/packages/react/dist/PlinthButton.d.ts +29 -0
- package/packages/react/dist/PlinthCallout.d.ts +16 -0
- package/packages/react/dist/PlinthCodeBlock.d.ts +24 -0
- package/packages/react/dist/PlinthConfirmDialog.d.ts +38 -0
- package/packages/react/dist/PlinthDataTable.d.ts +11 -0
- package/packages/react/dist/PlinthDataTableCell.d.ts +15 -0
- package/packages/react/dist/PlinthDataTableColumn.d.ts +13 -0
- package/packages/react/dist/PlinthDataTableEmpty.d.ts +9 -0
- package/packages/react/dist/PlinthDataTableRow.d.ts +9 -0
- package/packages/react/dist/PlinthDataTableSort.d.ts +4 -0
- package/packages/react/dist/PlinthDialog.d.ts +26 -0
- package/packages/react/dist/PlinthDrawer.d.ts +29 -0
- package/packages/react/dist/PlinthHeader.d.ts +12 -0
- package/packages/react/dist/PlinthIcon.d.ts +10 -0
- package/packages/react/dist/PlinthIconButton.d.ts +26 -0
- package/packages/react/dist/PlinthListCard.d.ts +14 -0
- package/packages/react/dist/PlinthListRow.d.ts +29 -0
- package/packages/react/dist/PlinthMenuSheet.d.ts +24 -0
- package/packages/react/dist/PlinthPill.d.ts +17 -0
- package/packages/react/dist/PlinthPillRow.d.ts +13 -0
- package/packages/react/dist/PlinthSectionMenu.d.ts +11 -0
- package/packages/react/dist/PlinthSectionMenuItem.d.ts +20 -0
- package/packages/react/dist/PlinthSheet.d.ts +26 -0
- package/packages/react/dist/avatar.d.ts +12 -0
- package/packages/react/dist/button.d.ts +21 -0
- package/packages/react/dist/callout.d.ts +19 -0
- package/packages/react/dist/code-block.d.ts +16 -0
- package/packages/react/dist/confirm-dialog.d.ts +23 -0
- package/packages/react/dist/data-table.d.ts +15 -0
- package/packages/react/dist/dialog.d.ts +10 -0
- package/packages/react/dist/drawer.d.ts +20 -0
- package/packages/react/dist/heading.d.ts +5 -0
- package/packages/react/dist/icon.d.ts +5 -0
- package/packages/react/dist/index.d.ts +40 -0
- package/packages/react/dist/index.js +1127 -0
- package/packages/react/dist/list-row.d.ts +5 -0
- package/packages/react/dist/markup.d.ts +9 -0
- package/packages/react/dist/sheet.d.ts +12 -0
- package/packages/react/dist/size.d.ts +13 -0
- package/packages/react/dist/swipe.d.ts +15 -0
- package/packages/react/dist/tones.d.ts +5 -0
- package/packages/tokens/dist/index.d.ts +218 -0
- package/packages/tokens/dist/index.js +3966 -0
- package/packages/tokens/dist/tailwind-theme.css +80 -0
- package/packages/tokens/dist/tokens.css +405 -0
- package/packages/tokens/dist/tokens.json +3762 -0
- package/packages/vue/dist/PlinthAvatar.vue.d.ts +24 -0
- package/packages/vue/dist/PlinthBadge.vue.d.ts +21 -0
- package/packages/vue/dist/PlinthButton.vue.d.ts +42 -0
- package/packages/vue/dist/PlinthCallout.vue.d.ts +31 -0
- package/packages/vue/dist/PlinthCodeBlock.vue.d.ts +49 -0
- package/packages/vue/dist/PlinthConfirmDialog.vue.d.ts +51 -0
- package/packages/vue/dist/PlinthDataTable.vue.d.ts +21 -0
- package/packages/vue/dist/PlinthDataTableCell.vue.d.ts +29 -0
- package/packages/vue/dist/PlinthDataTableColumn.vue.d.ts +26 -0
- package/packages/vue/dist/PlinthDataTableEmpty.vue.d.ts +19 -0
- package/packages/vue/dist/PlinthDataTableRow.vue.d.ts +19 -0
- package/packages/vue/dist/PlinthDataTableSort.vue.d.ts +13 -0
- package/packages/vue/dist/PlinthDialog.vue.d.ts +45 -0
- package/packages/vue/dist/PlinthDrawer.vue.d.ts +48 -0
- package/packages/vue/dist/PlinthHeader.vue.d.ts +19 -0
- package/packages/vue/dist/PlinthIcon.vue.d.ts +23 -0
- package/packages/vue/dist/PlinthIconButton.vue.d.ts +37 -0
- package/packages/vue/dist/PlinthListCard.vue.d.ts +26 -0
- package/packages/vue/dist/PlinthListRow.vue.d.ts +38 -0
- package/packages/vue/dist/PlinthMenuSheet.vue.d.ts +48 -0
- package/packages/vue/dist/PlinthPill.vue.d.ts +28 -0
- package/packages/vue/dist/PlinthPillRow.vue.d.ts +22 -0
- package/packages/vue/dist/PlinthSectionMenu.vue.d.ts +22 -0
- package/packages/vue/dist/PlinthSectionMenuItem.vue.d.ts +28 -0
- package/packages/vue/dist/PlinthSheet.vue.d.ts +45 -0
- package/packages/vue/dist/avatar.d.ts +12 -0
- package/packages/vue/dist/button.d.ts +5 -0
- package/packages/vue/dist/callout.d.ts +19 -0
- package/packages/vue/dist/code-block.d.ts +16 -0
- package/packages/vue/dist/confirm-dialog.d.ts +23 -0
- package/packages/vue/dist/data-table.d.ts +15 -0
- package/packages/vue/dist/dialog.d.ts +10 -0
- package/packages/vue/dist/drawer.d.ts +20 -0
- package/packages/vue/dist/heading.d.ts +5 -0
- package/packages/vue/dist/icon.d.ts +5 -0
- package/packages/vue/dist/index.d.ts +44 -0
- package/packages/vue/dist/index.js +1439 -0
- package/packages/vue/dist/list-row.d.ts +5 -0
- package/packages/vue/dist/sheet.d.ts +12 -0
- package/packages/vue/dist/size.d.ts +13 -0
- package/packages/vue/dist/slots.d.ts +3 -0
- package/packages/vue/dist/swipe.d.ts +12 -0
- package/packages/vue/dist/tones.d.ts +5 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Modal base: what the dialog (dialog.css), the sheet (sheet.css) and the drawer (drawer.css)
|
|
3
|
+
* share, written once so they can't drift apart (docs/adr/0005): the root's surface and motion,
|
|
4
|
+
* the backdrop, the scroll lock, the safe areas, the panel and the grab handle, and the header,
|
|
5
|
+
* title, description, close button, body and footer. It has no markup of its own and no class:
|
|
6
|
+
* each component keeps its own root and part classes (plinth-dialog, plinth-dialog-panel, …;
|
|
7
|
+
* plinth-sheet, plinth-sheet-panel, …), listed here in :is(). A page that loads the components
|
|
8
|
+
* one by one loads modal.css with them.
|
|
9
|
+
*
|
|
10
|
+
* The parts sit in a panel, the root's only child:
|
|
11
|
+
* <dialog class="plinth-sheet" …>
|
|
12
|
+
* <div class="plinth-sheet-panel">
|
|
13
|
+
* <div class="plinth-sheet-handle" aria-hidden="true"></div> (a bottom panel on a phone)
|
|
14
|
+
* <div class="plinth-sheet-header">…</div>
|
|
15
|
+
* <div class="plinth-sheet-body">…</div>
|
|
16
|
+
* <div class="plinth-sheet-footer">…</div> (optional)
|
|
17
|
+
* </div>
|
|
18
|
+
* </dialog>
|
|
19
|
+
*
|
|
20
|
+
* The rules sit in a layer inside plinth (plinth.modal), so a component's own rules win over them
|
|
21
|
+
* whatever the order the files load in, and a project's utilities win over both.
|
|
22
|
+
*
|
|
23
|
+
* Each component sets where its root sits and how big it is, and tunes the motion and the safe
|
|
24
|
+
* area through three custom properties on the root; it never sets translate or opacity itself:
|
|
25
|
+
* - --plinth-modal-offset: where the root slides in from and back out to (a translate value;
|
|
26
|
+
* 0 0, the default, is no slide);
|
|
27
|
+
* - --plinth-modal-closed-opacity: its opacity while it opens and closes (0 fades it, 1, the
|
|
28
|
+
* default, doesn't);
|
|
29
|
+
* - --plinth-modal-safe-area-end: env(safe-area-inset-bottom) when the root reaches the bottom
|
|
30
|
+
* edge of the screen, so its last part clears the home indicator; initial (the default) when
|
|
31
|
+
* it doesn't.
|
|
32
|
+
*
|
|
33
|
+
* The motion runs on the motion tokens, which drop to 0ms under prefers-reduced-motion. While a
|
|
34
|
+
* modal is open the page under it doesn't scroll (html:has(…:modal)), and it has no scrollbar
|
|
35
|
+
* gutter (scrollbar-gutter: auto, over a project's stable in an earlier layer): the top layer is
|
|
36
|
+
* never drawn in the gutter, so a gutter would leave a strip of the page beside a sheet on the end
|
|
37
|
+
* edge or the full width. Where the scrollbar takes space the page shifts by its width under the
|
|
38
|
+
* backdrop; overlay scrollbars take no space and nothing shifts. The body scrolls, with
|
|
39
|
+
* overscroll-behavior: contain; the header and footer stay put; the margins of the body's first and
|
|
40
|
+
* last child are reset, so its padding is its edge. On a phone the footer's actions share its
|
|
41
|
+
* width; from 768px up they sit at its end.
|
|
42
|
+
*
|
|
43
|
+
* A bottom panel on a phone (a sheet whose side below 768px is the bottom, a dialog with
|
|
44
|
+
* data-side-phone="bottom" that can be dismissed, and the drawer) has a grab handle, and the
|
|
45
|
+
* person drags it down to close it. The layers print the handle only there; its presence is what
|
|
46
|
+
* turns the dragging on, below 768px. Dragging is native scrolling: the root fills the screen and
|
|
47
|
+
* is a scroll-snap container, see-through, with a spacer as tall as the screen above the panel,
|
|
48
|
+
* so the browser gives the momentum and the snapping. Without script the handle shows and the
|
|
49
|
+
* close button and Escape close the panel. What the platform can't do, the layers add (a few
|
|
50
|
+
* lines, docs/adr/0002 section 6: x-init in Blade, useSwipeToClose in Vue and React):
|
|
51
|
+
* - on scrollend (scroll where it is missing), when the panel has gone off the bottom edge, they
|
|
52
|
+
* call requestClose() (close() where it is missing), so cancel fires first, as for Escape; when
|
|
53
|
+
* the app prevents it, the panel goes back to where it opened;
|
|
54
|
+
* - a tap on the root itself, above the panel, closes it when closedby="any", since the browser
|
|
55
|
+
* reads that tap as one on the dialog, not its backdrop;
|
|
56
|
+
* - on toggle to open, a drawer with data-snap scrolls to that snap point (a browser that fires no
|
|
57
|
+
* toggle on a dialog opens it at its largest).
|
|
58
|
+
* The snapping is the browser's own scrolling, and the layers' scroll is instant.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
@layer plinth {
|
|
62
|
+
@layer modal {
|
|
63
|
+
html:has(:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):modal) {
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
scrollbar-gutter: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer) {
|
|
69
|
+
--plinth-modal-offset: 0 0;
|
|
70
|
+
--plinth-modal-closed-opacity: 1;
|
|
71
|
+
--plinth-modal-safe-area-end: initial;
|
|
72
|
+
|
|
73
|
+
position: fixed;
|
|
74
|
+
box-sizing: border-box;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
max-inline-size: 100%;
|
|
77
|
+
padding: 0;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
font-family: var(--plinth-font-family-body);
|
|
80
|
+
color: var(--plinth-color-text);
|
|
81
|
+
background-color: var(--plinth-color-surface-raised);
|
|
82
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
83
|
+
border-radius: var(--plinth-radius-container);
|
|
84
|
+
box-shadow: var(--plinth-elevation-3);
|
|
85
|
+
opacity: var(--plinth-modal-closed-opacity);
|
|
86
|
+
translate: var(--plinth-modal-offset);
|
|
87
|
+
transition:
|
|
88
|
+
translate var(--plinth-duration-normal) var(--plinth-easing-exit),
|
|
89
|
+
opacity var(--plinth-duration-normal) var(--plinth-easing-exit),
|
|
90
|
+
overlay var(--plinth-duration-normal) allow-discrete,
|
|
91
|
+
display var(--plinth-duration-normal) allow-discrete;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open] {
|
|
95
|
+
display: flex;
|
|
96
|
+
opacity: 1;
|
|
97
|
+
translate: 0 0;
|
|
98
|
+
transition-timing-function: var(--plinth-easing-enter);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@starting-style {
|
|
102
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open] {
|
|
103
|
+
opacity: var(--plinth-modal-closed-opacity);
|
|
104
|
+
translate: var(--plinth-modal-offset);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)::backdrop {
|
|
109
|
+
background-color: var(--plinth-color-overlay);
|
|
110
|
+
opacity: 0;
|
|
111
|
+
transition:
|
|
112
|
+
opacity var(--plinth-duration-normal) var(--plinth-easing-standard),
|
|
113
|
+
overlay var(--plinth-duration-normal) allow-discrete,
|
|
114
|
+
display var(--plinth-duration-normal) allow-discrete;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open]::backdrop {
|
|
118
|
+
opacity: 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@starting-style {
|
|
122
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open]::backdrop {
|
|
123
|
+
opacity: 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* The panel holds the parts, the root's only child. It is the column the body scrolls in;
|
|
129
|
+
* the root is the surface, except for a bottom panel on a phone (below).
|
|
130
|
+
*/
|
|
131
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel) {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex: 1 1 auto;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
min-block-size: 0;
|
|
136
|
+
margin: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
:is(.plinth-dialog-header, .plinth-sheet-header, .plinth-drawer-header) {
|
|
140
|
+
display: grid;
|
|
141
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
142
|
+
gap: var(--plinth-space-1) var(--plinth-space-3);
|
|
143
|
+
align-items: center;
|
|
144
|
+
padding-block: var(--plinth-space-3);
|
|
145
|
+
padding-inline: var(--plinth-space-4) var(--plinth-space-3);
|
|
146
|
+
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* With no close button, the header's padding is even. */
|
|
150
|
+
:is(.plinth-dialog-header, .plinth-sheet-header, .plinth-drawer-header):not(
|
|
151
|
+
:has(> :is(.plinth-dialog-close, .plinth-sheet-close, .plinth-drawer-close))
|
|
152
|
+
) {
|
|
153
|
+
padding-inline-end: var(--plinth-space-4);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
:is(.plinth-dialog-title, .plinth-sheet-title, .plinth-drawer-title) {
|
|
157
|
+
grid-column: 1;
|
|
158
|
+
margin: 0;
|
|
159
|
+
font-size: var(--plinth-font-size-lg);
|
|
160
|
+
font-weight: var(--plinth-font-weight-semibold);
|
|
161
|
+
line-height: var(--plinth-font-leading-tight);
|
|
162
|
+
letter-spacing: var(--plinth-font-tracking-tight);
|
|
163
|
+
overflow-wrap: anywhere;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
:is(.plinth-dialog-description, .plinth-sheet-description, .plinth-drawer-description) {
|
|
167
|
+
grid-column: 1;
|
|
168
|
+
margin: 0;
|
|
169
|
+
font-size: var(--plinth-font-size-sm);
|
|
170
|
+
line-height: var(--plinth-font-leading-normal);
|
|
171
|
+
color: var(--plinth-color-text-muted);
|
|
172
|
+
overflow-wrap: anywhere;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
:is(.plinth-dialog-close, .plinth-sheet-close, .plinth-drawer-close) {
|
|
176
|
+
grid-row: 1;
|
|
177
|
+
grid-column: 2;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
|
|
181
|
+
flex: 1 1 auto;
|
|
182
|
+
min-block-size: 0;
|
|
183
|
+
padding: var(--plinth-space-4);
|
|
184
|
+
overflow-y: auto;
|
|
185
|
+
overscroll-behavior: contain;
|
|
186
|
+
font-size: var(--plinth-font-size-md);
|
|
187
|
+
line-height: var(--plinth-font-leading-normal);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* The body's padding is its edge: the first and last child's margins don't add to it. */
|
|
191
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) > :first-child {
|
|
192
|
+
margin-block-start: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) > :last-child {
|
|
196
|
+
margin-block-end: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* A browser can focus a scrolling area that has nothing focusable in it (Chrome and Firefox
|
|
201
|
+
* do, Safari doesn't yet), so a long body of text only is better with a link or a control in
|
|
202
|
+
* it.
|
|
203
|
+
*/
|
|
204
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body):focus-visible {
|
|
205
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
206
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* The last part clears the home indicator when the root reaches the bottom edge. */
|
|
210
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body):last-child {
|
|
211
|
+
padding-block-end: max(
|
|
212
|
+
var(--plinth-space-4),
|
|
213
|
+
var(--plinth-modal-safe-area-end, var(--plinth-space-4))
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
:is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
gap: var(--plinth-space-2);
|
|
221
|
+
justify-content: flex-end;
|
|
222
|
+
padding-block: var(--plinth-space-3)
|
|
223
|
+
max(var(--plinth-space-3), var(--plinth-modal-safe-area-end, var(--plinth-space-3)));
|
|
224
|
+
padding-inline: var(--plinth-space-4);
|
|
225
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* On a phone the footer's actions share its width. */
|
|
229
|
+
:is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) > * {
|
|
230
|
+
flex: 1 1 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
* A bottom panel on a phone, dragged down to close: a root whose panel holds a handle (the
|
|
235
|
+
* layers print one only where the panel is on the bottom edge below 768px, and can be
|
|
236
|
+
* dismissed). The root fills the screen and is a scroll-snap container, see-through; its
|
|
237
|
+
* ::before is a spacer as tall as the screen above the panel, so scrolling it down is the
|
|
238
|
+
* panel going off the bottom edge. The flex column runs bottom up (column-reverse), so the
|
|
239
|
+
* root opens scrolled to its end, with the panel showing. Two snap positions: the panel's
|
|
240
|
+
* end on the screen's bottom edge (open), and the spacer filling the screen (closed); the
|
|
241
|
+
* layers close the dialog when the scrolling settles there. The panel is the surface, and the
|
|
242
|
+
* body hands a swipe on to the root once it is scrolled to its top.
|
|
243
|
+
*/
|
|
244
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
245
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
246
|
+
) {
|
|
247
|
+
flex-direction: column-reverse;
|
|
248
|
+
overflow-y: auto;
|
|
249
|
+
overscroll-behavior: contain;
|
|
250
|
+
scroll-snap-type: y mandatory;
|
|
251
|
+
scrollbar-width: none;
|
|
252
|
+
background-color: transparent;
|
|
253
|
+
box-shadow: none;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* As a scrolling area the root can take focus (Chrome and Firefox), so it shows the ring. */
|
|
257
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
258
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
259
|
+
):focus-visible {
|
|
260
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
261
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
265
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
266
|
+
)::before {
|
|
267
|
+
flex: none;
|
|
268
|
+
order: 1;
|
|
269
|
+
block-size: 100%;
|
|
270
|
+
content: '';
|
|
271
|
+
scroll-snap-align: start;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
275
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
276
|
+
) {
|
|
277
|
+
flex: none;
|
|
278
|
+
max-block-size: calc(
|
|
279
|
+
100% - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
|
|
280
|
+
);
|
|
281
|
+
padding-inline: max(env(safe-area-inset-left), env(safe-area-inset-right));
|
|
282
|
+
scroll-snap-align: end;
|
|
283
|
+
background-color: var(--plinth-color-surface-raised);
|
|
284
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
285
|
+
border-start-start-radius: var(--plinth-radius-container);
|
|
286
|
+
border-start-end-radius: var(--plinth-radius-container);
|
|
287
|
+
box-shadow: var(--plinth-elevation-3);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
* The grab handle, a short pill centred at the top; decorative, since Escape and the close
|
|
292
|
+
* button close the panel too. From 768px up it is hidden.
|
|
293
|
+
*/
|
|
294
|
+
:is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
|
|
295
|
+
display: block;
|
|
296
|
+
flex: none;
|
|
297
|
+
align-self: center;
|
|
298
|
+
inline-size: var(--plinth-space-10);
|
|
299
|
+
block-size: var(--plinth-space-1);
|
|
300
|
+
margin-block-start: var(--plinth-space-2);
|
|
301
|
+
background-color: var(--plinth-color-border-strong);
|
|
302
|
+
border-radius: var(--plinth-radius-pill);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
306
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
307
|
+
)
|
|
308
|
+
> :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
|
|
309
|
+
overscroll-behavior: auto;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@media (forced-colors: active) {
|
|
313
|
+
:is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
|
|
314
|
+
background-color: CanvasText;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@media (min-width: 768px) {
|
|
319
|
+
:is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) > * {
|
|
320
|
+
flex: none;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* From 768px up no panel is dragged: the root is the surface again. */
|
|
324
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
325
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
326
|
+
) {
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
overflow: hidden;
|
|
329
|
+
overscroll-behavior: auto;
|
|
330
|
+
scroll-snap-type: none;
|
|
331
|
+
scrollbar-width: auto;
|
|
332
|
+
background-color: var(--plinth-color-surface-raised);
|
|
333
|
+
box-shadow: var(--plinth-elevation-3);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
337
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
338
|
+
)::before {
|
|
339
|
+
content: none;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
343
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
344
|
+
) {
|
|
345
|
+
flex: 1 1 auto;
|
|
346
|
+
max-block-size: none;
|
|
347
|
+
padding-inline: 0;
|
|
348
|
+
scroll-snap-align: none;
|
|
349
|
+
background-color: transparent;
|
|
350
|
+
border-block-start: 0;
|
|
351
|
+
border-radius: 0;
|
|
352
|
+
box-shadow: none;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
:is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
|
|
356
|
+
display: none;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
360
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
361
|
+
)
|
|
362
|
+
> :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
|
|
363
|
+
overscroll-behavior: contain;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pill row: a row of pills for filters, segments or sub-pages.
|
|
3
|
+
*
|
|
4
|
+
* <div class="plinth-pill-row" role="group" aria-label="Status">
|
|
5
|
+
* <button type="button" class="plinth-pill" aria-pressed="true">
|
|
6
|
+
* <span class="plinth-pill-label">Open</span>
|
|
7
|
+
* <span class="plinth-pill-count">12</span>
|
|
8
|
+
* </button>
|
|
9
|
+
* <button type="button" class="plinth-pill" aria-pressed="false">
|
|
10
|
+
* <span class="plinth-pill-label">Closed</span>
|
|
11
|
+
* </button>
|
|
12
|
+
* </div>
|
|
13
|
+
*
|
|
14
|
+
* The root is a <div role="group">, named by aria-label (label in Blade and Vue) or by the app's
|
|
15
|
+
* own aria-labelledby. A row of links that navigates is <nav class="plinth-pill-row"> instead,
|
|
16
|
+
* with no role (nav in Blade and Vue), and its pills are links:
|
|
17
|
+
*
|
|
18
|
+
* <a class="plinth-pill" href="…" aria-current="page"><span class="plinth-pill-label">…</span></a>
|
|
19
|
+
*
|
|
20
|
+
* Each pill is its own component (<x-plinth::pill>, PlinthPill), so the app's attributes and
|
|
21
|
+
* listeners land on the pill (ADR 2). The count is optional. plinth-pill-label has no rule of its
|
|
22
|
+
* own: it is a hook for a project's styles.
|
|
23
|
+
*
|
|
24
|
+
* States, set by the app:
|
|
25
|
+
* - selected: a button pill always prints aria-pressed, "true" or "false"; a link pill prints
|
|
26
|
+
* aria-current="page" for the page being shown. Selected takes the selection tint
|
|
27
|
+
* (accent-subtle, accent-text) inside a 2px accent edge (border-width.thick), where the others
|
|
28
|
+
* have a 1px neutral one, so colour is not its only cue. A pressed button still answers hover
|
|
29
|
+
* and press with a step on the tint (accent-subtle-hover, accent-subtle-active).
|
|
30
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
31
|
+
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
32
|
+
*
|
|
33
|
+
* Layout: on a phone the row scrolls sideways, with no visible scrollbar, and snaps to the pills;
|
|
34
|
+
* from 768px it wraps. The row's padding keeps the focus ring and the 40px touch target of the
|
|
35
|
+
* pills inside the scrolling area, whatever --plinth-pill-height is (32px by default). A pill is
|
|
36
|
+
* at least 40px wide.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
@layer plinth {
|
|
40
|
+
.plinth-pill-row {
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: var(--plinth-space-2);
|
|
44
|
+
align-items: center;
|
|
45
|
+
max-inline-size: 100%;
|
|
46
|
+
padding-block: max(
|
|
47
|
+
var(--plinth-space-1),
|
|
48
|
+
(var(--plinth-touch-target) - var(--plinth-pill-height)) / 2
|
|
49
|
+
);
|
|
50
|
+
padding-inline: var(--plinth-space-1);
|
|
51
|
+
margin: 0;
|
|
52
|
+
overflow-x: auto;
|
|
53
|
+
overscroll-behavior-inline: contain;
|
|
54
|
+
scroll-snap-type: inline proximity;
|
|
55
|
+
scroll-padding-inline: var(--plinth-space-1);
|
|
56
|
+
scrollbar-width: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.plinth-pill {
|
|
60
|
+
position: relative;
|
|
61
|
+
box-sizing: border-box;
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
flex: none;
|
|
64
|
+
gap: var(--plinth-space-2);
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
min-inline-size: var(--plinth-touch-target);
|
|
68
|
+
min-block-size: var(--plinth-pill-height);
|
|
69
|
+
padding-block: 0;
|
|
70
|
+
padding-inline: var(--plinth-space-3);
|
|
71
|
+
margin: 0;
|
|
72
|
+
font-family: var(--plinth-font-family-body);
|
|
73
|
+
font-size: var(--plinth-font-size-sm);
|
|
74
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
75
|
+
line-height: var(--plinth-font-leading-tight);
|
|
76
|
+
color: var(--plinth-color-text);
|
|
77
|
+
white-space: nowrap;
|
|
78
|
+
text-decoration: none;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
user-select: none;
|
|
81
|
+
scroll-snap-align: start;
|
|
82
|
+
background-color: transparent;
|
|
83
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
84
|
+
border-radius: var(--plinth-radius-pill);
|
|
85
|
+
transition:
|
|
86
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
87
|
+
border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
88
|
+
box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
89
|
+
color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* A 40px touch target through an invisible hit area, as on the button (ADR 3). */
|
|
93
|
+
@media (pointer: coarse) {
|
|
94
|
+
.plinth-pill::before {
|
|
95
|
+
position: absolute;
|
|
96
|
+
inset-block-start: 50%;
|
|
97
|
+
inset-inline: 0;
|
|
98
|
+
block-size: max(100%, var(--plinth-touch-target));
|
|
99
|
+
content: '';
|
|
100
|
+
translate: 0 -50%;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@media (hover: hover) {
|
|
105
|
+
a.plinth-pill[href]:not([aria-current='page']):hover,
|
|
106
|
+
button.plinth-pill:enabled:not([aria-pressed='true']):hover {
|
|
107
|
+
background-color: var(--plinth-color-surface-hover);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
a.plinth-pill[href]:not([aria-current='page']):active,
|
|
112
|
+
button.plinth-pill:enabled:not([aria-pressed='true']):active {
|
|
113
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* The border and an inset ring make the 2px edge, so selecting doesn't move the label. */
|
|
117
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) {
|
|
118
|
+
color: var(--plinth-color-accent-text);
|
|
119
|
+
background-color: var(--plinth-color-accent-subtle);
|
|
120
|
+
border-color: var(--plinth-color-accent);
|
|
121
|
+
box-shadow: inset 0 0 0 calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
|
|
122
|
+
var(--plinth-color-accent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* A pressed button can be pressed again, so it answers hover and press with a step on the tint.
|
|
127
|
+
* Not in forced colours, where the tint would replace the system highlight under the label.
|
|
128
|
+
*/
|
|
129
|
+
@media (hover: hover) and (forced-colors: none) {
|
|
130
|
+
button.plinth-pill:enabled[aria-pressed='true']:hover {
|
|
131
|
+
background-color: var(--plinth-color-accent-subtle-hover);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (forced-colors: none) {
|
|
136
|
+
button.plinth-pill:enabled[aria-pressed='true']:active {
|
|
137
|
+
background-color: var(--plinth-color-accent-subtle-active);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.plinth-pill:disabled,
|
|
142
|
+
.plinth-pill[aria-disabled='true'] {
|
|
143
|
+
cursor: not-allowed;
|
|
144
|
+
opacity: var(--plinth-opacity-disabled);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.plinth-pill:focus-visible {
|
|
148
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
149
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* A gap between the ring and the accent edge, so focus and selection read apart. */
|
|
153
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']):focus-visible {
|
|
154
|
+
outline-offset: var(--plinth-focus-ring-offset-filled);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.plinth-pill-count {
|
|
158
|
+
font-weight: var(--plinth-font-weight-regular);
|
|
159
|
+
font-variant-numeric: tabular-nums;
|
|
160
|
+
color: var(--plinth-color-text-muted);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) .plinth-pill-count {
|
|
164
|
+
color: inherit;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Wider screens have room: the pills wrap onto more lines instead of scrolling. */
|
|
168
|
+
@media (min-width: 768px) {
|
|
169
|
+
.plinth-pill-row {
|
|
170
|
+
flex-wrap: wrap;
|
|
171
|
+
overflow-x: visible;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media (forced-colors: active) {
|
|
176
|
+
.plinth-pill {
|
|
177
|
+
border-color: ButtonText;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) {
|
|
181
|
+
color: HighlightText;
|
|
182
|
+
background-color: Highlight;
|
|
183
|
+
border-color: Highlight;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) .plinth-pill-count {
|
|
187
|
+
color: HighlightText;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.plinth-pill:disabled,
|
|
191
|
+
.plinth-pill[aria-disabled='true'] {
|
|
192
|
+
color: GrayText;
|
|
193
|
+
background-color: Canvas;
|
|
194
|
+
border-color: GrayText;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|