@ptbdynamic/plinth 0.0.0-stage → 0.2.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/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/menu.css +245 -0
- package/packages/css/dist/modal.css +367 -0
- package/packages/css/dist/page-header.css +124 -0
- package/packages/css/dist/pill-row.css +197 -0
- package/packages/css/dist/plinth.css +4446 -0
- package/packages/css/dist/section-menu.css +226 -0
- package/packages/css/dist/sheet.css +239 -0
- package/packages/css/dist/text-field.css +230 -0
- package/packages/css/dist/textarea.css +169 -0
- package/packages/css/dist/tokens.css +413 -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/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/PlinthMenu.d.ts +12 -0
- package/packages/react/dist/PlinthMenuGroup.d.ts +9 -0
- package/packages/react/dist/PlinthMenuItem.d.ts +26 -0
- package/packages/react/dist/PlinthMenuSeparator.d.ts +4 -0
- package/packages/react/dist/PlinthMenuSheet.d.ts +24 -0
- package/packages/react/dist/PlinthPageHeader.d.ts +17 -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/PlinthTextField.d.ts +24 -0
- package/packages/react/dist/PlinthTextarea.d.ts +24 -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 +49 -0
- package/packages/react/dist/index.js +1326 -0
- package/packages/react/dist/list-row.d.ts +5 -0
- package/packages/react/dist/markup.d.ts +9 -0
- package/packages/react/dist/menu.d.ts +7 -0
- package/packages/react/dist/page-header.d.ts +5 -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/text-field.d.ts +10 -0
- package/packages/react/dist/tones.d.ts +5 -0
- package/packages/tokens/dist/index.d.ts +230 -0
- package/packages/tokens/dist/index.js +4091 -0
- package/packages/tokens/dist/tailwind-theme.css +80 -0
- package/packages/tokens/dist/tokens.css +413 -0
- package/packages/tokens/dist/tokens.json +3875 -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/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/PlinthMenu.vue.d.ts +24 -0
- package/packages/vue/dist/PlinthMenuGroup.vue.d.ts +17 -0
- package/packages/vue/dist/PlinthMenuItem.vue.d.ts +34 -0
- package/packages/vue/dist/PlinthMenuSeparator.vue.d.ts +3 -0
- package/packages/vue/dist/PlinthMenuSheet.vue.d.ts +48 -0
- package/packages/vue/dist/PlinthPageHeader.vue.d.ts +27 -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/PlinthTextField.vue.d.ts +51 -0
- package/packages/vue/dist/PlinthTextarea.vue.d.ts +42 -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 +53 -0
- package/packages/vue/dist/index.js +1732 -0
- package/packages/vue/dist/list-row.d.ts +5 -0
- package/packages/vue/dist/menu.d.ts +8 -0
- package/packages/vue/dist/page-header.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/text-field.d.ts +10 -0
- package/packages/vue/dist/tones.d.ts +5 -0
|
@@ -0,0 +1,4446 @@
|
|
|
1
|
+
/* Built from packages/tokens/tokens/*.json. Do not edit by hand. */
|
|
2
|
+
|
|
3
|
+
@layer plinth {
|
|
4
|
+
:root {
|
|
5
|
+
--plinth-ref-color-neutral-0: oklch(1 0 0);
|
|
6
|
+
--plinth-ref-color-neutral-50: oklch(0.985 0.002 260);
|
|
7
|
+
--plinth-ref-color-neutral-100: oklch(0.965 0.003 260);
|
|
8
|
+
--plinth-ref-color-neutral-150: oklch(0.945 0.004 260);
|
|
9
|
+
--plinth-ref-color-neutral-200: oklch(0.92 0.004 260);
|
|
10
|
+
--plinth-ref-color-neutral-300: oklch(0.82 0.005 260);
|
|
11
|
+
--plinth-ref-color-neutral-400: oklch(0.71 0.008 260);
|
|
12
|
+
--plinth-ref-color-neutral-500: oklch(0.54 0.01 260);
|
|
13
|
+
--plinth-ref-color-neutral-600: oklch(0.45 0.01 260);
|
|
14
|
+
--plinth-ref-color-neutral-700: oklch(0.37 0.01 260);
|
|
15
|
+
--plinth-ref-color-neutral-800: oklch(0.27 0.008 260);
|
|
16
|
+
--plinth-ref-color-neutral-850: oklch(0.235 0.007 260);
|
|
17
|
+
--plinth-ref-color-neutral-900: oklch(0.205 0.006 260);
|
|
18
|
+
--plinth-ref-color-neutral-950: oklch(0.18 0.005 260);
|
|
19
|
+
--plinth-ref-color-neutral-1000: oklch(0.145 0.004 260);
|
|
20
|
+
--plinth-ref-color-brand-50: #eff6ff;
|
|
21
|
+
--plinth-ref-color-brand-100: #dbeafe;
|
|
22
|
+
--plinth-ref-color-brand-200: #bfdbfe;
|
|
23
|
+
--plinth-ref-color-brand-300: #93c5fd;
|
|
24
|
+
--plinth-ref-color-brand-400: #60a5fa;
|
|
25
|
+
--plinth-ref-color-brand-500: #3b82f6;
|
|
26
|
+
--plinth-ref-color-brand-600: #2563eb;
|
|
27
|
+
--plinth-ref-color-brand-700: #1d4ed8;
|
|
28
|
+
--plinth-ref-color-brand-800: #1e40af;
|
|
29
|
+
--plinth-ref-color-brand-900: #1e3a8a;
|
|
30
|
+
--plinth-ref-color-brand-950: #172554;
|
|
31
|
+
--plinth-ref-color-green-50: #f0fdf4;
|
|
32
|
+
--plinth-ref-color-green-100: #dcfce7;
|
|
33
|
+
--plinth-ref-color-green-200: #bbf7d0;
|
|
34
|
+
--plinth-ref-color-green-400: #4ade80;
|
|
35
|
+
--plinth-ref-color-green-600: #16a34a;
|
|
36
|
+
--plinth-ref-color-green-700: #15803d;
|
|
37
|
+
--plinth-ref-color-green-800: #166534;
|
|
38
|
+
--plinth-ref-color-green-950: #052e16;
|
|
39
|
+
--plinth-ref-color-amber-50: #fffbeb;
|
|
40
|
+
--plinth-ref-color-amber-100: #fef3c7;
|
|
41
|
+
--plinth-ref-color-amber-200: #fde68a;
|
|
42
|
+
--plinth-ref-color-amber-400: #fbbf24;
|
|
43
|
+
--plinth-ref-color-amber-500: #f59e0b;
|
|
44
|
+
--plinth-ref-color-amber-700: #b45309;
|
|
45
|
+
--plinth-ref-color-amber-800: #92400e;
|
|
46
|
+
--plinth-ref-color-amber-950: #451a03;
|
|
47
|
+
--plinth-ref-color-red-50: #fef2f2;
|
|
48
|
+
--plinth-ref-color-red-100: #fee2e2;
|
|
49
|
+
--plinth-ref-color-red-200: #fecaca;
|
|
50
|
+
--plinth-ref-color-red-400: #f87171;
|
|
51
|
+
--plinth-ref-color-red-600: #dc2626;
|
|
52
|
+
--plinth-ref-color-red-700: #b91c1c;
|
|
53
|
+
--plinth-ref-color-red-800: #991b1b;
|
|
54
|
+
--plinth-ref-color-red-950: #450a0a;
|
|
55
|
+
--plinth-ref-color-black-alpha-10: #0000001a;
|
|
56
|
+
--plinth-ref-color-black-alpha-15: #00000026;
|
|
57
|
+
--plinth-ref-color-black-alpha-30: #0000004d;
|
|
58
|
+
--plinth-ref-color-black-alpha-50: #00000080;
|
|
59
|
+
--plinth-ref-color-black-alpha-60: #00000099;
|
|
60
|
+
--plinth-ref-color-black-alpha-70: #000000b3;
|
|
61
|
+
--plinth-ref-color-ink-alpha-6: oklch(0.18 0.005 260 / 0.06);
|
|
62
|
+
--plinth-ref-color-ink-alpha-10: oklch(0.18 0.005 260 / 0.1);
|
|
63
|
+
--plinth-ref-color-ink-alpha-20: oklch(0.18 0.005 260 / 0.2);
|
|
64
|
+
--plinth-ref-color-chalk-alpha-6: oklch(0.965 0.003 260 / 0.06);
|
|
65
|
+
--plinth-ref-color-chalk-alpha-10: oklch(0.965 0.003 260 / 0.1);
|
|
66
|
+
--plinth-ref-color-chalk-alpha-20: oklch(0.965 0.003 260 / 0.2);
|
|
67
|
+
--plinth-ref-color-white-alpha-12: #ffffff1f;
|
|
68
|
+
--plinth-ref-space-0: 0;
|
|
69
|
+
--plinth-ref-space-1: 0.25rem;
|
|
70
|
+
--plinth-ref-space-2: 0.5rem;
|
|
71
|
+
--plinth-ref-space-3: 0.75rem;
|
|
72
|
+
--plinth-ref-space-4: 1rem;
|
|
73
|
+
--plinth-ref-space-5: 1.25rem;
|
|
74
|
+
--plinth-ref-space-6: 1.5rem;
|
|
75
|
+
--plinth-ref-space-8: 2rem;
|
|
76
|
+
--plinth-ref-space-10: 2.5rem;
|
|
77
|
+
--plinth-ref-space-12: 3rem;
|
|
78
|
+
--plinth-ref-space-14: 3.5rem;
|
|
79
|
+
--plinth-ref-space-16: 4rem;
|
|
80
|
+
--plinth-ref-space-0-5: 0.125rem;
|
|
81
|
+
--plinth-ref-size-16: 1rem;
|
|
82
|
+
--plinth-ref-size-20: 1.25rem;
|
|
83
|
+
--plinth-ref-size-24: 1.5rem;
|
|
84
|
+
--plinth-ref-size-28: 1.75rem;
|
|
85
|
+
--plinth-ref-size-32: 2rem;
|
|
86
|
+
--plinth-ref-size-40: 2.5rem;
|
|
87
|
+
--plinth-ref-size-44: 2.75rem;
|
|
88
|
+
--plinth-ref-size-56: 3.5rem;
|
|
89
|
+
--plinth-ref-size-160: 10rem;
|
|
90
|
+
--plinth-ref-size-192: 12rem;
|
|
91
|
+
--plinth-ref-size-320: 20rem;
|
|
92
|
+
--plinth-ref-size-400: 25rem;
|
|
93
|
+
--plinth-ref-size-480: 30rem;
|
|
94
|
+
--plinth-ref-radius-0: 0;
|
|
95
|
+
--plinth-ref-radius-2: 0.125rem;
|
|
96
|
+
--plinth-ref-radius-4: 0.25rem;
|
|
97
|
+
--plinth-ref-radius-6: 0.375rem;
|
|
98
|
+
--plinth-ref-radius-8: 0.5rem;
|
|
99
|
+
--plinth-ref-radius-10: 0.625rem;
|
|
100
|
+
--plinth-ref-radius-12: 0.75rem;
|
|
101
|
+
--plinth-ref-radius-16: 1rem;
|
|
102
|
+
--plinth-ref-radius-full: 9999px;
|
|
103
|
+
--plinth-ref-border-width-1: 1px;
|
|
104
|
+
--plinth-ref-border-width-2: 2px;
|
|
105
|
+
--plinth-ref-border-width-3: 3px;
|
|
106
|
+
--plinth-ref-font-family-sans: 'Geist Variable', Geist, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
107
|
+
--plinth-ref-font-family-mono: 'Geist Mono Variable', 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
108
|
+
--plinth-ref-font-size-12: 0.75rem;
|
|
109
|
+
--plinth-ref-font-size-13: 0.8125rem;
|
|
110
|
+
--plinth-ref-font-size-14: 0.875rem;
|
|
111
|
+
--plinth-ref-font-size-15: 0.9375rem;
|
|
112
|
+
--plinth-ref-font-size-16: 1rem;
|
|
113
|
+
--plinth-ref-font-size-18: 1.125rem;
|
|
114
|
+
--plinth-ref-font-size-24: 1.5rem;
|
|
115
|
+
--plinth-ref-font-size-30: 1.875rem;
|
|
116
|
+
--plinth-ref-font-size-36: 2.25rem;
|
|
117
|
+
--plinth-ref-font-size-48: 3rem;
|
|
118
|
+
--plinth-ref-font-size-64: 4rem;
|
|
119
|
+
--plinth-ref-font-weight-400: 400;
|
|
120
|
+
--plinth-ref-font-weight-500: 500;
|
|
121
|
+
--plinth-ref-font-weight-600: 600;
|
|
122
|
+
--plinth-ref-font-weight-700: 700;
|
|
123
|
+
--plinth-ref-font-leading-105: 1.05;
|
|
124
|
+
--plinth-ref-font-leading-125: 1.25;
|
|
125
|
+
--plinth-ref-font-leading-150: 1.5;
|
|
126
|
+
--plinth-ref-font-tracking-0: 0;
|
|
127
|
+
--plinth-ref-font-tracking-neg-1: -0.01em;
|
|
128
|
+
--plinth-ref-font-tracking-neg-2: -0.02em;
|
|
129
|
+
--plinth-ref-font-tracking-neg-4: -0.04em;
|
|
130
|
+
--plinth-ref-duration-120: 120ms;
|
|
131
|
+
--plinth-ref-duration-200: 200ms;
|
|
132
|
+
--plinth-ref-duration-320: 320ms;
|
|
133
|
+
--plinth-ref-easing-standard: cubic-bezier(0.2, 0, 0, 1);
|
|
134
|
+
--plinth-ref-easing-enter: cubic-bezier(0, 0, 0, 1);
|
|
135
|
+
--plinth-ref-easing-exit: cubic-bezier(0.3, 0, 1, 1);
|
|
136
|
+
--plinth-space-0: var(--plinth-ref-space-0);
|
|
137
|
+
--plinth-space-1: var(--plinth-ref-space-1);
|
|
138
|
+
--plinth-space-2: var(--plinth-ref-space-2);
|
|
139
|
+
--plinth-space-3: var(--plinth-ref-space-3);
|
|
140
|
+
--plinth-space-4: var(--plinth-ref-space-4);
|
|
141
|
+
--plinth-space-5: var(--plinth-ref-space-5);
|
|
142
|
+
--plinth-space-6: var(--plinth-ref-space-6);
|
|
143
|
+
--plinth-space-8: var(--plinth-ref-space-8);
|
|
144
|
+
--plinth-space-10: var(--plinth-ref-space-10);
|
|
145
|
+
--plinth-space-12: var(--plinth-ref-space-12);
|
|
146
|
+
--plinth-space-14: var(--plinth-ref-space-14);
|
|
147
|
+
--plinth-space-16: var(--plinth-ref-space-16);
|
|
148
|
+
--plinth-space-0-5: var(--plinth-ref-space-0-5);
|
|
149
|
+
--plinth-radius-control: var(--plinth-ref-radius-6);
|
|
150
|
+
--plinth-radius-container: var(--plinth-ref-radius-10);
|
|
151
|
+
--plinth-radius-pill: var(--plinth-ref-radius-full);
|
|
152
|
+
--plinth-border-width-thin: var(--plinth-ref-border-width-1);
|
|
153
|
+
--plinth-border-width-thick: var(--plinth-ref-border-width-2);
|
|
154
|
+
--plinth-opacity-disabled: 0.5;
|
|
155
|
+
--plinth-focus-ring-width: var(--plinth-ref-border-width-2);
|
|
156
|
+
--plinth-focus-ring-offset: 0;
|
|
157
|
+
--plinth-focus-ring-offset-filled: var(--plinth-ref-border-width-2);
|
|
158
|
+
--plinth-focus-ring-offset-inset: -2px;
|
|
159
|
+
--plinth-font-family-body: var(--plinth-ref-font-family-sans);
|
|
160
|
+
--plinth-font-family-mono: var(--plinth-ref-font-family-mono);
|
|
161
|
+
--plinth-font-size-xs: var(--plinth-ref-font-size-12);
|
|
162
|
+
--plinth-font-size-sm: var(--plinth-ref-font-size-13);
|
|
163
|
+
--plinth-font-size-md: var(--plinth-ref-font-size-14);
|
|
164
|
+
--plinth-font-size-lg: var(--plinth-ref-font-size-15);
|
|
165
|
+
--plinth-font-size-xl: var(--plinth-ref-font-size-18);
|
|
166
|
+
--plinth-font-size-2xl: var(--plinth-ref-font-size-24);
|
|
167
|
+
--plinth-font-size-3xl: var(--plinth-ref-font-size-30);
|
|
168
|
+
--plinth-font-size-4xl: var(--plinth-ref-font-size-36);
|
|
169
|
+
--plinth-font-size-5xl: var(--plinth-ref-font-size-48);
|
|
170
|
+
--plinth-font-size-6xl: var(--plinth-ref-font-size-64);
|
|
171
|
+
--plinth-font-size-field: var(--plinth-ref-font-size-16);
|
|
172
|
+
--plinth-font-weight-regular: var(--plinth-ref-font-weight-400);
|
|
173
|
+
--plinth-font-weight-medium: var(--plinth-ref-font-weight-500);
|
|
174
|
+
--plinth-font-weight-semibold: var(--plinth-ref-font-weight-600);
|
|
175
|
+
--plinth-font-weight-bold: var(--plinth-ref-font-weight-700);
|
|
176
|
+
--plinth-font-leading-display: var(--plinth-ref-font-leading-105);
|
|
177
|
+
--plinth-font-leading-tight: var(--plinth-ref-font-leading-125);
|
|
178
|
+
--plinth-font-leading-normal: var(--plinth-ref-font-leading-150);
|
|
179
|
+
--plinth-font-tracking-normal: var(--plinth-ref-font-tracking-0);
|
|
180
|
+
--plinth-font-tracking-snug: var(--plinth-ref-font-tracking-neg-1);
|
|
181
|
+
--plinth-font-tracking-tight: var(--plinth-ref-font-tracking-neg-2);
|
|
182
|
+
--plinth-font-tracking-tighter: var(--plinth-ref-font-tracking-neg-4);
|
|
183
|
+
--plinth-duration-fast: var(--plinth-ref-duration-120);
|
|
184
|
+
--plinth-duration-normal: var(--plinth-ref-duration-200);
|
|
185
|
+
--plinth-duration-slow: var(--plinth-ref-duration-320);
|
|
186
|
+
--plinth-easing-standard: var(--plinth-ref-easing-standard);
|
|
187
|
+
--plinth-easing-enter: var(--plinth-ref-easing-enter);
|
|
188
|
+
--plinth-easing-exit: var(--plinth-ref-easing-exit);
|
|
189
|
+
--plinth-control-sm: var(--plinth-ref-size-28);
|
|
190
|
+
--plinth-control-md: var(--plinth-ref-size-32);
|
|
191
|
+
--plinth-control-lg: var(--plinth-ref-size-40);
|
|
192
|
+
--plinth-touch-target: var(--plinth-ref-size-40);
|
|
193
|
+
--plinth-size-icon-sm: var(--plinth-ref-size-16);
|
|
194
|
+
--plinth-size-icon-md: var(--plinth-ref-size-20);
|
|
195
|
+
--plinth-size-icon-lg: var(--plinth-ref-size-24);
|
|
196
|
+
--plinth-size-row: var(--plinth-ref-size-44);
|
|
197
|
+
--plinth-size-avatar-sm: var(--plinth-ref-size-24);
|
|
198
|
+
--plinth-size-avatar-md: var(--plinth-ref-size-32);
|
|
199
|
+
--plinth-size-avatar-lg: var(--plinth-ref-size-40);
|
|
200
|
+
--plinth-size-avatar-xl: var(--plinth-ref-size-56);
|
|
201
|
+
--plinth-size-dialog: var(--plinth-ref-size-480);
|
|
202
|
+
--plinth-size-panel: var(--plinth-ref-size-400);
|
|
203
|
+
--plinth-size-peek: var(--plinth-ref-size-160);
|
|
204
|
+
--plinth-size-menu-min: var(--plinth-ref-size-192);
|
|
205
|
+
--plinth-size-menu-max: var(--plinth-ref-size-320);
|
|
206
|
+
--plinth-layer-base: 0;
|
|
207
|
+
--plinth-layer-sticky: 100;
|
|
208
|
+
--plinth-layer-overlay: 200;
|
|
209
|
+
--plinth-layer-sheet: 300;
|
|
210
|
+
--plinth-layer-toast: 400;
|
|
211
|
+
--plinth-breakpoint-md: 768px;
|
|
212
|
+
--plinth-breakpoint-lg: 1280px;
|
|
213
|
+
--plinth-page-header-height: var(--plinth-space-14);
|
|
214
|
+
--plinth-page-header-offset: var(--plinth-space-0);
|
|
215
|
+
--plinth-data-table-row-height: var(--plinth-size-row);
|
|
216
|
+
--plinth-list-row-height: var(--plinth-size-row);
|
|
217
|
+
--plinth-pill-height: var(--plinth-control-md);
|
|
218
|
+
--plinth-dialog-width: var(--plinth-size-dialog);
|
|
219
|
+
--plinth-sheet-side-width: var(--plinth-size-panel);
|
|
220
|
+
--plinth-drawer-peek-height: var(--plinth-size-peek);
|
|
221
|
+
--plinth-section-menu-height: var(--plinth-space-14);
|
|
222
|
+
--plinth-section-menu-item-height: var(--plinth-control-lg);
|
|
223
|
+
--plinth-menu-min-width: var(--plinth-size-menu-min);
|
|
224
|
+
--plinth-menu-max-width: var(--plinth-size-menu-max);
|
|
225
|
+
--plinth-text-field-trailing-size: var(--plinth-control-md);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
:root,
|
|
229
|
+
[data-theme="light"] {
|
|
230
|
+
color-scheme: light;
|
|
231
|
+
--plinth-color-surface: var(--plinth-ref-color-neutral-50);
|
|
232
|
+
--plinth-color-surface-raised: var(--plinth-ref-color-neutral-0);
|
|
233
|
+
--plinth-color-surface-sunken: var(--plinth-ref-color-neutral-100);
|
|
234
|
+
--plinth-color-surface-hover: var(--plinth-ref-color-neutral-150);
|
|
235
|
+
--plinth-color-text: var(--plinth-ref-color-neutral-950);
|
|
236
|
+
--plinth-color-text-muted: var(--plinth-ref-color-neutral-600);
|
|
237
|
+
--plinth-color-text-subtle: var(--plinth-ref-color-neutral-500);
|
|
238
|
+
--plinth-color-border: var(--plinth-ref-color-ink-alpha-10);
|
|
239
|
+
--plinth-color-border-strong: var(--plinth-ref-color-ink-alpha-20);
|
|
240
|
+
--plinth-color-divider: var(--plinth-ref-color-ink-alpha-6);
|
|
241
|
+
--plinth-color-primary: var(--plinth-ref-color-neutral-950);
|
|
242
|
+
--plinth-color-primary-hover: var(--plinth-ref-color-neutral-800);
|
|
243
|
+
--plinth-color-primary-active: var(--plinth-ref-color-neutral-700);
|
|
244
|
+
--plinth-color-on-primary: var(--plinth-ref-color-neutral-0);
|
|
245
|
+
--plinth-color-primary-edge: var(--plinth-ref-color-white-alpha-12);
|
|
246
|
+
--plinth-color-accent: var(--plinth-ref-color-brand-600);
|
|
247
|
+
--plinth-color-accent-text: var(--plinth-ref-color-brand-700);
|
|
248
|
+
--plinth-color-on-accent: var(--plinth-ref-color-neutral-0);
|
|
249
|
+
--plinth-color-focus-ring: var(--plinth-ref-color-brand-600);
|
|
250
|
+
--plinth-color-overlay: var(--plinth-ref-color-black-alpha-50);
|
|
251
|
+
--plinth-color-info: var(--plinth-ref-color-brand-600);
|
|
252
|
+
--plinth-color-info-surface: var(--plinth-ref-color-brand-100);
|
|
253
|
+
--plinth-color-info-text: var(--plinth-ref-color-brand-800);
|
|
254
|
+
--plinth-color-success: var(--plinth-ref-color-green-600);
|
|
255
|
+
--plinth-color-success-surface: var(--plinth-ref-color-green-100);
|
|
256
|
+
--plinth-color-success-text: var(--plinth-ref-color-green-800);
|
|
257
|
+
--plinth-color-warning: var(--plinth-ref-color-amber-500);
|
|
258
|
+
--plinth-color-warning-surface: var(--plinth-ref-color-amber-100);
|
|
259
|
+
--plinth-color-warning-text: var(--plinth-ref-color-amber-800);
|
|
260
|
+
--plinth-color-danger: var(--plinth-ref-color-red-600);
|
|
261
|
+
--plinth-color-danger-surface: var(--plinth-ref-color-red-100);
|
|
262
|
+
--plinth-color-danger-text: var(--plinth-ref-color-red-800);
|
|
263
|
+
--plinth-color-accent-hover: var(--plinth-ref-color-brand-700);
|
|
264
|
+
--plinth-color-accent-active: var(--plinth-ref-color-brand-800);
|
|
265
|
+
--plinth-color-danger-solid: var(--plinth-ref-color-red-600);
|
|
266
|
+
--plinth-color-danger-solid-hover: var(--plinth-ref-color-red-700);
|
|
267
|
+
--plinth-color-danger-solid-active: var(--plinth-ref-color-red-800);
|
|
268
|
+
--plinth-color-on-danger: var(--plinth-ref-color-neutral-0);
|
|
269
|
+
--plinth-color-accent-subtle: var(--plinth-ref-color-brand-50);
|
|
270
|
+
--plinth-color-accent-subtle-hover: var(--plinth-ref-color-brand-100);
|
|
271
|
+
--plinth-color-accent-subtle-active: var(--plinth-ref-color-brand-200);
|
|
272
|
+
--plinth-elevation-1: 0 1px 2px 0 #0000001a;
|
|
273
|
+
--plinth-elevation-2: 0 4px 12px 0 #00000026;
|
|
274
|
+
--plinth-elevation-3: 0 12px 32px 0 #00000026;
|
|
275
|
+
--plinth-callout-neutral-surface: var(--plinth-color-surface-sunken);
|
|
276
|
+
--plinth-callout-neutral-border: var(--plinth-color-border);
|
|
277
|
+
--plinth-callout-info-surface: color-mix(in srgb, var(--plinth-color-info) 6%, transparent);
|
|
278
|
+
--plinth-callout-info-border: color-mix(in srgb, var(--plinth-color-info) 24%, transparent);
|
|
279
|
+
--plinth-callout-success-surface: color-mix(in srgb, var(--plinth-color-success) 6%, transparent);
|
|
280
|
+
--plinth-callout-success-border: color-mix(in srgb, var(--plinth-color-success) 24%, transparent);
|
|
281
|
+
--plinth-callout-warning-surface: color-mix(in srgb, var(--plinth-color-warning) 6%, transparent);
|
|
282
|
+
--plinth-callout-warning-border: color-mix(in srgb, var(--plinth-color-warning) 24%, transparent);
|
|
283
|
+
--plinth-callout-danger-surface: color-mix(in srgb, var(--plinth-color-danger) 6%, transparent);
|
|
284
|
+
--plinth-callout-danger-border: color-mix(in srgb, var(--plinth-color-danger) 24%, transparent);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
[data-theme="dark"],
|
|
288
|
+
.dark {
|
|
289
|
+
color-scheme: dark;
|
|
290
|
+
--plinth-color-surface: var(--plinth-ref-color-neutral-950);
|
|
291
|
+
--plinth-color-surface-raised: var(--plinth-ref-color-neutral-900);
|
|
292
|
+
--plinth-color-surface-sunken: var(--plinth-ref-color-neutral-1000);
|
|
293
|
+
--plinth-color-surface-hover: var(--plinth-ref-color-neutral-850);
|
|
294
|
+
--plinth-color-text: var(--plinth-ref-color-neutral-100);
|
|
295
|
+
--plinth-color-text-muted: var(--plinth-ref-color-neutral-300);
|
|
296
|
+
--plinth-color-text-subtle: var(--plinth-ref-color-neutral-400);
|
|
297
|
+
--plinth-color-border: var(--plinth-ref-color-chalk-alpha-10);
|
|
298
|
+
--plinth-color-border-strong: var(--plinth-ref-color-chalk-alpha-20);
|
|
299
|
+
--plinth-color-divider: var(--plinth-ref-color-chalk-alpha-6);
|
|
300
|
+
--plinth-color-primary: var(--plinth-ref-color-neutral-100);
|
|
301
|
+
--plinth-color-primary-hover: var(--plinth-ref-color-neutral-200);
|
|
302
|
+
--plinth-color-primary-active: var(--plinth-ref-color-neutral-300);
|
|
303
|
+
--plinth-color-on-primary: var(--plinth-ref-color-neutral-950);
|
|
304
|
+
--plinth-color-primary-edge: var(--plinth-ref-color-black-alpha-10);
|
|
305
|
+
--plinth-color-accent: var(--plinth-ref-color-brand-600);
|
|
306
|
+
--plinth-color-accent-text: var(--plinth-ref-color-brand-300);
|
|
307
|
+
--plinth-color-on-accent: var(--plinth-ref-color-neutral-0);
|
|
308
|
+
--plinth-color-focus-ring: var(--plinth-ref-color-brand-300);
|
|
309
|
+
--plinth-color-overlay: var(--plinth-ref-color-black-alpha-70);
|
|
310
|
+
--plinth-color-info: var(--plinth-ref-color-brand-400);
|
|
311
|
+
--plinth-color-info-surface: var(--plinth-ref-color-brand-950);
|
|
312
|
+
--plinth-color-info-text: var(--plinth-ref-color-brand-200);
|
|
313
|
+
--plinth-color-success: var(--plinth-ref-color-green-400);
|
|
314
|
+
--plinth-color-success-surface: var(--plinth-ref-color-green-950);
|
|
315
|
+
--plinth-color-success-text: var(--plinth-ref-color-green-200);
|
|
316
|
+
--plinth-color-warning: var(--plinth-ref-color-amber-400);
|
|
317
|
+
--plinth-color-warning-surface: var(--plinth-ref-color-amber-950);
|
|
318
|
+
--plinth-color-warning-text: var(--plinth-ref-color-amber-200);
|
|
319
|
+
--plinth-color-danger: var(--plinth-ref-color-red-400);
|
|
320
|
+
--plinth-color-danger-surface: var(--plinth-ref-color-red-950);
|
|
321
|
+
--plinth-color-danger-text: var(--plinth-ref-color-red-200);
|
|
322
|
+
--plinth-color-accent-hover: var(--plinth-ref-color-brand-700);
|
|
323
|
+
--plinth-color-accent-active: var(--plinth-ref-color-brand-800);
|
|
324
|
+
--plinth-color-danger-solid: var(--plinth-ref-color-red-600);
|
|
325
|
+
--plinth-color-danger-solid-hover: var(--plinth-ref-color-red-700);
|
|
326
|
+
--plinth-color-danger-solid-active: var(--plinth-ref-color-red-800);
|
|
327
|
+
--plinth-color-on-danger: var(--plinth-ref-color-neutral-0);
|
|
328
|
+
--plinth-color-accent-subtle: var(--plinth-ref-color-brand-950);
|
|
329
|
+
--plinth-color-accent-subtle-hover: var(--plinth-ref-color-brand-900);
|
|
330
|
+
--plinth-color-accent-subtle-active: var(--plinth-ref-color-brand-800);
|
|
331
|
+
--plinth-elevation-1: 0 1px 2px 0 #0000004d;
|
|
332
|
+
--plinth-elevation-2: 0 4px 12px 0 #00000080;
|
|
333
|
+
--plinth-elevation-3: 0 12px 32px 0 #00000099;
|
|
334
|
+
--plinth-callout-neutral-surface: var(--plinth-color-surface-sunken);
|
|
335
|
+
--plinth-callout-neutral-border: var(--plinth-color-border);
|
|
336
|
+
--plinth-callout-info-surface: color-mix(in srgb, var(--plinth-color-info) 8%, transparent);
|
|
337
|
+
--plinth-callout-info-border: color-mix(in srgb, var(--plinth-color-info) 24%, transparent);
|
|
338
|
+
--plinth-callout-success-surface: color-mix(in srgb, var(--plinth-color-success) 8%, transparent);
|
|
339
|
+
--plinth-callout-success-border: color-mix(in srgb, var(--plinth-color-success) 24%, transparent);
|
|
340
|
+
--plinth-callout-warning-surface: color-mix(in srgb, var(--plinth-color-warning) 8%, transparent);
|
|
341
|
+
--plinth-callout-warning-border: color-mix(in srgb, var(--plinth-color-warning) 24%, transparent);
|
|
342
|
+
--plinth-callout-danger-surface: color-mix(in srgb, var(--plinth-color-danger) 8%, transparent);
|
|
343
|
+
--plinth-callout-danger-border: color-mix(in srgb, var(--plinth-color-danger) 24%, transparent);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@media (prefers-color-scheme: dark) {
|
|
347
|
+
:root:not([data-theme="light"], .light) {
|
|
348
|
+
color-scheme: dark;
|
|
349
|
+
--plinth-color-surface: var(--plinth-ref-color-neutral-950);
|
|
350
|
+
--plinth-color-surface-raised: var(--plinth-ref-color-neutral-900);
|
|
351
|
+
--plinth-color-surface-sunken: var(--plinth-ref-color-neutral-1000);
|
|
352
|
+
--plinth-color-surface-hover: var(--plinth-ref-color-neutral-850);
|
|
353
|
+
--plinth-color-text: var(--plinth-ref-color-neutral-100);
|
|
354
|
+
--plinth-color-text-muted: var(--plinth-ref-color-neutral-300);
|
|
355
|
+
--plinth-color-text-subtle: var(--plinth-ref-color-neutral-400);
|
|
356
|
+
--plinth-color-border: var(--plinth-ref-color-chalk-alpha-10);
|
|
357
|
+
--plinth-color-border-strong: var(--plinth-ref-color-chalk-alpha-20);
|
|
358
|
+
--plinth-color-divider: var(--plinth-ref-color-chalk-alpha-6);
|
|
359
|
+
--plinth-color-primary: var(--plinth-ref-color-neutral-100);
|
|
360
|
+
--plinth-color-primary-hover: var(--plinth-ref-color-neutral-200);
|
|
361
|
+
--plinth-color-primary-active: var(--plinth-ref-color-neutral-300);
|
|
362
|
+
--plinth-color-on-primary: var(--plinth-ref-color-neutral-950);
|
|
363
|
+
--plinth-color-primary-edge: var(--plinth-ref-color-black-alpha-10);
|
|
364
|
+
--plinth-color-accent: var(--plinth-ref-color-brand-600);
|
|
365
|
+
--plinth-color-accent-text: var(--plinth-ref-color-brand-300);
|
|
366
|
+
--plinth-color-on-accent: var(--plinth-ref-color-neutral-0);
|
|
367
|
+
--plinth-color-focus-ring: var(--plinth-ref-color-brand-300);
|
|
368
|
+
--plinth-color-overlay: var(--plinth-ref-color-black-alpha-70);
|
|
369
|
+
--plinth-color-info: var(--plinth-ref-color-brand-400);
|
|
370
|
+
--plinth-color-info-surface: var(--plinth-ref-color-brand-950);
|
|
371
|
+
--plinth-color-info-text: var(--plinth-ref-color-brand-200);
|
|
372
|
+
--plinth-color-success: var(--plinth-ref-color-green-400);
|
|
373
|
+
--plinth-color-success-surface: var(--plinth-ref-color-green-950);
|
|
374
|
+
--plinth-color-success-text: var(--plinth-ref-color-green-200);
|
|
375
|
+
--plinth-color-warning: var(--plinth-ref-color-amber-400);
|
|
376
|
+
--plinth-color-warning-surface: var(--plinth-ref-color-amber-950);
|
|
377
|
+
--plinth-color-warning-text: var(--plinth-ref-color-amber-200);
|
|
378
|
+
--plinth-color-danger: var(--plinth-ref-color-red-400);
|
|
379
|
+
--plinth-color-danger-surface: var(--plinth-ref-color-red-950);
|
|
380
|
+
--plinth-color-danger-text: var(--plinth-ref-color-red-200);
|
|
381
|
+
--plinth-color-accent-hover: var(--plinth-ref-color-brand-700);
|
|
382
|
+
--plinth-color-accent-active: var(--plinth-ref-color-brand-800);
|
|
383
|
+
--plinth-color-danger-solid: var(--plinth-ref-color-red-600);
|
|
384
|
+
--plinth-color-danger-solid-hover: var(--plinth-ref-color-red-700);
|
|
385
|
+
--plinth-color-danger-solid-active: var(--plinth-ref-color-red-800);
|
|
386
|
+
--plinth-color-on-danger: var(--plinth-ref-color-neutral-0);
|
|
387
|
+
--plinth-color-accent-subtle: var(--plinth-ref-color-brand-950);
|
|
388
|
+
--plinth-color-accent-subtle-hover: var(--plinth-ref-color-brand-900);
|
|
389
|
+
--plinth-color-accent-subtle-active: var(--plinth-ref-color-brand-800);
|
|
390
|
+
--plinth-elevation-1: 0 1px 2px 0 #0000004d;
|
|
391
|
+
--plinth-elevation-2: 0 4px 12px 0 #00000080;
|
|
392
|
+
--plinth-elevation-3: 0 12px 32px 0 #00000099;
|
|
393
|
+
--plinth-callout-neutral-surface: var(--plinth-color-surface-sunken);
|
|
394
|
+
--plinth-callout-neutral-border: var(--plinth-color-border);
|
|
395
|
+
--plinth-callout-info-surface: color-mix(in srgb, var(--plinth-color-info) 8%, transparent);
|
|
396
|
+
--plinth-callout-info-border: color-mix(in srgb, var(--plinth-color-info) 24%, transparent);
|
|
397
|
+
--plinth-callout-success-surface: color-mix(in srgb, var(--plinth-color-success) 8%, transparent);
|
|
398
|
+
--plinth-callout-success-border: color-mix(in srgb, var(--plinth-color-success) 24%, transparent);
|
|
399
|
+
--plinth-callout-warning-surface: color-mix(in srgb, var(--plinth-color-warning) 8%, transparent);
|
|
400
|
+
--plinth-callout-warning-border: color-mix(in srgb, var(--plinth-color-warning) 24%, transparent);
|
|
401
|
+
--plinth-callout-danger-surface: color-mix(in srgb, var(--plinth-color-danger) 8%, transparent);
|
|
402
|
+
--plinth-callout-danger-border: color-mix(in srgb, var(--plinth-color-danger) 24%, transparent);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@media (prefers-reduced-motion: reduce) {
|
|
407
|
+
:root {
|
|
408
|
+
--plinth-duration-fast: 0ms;
|
|
409
|
+
--plinth-duration-normal: 0ms;
|
|
410
|
+
--plinth-duration-slow: 0ms;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* avatar.css */
|
|
416
|
+
/*
|
|
417
|
+
* Avatar: a person or an organisation, as an image, initials, or a person glyph.
|
|
418
|
+
*
|
|
419
|
+
* <span class="plinth-avatar" data-size="md" role="img" aria-label="Ana Lima, Online">
|
|
420
|
+
* <img class="plinth-avatar-image" src="…" alt="">
|
|
421
|
+
* or <span class="plinth-avatar-initials" aria-hidden="true">AL</span>
|
|
422
|
+
* or <svg class="plinth-avatar-glyph" aria-hidden="true">…</svg>
|
|
423
|
+
* <span class="plinth-avatar-status" data-tone="success" aria-hidden="true"></span>
|
|
424
|
+
* </span>
|
|
425
|
+
*
|
|
426
|
+
* The root carries the name (role="img", aria-label), with the status label after a comma. A
|
|
427
|
+
* decorative avatar, next to the name it shows, has aria-hidden="true" and no role or label.
|
|
428
|
+
* Image first, then initials (from the name, or given), then the glyph.
|
|
429
|
+
*
|
|
430
|
+
* Sizes (data-size): sm 24px, md 32px (default), lg 40px, xl 56px. Round by default
|
|
431
|
+
* (radius.pill), so a brand can square it. The status dot takes the badge's tones (data-tone:
|
|
432
|
+
* neutral, success, warning, danger) and a ring in the page colour; on another surface, set
|
|
433
|
+
* --plinth-avatar-ring on the container (a card: --plinth-color-surface-raised).
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
@layer plinth {
|
|
437
|
+
.plinth-avatar {
|
|
438
|
+
--plinth-avatar-size: var(--plinth-size-avatar-md);
|
|
439
|
+
--plinth-avatar-surface: var(--plinth-color-surface-sunken);
|
|
440
|
+
--plinth-avatar-status: var(--plinth-color-text-subtle);
|
|
441
|
+
--plinth-avatar-ring: var(--plinth-color-surface);
|
|
442
|
+
|
|
443
|
+
position: relative;
|
|
444
|
+
display: inline-flex;
|
|
445
|
+
flex: none;
|
|
446
|
+
align-items: center;
|
|
447
|
+
justify-content: center;
|
|
448
|
+
inline-size: var(--plinth-avatar-size);
|
|
449
|
+
block-size: var(--plinth-avatar-size);
|
|
450
|
+
font-family: var(--plinth-font-family-body);
|
|
451
|
+
font-size: var(--plinth-font-size-xs);
|
|
452
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
453
|
+
line-height: 1;
|
|
454
|
+
vertical-align: middle;
|
|
455
|
+
color: var(--plinth-color-text-muted);
|
|
456
|
+
user-select: none;
|
|
457
|
+
background-color: var(--plinth-avatar-surface);
|
|
458
|
+
border-radius: var(--plinth-radius-pill);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/*
|
|
462
|
+
* The hairline ring: over an image (positioned boxes paint after in-flow ones) and under the
|
|
463
|
+
* status dot (which comes later). A border, so forced-colours mode keeps the circle.
|
|
464
|
+
*/
|
|
465
|
+
.plinth-avatar::before {
|
|
466
|
+
position: absolute;
|
|
467
|
+
inset: 0;
|
|
468
|
+
box-sizing: border-box;
|
|
469
|
+
pointer-events: none;
|
|
470
|
+
content: '';
|
|
471
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
472
|
+
border-radius: inherit;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.plinth-avatar[data-size='sm'] {
|
|
476
|
+
--plinth-avatar-size: var(--plinth-size-avatar-sm);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.plinth-avatar[data-size='lg'] {
|
|
480
|
+
--plinth-avatar-size: var(--plinth-size-avatar-lg);
|
|
481
|
+
|
|
482
|
+
font-size: var(--plinth-font-size-md);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.plinth-avatar[data-size='xl'] {
|
|
486
|
+
--plinth-avatar-size: var(--plinth-size-avatar-xl);
|
|
487
|
+
|
|
488
|
+
font-size: var(--plinth-font-size-xl);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.plinth-avatar-image {
|
|
492
|
+
inline-size: 100%;
|
|
493
|
+
block-size: 100%;
|
|
494
|
+
object-fit: cover;
|
|
495
|
+
border-radius: inherit;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.plinth-avatar-initials {
|
|
499
|
+
letter-spacing: var(--plinth-font-tracking-snug);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.plinth-avatar-glyph {
|
|
503
|
+
inline-size: 60%;
|
|
504
|
+
block-size: 60%;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.plinth-avatar-status {
|
|
508
|
+
position: absolute;
|
|
509
|
+
inset-block-end: 0;
|
|
510
|
+
inset-inline-end: 0;
|
|
511
|
+
inline-size: calc(var(--plinth-avatar-size) * 0.3);
|
|
512
|
+
block-size: calc(var(--plinth-avatar-size) * 0.3);
|
|
513
|
+
background-color: var(--plinth-avatar-status);
|
|
514
|
+
|
|
515
|
+
/* Stays round when a brand squares the avatar. */
|
|
516
|
+
border-radius: 50%;
|
|
517
|
+
box-shadow: 0 0 0 var(--plinth-border-width-thick) var(--plinth-avatar-ring);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.plinth-avatar-status[data-tone='success'] {
|
|
521
|
+
--plinth-avatar-status: var(--plinth-color-success);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.plinth-avatar-status[data-tone='warning'] {
|
|
525
|
+
--plinth-avatar-status: var(--plinth-color-warning);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.plinth-avatar-status[data-tone='danger'] {
|
|
529
|
+
--plinth-avatar-status: var(--plinth-color-danger);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@media (forced-colors: active) {
|
|
533
|
+
.plinth-avatar-status {
|
|
534
|
+
background-color: CanvasText;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* badge.css */
|
|
540
|
+
/*
|
|
541
|
+
* Badge: a short label with an optional dot and a tone.
|
|
542
|
+
*
|
|
543
|
+
* <span class="plinth-badge" data-tone="success">
|
|
544
|
+
* <span class="plinth-badge-dot" aria-hidden="true"></span>
|
|
545
|
+
* <span class="plinth-badge-label">Paid</span>
|
|
546
|
+
* </span>
|
|
547
|
+
*
|
|
548
|
+
* Tones: neutral (default, also when data-tone is missing), success, warning, danger.
|
|
549
|
+
* The label carries the meaning; the tone and the dot only reinforce it.
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
@layer plinth {
|
|
553
|
+
.plinth-badge {
|
|
554
|
+
--plinth-badge-surface: var(--plinth-color-surface-sunken);
|
|
555
|
+
--plinth-badge-text: var(--plinth-color-text-muted);
|
|
556
|
+
--plinth-badge-accent: var(--plinth-color-text-muted);
|
|
557
|
+
|
|
558
|
+
display: inline-flex;
|
|
559
|
+
gap: var(--plinth-space-1);
|
|
560
|
+
align-items: center;
|
|
561
|
+
min-inline-size: 0;
|
|
562
|
+
max-inline-size: 100%;
|
|
563
|
+
padding-block: var(--plinth-space-0-5);
|
|
564
|
+
padding-inline: var(--plinth-space-2);
|
|
565
|
+
font-family: var(--plinth-font-family-body);
|
|
566
|
+
font-size: var(--plinth-font-size-xs);
|
|
567
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
568
|
+
line-height: var(--plinth-font-leading-normal);
|
|
569
|
+
vertical-align: middle;
|
|
570
|
+
color: var(--plinth-badge-text);
|
|
571
|
+
white-space: nowrap;
|
|
572
|
+
background-color: var(--plinth-badge-surface);
|
|
573
|
+
|
|
574
|
+
/* Invisible normally; drawn by the system in forced-colours mode so the badge keeps its edge. */
|
|
575
|
+
border: 1px solid transparent;
|
|
576
|
+
border-radius: var(--plinth-radius-pill);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.plinth-badge[data-tone='success'] {
|
|
580
|
+
--plinth-badge-surface: var(--plinth-color-success-surface);
|
|
581
|
+
--plinth-badge-text: var(--plinth-color-success-text);
|
|
582
|
+
--plinth-badge-accent: var(--plinth-color-success);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.plinth-badge[data-tone='warning'] {
|
|
586
|
+
--plinth-badge-surface: var(--plinth-color-warning-surface);
|
|
587
|
+
--plinth-badge-text: var(--plinth-color-warning-text);
|
|
588
|
+
--plinth-badge-accent: var(--plinth-color-warning);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.plinth-badge[data-tone='danger'] {
|
|
592
|
+
--plinth-badge-surface: var(--plinth-color-danger-surface);
|
|
593
|
+
--plinth-badge-text: var(--plinth-color-danger-text);
|
|
594
|
+
--plinth-badge-accent: var(--plinth-color-danger);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.plinth-badge-dot {
|
|
598
|
+
flex: none;
|
|
599
|
+
inline-size: var(--plinth-space-2);
|
|
600
|
+
block-size: var(--plinth-space-2);
|
|
601
|
+
background-color: var(--plinth-badge-accent);
|
|
602
|
+
border-radius: var(--plinth-radius-pill);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.plinth-badge-label {
|
|
606
|
+
overflow: hidden;
|
|
607
|
+
text-overflow: ellipsis;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
@media (forced-colors: active) {
|
|
611
|
+
.plinth-badge-dot {
|
|
612
|
+
background-color: currentcolor;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* button.css */
|
|
618
|
+
/*
|
|
619
|
+
* Button: the main action control.
|
|
620
|
+
*
|
|
621
|
+
* <button type="button" class="plinth-button" data-variant="primary" data-size="md">
|
|
622
|
+
* <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
|
|
623
|
+
* <span class="plinth-button-label">Save</span>
|
|
624
|
+
* <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
|
|
625
|
+
* </button>
|
|
626
|
+
*
|
|
627
|
+
* With an href it is <a class="plinth-button" href="…"> instead; the icons are optional.
|
|
628
|
+
*
|
|
629
|
+
* data-mirror on an icon flips it in right-to-left text, for arrows (mirror-icon and
|
|
630
|
+
* mirror-trailing in Blade, Vue and React; mirror on the icon button).
|
|
631
|
+
*
|
|
632
|
+
* Variants (data-variant): primary (default, also when missing; inverted neutral unless a brand
|
|
633
|
+
* colours it), accent (the brand's colour, for marketing and brand moments), secondary, ghost, danger, and
|
|
634
|
+
* danger-ghost for a low-emphasis destructive action (Remove next to an item).
|
|
635
|
+
* Sizes (data-size): sm 28px, md 32px (default), lg 40px. On a touch screen every size keeps a
|
|
636
|
+
* 40px touch target through an invisible hit area. Icons inside are 16px on sm and 20px otherwise.
|
|
637
|
+
*
|
|
638
|
+
* States:
|
|
639
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
640
|
+
* - busy: aria-busy="true" and aria-disabled="true", with
|
|
641
|
+
* <span class="plinth-button-spinner" aria-hidden="true"></span> in the leading icon's place.
|
|
642
|
+
* It keeps focus; the app ignores activation while busy.
|
|
643
|
+
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
644
|
+
*
|
|
645
|
+
* Icon button: a button with only an icon, square at the button's height.
|
|
646
|
+
*
|
|
647
|
+
* <button type="button" class="plinth-button plinth-icon-button" data-variant="ghost"
|
|
648
|
+
* data-size="md" aria-label="More actions">
|
|
649
|
+
* <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
|
|
650
|
+
* </button>
|
|
651
|
+
*
|
|
652
|
+
* With an href it is a link instead, with the same disabled state as the button's link:
|
|
653
|
+
*
|
|
654
|
+
* <a class="plinth-button plinth-icon-button" href="…" data-variant="secondary"
|
|
655
|
+
* data-size="md" aria-label="Next page">
|
|
656
|
+
* <span class="plinth-icon" data-size="md" data-mirror aria-hidden="true"><svg>…</svg></span>
|
|
657
|
+
* </a>
|
|
658
|
+
*
|
|
659
|
+
* aria-label is required: it is the only name the button has. A toggle has aria-pressed
|
|
660
|
+
* ("true" or "false"), and pressed shows the selection tint (accent-subtle, accent-text) inside
|
|
661
|
+
* a 2px accent edge (border-width.thick), so colour is not its only cue; it still answers hover
|
|
662
|
+
* and press with a step on the tint. In forced colours it takes the Highlight system colour. A
|
|
663
|
+
* link is never a toggle: aria-pressed is never printed on one.
|
|
664
|
+
*/
|
|
665
|
+
|
|
666
|
+
@layer plinth {
|
|
667
|
+
.plinth-button {
|
|
668
|
+
--plinth-button-surface: var(--plinth-color-primary);
|
|
669
|
+
--plinth-button-surface-hover: var(--plinth-color-primary-hover);
|
|
670
|
+
--plinth-button-surface-active: var(--plinth-color-primary-active);
|
|
671
|
+
--plinth-button-text: var(--plinth-color-on-primary);
|
|
672
|
+
--plinth-button-edge: transparent;
|
|
673
|
+
--plinth-button-inner-edge: inset 0 0 0 1px var(--plinth-color-primary-edge);
|
|
674
|
+
--plinth-button-height: var(--plinth-control-md);
|
|
675
|
+
--plinth-button-padding: var(--plinth-space-4);
|
|
676
|
+
--plinth-button-icon-size: var(--plinth-size-icon-md);
|
|
677
|
+
|
|
678
|
+
position: relative;
|
|
679
|
+
display: inline-flex;
|
|
680
|
+
gap: var(--plinth-space-2);
|
|
681
|
+
align-items: center;
|
|
682
|
+
justify-content: center;
|
|
683
|
+
max-inline-size: 100%;
|
|
684
|
+
min-block-size: var(--plinth-button-height);
|
|
685
|
+
padding-block: 0;
|
|
686
|
+
padding-inline: var(--plinth-button-padding);
|
|
687
|
+
font-family: var(--plinth-font-family-body);
|
|
688
|
+
font-size: var(--plinth-font-size-md);
|
|
689
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
690
|
+
line-height: var(--plinth-font-leading-tight);
|
|
691
|
+
vertical-align: middle;
|
|
692
|
+
color: var(--plinth-button-text);
|
|
693
|
+
white-space: nowrap;
|
|
694
|
+
text-decoration: none;
|
|
695
|
+
cursor: pointer;
|
|
696
|
+
user-select: none;
|
|
697
|
+
background-color: var(--plinth-button-surface);
|
|
698
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-button-edge);
|
|
699
|
+
border-radius: var(--plinth-radius-control);
|
|
700
|
+
box-shadow: var(--plinth-button-inner-edge);
|
|
701
|
+
transition:
|
|
702
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
703
|
+
border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
704
|
+
box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/* The brand's colour, for marketing and brand moments (Upgrade plan): at most one per screen. */
|
|
708
|
+
.plinth-button[data-variant='accent'] {
|
|
709
|
+
--plinth-button-surface: var(--plinth-color-accent);
|
|
710
|
+
--plinth-button-surface-hover: var(--plinth-color-accent-hover);
|
|
711
|
+
--plinth-button-surface-active: var(--plinth-color-accent-active);
|
|
712
|
+
--plinth-button-text: var(--plinth-color-on-accent);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.plinth-button[data-variant='secondary'] {
|
|
716
|
+
--plinth-button-inner-edge: none;
|
|
717
|
+
--plinth-button-surface: var(--plinth-color-surface-raised);
|
|
718
|
+
--plinth-button-surface-hover: var(--plinth-color-surface-hover);
|
|
719
|
+
--plinth-button-surface-active: var(--plinth-color-surface-sunken);
|
|
720
|
+
--plinth-button-text: var(--plinth-color-text);
|
|
721
|
+
--plinth-button-edge: var(--plinth-color-border-strong);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.plinth-button[data-variant='ghost'] {
|
|
725
|
+
--plinth-button-inner-edge: none;
|
|
726
|
+
--plinth-button-surface: transparent;
|
|
727
|
+
--plinth-button-surface-hover: var(--plinth-color-surface-hover);
|
|
728
|
+
--plinth-button-surface-active: var(--plinth-color-surface-sunken);
|
|
729
|
+
--plinth-button-text: var(--plinth-color-text);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.plinth-button[data-variant='danger'] {
|
|
733
|
+
--plinth-button-inner-edge: none;
|
|
734
|
+
--plinth-button-surface: var(--plinth-color-danger-solid);
|
|
735
|
+
--plinth-button-surface-hover: var(--plinth-color-danger-solid-hover);
|
|
736
|
+
--plinth-button-surface-active: var(--plinth-color-danger-solid-active);
|
|
737
|
+
--plinth-button-text: var(--plinth-color-on-danger);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.plinth-button[data-variant='danger-ghost'] {
|
|
741
|
+
--plinth-button-inner-edge: none;
|
|
742
|
+
--plinth-button-surface: transparent;
|
|
743
|
+
--plinth-button-surface-hover: var(--plinth-color-danger-surface);
|
|
744
|
+
--plinth-button-surface-active: var(--plinth-color-danger-surface);
|
|
745
|
+
--plinth-button-text: var(--plinth-color-danger-text);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.plinth-button[data-size='sm'] {
|
|
749
|
+
--plinth-button-height: var(--plinth-control-sm);
|
|
750
|
+
--plinth-button-padding: var(--plinth-space-3);
|
|
751
|
+
--plinth-button-icon-size: var(--plinth-size-icon-sm);
|
|
752
|
+
|
|
753
|
+
font-size: var(--plinth-font-size-sm);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.plinth-button[data-size='lg'] {
|
|
757
|
+
--plinth-button-height: var(--plinth-control-lg);
|
|
758
|
+
--plinth-button-padding: var(--plinth-space-5);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/*
|
|
762
|
+
* On a touch screen, every button keeps a 40px touch target through an invisible hit area,
|
|
763
|
+
* centred on it, without changing its look (ADR 3: controls look small, touch stays at 40px).
|
|
764
|
+
*/
|
|
765
|
+
@media (pointer: coarse) {
|
|
766
|
+
.plinth-button::before {
|
|
767
|
+
position: absolute;
|
|
768
|
+
inset-block-start: 50%;
|
|
769
|
+
inset-inline: 0;
|
|
770
|
+
block-size: max(100%, var(--plinth-touch-target));
|
|
771
|
+
content: '';
|
|
772
|
+
translate: 0 -50%;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* An icon button is narrower than 40px, so its hit area grows sideways too. */
|
|
776
|
+
.plinth-icon-button::before {
|
|
777
|
+
inset-inline: min(0%, 50% - var(--plinth-touch-target) / 2);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
@media (hover: hover) {
|
|
782
|
+
.plinth-button:hover {
|
|
783
|
+
background-color: var(--plinth-button-surface-hover);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.plinth-button:active {
|
|
788
|
+
background-color: var(--plinth-button-surface-active);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.plinth-button:disabled,
|
|
792
|
+
.plinth-button[aria-disabled='true'] {
|
|
793
|
+
cursor: not-allowed;
|
|
794
|
+
background-color: var(--plinth-button-surface);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.plinth-button:disabled,
|
|
798
|
+
.plinth-button[aria-disabled='true']:not([aria-busy='true']) {
|
|
799
|
+
opacity: var(--plinth-opacity-disabled);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.plinth-button[aria-busy='true'] {
|
|
803
|
+
cursor: progress;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.plinth-button:focus-visible {
|
|
807
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
808
|
+
outline-offset: var(--plinth-focus-ring-offset-filled);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.plinth-button:is(
|
|
812
|
+
[data-variant='secondary'],
|
|
813
|
+
[data-variant='ghost'],
|
|
814
|
+
[data-variant='danger-ghost']
|
|
815
|
+
):focus-visible {
|
|
816
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.plinth-icon-button {
|
|
820
|
+
flex: none;
|
|
821
|
+
inline-size: var(--plinth-button-height);
|
|
822
|
+
padding-inline: 0;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/* The border and an inset ring make the 2px edge, so pressed doesn't move the icon. */
|
|
826
|
+
.plinth-icon-button[aria-pressed='true'] {
|
|
827
|
+
--plinth-button-surface: var(--plinth-color-accent-subtle);
|
|
828
|
+
--plinth-button-surface-hover: var(--plinth-color-accent-subtle-hover);
|
|
829
|
+
--plinth-button-surface-active: var(--plinth-color-accent-subtle-active);
|
|
830
|
+
--plinth-button-text: var(--plinth-color-accent-text);
|
|
831
|
+
--plinth-button-edge: var(--plinth-color-accent);
|
|
832
|
+
--plinth-button-inner-edge: inset 0 0 0
|
|
833
|
+
calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
|
|
834
|
+
var(--plinth-color-accent);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/* A gap between the ring and the pressed accent edge, so focus and pressed read apart. */
|
|
838
|
+
.plinth-icon-button[aria-pressed='true']:focus-visible {
|
|
839
|
+
outline-offset: var(--plinth-focus-ring-offset-filled);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.plinth-button-label {
|
|
843
|
+
overflow: hidden;
|
|
844
|
+
text-overflow: ellipsis;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/* The same size as the icon it replaces, so the button keeps its width when it goes busy. */
|
|
848
|
+
.plinth-button-spinner {
|
|
849
|
+
flex: none;
|
|
850
|
+
inline-size: var(--plinth-button-icon-size);
|
|
851
|
+
block-size: var(--plinth-button-icon-size);
|
|
852
|
+
border: var(--plinth-border-width-thick) solid currentcolor;
|
|
853
|
+
border-inline-end-color: transparent;
|
|
854
|
+
border-radius: 50%;
|
|
855
|
+
animation: plinth-button-spin calc(var(--plinth-duration-slow) * 3) linear infinite;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
@keyframes plinth-button-spin {
|
|
859
|
+
to {
|
|
860
|
+
transform: rotate(1turn);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
@media (forced-colors: active) {
|
|
865
|
+
.plinth-button {
|
|
866
|
+
border-color: ButtonText;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.plinth-button:disabled,
|
|
870
|
+
.plinth-button[aria-disabled='true']:not([aria-busy='true']) {
|
|
871
|
+
color: GrayText;
|
|
872
|
+
border-color: GrayText;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* The pressed tint is dropped in forced colours, so pressed takes the selection colours,
|
|
876
|
+
unless it is disabled, which keeps GrayText. */
|
|
877
|
+
.plinth-icon-button[aria-pressed='true']:not(
|
|
878
|
+
:disabled,
|
|
879
|
+
[aria-disabled='true']:not([aria-busy='true'])
|
|
880
|
+
) {
|
|
881
|
+
color: HighlightText;
|
|
882
|
+
background-color: Highlight;
|
|
883
|
+
border-color: Highlight;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.plinth-button-spinner {
|
|
887
|
+
forced-color-adjust: none;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/* callout.css */
|
|
893
|
+
/*
|
|
894
|
+
* Callout: a note inside a page, with a tone, an icon, an optional title and actions.
|
|
895
|
+
*
|
|
896
|
+
* <div class="plinth-callout" data-tone="warning" role="note">
|
|
897
|
+
* <span class="plinth-icon plinth-callout-icon" data-size="md" aria-hidden="true">
|
|
898
|
+
* <svg viewBox="0 0 24 24">…</svg>
|
|
899
|
+
* </span>
|
|
900
|
+
* <div class="plinth-callout-body">
|
|
901
|
+
* <p class="plinth-callout-title">Invoices are sent at 9:00</p>
|
|
902
|
+
* <div class="plinth-callout-text">Drafts saved after that go out the next morning.</div>
|
|
903
|
+
* <div class="plinth-callout-actions">…buttons or links…</div>
|
|
904
|
+
* </div>
|
|
905
|
+
* </div>
|
|
906
|
+
*
|
|
907
|
+
* Tones (data-tone): neutral (default, also when the attribute is missing), info, success,
|
|
908
|
+
* warning, danger. Each tints the surface, the hairline border and the icon, and has its own icon
|
|
909
|
+
* shape; the title or the text still says what kind of note it is, so the tone is never the only
|
|
910
|
+
* signal. The tints are the component tokens --plinth-callout-<tone>-surface and -border: faint
|
|
911
|
+
* and translucent, quieter than the status surfaces a badge uses.
|
|
912
|
+
*
|
|
913
|
+
* role: note (default) for a note that is part of the page; status for a message the app adds
|
|
914
|
+
* after something happens (announced politely); alert for an urgent one (announced at once). A
|
|
915
|
+
* live region inserted with its content is often not announced, so for a status the app keeps an
|
|
916
|
+
* empty container with role="status" in the page and puts the callout in it.
|
|
917
|
+
*
|
|
918
|
+
* The text may hold paragraphs, links (underlined, in the text's colour) and inline code.
|
|
919
|
+
*
|
|
920
|
+
* The icon is decorative (aria-hidden). The tone's icon comes with the component; a project puts
|
|
921
|
+
* its own SVG in its place. Title, text and actions are each printed only when they have content.
|
|
922
|
+
*/
|
|
923
|
+
|
|
924
|
+
@layer plinth {
|
|
925
|
+
.plinth-callout {
|
|
926
|
+
--plinth-callout-surface: var(--plinth-callout-neutral-surface);
|
|
927
|
+
--plinth-callout-border: var(--plinth-callout-neutral-border);
|
|
928
|
+
--plinth-callout-icon-color: var(--plinth-color-text-muted);
|
|
929
|
+
|
|
930
|
+
display: flex;
|
|
931
|
+
gap: var(--plinth-space-3);
|
|
932
|
+
align-items: flex-start;
|
|
933
|
+
min-inline-size: 0;
|
|
934
|
+
padding-block: var(--plinth-space-3);
|
|
935
|
+
padding-inline: var(--plinth-space-4);
|
|
936
|
+
font-family: var(--plinth-font-family-body);
|
|
937
|
+
font-size: var(--plinth-font-size-md);
|
|
938
|
+
line-height: var(--plinth-font-leading-normal);
|
|
939
|
+
color: var(--plinth-color-text);
|
|
940
|
+
overflow-wrap: anywhere;
|
|
941
|
+
background-color: var(--plinth-callout-surface);
|
|
942
|
+
border: 1px solid var(--plinth-callout-border);
|
|
943
|
+
border-radius: var(--plinth-radius-container);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.plinth-callout[data-tone='info'] {
|
|
947
|
+
--plinth-callout-surface: var(--plinth-callout-info-surface);
|
|
948
|
+
--plinth-callout-border: var(--plinth-callout-info-border);
|
|
949
|
+
--plinth-callout-icon-color: var(--plinth-color-info-text);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.plinth-callout[data-tone='success'] {
|
|
953
|
+
--plinth-callout-surface: var(--plinth-callout-success-surface);
|
|
954
|
+
--plinth-callout-border: var(--plinth-callout-success-border);
|
|
955
|
+
--plinth-callout-icon-color: var(--plinth-color-success-text);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.plinth-callout[data-tone='warning'] {
|
|
959
|
+
--plinth-callout-surface: var(--plinth-callout-warning-surface);
|
|
960
|
+
--plinth-callout-border: var(--plinth-callout-warning-border);
|
|
961
|
+
--plinth-callout-icon-color: var(--plinth-color-warning-text);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.plinth-callout[data-tone='danger'] {
|
|
965
|
+
--plinth-callout-surface: var(--plinth-callout-danger-surface);
|
|
966
|
+
--plinth-callout-border: var(--plinth-callout-danger-border);
|
|
967
|
+
--plinth-callout-icon-color: var(--plinth-color-danger-text);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/* Centred on the first line of text, however tall the line is. */
|
|
971
|
+
.plinth-callout-icon {
|
|
972
|
+
margin-block: calc((1lh - var(--plinth-size-icon-md)) / 2);
|
|
973
|
+
color: var(--plinth-callout-icon-color);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.plinth-callout-body {
|
|
977
|
+
display: flex;
|
|
978
|
+
flex: 1;
|
|
979
|
+
flex-direction: column;
|
|
980
|
+
gap: var(--plinth-space-1);
|
|
981
|
+
min-inline-size: 0;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.plinth-callout-title {
|
|
985
|
+
margin: 0;
|
|
986
|
+
font-weight: var(--plinth-font-weight-semibold);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.plinth-callout-text > * {
|
|
990
|
+
margin-block: 0;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.plinth-callout-text > * + * {
|
|
994
|
+
margin-block-start: var(--plinth-space-2);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.plinth-callout-text a {
|
|
998
|
+
color: inherit;
|
|
999
|
+
text-decoration-line: underline;
|
|
1000
|
+
text-decoration-color: var(--plinth-color-text-muted);
|
|
1001
|
+
text-underline-offset: 0.2em;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
@media (hover: hover) {
|
|
1005
|
+
.plinth-callout-text a:hover {
|
|
1006
|
+
text-decoration-color: currentcolor;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.plinth-callout-text a:focus-visible {
|
|
1011
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
1012
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
1013
|
+
border-radius: var(--plinth-radius-control);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.plinth-callout-text code {
|
|
1017
|
+
font-family: var(--plinth-font-family-mono);
|
|
1018
|
+
font-size: var(--plinth-font-size-sm);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.plinth-callout-actions {
|
|
1022
|
+
display: flex;
|
|
1023
|
+
flex-wrap: wrap;
|
|
1024
|
+
gap: var(--plinth-space-2);
|
|
1025
|
+
align-items: center;
|
|
1026
|
+
margin-block-start: var(--plinth-space-2);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/* Small buttons grow a 40px hit area on a touch screen; wrapped rows keep theirs apart. */
|
|
1030
|
+
@media (pointer: coarse) {
|
|
1031
|
+
.plinth-callout-actions {
|
|
1032
|
+
row-gap: var(--plinth-space-3);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/* code-block.css */
|
|
1038
|
+
/*
|
|
1039
|
+
* Code block: a block of code with an optional file name or label, a copy button and optional
|
|
1040
|
+
* language tabs.
|
|
1041
|
+
*
|
|
1042
|
+
* <div class="plinth-code-block">
|
|
1043
|
+
* <div class="plinth-code-block-header">
|
|
1044
|
+
* <span class="plinth-code-block-label">resources/css/brand.css</span>
|
|
1045
|
+
* <button type="button" class="plinth-button plinth-code-block-copy" data-variant="ghost"
|
|
1046
|
+
* data-size="sm" hidden>
|
|
1047
|
+
* <span class="plinth-button-label">Copy</span>
|
|
1048
|
+
* </button>
|
|
1049
|
+
* <span class="plinth-code-block-status" role="status"></span>
|
|
1050
|
+
* </div>
|
|
1051
|
+
* <pre class="plinth-code-block-code" dir="ltr" tabindex="0"><code>…</code></pre>
|
|
1052
|
+
* </div>
|
|
1053
|
+
*
|
|
1054
|
+
* With language tabs, the header holds a radio group after the label, and there is one <pre> per
|
|
1055
|
+
* tab, in the same order:
|
|
1056
|
+
*
|
|
1057
|
+
* <div class="plinth-code-block-tabs" role="radiogroup" aria-label="Language">
|
|
1058
|
+
* <label class="plinth-code-block-tab">
|
|
1059
|
+
* <input type="radio" class="plinth-code-block-tab-input" name="install-language" checked>
|
|
1060
|
+
* <span class="plinth-code-block-tab-label">Blade</span>
|
|
1061
|
+
* </label>
|
|
1062
|
+
* <label class="plinth-code-block-tab">…Vue…</label>
|
|
1063
|
+
* </div>
|
|
1064
|
+
* …
|
|
1065
|
+
* <pre class="plinth-code-block-code" dir="ltr" tabindex="0"><code>…Blade…</code></pre>
|
|
1066
|
+
* <pre class="plinth-code-block-code" dir="ltr" tabindex="0"><code>…Vue…</code></pre>
|
|
1067
|
+
*
|
|
1068
|
+
* The root is the <div>, and takes the app's id. The label is optional, and is text or a little
|
|
1069
|
+
* markup (a badge). The tabs are native radio buttons (ADR 2): the browser moves between them with
|
|
1070
|
+
* the arrow keys and remembers the checked one, and CSS shows the <pre> in the checked tab's
|
|
1071
|
+
* place, with no script. Their name is the root's id with -language (one is generated when the
|
|
1072
|
+
* root has no id), and the group's name is tabs-label in Blade, Vue and React ("Language" by
|
|
1073
|
+
* default). In plain HTML, give each block on a page its own name, or their tabs become one group.
|
|
1074
|
+
* Up to six tabs (the layers drop the rest); the first is checked unless the app picks another
|
|
1075
|
+
* (selected), and after that the browser keeps the reader's choice. Inside a <form> the radios
|
|
1076
|
+
* are part of it: they are sent with it (name=on), and a reset checks the starting tab again.
|
|
1077
|
+
*
|
|
1078
|
+
* The code is text, or the markup a highlighter prints (Phiki, Shiki: the spans inside its
|
|
1079
|
+
* <code>). Plinth styles the block, not the tokens: their colours are the highlighter's. The
|
|
1080
|
+
* <pre> keeps the code's lines and scrolls sideways inside its own box, so it takes tabindex="0"
|
|
1081
|
+
* (a scrolling region must be reachable by keyboard), and dir="ltr", as code reads left to right
|
|
1082
|
+
* on any page.
|
|
1083
|
+
*
|
|
1084
|
+
* Copy: the button (button.css) is printed hidden, and the Blade (Alpine), Vue and React layers
|
|
1085
|
+
* show it when the clipboard is there. It copies the text of the <pre> that shows, says Copied
|
|
1086
|
+
* (copied-label) for two seconds and puts the same word in the status, a polite live region,
|
|
1087
|
+
* hidden from sight. The plain HTML works without it, and the header keeps its place for the
|
|
1088
|
+
* button either way. copy: false in Blade, Vue and React leaves
|
|
1089
|
+
* the button and the status out.
|
|
1090
|
+
*
|
|
1091
|
+
* States: the checked tab (input:checked) is a raised surface with a strong edge; hover only
|
|
1092
|
+
* where the device can hover; the focus ring on :focus-visible, on the tab's input (it covers the
|
|
1093
|
+
* tab) and inset on the <pre>. On a touch screen each tab keeps a 40px touch target, as the
|
|
1094
|
+
* button does. The header hides itself when it has nothing to show.
|
|
1095
|
+
*/
|
|
1096
|
+
|
|
1097
|
+
@layer plinth {
|
|
1098
|
+
.plinth-code-block {
|
|
1099
|
+
box-sizing: border-box;
|
|
1100
|
+
display: flex;
|
|
1101
|
+
flex-direction: column;
|
|
1102
|
+
min-inline-size: 0;
|
|
1103
|
+
max-inline-size: 100%;
|
|
1104
|
+
color: var(--plinth-color-text);
|
|
1105
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
1106
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
1107
|
+
border-radius: var(--plinth-radius-container);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.plinth-code-block-header {
|
|
1111
|
+
box-sizing: border-box;
|
|
1112
|
+
display: flex;
|
|
1113
|
+
flex-wrap: wrap;
|
|
1114
|
+
gap: var(--plinth-space-2);
|
|
1115
|
+
align-items: center;
|
|
1116
|
+
min-block-size: var(--plinth-touch-target);
|
|
1117
|
+
padding-block: calc((var(--plinth-touch-target) - var(--plinth-control-sm)) / 2);
|
|
1118
|
+
padding-inline: var(--plinth-space-4)
|
|
1119
|
+
calc((var(--plinth-touch-target) - var(--plinth-control-sm)) / 2);
|
|
1120
|
+
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/* Tabs first, with no label: the header's start matches its end. */
|
|
1124
|
+
.plinth-code-block-header:not(:has(.plinth-code-block-label)) {
|
|
1125
|
+
padding-inline-start: calc((var(--plinth-touch-target) - var(--plinth-control-sm)) / 2);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/*
|
|
1129
|
+
* Nothing to show: no label, no tabs and no copy button (copy off). A hidden copy button keeps
|
|
1130
|
+
* the header, so the code doesn't move down when the layer shows it.
|
|
1131
|
+
*/
|
|
1132
|
+
.plinth-code-block-header:not(
|
|
1133
|
+
:has(.plinth-code-block-label, .plinth-code-block-tabs, .plinth-code-block-copy)
|
|
1134
|
+
) {
|
|
1135
|
+
display: none;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.plinth-code-block-label {
|
|
1139
|
+
display: inline-flex;
|
|
1140
|
+
flex: 0 1 auto;
|
|
1141
|
+
gap: var(--plinth-space-2);
|
|
1142
|
+
align-items: center;
|
|
1143
|
+
min-inline-size: 0;
|
|
1144
|
+
overflow: hidden;
|
|
1145
|
+
text-overflow: ellipsis;
|
|
1146
|
+
font-family: var(--plinth-font-family-mono);
|
|
1147
|
+
font-size: var(--plinth-font-size-xs);
|
|
1148
|
+
line-height: var(--plinth-font-leading-tight);
|
|
1149
|
+
color: var(--plinth-color-text-muted);
|
|
1150
|
+
white-space: nowrap;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.plinth-code-block-label:has(+ .plinth-code-block-tabs) {
|
|
1154
|
+
margin-inline-end: var(--plinth-space-2);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/* The tabs follow the label; the copy button sits at the end. */
|
|
1158
|
+
.plinth-code-block-tabs {
|
|
1159
|
+
display: flex;
|
|
1160
|
+
flex: 0 1 auto;
|
|
1161
|
+
flex-wrap: wrap;
|
|
1162
|
+
gap: var(--plinth-space-1);
|
|
1163
|
+
align-items: center;
|
|
1164
|
+
min-inline-size: 0;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.plinth-code-block-tab {
|
|
1168
|
+
position: relative;
|
|
1169
|
+
box-sizing: border-box;
|
|
1170
|
+
display: inline-flex;
|
|
1171
|
+
align-items: center;
|
|
1172
|
+
justify-content: center;
|
|
1173
|
+
min-block-size: var(--plinth-control-sm);
|
|
1174
|
+
padding-block: 0;
|
|
1175
|
+
padding-inline: var(--plinth-space-3);
|
|
1176
|
+
font-family: var(--plinth-font-family-body);
|
|
1177
|
+
font-size: var(--plinth-font-size-sm);
|
|
1178
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
1179
|
+
line-height: var(--plinth-font-leading-tight);
|
|
1180
|
+
color: var(--plinth-color-text-muted);
|
|
1181
|
+
white-space: nowrap;
|
|
1182
|
+
cursor: pointer;
|
|
1183
|
+
user-select: none;
|
|
1184
|
+
background-color: transparent;
|
|
1185
|
+
border: var(--plinth-border-width-thin) solid transparent;
|
|
1186
|
+
border-radius: var(--plinth-radius-control);
|
|
1187
|
+
transition:
|
|
1188
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
1189
|
+
border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
1190
|
+
color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/* The radio covers its tab, so the tab is what is focused, clicked and ringed. */
|
|
1194
|
+
.plinth-code-block-tab-input {
|
|
1195
|
+
position: absolute;
|
|
1196
|
+
inset: calc(-1 * var(--plinth-border-width-thin));
|
|
1197
|
+
margin: 0;
|
|
1198
|
+
appearance: none;
|
|
1199
|
+
cursor: pointer;
|
|
1200
|
+
background: none;
|
|
1201
|
+
border: 0;
|
|
1202
|
+
border-radius: var(--plinth-radius-control);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.plinth-code-block-tab-input:focus-visible {
|
|
1206
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
1207
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.plinth-code-block-tab:has(> .plinth-code-block-tab-input:checked) {
|
|
1211
|
+
color: var(--plinth-color-text);
|
|
1212
|
+
background-color: var(--plinth-color-surface-raised);
|
|
1213
|
+
border-color: var(--plinth-color-border-strong);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
@media (hover: hover) {
|
|
1217
|
+
.plinth-code-block-tab:not(:has(> .plinth-code-block-tab-input:checked)):hover {
|
|
1218
|
+
color: var(--plinth-color-text);
|
|
1219
|
+
background-color: var(--plinth-color-surface-hover);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.plinth-code-block-tab:not(:has(> .plinth-code-block-tab-input:checked)):active {
|
|
1224
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/* A 40px touch target through an invisible hit area, as on the button (ADR 3). */
|
|
1228
|
+
@media (pointer: coarse) {
|
|
1229
|
+
.plinth-code-block-tab::before {
|
|
1230
|
+
position: absolute;
|
|
1231
|
+
inset-block-start: 50%;
|
|
1232
|
+
inset-inline: min(0%, 50% - var(--plinth-touch-target) / 2);
|
|
1233
|
+
block-size: max(100%, var(--plinth-touch-target));
|
|
1234
|
+
content: '';
|
|
1235
|
+
translate: 0 -50%;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.plinth-code-block-copy {
|
|
1240
|
+
margin-inline-start: auto;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/* The button's own display would beat the hidden attribute. */
|
|
1244
|
+
.plinth-code-block-copy[hidden] {
|
|
1245
|
+
display: none;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/* Read out, not shown. */
|
|
1249
|
+
.plinth-code-block-status {
|
|
1250
|
+
position: absolute;
|
|
1251
|
+
inline-size: 1px;
|
|
1252
|
+
block-size: 1px;
|
|
1253
|
+
padding: 0;
|
|
1254
|
+
margin: calc(-1 * var(--plinth-border-width-thin));
|
|
1255
|
+
overflow: hidden;
|
|
1256
|
+
white-space: nowrap;
|
|
1257
|
+
border: 0;
|
|
1258
|
+
clip-path: inset(50%);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.plinth-code-block-code {
|
|
1262
|
+
box-sizing: border-box;
|
|
1263
|
+
max-inline-size: 100%;
|
|
1264
|
+
padding: var(--plinth-space-4);
|
|
1265
|
+
margin: 0;
|
|
1266
|
+
overflow-x: auto;
|
|
1267
|
+
overscroll-behavior-inline: contain;
|
|
1268
|
+
font-family: var(--plinth-font-family-mono);
|
|
1269
|
+
font-size: var(--plinth-font-size-sm);
|
|
1270
|
+
line-height: var(--plinth-font-leading-normal);
|
|
1271
|
+
color: var(--plinth-color-text);
|
|
1272
|
+
tab-size: 2;
|
|
1273
|
+
white-space: pre;
|
|
1274
|
+
border-radius: var(--plinth-radius-container);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.plinth-code-block-code:focus-visible {
|
|
1278
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
1279
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.plinth-code-block-code > code {
|
|
1283
|
+
font: inherit;
|
|
1284
|
+
color: inherit;
|
|
1285
|
+
background: none;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/*
|
|
1289
|
+
* With tabs, only the checked tab's <pre> shows: the nth tab shows the nth <pre>. Nothing
|
|
1290
|
+
* checked (a reset form) shows the first.
|
|
1291
|
+
*/
|
|
1292
|
+
.plinth-code-block:has(.plinth-code-block-tabs) > .plinth-code-block-code {
|
|
1293
|
+
display: none;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
.plinth-code-block:not(:has(.plinth-code-block-tab-input:checked))
|
|
1297
|
+
> .plinth-code-block-code:first-of-type,
|
|
1298
|
+
.plinth-code-block:has(.plinth-code-block-tab:nth-child(1) > :checked)
|
|
1299
|
+
> .plinth-code-block-code:nth-of-type(1),
|
|
1300
|
+
.plinth-code-block:has(.plinth-code-block-tab:nth-child(2) > :checked)
|
|
1301
|
+
> .plinth-code-block-code:nth-of-type(2),
|
|
1302
|
+
.plinth-code-block:has(.plinth-code-block-tab:nth-child(3) > :checked)
|
|
1303
|
+
> .plinth-code-block-code:nth-of-type(3),
|
|
1304
|
+
.plinth-code-block:has(.plinth-code-block-tab:nth-child(4) > :checked)
|
|
1305
|
+
> .plinth-code-block-code:nth-of-type(4),
|
|
1306
|
+
.plinth-code-block:has(.plinth-code-block-tab:nth-child(5) > :checked)
|
|
1307
|
+
> .plinth-code-block-code:nth-of-type(5),
|
|
1308
|
+
.plinth-code-block:has(.plinth-code-block-tab:nth-child(6) > :checked)
|
|
1309
|
+
> .plinth-code-block-code:nth-of-type(6) {
|
|
1310
|
+
display: block;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
@media (forced-colors: active) {
|
|
1314
|
+
.plinth-code-block-tab:has(> .plinth-code-block-tab-input:checked) {
|
|
1315
|
+
color: HighlightText;
|
|
1316
|
+
background-color: Highlight;
|
|
1317
|
+
border-color: Highlight;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/* confirm-dialog.css */
|
|
1323
|
+
/*
|
|
1324
|
+
* Confirmation dialog: a dialog that asks a person to confirm an action before it happens, in
|
|
1325
|
+
* three kinds: simple ("Archive 3 tickets?"), type to confirm ("Type harbour to delete this
|
|
1326
|
+
* project") and password ("Enter your password to change the billing email").
|
|
1327
|
+
*
|
|
1328
|
+
* <dialog id="delete" class="plinth-dialog plinth-confirm-dialog" role="alertdialog"
|
|
1329
|
+
* closedby="closerequest" aria-labelledby="delete-title" aria-describedby="delete-message">
|
|
1330
|
+
* <form method="dialog" class="plinth-confirm-dialog-form">
|
|
1331
|
+
* <div class="plinth-dialog-header">
|
|
1332
|
+
* <h2 id="delete-title" class="plinth-dialog-title">Delete this project?</h2>
|
|
1333
|
+
* </div>
|
|
1334
|
+
* <div class="plinth-dialog-body">
|
|
1335
|
+
* <p id="delete-message" class="plinth-confirm-dialog-message">Its tickets go too.</p>
|
|
1336
|
+
* <div class="plinth-confirm-dialog-field">
|
|
1337
|
+
* <label for="delete-field" class="plinth-confirm-dialog-label">
|
|
1338
|
+
* Type <strong>harbour</strong> to confirm</label>
|
|
1339
|
+
* <input id="delete-field" class="plinth-confirm-dialog-input" type="text"
|
|
1340
|
+
* name="confirmation" required pattern="harbour" autocomplete="off"
|
|
1341
|
+
* autocapitalize="none" spellcheck="false">
|
|
1342
|
+
* </div>
|
|
1343
|
+
* <p id="delete-error" class="plinth-confirm-dialog-error" role="alert">…</p>
|
|
1344
|
+
* </div>
|
|
1345
|
+
* <div class="plinth-dialog-footer">
|
|
1346
|
+
* <button type="button" class="plinth-button" data-variant="secondary" data-size="md"
|
|
1347
|
+
* commandfor="delete" command="close" value="cancel">
|
|
1348
|
+
* <span class="plinth-button-label">Cancel</span></button>
|
|
1349
|
+
* <button type="submit" class="plinth-button plinth-confirm-dialog-confirm"
|
|
1350
|
+
* data-variant="danger" data-size="md" value="confirm">
|
|
1351
|
+
* <span class="plinth-button-label">Delete</span></button>
|
|
1352
|
+
* </div>
|
|
1353
|
+
* </form>
|
|
1354
|
+
* </dialog>
|
|
1355
|
+
*
|
|
1356
|
+
* It is a dialog (dialog.css, on modal.css, with button.css for its actions) that is never
|
|
1357
|
+
* dismissible: role="alertdialog", closedby="closerequest" and no close button, so a tap on the
|
|
1358
|
+
* backdrop leaves it open, and it closes through its buttons, Escape acting as Cancel. The root
|
|
1359
|
+
* needs an id; the title's, message's, field's and error's ids are the root's id with -title,
|
|
1360
|
+
* -message, -field and -error. The title and the message are required: they are the alert
|
|
1361
|
+
* dialog's name and description. data-side-phone="bottom" makes it a bottom sheet below 768px, as
|
|
1362
|
+
* on the dialog. A project may set its own role and closedby.
|
|
1363
|
+
*
|
|
1364
|
+
* Its content is a <form method="dialog">. Cancel is a button with command="close" and
|
|
1365
|
+
* value="cancel", which closes the dialog with returnValue "cancel" and never submits, so it works
|
|
1366
|
+
* whatever is in the field; Escape closes it with an empty returnValue.
|
|
1367
|
+
* Confirm is the form's only submit button, so Enter in the field confirms too: once the browser
|
|
1368
|
+
* has checked the field, the form fires submit (it bubbles to the dialog, where the app listens)
|
|
1369
|
+
* and the dialog closes with returnValue "confirm". The browser focuses the first control when it
|
|
1370
|
+
* opens: the field, or Cancel when there is none. The browser doesn't empty the field when the
|
|
1371
|
+
* dialog closes, so the app calls reset() on the form on close: the phrase is typed afresh each
|
|
1372
|
+
* time, and a password doesn't stay in the page.
|
|
1373
|
+
*
|
|
1374
|
+
* Kinds (no attribute: the markup is what differs):
|
|
1375
|
+
* - simple: the message only.
|
|
1376
|
+
* - type to confirm: a text field with required and pattern, the phrase with its pattern
|
|
1377
|
+
* characters escaped. The browser blocks the submit until the text matches, and while the form
|
|
1378
|
+
* is :invalid the confirm button looks disabled, through CSS alone (it isn't disabled: a press
|
|
1379
|
+
* shows the browser's message and focuses the field). The label shows the phrase in <strong>;
|
|
1380
|
+
* a label of the app's own must keep it (:phrase), or nobody can tell what to type.
|
|
1381
|
+
* - password: a field with type="password", name="password", autocomplete="current-password" and
|
|
1382
|
+
* required, so the confirm button looks disabled until something is typed. Plinth never checks
|
|
1383
|
+
* a password: the app calls preventDefault() on the submit, sets
|
|
1384
|
+
* the confirm button busy while it checks, then closes the dialog (close("confirm")) or shows
|
|
1385
|
+
* the error.
|
|
1386
|
+
*
|
|
1387
|
+
* States:
|
|
1388
|
+
* - error: a message under the field, in danger-text with role="alert" so it is announced when it
|
|
1389
|
+
* appears; the field has aria-invalid="true", aria-describedby pointing at it, and a thick
|
|
1390
|
+
* danger edge (thick, so it isn't only a colour).
|
|
1391
|
+
* - busy: the confirm button's busy state (button.css: aria-busy, aria-disabled and a spinner).
|
|
1392
|
+
* The app ignores another submit while it is busy.
|
|
1393
|
+
* - tone: neutral (default) makes the confirm button primary; danger makes it the danger
|
|
1394
|
+
* variant, for an action that destroys something.
|
|
1395
|
+
*
|
|
1396
|
+
* Fields are 16px text on a phone, so iOS doesn't zoom the page when one takes focus, and md from
|
|
1397
|
+
* 768px up; they are control.md tall, and 40px on a touch screen.
|
|
1398
|
+
*/
|
|
1399
|
+
|
|
1400
|
+
@layer plinth {
|
|
1401
|
+
/* The form takes the dialog's place as the column of parts, so the body still scrolls. */
|
|
1402
|
+
.plinth-confirm-dialog-form {
|
|
1403
|
+
display: flex;
|
|
1404
|
+
flex: 1 1 auto;
|
|
1405
|
+
flex-direction: column;
|
|
1406
|
+
min-block-size: 0;
|
|
1407
|
+
margin: 0;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.plinth-confirm-dialog-message {
|
|
1411
|
+
margin: 0;
|
|
1412
|
+
overflow-wrap: anywhere;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
.plinth-confirm-dialog-field {
|
|
1416
|
+
display: grid;
|
|
1417
|
+
gap: var(--plinth-space-2);
|
|
1418
|
+
margin-block-start: var(--plinth-space-4);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.plinth-confirm-dialog-label {
|
|
1422
|
+
font-size: var(--plinth-font-size-sm);
|
|
1423
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
1424
|
+
line-height: var(--plinth-font-leading-normal);
|
|
1425
|
+
color: var(--plinth-color-text-muted);
|
|
1426
|
+
overflow-wrap: anywhere;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.plinth-confirm-dialog-label strong {
|
|
1430
|
+
font-family: var(--plinth-font-family-mono);
|
|
1431
|
+
font-weight: var(--plinth-font-weight-semibold);
|
|
1432
|
+
color: var(--plinth-color-text);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.plinth-confirm-dialog-input {
|
|
1436
|
+
box-sizing: border-box;
|
|
1437
|
+
inline-size: 100%;
|
|
1438
|
+
min-block-size: var(--plinth-control-md);
|
|
1439
|
+
padding-block: 0;
|
|
1440
|
+
padding-inline: var(--plinth-space-3);
|
|
1441
|
+
margin: 0;
|
|
1442
|
+
font-family: var(--plinth-font-family-body);
|
|
1443
|
+
font-size: var(--plinth-font-size-field);
|
|
1444
|
+
line-height: var(--plinth-font-leading-tight);
|
|
1445
|
+
color: var(--plinth-color-text);
|
|
1446
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
1447
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
1448
|
+
border-radius: var(--plinth-radius-control);
|
|
1449
|
+
transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/* The phrase is typed as it shows in the label. */
|
|
1453
|
+
.plinth-confirm-dialog-input[type='text'] {
|
|
1454
|
+
font-family: var(--plinth-font-family-mono);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.plinth-confirm-dialog-input:focus-visible {
|
|
1458
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
1459
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.plinth-confirm-dialog-input[aria-invalid='true'] {
|
|
1463
|
+
border-color: var(--plinth-color-danger);
|
|
1464
|
+
border-width: var(--plinth-border-width-thick);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.plinth-confirm-dialog-error {
|
|
1468
|
+
margin-block: var(--plinth-space-2) 0;
|
|
1469
|
+
font-size: var(--plinth-font-size-sm);
|
|
1470
|
+
line-height: var(--plinth-font-leading-normal);
|
|
1471
|
+
color: var(--plinth-color-danger-text);
|
|
1472
|
+
overflow-wrap: anywhere;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/* Until the field matches, the confirm button looks disabled; it keeps its contrast. */
|
|
1476
|
+
.plinth-confirm-dialog-form:invalid .plinth-confirm-dialog-confirm:not([aria-busy='true']) {
|
|
1477
|
+
--plinth-button-surface: var(--plinth-color-surface-sunken);
|
|
1478
|
+
--plinth-button-surface-hover: var(--plinth-color-surface-sunken);
|
|
1479
|
+
--plinth-button-surface-active: var(--plinth-color-surface-sunken);
|
|
1480
|
+
--plinth-button-text: var(--plinth-color-text-muted);
|
|
1481
|
+
--plinth-button-edge: var(--plinth-color-border);
|
|
1482
|
+
--plinth-button-inner-edge: none;
|
|
1483
|
+
|
|
1484
|
+
cursor: not-allowed;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
@media (pointer: coarse) {
|
|
1488
|
+
.plinth-confirm-dialog-input {
|
|
1489
|
+
min-block-size: var(--plinth-touch-target);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
@media (min-width: 768px) {
|
|
1494
|
+
.plinth-confirm-dialog-input {
|
|
1495
|
+
font-size: var(--plinth-font-size-md);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
@media (forced-colors: active) {
|
|
1500
|
+
.plinth-confirm-dialog-form:invalid .plinth-confirm-dialog-confirm:not([aria-busy='true']) {
|
|
1501
|
+
color: GrayText;
|
|
1502
|
+
border-color: GrayText;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/* data-table.css */
|
|
1508
|
+
/*
|
|
1509
|
+
* Data table: a collection of records (orders, users, invoices) from one markup, a stack of cards
|
|
1510
|
+
* on a phone and a table from 768px up. No script: the CSS alone changes the layout.
|
|
1511
|
+
*
|
|
1512
|
+
* <table class="plinth-data-table" role="table">
|
|
1513
|
+
* <caption class="plinth-data-table-caption">Orders</caption>
|
|
1514
|
+
* <thead class="plinth-data-table-head" role="rowgroup">
|
|
1515
|
+
* <tr class="plinth-data-table-head-row" role="row">
|
|
1516
|
+
* <th class="plinth-data-table-column" role="columnheader" scope="col">Order</th>
|
|
1517
|
+
* <th class="plinth-data-table-column" role="columnheader" scope="col" aria-sort="descending">
|
|
1518
|
+
* <button type="button" class="plinth-data-table-sort">
|
|
1519
|
+
* <span class="plinth-data-table-sort-label">Date</span>
|
|
1520
|
+
* <span class="plinth-icon plinth-data-table-sort-icon" data-size="sm" aria-hidden="true">
|
|
1521
|
+
* <svg>…<path class="plinth-data-table-sort-up"/><path class="plinth-data-table-sort-down"/></svg>
|
|
1522
|
+
* </span>
|
|
1523
|
+
* </button>
|
|
1524
|
+
* </th>
|
|
1525
|
+
* <th class="plinth-data-table-column" role="columnheader" scope="col" data-numeric>Total</th>
|
|
1526
|
+
* <th class="plinth-data-table-column" role="columnheader" scope="col" data-phone="hidden">…</th>
|
|
1527
|
+
* </tr>
|
|
1528
|
+
* </thead>
|
|
1529
|
+
* <tbody class="plinth-data-table-body" role="rowgroup">
|
|
1530
|
+
* <tr class="plinth-data-table-row" role="row">
|
|
1531
|
+
* <td class="plinth-data-table-cell" role="cell" data-kind="title">
|
|
1532
|
+
* <span class="plinth-data-table-label" aria-hidden="true">Order</span>
|
|
1533
|
+
* <span class="plinth-data-table-value">#1042</span>
|
|
1534
|
+
* </td>
|
|
1535
|
+
* <td class="plinth-data-table-cell" role="cell" data-kind="field">…</td>
|
|
1536
|
+
* <td class="plinth-data-table-cell" role="cell" data-kind="field" data-numeric>…</td>
|
|
1537
|
+
* <td class="plinth-data-table-cell" role="cell" data-kind="actions">
|
|
1538
|
+
* <span class="plinth-data-table-value">buttons</span>
|
|
1539
|
+
* </td>
|
|
1540
|
+
* </tr>
|
|
1541
|
+
* <tr class="plinth-data-table-row" role="row" aria-selected="true">…</tr>
|
|
1542
|
+
* </tbody>
|
|
1543
|
+
* </table>
|
|
1544
|
+
*
|
|
1545
|
+
* With no rows, the body holds the empty state:
|
|
1546
|
+
*
|
|
1547
|
+
* <tbody class="plinth-data-table-body" role="rowgroup">
|
|
1548
|
+
* <tr class="plinth-data-table-empty" role="row">
|
|
1549
|
+
* <td class="plinth-data-table-empty-cell" role="cell" colspan="4">No orders yet.</td>
|
|
1550
|
+
* </tr>
|
|
1551
|
+
* </tbody>
|
|
1552
|
+
*
|
|
1553
|
+
* The explicit roles (table, rowgroup, row, columnheader, cell) are part of the contract: some
|
|
1554
|
+
* browsers, Safari in particular, drop a table's semantics once its rows stop displaying as table
|
|
1555
|
+
* rows. The caption is optional; without it, name the table with aria-label or aria-labelledby.
|
|
1556
|
+
*
|
|
1557
|
+
* Each cell repeats its column's name in plinth-data-table-label, shown only on a phone's card
|
|
1558
|
+
* and aria-hidden, since the column header already names the cell. The label is optional (an
|
|
1559
|
+
* actions cell has none). The header row stays in the accessibility tree on a phone, visually
|
|
1560
|
+
* hidden, so every cell keeps its header.
|
|
1561
|
+
*
|
|
1562
|
+
* Choices, on the cells and the column headers:
|
|
1563
|
+
* - data-kind on a cell: field (the default; the label, then the value), title (the card's title,
|
|
1564
|
+
* first on the card, with no label) or actions (row actions, last on the card, with no label).
|
|
1565
|
+
* - data-numeric: numbers, aligned to the end, with figures of one width.
|
|
1566
|
+
* - data-phone="hidden": a less important column, left out of a phone's card and shown from 768px
|
|
1567
|
+
* up. Put it on the column header and on every cell of the column, so the two always match.
|
|
1568
|
+
* The title cell is the row's first cell and the actions cell its last: the card moves them to
|
|
1569
|
+
* its top and bottom with `order`, which must not change the reading and focus order.
|
|
1570
|
+
*
|
|
1571
|
+
* The app owns sorting, paging and selection (ADR 2); the table only shows them:
|
|
1572
|
+
* - aria-sort on a sortable column header: none, ascending or descending (one column at a time).
|
|
1573
|
+
* Its label is a plinth-data-table-sort button, which the app listens to; its icon shows both
|
|
1574
|
+
* arrows (none) or the one of the order. On a phone the sortable headers stay visible above the
|
|
1575
|
+
* cards as a row of sort buttons; the other headers are visually hidden.
|
|
1576
|
+
* - aria-selected on a row of a table whose rows can be selected: true for a selected row, false
|
|
1577
|
+
* for the others. A selected row takes the selection tint (accent-subtle) and an accent edge at
|
|
1578
|
+
* its start. aria-selected drives the styling only: screen readers don't announce it in a
|
|
1579
|
+
* table, so the control that selects a row (a checkbox, a pressed button) is the app's, and it
|
|
1580
|
+
* carries the state.
|
|
1581
|
+
* - the sort button's states: hover where the device can hover, pressed (:active) a surface step
|
|
1582
|
+
* down, disabled (while a sort loads, say) dimmed with opacity.disabled.
|
|
1583
|
+
* - the empty state: one plinth-data-table-empty row in the body, whose plinth-data-table-empty-cell
|
|
1584
|
+
* spans every column (colspan).
|
|
1585
|
+
* Paging controls sit outside the table.
|
|
1586
|
+
*
|
|
1587
|
+
* From 768px up the actions sit at the start of their cell, under their column header.
|
|
1588
|
+
*
|
|
1589
|
+
* Text wraps in its cell. A table wider than its container, from 768px up, belongs in a
|
|
1590
|
+
* scrolling container the app adds. --plinth-data-table-row-height is a body row's height (44px).
|
|
1591
|
+
*/
|
|
1592
|
+
|
|
1593
|
+
@layer plinth {
|
|
1594
|
+
/* The phone: a stack of cards. */
|
|
1595
|
+
.plinth-data-table {
|
|
1596
|
+
position: relative;
|
|
1597
|
+
display: block;
|
|
1598
|
+
inline-size: 100%;
|
|
1599
|
+
font-family: var(--plinth-font-family-body);
|
|
1600
|
+
font-size: var(--plinth-font-size-md);
|
|
1601
|
+
line-height: var(--plinth-font-leading-tight);
|
|
1602
|
+
color: var(--plinth-color-text);
|
|
1603
|
+
border-spacing: 0;
|
|
1604
|
+
border-collapse: separate;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
.plinth-data-table-caption {
|
|
1608
|
+
display: block;
|
|
1609
|
+
padding-inline: var(--plinth-space-4);
|
|
1610
|
+
margin-block-end: var(--plinth-space-2);
|
|
1611
|
+
font-size: var(--plinth-font-size-sm);
|
|
1612
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
1613
|
+
line-height: var(--plinth-font-leading-normal);
|
|
1614
|
+
color: var(--plinth-color-text-muted);
|
|
1615
|
+
text-align: start;
|
|
1616
|
+
overflow-wrap: anywhere;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.plinth-data-table-head {
|
|
1620
|
+
display: block;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.plinth-data-table-head-row {
|
|
1624
|
+
display: flex;
|
|
1625
|
+
flex-wrap: wrap;
|
|
1626
|
+
gap: var(--plinth-space-2);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
.plinth-data-table-head-row:has(> [aria-sort]:not([data-phone='hidden'])) {
|
|
1630
|
+
margin-block-end: var(--plinth-space-3);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
.plinth-data-table-column {
|
|
1634
|
+
display: block;
|
|
1635
|
+
padding: 0;
|
|
1636
|
+
font-size: var(--plinth-font-size-sm);
|
|
1637
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
1638
|
+
color: var(--plinth-color-text-muted);
|
|
1639
|
+
text-align: start;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/* Visually hidden, still a column header to assistive technology. */
|
|
1643
|
+
.plinth-data-table-column:not([aria-sort]) {
|
|
1644
|
+
position: absolute;
|
|
1645
|
+
inline-size: 1px;
|
|
1646
|
+
block-size: 1px;
|
|
1647
|
+
overflow: hidden;
|
|
1648
|
+
white-space: nowrap;
|
|
1649
|
+
clip-path: inset(50%);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
.plinth-data-table-sort {
|
|
1653
|
+
display: inline-flex;
|
|
1654
|
+
gap: var(--plinth-space-1);
|
|
1655
|
+
align-items: center;
|
|
1656
|
+
min-inline-size: var(--plinth-touch-target);
|
|
1657
|
+
min-block-size: var(--plinth-touch-target);
|
|
1658
|
+
padding-inline: var(--plinth-space-3);
|
|
1659
|
+
margin: 0;
|
|
1660
|
+
font: inherit;
|
|
1661
|
+
color: inherit;
|
|
1662
|
+
text-align: inherit;
|
|
1663
|
+
cursor: pointer;
|
|
1664
|
+
background-color: var(--plinth-color-surface-raised);
|
|
1665
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
1666
|
+
border-radius: var(--plinth-radius-pill);
|
|
1667
|
+
transition:
|
|
1668
|
+
color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
1669
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
.plinth-data-table-sort:focus-visible {
|
|
1673
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
1674
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
:is([aria-sort='ascending'], [aria-sort='descending']) > .plinth-data-table-sort {
|
|
1678
|
+
color: var(--plinth-color-text);
|
|
1679
|
+
border-color: var(--plinth-color-border-strong);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
@media (hover: hover) {
|
|
1683
|
+
.plinth-data-table-sort:hover {
|
|
1684
|
+
color: var(--plinth-color-text);
|
|
1685
|
+
background-color: var(--plinth-color-surface-hover);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
.plinth-data-table-sort:disabled {
|
|
1690
|
+
cursor: not-allowed;
|
|
1691
|
+
opacity: var(--plinth-opacity-disabled);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
.plinth-data-table-sort:enabled:active {
|
|
1695
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.plinth-data-table-sort-label {
|
|
1699
|
+
overflow-wrap: anywhere;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
[aria-sort='none'] .plinth-data-table-sort-icon {
|
|
1703
|
+
color: var(--plinth-color-text-subtle);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
[aria-sort='ascending'] .plinth-data-table-sort-down,
|
|
1707
|
+
[aria-sort='descending'] .plinth-data-table-sort-up {
|
|
1708
|
+
display: none;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
.plinth-data-table-body {
|
|
1712
|
+
display: flex;
|
|
1713
|
+
flex-direction: column;
|
|
1714
|
+
gap: var(--plinth-space-3);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
.plinth-data-table-row {
|
|
1718
|
+
position: relative;
|
|
1719
|
+
display: flex;
|
|
1720
|
+
flex-direction: column;
|
|
1721
|
+
padding-block: var(--plinth-space-1);
|
|
1722
|
+
background-color: var(--plinth-color-surface-raised);
|
|
1723
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
1724
|
+
border-radius: var(--plinth-radius-container);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
.plinth-data-table-cell {
|
|
1728
|
+
display: flex;
|
|
1729
|
+
gap: var(--plinth-space-3);
|
|
1730
|
+
align-items: baseline;
|
|
1731
|
+
justify-content: space-between;
|
|
1732
|
+
padding-block: var(--plinth-space-2);
|
|
1733
|
+
padding-inline: var(--plinth-space-4);
|
|
1734
|
+
overflow-wrap: anywhere;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
.plinth-data-table-label {
|
|
1738
|
+
flex: none;
|
|
1739
|
+
max-inline-size: 50%;
|
|
1740
|
+
font-size: var(--plinth-font-size-sm);
|
|
1741
|
+
color: var(--plinth-color-text-muted);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
.plinth-data-table-value {
|
|
1745
|
+
min-inline-size: 0;
|
|
1746
|
+
text-align: end;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
.plinth-data-table-cell[data-kind='title'] {
|
|
1750
|
+
order: -1;
|
|
1751
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
.plinth-data-table-cell:is([data-kind='title'], [data-kind='actions']) .plinth-data-table-label {
|
|
1755
|
+
display: none;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
.plinth-data-table-cell[data-kind='title'] .plinth-data-table-value {
|
|
1759
|
+
text-align: start;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.plinth-data-table-cell[data-kind='actions'] {
|
|
1763
|
+
justify-content: flex-end;
|
|
1764
|
+
order: 1;
|
|
1765
|
+
padding-block-start: var(--plinth-space-3);
|
|
1766
|
+
margin-block-start: var(--plinth-space-1);
|
|
1767
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
.plinth-data-table-cell[data-kind='actions'] .plinth-data-table-value {
|
|
1771
|
+
display: flex;
|
|
1772
|
+
flex-wrap: wrap;
|
|
1773
|
+
gap: var(--plinth-space-2);
|
|
1774
|
+
align-items: center;
|
|
1775
|
+
justify-content: flex-end;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
.plinth-data-table [data-numeric] {
|
|
1779
|
+
font-variant-numeric: tabular-nums;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
.plinth-data-table [data-phone='hidden'] {
|
|
1783
|
+
display: none;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
.plinth-data-table-row[aria-selected='true'] {
|
|
1787
|
+
background-color: var(--plinth-color-accent-subtle);
|
|
1788
|
+
border-color: var(--plinth-color-accent);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/* The selected row's edge, so the tint is not the only sign of it. */
|
|
1792
|
+
.plinth-data-table-row[aria-selected='true']::before {
|
|
1793
|
+
position: absolute;
|
|
1794
|
+
inset-block: var(--plinth-space-3);
|
|
1795
|
+
inset-inline-start: 0;
|
|
1796
|
+
inline-size: var(--plinth-border-width-thick);
|
|
1797
|
+
content: '';
|
|
1798
|
+
background-color: var(--plinth-color-accent);
|
|
1799
|
+
border-radius: var(--plinth-radius-pill);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
.plinth-data-table-empty {
|
|
1803
|
+
display: block;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
.plinth-data-table-empty-cell {
|
|
1807
|
+
display: block;
|
|
1808
|
+
padding-block: var(--plinth-space-8);
|
|
1809
|
+
padding-inline: var(--plinth-space-4);
|
|
1810
|
+
color: var(--plinth-color-text-muted);
|
|
1811
|
+
text-align: center;
|
|
1812
|
+
overflow-wrap: anywhere;
|
|
1813
|
+
background-color: var(--plinth-color-surface-raised);
|
|
1814
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
1815
|
+
border-radius: var(--plinth-radius-container);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/* From 768px: a table. */
|
|
1819
|
+
@media (min-width: 768px) {
|
|
1820
|
+
.plinth-data-table {
|
|
1821
|
+
display: table;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
.plinth-data-table-caption {
|
|
1825
|
+
display: table-caption;
|
|
1826
|
+
padding-inline: 0;
|
|
1827
|
+
caption-side: top;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
.plinth-data-table-head {
|
|
1831
|
+
display: table-header-group;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
.plinth-data-table-head-row,
|
|
1835
|
+
.plinth-data-table-head-row:has(> [aria-sort]:not([data-phone='hidden'])) {
|
|
1836
|
+
display: table-row;
|
|
1837
|
+
margin: 0;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
.plinth-data-table-column,
|
|
1841
|
+
.plinth-data-table-column:not([aria-sort]),
|
|
1842
|
+
.plinth-data-table [data-phone='hidden'].plinth-data-table-column {
|
|
1843
|
+
position: static;
|
|
1844
|
+
box-sizing: border-box;
|
|
1845
|
+
display: table-cell;
|
|
1846
|
+
inline-size: auto;
|
|
1847
|
+
block-size: var(--plinth-touch-target);
|
|
1848
|
+
padding-block: 0;
|
|
1849
|
+
padding-inline: var(--plinth-space-3);
|
|
1850
|
+
overflow: visible;
|
|
1851
|
+
vertical-align: middle;
|
|
1852
|
+
white-space: normal;
|
|
1853
|
+
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
1854
|
+
clip-path: none;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
.plinth-data-table-sort {
|
|
1858
|
+
padding-inline: var(--plinth-space-1);
|
|
1859
|
+
margin-inline: calc(var(--plinth-space-1) * -1);
|
|
1860
|
+
background-color: transparent;
|
|
1861
|
+
border: 0;
|
|
1862
|
+
border-radius: var(--plinth-radius-control);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
@media (hover: hover) {
|
|
1866
|
+
.plinth-data-table-sort:hover {
|
|
1867
|
+
background-color: transparent;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
.plinth-data-table-sort:enabled:active {
|
|
1872
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
.plinth-data-table-body {
|
|
1876
|
+
display: table-row-group;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
.plinth-data-table-row {
|
|
1880
|
+
position: static;
|
|
1881
|
+
display: table-row;
|
|
1882
|
+
padding: 0;
|
|
1883
|
+
background-color: transparent;
|
|
1884
|
+
border: 0;
|
|
1885
|
+
border-radius: 0;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
.plinth-data-table-cell,
|
|
1889
|
+
.plinth-data-table-cell[data-kind='actions'],
|
|
1890
|
+
.plinth-data-table [data-phone='hidden'].plinth-data-table-cell {
|
|
1891
|
+
position: relative;
|
|
1892
|
+
box-sizing: border-box;
|
|
1893
|
+
display: table-cell;
|
|
1894
|
+
block-size: var(--plinth-data-table-row-height);
|
|
1895
|
+
padding-block: var(--plinth-space-2);
|
|
1896
|
+
padding-inline: var(--plinth-space-3);
|
|
1897
|
+
margin: 0;
|
|
1898
|
+
vertical-align: middle;
|
|
1899
|
+
text-align: start;
|
|
1900
|
+
overflow-wrap: break-word;
|
|
1901
|
+
border: 0;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
.plinth-data-table-row + .plinth-data-table-row > .plinth-data-table-cell {
|
|
1905
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
.plinth-data-table-label {
|
|
1909
|
+
display: none;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
.plinth-data-table-value {
|
|
1913
|
+
text-align: inherit;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
.plinth-data-table-cell[data-kind='actions'] .plinth-data-table-value {
|
|
1917
|
+
display: inline-flex;
|
|
1918
|
+
flex-wrap: nowrap;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
.plinth-data-table-column[data-numeric],
|
|
1922
|
+
.plinth-data-table-cell[data-numeric] {
|
|
1923
|
+
text-align: end;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
.plinth-data-table-row[aria-selected='true'] {
|
|
1927
|
+
border-color: transparent;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
.plinth-data-table-row[aria-selected='true'] > .plinth-data-table-cell {
|
|
1931
|
+
background-color: var(--plinth-color-accent-subtle);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
.plinth-data-table-row[aria-selected='true']::before {
|
|
1935
|
+
content: none;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
.plinth-data-table-row[aria-selected='true'] > .plinth-data-table-cell:first-child::before {
|
|
1939
|
+
position: absolute;
|
|
1940
|
+
inset-block: 0;
|
|
1941
|
+
inset-inline-start: 0;
|
|
1942
|
+
inline-size: var(--plinth-border-width-thick);
|
|
1943
|
+
content: '';
|
|
1944
|
+
background-color: var(--plinth-color-accent);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
.plinth-data-table-empty {
|
|
1948
|
+
display: table-row;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
.plinth-data-table-empty-cell {
|
|
1952
|
+
display: table-cell;
|
|
1953
|
+
background-color: transparent;
|
|
1954
|
+
border: 0;
|
|
1955
|
+
border-radius: 0;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
@media (min-width: 1280px) {
|
|
1960
|
+
.plinth-data-table-column,
|
|
1961
|
+
.plinth-data-table-column:not([aria-sort]),
|
|
1962
|
+
.plinth-data-table [data-phone='hidden'].plinth-data-table-column,
|
|
1963
|
+
.plinth-data-table-cell,
|
|
1964
|
+
.plinth-data-table-cell[data-kind='actions'],
|
|
1965
|
+
.plinth-data-table [data-phone='hidden'].plinth-data-table-cell {
|
|
1966
|
+
padding-inline: var(--plinth-space-4);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
@media (forced-colors: active) {
|
|
1971
|
+
.plinth-data-table-row[aria-selected='true'],
|
|
1972
|
+
.plinth-data-table-row[aria-selected='true'] > .plinth-data-table-cell {
|
|
1973
|
+
color: HighlightText;
|
|
1974
|
+
background-color: Highlight;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/* dialog.css */
|
|
1980
|
+
/*
|
|
1981
|
+
* Dialog: a modal for a form, the details of something or its settings, centred on the screen at
|
|
1982
|
+
* every width; with data-side-phone="bottom" it is a bottom sheet below 768px.
|
|
1983
|
+
*
|
|
1984
|
+
* <dialog id="settings" class="plinth-dialog" closedby="any"
|
|
1985
|
+
* aria-labelledby="settings-title" aria-describedby="settings-description">
|
|
1986
|
+
* <div class="plinth-dialog-panel">
|
|
1987
|
+
* <div class="plinth-dialog-handle" aria-hidden="true"></div>
|
|
1988
|
+
* <div class="plinth-dialog-header">
|
|
1989
|
+
* <h2 id="settings-title" class="plinth-dialog-title">Settings</h2>
|
|
1990
|
+
* <p id="settings-description" class="plinth-dialog-description">Changes apply at once.</p>
|
|
1991
|
+
* <button type="button" class="plinth-button plinth-icon-button plinth-dialog-close"
|
|
1992
|
+
* data-variant="ghost" data-size="md" aria-label="Close" commandfor="settings"
|
|
1993
|
+
* command="close">
|
|
1994
|
+
* <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
|
|
1995
|
+
* </button>
|
|
1996
|
+
* </div>
|
|
1997
|
+
* <div class="plinth-dialog-body">…</div>
|
|
1998
|
+
* <div class="plinth-dialog-footer">…</div>
|
|
1999
|
+
* </div>
|
|
2000
|
+
* </dialog>
|
|
2001
|
+
*
|
|
2002
|
+
* The root is the <dialog>, and it needs an id: commandfor and aria-labelledby point at it, and
|
|
2003
|
+
* the title's and description's ids are the root's id with -title and -description. The panel is
|
|
2004
|
+
* the root's only child and holds the parts. The handle is there only on a dismissible dialog
|
|
2005
|
+
* with data-side-phone="bottom": decorative (aria-hidden), shown below 768px, where the dialog is
|
|
2006
|
+
* dragged down to close (modal.css). The title is
|
|
2007
|
+
* required; the description (and its aria-describedby) and the footer are optional. The close
|
|
2008
|
+
* button is an icon button (button.css and icon.css). It needs modal.css, the base it shares with
|
|
2009
|
+
* the sheet: the surface, the backdrop, the scroll lock, the motion, the safe areas and the parts
|
|
2010
|
+
* are there, and this file sets only where the dialog sits and how big it is.
|
|
2011
|
+
*
|
|
2012
|
+
* Open it with showModal() or a <button commandfor="settings" command="show-modal">. Open is the
|
|
2013
|
+
* native state, [open]; the browser puts the dialog in the top layer, makes the page inert, traps
|
|
2014
|
+
* focus, and returns focus to what opened it. The dialog fires close when it closes.
|
|
2015
|
+
*
|
|
2016
|
+
* Dismissible (the default): closedby="any" and the close button; the close button, Escape and a
|
|
2017
|
+
* tap on the backdrop close it (closedby="any" is an enhancement Safari doesn't have yet, so the
|
|
2018
|
+
* close button is always there). Not dismissible, for a choice the person must make:
|
|
2019
|
+
* role="alertdialog", closedby="closerequest" and no close button, so a stray tap on the backdrop
|
|
2020
|
+
* does nothing and it closes through its own buttons, with Escape acting as Cancel (the browser
|
|
2021
|
+
* fires cancel, then close, with an empty returnValue). It needs an action that closes it,
|
|
2022
|
+
* usually in the footer, the least destructive first: a touch screen has no Escape, and the
|
|
2023
|
+
* browser focuses the first control. Give it a description: an alert dialog's message is its
|
|
2024
|
+
* accessible description. A project may set its own closedby.
|
|
2025
|
+
*
|
|
2026
|
+
* Presentation: centred at every width, dialog.width wide (less the screen's margin on a phone),
|
|
2027
|
+
* its height capped by the screen's, fading and rising into place. data-side-phone="bottom" (the
|
|
2028
|
+
* only value; leave it out for centred) makes it a bottom sheet below 768px: the screen's width,
|
|
2029
|
+
* on the bottom edge, sliding up, its last part clearing the home indicator; from 768px up it is
|
|
2030
|
+
* centred like any dialog.
|
|
2031
|
+
*
|
|
2032
|
+
* On a phone the dialog's height is capped by 100dvh, which doesn't, on its own, clear the
|
|
2033
|
+
* on-screen keyboard: a page with a form in a dialog sets interactive-widget=resizes-content in
|
|
2034
|
+
* its viewport meta where the browser supports it, as for the sheet.
|
|
2035
|
+
*
|
|
2036
|
+
* In Livewire, a re-render would morph the open dialog back to the server's markup and drop its
|
|
2037
|
+
* open attribute: put wire:ignore.self on the dialog (or keep it outside the component that
|
|
2038
|
+
* re-renders), and always pass an id, since a generated one changes on every render.
|
|
2039
|
+
*/
|
|
2040
|
+
|
|
2041
|
+
@layer plinth {
|
|
2042
|
+
.plinth-dialog {
|
|
2043
|
+
--plinth-modal-offset: 0 var(--plinth-space-4);
|
|
2044
|
+
--plinth-modal-closed-opacity: 0;
|
|
2045
|
+
|
|
2046
|
+
inset: 0;
|
|
2047
|
+
inline-size: min(100% - 2 * var(--plinth-space-4), var(--plinth-dialog-width));
|
|
2048
|
+
max-block-size: calc(100dvh - 2 * var(--plinth-space-4));
|
|
2049
|
+
margin: auto;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
.plinth-dialog[data-side-phone='bottom'] {
|
|
2053
|
+
--plinth-modal-offset: 0 100%;
|
|
2054
|
+
--plinth-modal-closed-opacity: 1;
|
|
2055
|
+
--plinth-modal-safe-area-end: env(safe-area-inset-bottom);
|
|
2056
|
+
|
|
2057
|
+
inset-block: auto 0;
|
|
2058
|
+
inline-size: 100%;
|
|
2059
|
+
max-block-size: calc(100dvh - var(--plinth-space-12));
|
|
2060
|
+
margin: 0;
|
|
2061
|
+
border-block-end-width: 0;
|
|
2062
|
+
border-end-start-radius: 0;
|
|
2063
|
+
border-end-end-radius: 0;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/* One that can be dismissed is dragged down to close (modal.css): the root fills the screen. */
|
|
2067
|
+
.plinth-dialog[data-side-phone='bottom']:has(> * > .plinth-dialog-handle) {
|
|
2068
|
+
inset: 0;
|
|
2069
|
+
block-size: auto;
|
|
2070
|
+
max-block-size: none;
|
|
2071
|
+
border-width: 0;
|
|
2072
|
+
border-radius: 0;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
@media (min-width: 768px) {
|
|
2076
|
+
.plinth-dialog,
|
|
2077
|
+
.plinth-dialog[data-side-phone='bottom'],
|
|
2078
|
+
.plinth-dialog[data-side-phone='bottom']:has(> * > .plinth-dialog-handle) {
|
|
2079
|
+
--plinth-modal-offset: 0 var(--plinth-space-4);
|
|
2080
|
+
--plinth-modal-closed-opacity: 0;
|
|
2081
|
+
--plinth-modal-safe-area-end: initial;
|
|
2082
|
+
|
|
2083
|
+
inset: 0;
|
|
2084
|
+
inline-size: min(100% - 2 * var(--plinth-space-8), var(--plinth-dialog-width));
|
|
2085
|
+
block-size: fit-content;
|
|
2086
|
+
max-block-size: calc(100dvh - 2 * var(--plinth-space-8));
|
|
2087
|
+
margin: auto;
|
|
2088
|
+
border-width: var(--plinth-border-width-thin);
|
|
2089
|
+
border-radius: var(--plinth-radius-container);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
/* drawer.css */
|
|
2095
|
+
/*
|
|
2096
|
+
* Drawer: a phone-first panel on the bottom edge that the person drags, like a maps app's panel,
|
|
2097
|
+
* with optional snap points (a peek, half and full height). From 768px up it is a dialog or a
|
|
2098
|
+
* sheet.
|
|
2099
|
+
*
|
|
2100
|
+
* <dialog id="route" class="plinth-drawer" data-desktop="dialog"
|
|
2101
|
+
* data-snap-points="peek half full" data-snap="peek" closedby="any"
|
|
2102
|
+
* aria-labelledby="route-title" aria-describedby="route-description">
|
|
2103
|
+
* <div class="plinth-drawer-panel">
|
|
2104
|
+
* <div class="plinth-drawer-handle" aria-hidden="true"></div>
|
|
2105
|
+
* <div class="plinth-drawer-header">
|
|
2106
|
+
* <h2 id="route-title" class="plinth-drawer-title">Route</h2>
|
|
2107
|
+
* <p id="route-description" class="plinth-drawer-description">12 min by bike.</p>
|
|
2108
|
+
* <button type="button" class="plinth-button plinth-icon-button plinth-drawer-close"
|
|
2109
|
+
* data-variant="ghost" data-size="md" aria-label="Close" commandfor="route"
|
|
2110
|
+
* command="close">
|
|
2111
|
+
* <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
|
|
2112
|
+
* </button>
|
|
2113
|
+
* </div>
|
|
2114
|
+
* <div class="plinth-drawer-body">…</div>
|
|
2115
|
+
* <div class="plinth-drawer-footer">…</div>
|
|
2116
|
+
* </div>
|
|
2117
|
+
* </dialog>
|
|
2118
|
+
*
|
|
2119
|
+
* The root is the <dialog>, and it needs an id: commandfor and aria-labelledby point at it, and
|
|
2120
|
+
* the title's and description's ids are the root's id with -title and -description. The panel is
|
|
2121
|
+
* the root's only child and holds the parts; the handle is always there, first, and decorative
|
|
2122
|
+
* (aria-hidden): Escape and the close button, which is always there too, are the ways to close it
|
|
2123
|
+
* without dragging. The title is required; the description (and its aria-describedby) and the
|
|
2124
|
+
* footer are optional. The close button is an icon button (button.css and icon.css). It needs
|
|
2125
|
+
* modal.css, the base it shares with the dialog and the sheet, where the dragging is described:
|
|
2126
|
+
* this file sets where the drawer sits, how big it is and its snap points.
|
|
2127
|
+
*
|
|
2128
|
+
* Open it with showModal() or a <button commandfor="route" command="show-modal">. Open is the
|
|
2129
|
+
* native state, [open]; the browser puts the drawer in the top layer, makes the page inert, traps
|
|
2130
|
+
* focus, closes on Escape and returns focus. The dialog fires close when it closes, whichever way
|
|
2131
|
+
* it closed.
|
|
2132
|
+
*
|
|
2133
|
+
* On a phone (below 768px) it is the screen's width on the bottom edge and slides up. Dragged
|
|
2134
|
+
* down past its lowest snap point it closes; the layers do that part (modal.css). With no snap
|
|
2135
|
+
* points it is as tall as its content, up to the screen's height less a gap.
|
|
2136
|
+
*
|
|
2137
|
+
* Snap points (data-snap-points, optional): any of peek, half and full, separated by spaces.
|
|
2138
|
+
* With them the panel is the screen's height less a gap, and it rests where it is let go at the
|
|
2139
|
+
* nearest one: peek shows drawer.peek-height of it (the handle, the header and the top of the
|
|
2140
|
+
* body), half the screen's height, full all of it. The body scrolls inside the panel at any of
|
|
2141
|
+
* them, so a drawer with a long body is dragged by its handle and header. data-snap is the snap
|
|
2142
|
+
* point it opens at, one of those listed (the layers print the largest listed when it is missing
|
|
2143
|
+
* or unknown); the browser opens it at full, or the largest listed, and the layers then scroll it
|
|
2144
|
+
* to data-snap. The drawer remembers nothing: where it rests is the person's.
|
|
2145
|
+
*
|
|
2146
|
+
* Desktop (data-desktop): its presentation from 768px up. dialog (the default, also when missing
|
|
2147
|
+
* or unknown) centres it like the dialog, at dialog.width; sheet makes it a full-height panel on
|
|
2148
|
+
* the inline-end edge, sheet.side-width wide, like the sheet. From 768px up there is no handle
|
|
2149
|
+
* and no dragging, the snap points don't apply, and a tap on the backdrop closes it
|
|
2150
|
+
* (closedby="any"; Safari doesn't have it yet, so the close button is always there).
|
|
2151
|
+
*
|
|
2152
|
+
* The motion runs on the motion tokens, which drop to 0ms under prefers-reduced-motion; the
|
|
2153
|
+
* snapping is the browser's scrolling.
|
|
2154
|
+
*
|
|
2155
|
+
* In Livewire, a re-render would morph the open dialog back to the server's markup and drop its
|
|
2156
|
+
* open attribute: put wire:ignore.self on the drawer (or keep it outside the component that
|
|
2157
|
+
* re-renders), and always pass an id, since a generated one changes on every render.
|
|
2158
|
+
*/
|
|
2159
|
+
|
|
2160
|
+
@layer plinth {
|
|
2161
|
+
.plinth-drawer {
|
|
2162
|
+
--plinth-modal-offset: 0 100%;
|
|
2163
|
+
--plinth-modal-safe-area-end: env(safe-area-inset-bottom);
|
|
2164
|
+
|
|
2165
|
+
/* Translate isn't logical: -1 (under :dir(rtl), below) turns the sheet's slide around. */
|
|
2166
|
+
--plinth-drawer-direction: 1;
|
|
2167
|
+
|
|
2168
|
+
inset: 0;
|
|
2169
|
+
inline-size: 100%;
|
|
2170
|
+
block-size: auto;
|
|
2171
|
+
max-block-size: none;
|
|
2172
|
+
margin: 0;
|
|
2173
|
+
border-width: 0;
|
|
2174
|
+
border-radius: 0;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/* With snap points the panel is its full height, and the snap points are where it rests. */
|
|
2178
|
+
.plinth-drawer[data-snap-points] > .plinth-drawer-panel {
|
|
2179
|
+
block-size: calc(
|
|
2180
|
+
100% - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/* Full is the panel's end on the screen's bottom edge, the base's open position. */
|
|
2185
|
+
.plinth-drawer[data-snap-points]:not([data-snap-points~='full']) > .plinth-drawer-panel {
|
|
2186
|
+
scroll-snap-align: none;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
/*
|
|
2190
|
+
* Peek and half are the panel's top that far from the screen's bottom edge. The snap areas are
|
|
2191
|
+
* the handle (peek) and the header (half), the first parts of the panel, so their margins add
|
|
2192
|
+
* what lies above them: the handle's margin, then the handle (modal.css; change them together).
|
|
2193
|
+
*/
|
|
2194
|
+
.plinth-drawer[data-snap-points~='peek'] .plinth-drawer-handle {
|
|
2195
|
+
scroll-snap-align: start;
|
|
2196
|
+
scroll-margin-block-start: calc(
|
|
2197
|
+
100dvh - var(--plinth-drawer-peek-height) + var(--plinth-space-2)
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
.plinth-drawer[data-snap-points~='half'] .plinth-drawer-header {
|
|
2202
|
+
scroll-snap-align: start;
|
|
2203
|
+
scroll-margin-block-start: calc(50dvh + var(--plinth-space-2) + var(--plinth-space-1));
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.plinth-drawer:dir(rtl) {
|
|
2207
|
+
--plinth-drawer-direction: -1;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
@media (min-width: 768px) {
|
|
2211
|
+
.plinth-drawer {
|
|
2212
|
+
--plinth-modal-offset: 0 var(--plinth-space-4);
|
|
2213
|
+
--plinth-modal-closed-opacity: 0;
|
|
2214
|
+
--plinth-modal-safe-area-end: initial;
|
|
2215
|
+
|
|
2216
|
+
inset: 0;
|
|
2217
|
+
inline-size: min(100% - 2 * var(--plinth-space-8), var(--plinth-dialog-width));
|
|
2218
|
+
block-size: fit-content;
|
|
2219
|
+
max-block-size: calc(100dvh - 2 * var(--plinth-space-8));
|
|
2220
|
+
margin: auto;
|
|
2221
|
+
border-width: var(--plinth-border-width-thin);
|
|
2222
|
+
border-radius: var(--plinth-radius-container);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
.plinth-drawer[data-desktop='sheet'] {
|
|
2226
|
+
--plinth-modal-offset: calc(100% * var(--plinth-drawer-direction)) 0;
|
|
2227
|
+
--plinth-modal-closed-opacity: 1;
|
|
2228
|
+
--plinth-modal-safe-area-end: env(safe-area-inset-bottom);
|
|
2229
|
+
|
|
2230
|
+
inset-block: 0;
|
|
2231
|
+
inset-inline: auto 0;
|
|
2232
|
+
inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
|
|
2233
|
+
block-size: auto;
|
|
2234
|
+
max-block-size: none;
|
|
2235
|
+
padding-block: env(safe-area-inset-top) 0;
|
|
2236
|
+
margin: 0;
|
|
2237
|
+
border-width: 0;
|
|
2238
|
+
border-inline-start-width: var(--plinth-border-width-thin);
|
|
2239
|
+
border-radius: 0;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
.plinth-drawer[data-snap-points] > .plinth-drawer-panel {
|
|
2243
|
+
block-size: auto;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
.plinth-drawer[data-snap-points] :is(.plinth-drawer-handle, .plinth-drawer-header) {
|
|
2247
|
+
scroll-snap-align: none;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
/* icon.css */
|
|
2253
|
+
/*
|
|
2254
|
+
* Icon: a project's own SVG, sized by Plinth.
|
|
2255
|
+
*
|
|
2256
|
+
* Decorative (the default), hidden from assistive technology:
|
|
2257
|
+
* <span class="plinth-icon" data-size="md" aria-hidden="true">
|
|
2258
|
+
* <svg viewBox="0 0 24 24">…</svg>
|
|
2259
|
+
* </span>
|
|
2260
|
+
*
|
|
2261
|
+
* Meaningful, with a label (an img role makes the SVG inside presentational). Blade, Vue and
|
|
2262
|
+
* React take it as `label`, or as an `aria-label` attribute; a blank label leaves the icon
|
|
2263
|
+
* decorative:
|
|
2264
|
+
* <span class="plinth-icon" data-size="md" role="img" aria-label="Warning">
|
|
2265
|
+
* <svg viewBox="0 0 24 24">…</svg>
|
|
2266
|
+
* </span>
|
|
2267
|
+
*
|
|
2268
|
+
* Sizes (data-size): sm 16px, md 20px (default, also when the attribute is missing or unknown),
|
|
2269
|
+
* lg 24px. data-mirror flips the icon in right-to-left text, for icons that point a direction.
|
|
2270
|
+
*
|
|
2271
|
+
* The SVG comes from the project, inline or as <use href="sprite.svg#name">. It should draw with
|
|
2272
|
+
* currentColor; Plinth sets no fill or stroke, so outline and filled icon sets both keep theirs.
|
|
2273
|
+
* The icon takes its colour from the text around it.
|
|
2274
|
+
*/
|
|
2275
|
+
|
|
2276
|
+
@layer plinth {
|
|
2277
|
+
.plinth-icon {
|
|
2278
|
+
--plinth-icon-current-size: var(--plinth-size-icon-md);
|
|
2279
|
+
|
|
2280
|
+
display: inline-flex;
|
|
2281
|
+
flex: none;
|
|
2282
|
+
inline-size: var(--plinth-icon-current-size);
|
|
2283
|
+
block-size: var(--plinth-icon-current-size);
|
|
2284
|
+
vertical-align: middle;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
.plinth-icon[data-size='sm'] {
|
|
2288
|
+
--plinth-icon-current-size: var(--plinth-size-icon-sm);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
.plinth-icon[data-size='lg'] {
|
|
2292
|
+
--plinth-icon-current-size: var(--plinth-size-icon-lg);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
.plinth-icon > svg {
|
|
2296
|
+
inline-size: 100%;
|
|
2297
|
+
block-size: 100%;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
.plinth-icon[data-mirror]:dir(rtl) > svg {
|
|
2301
|
+
transform: scaleX(-1);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
/* list-card.css */
|
|
2306
|
+
/*
|
|
2307
|
+
* List card: a group of list rows on a raised surface, with an optional heading and footer note.
|
|
2308
|
+
*
|
|
2309
|
+
* <div class="plinth-list-card">
|
|
2310
|
+
* <h2 class="plinth-list-card-heading">Account</h2>
|
|
2311
|
+
* <ul class="plinth-list-card-list" role="list">
|
|
2312
|
+
* <li><a class="plinth-list-row" href="…">…</a></li>
|
|
2313
|
+
* <li><div class="plinth-list-row">…</div></li>
|
|
2314
|
+
* </ul>
|
|
2315
|
+
* <p class="plinth-list-card-footer">Changes apply to every device.</p>
|
|
2316
|
+
* </div>
|
|
2317
|
+
*
|
|
2318
|
+
* The heading and the footer are optional, and sit outside the raised surface: the heading above
|
|
2319
|
+
* it, the footer note below. The heading is an <h2> by default; pick the level that fits the page
|
|
2320
|
+
* (heading-level h2 to h6 in Blade, headingLevel in Vue).
|
|
2321
|
+
*
|
|
2322
|
+
* The list is the raised surface (color.surface-raised, radius.container, a hairline in
|
|
2323
|
+
* color.border; no shadow, per ADR 3: the card does not float). The card resets the list's
|
|
2324
|
+
* margin, padding and bullets, and prints role="list" so Safari keeps announcing it. Each row
|
|
2325
|
+
* goes in its own <li> and keeps its own root, states and divider; the surface clips the rows'
|
|
2326
|
+
* hover and selection tints to its corners, and anything a row positions outside itself (a
|
|
2327
|
+
* popover or a dialog is in the top layer, so it is not). The first and last rows take the
|
|
2328
|
+
* surface's inner radius, so their focus ring, drawn inside the row, follows the corners.
|
|
2329
|
+
*/
|
|
2330
|
+
|
|
2331
|
+
@layer plinth {
|
|
2332
|
+
.plinth-list-card {
|
|
2333
|
+
display: flex;
|
|
2334
|
+
flex-direction: column;
|
|
2335
|
+
gap: var(--plinth-space-2);
|
|
2336
|
+
min-inline-size: 0;
|
|
2337
|
+
font-family: var(--plinth-font-family-body);
|
|
2338
|
+
color: var(--plinth-color-text);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
.plinth-list-card-heading {
|
|
2342
|
+
padding-inline: var(--plinth-space-4);
|
|
2343
|
+
margin: 0;
|
|
2344
|
+
font-size: var(--plinth-font-size-sm);
|
|
2345
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
2346
|
+
line-height: var(--plinth-font-leading-normal);
|
|
2347
|
+
color: var(--plinth-color-text-muted);
|
|
2348
|
+
overflow-wrap: anywhere;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
.plinth-list-card-list {
|
|
2352
|
+
padding: 0;
|
|
2353
|
+
margin: 0;
|
|
2354
|
+
overflow: hidden;
|
|
2355
|
+
list-style: none;
|
|
2356
|
+
background-color: var(--plinth-color-surface-raised);
|
|
2357
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
2358
|
+
border-radius: var(--plinth-radius-container);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
.plinth-list-card-footer {
|
|
2362
|
+
padding-inline: var(--plinth-space-4);
|
|
2363
|
+
margin: 0;
|
|
2364
|
+
font-size: var(--plinth-font-size-sm);
|
|
2365
|
+
line-height: var(--plinth-font-leading-normal);
|
|
2366
|
+
color: var(--plinth-color-text-muted);
|
|
2367
|
+
overflow-wrap: anywhere;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
.plinth-list-card-list > li:first-child > .plinth-list-row {
|
|
2371
|
+
border-start-start-radius: calc(
|
|
2372
|
+
var(--plinth-radius-container) - var(--plinth-border-width-thin)
|
|
2373
|
+
);
|
|
2374
|
+
border-start-end-radius: calc(var(--plinth-radius-container) - var(--plinth-border-width-thin));
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
.plinth-list-card-list > li:last-child > .plinth-list-row {
|
|
2378
|
+
border-end-start-radius: calc(var(--plinth-radius-container) - var(--plinth-border-width-thin));
|
|
2379
|
+
border-end-end-radius: calc(var(--plinth-radius-container) - var(--plinth-border-width-thin));
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/* list-row.css */
|
|
2384
|
+
/*
|
|
2385
|
+
* List row: one row of a list or a sheet, 44px tall at least.
|
|
2386
|
+
*
|
|
2387
|
+
* <div class="plinth-list-row">
|
|
2388
|
+
* <span class="plinth-list-row-leading">an icon or an avatar</span>
|
|
2389
|
+
* <span class="plinth-list-row-content">
|
|
2390
|
+
* <span class="plinth-list-row-title">Notifications</span>
|
|
2391
|
+
* <span class="plinth-list-row-description">Email and push</span>
|
|
2392
|
+
* </span>
|
|
2393
|
+
* <span class="plinth-list-row-trailing">
|
|
2394
|
+
* a value, a badge or a control
|
|
2395
|
+
* <span class="plinth-icon plinth-list-row-chevron" data-size="sm" data-mirror
|
|
2396
|
+
* aria-hidden="true"><svg>…</svg></span>
|
|
2397
|
+
* </span>
|
|
2398
|
+
* </div>
|
|
2399
|
+
*
|
|
2400
|
+
* The leading slot, the description and the trailing slot are optional; the chevron is the
|
|
2401
|
+
* row's own (chevron in Blade and Vue), and points the other way in right-to-left text.
|
|
2402
|
+
*
|
|
2403
|
+
* The root is the element the app talks to (ADR 2):
|
|
2404
|
+
* - a static row is a <div>;
|
|
2405
|
+
* - a row that navigates is <a class="plinth-list-row" href="…">;
|
|
2406
|
+
* - a row that acts is <button type="button" class="plinth-list-row">.
|
|
2407
|
+
* A link or button row holds no other control: a switch or an icon button in the trailing slot
|
|
2408
|
+
* needs a static row, and its own accessible name.
|
|
2409
|
+
*
|
|
2410
|
+
* States:
|
|
2411
|
+
* - current: aria-current="page" on a link row (or a static row) for the page being shown, set
|
|
2412
|
+
* by the app; the selection tint (accent-subtle, accent-text) and a heavier title. A button
|
|
2413
|
+
* row never prints it: a button is not a page.
|
|
2414
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
2415
|
+
* - hover only where the device can hover; the focus ring on :focus-visible, drawn inside the
|
|
2416
|
+
* row (focus-ring.offset-inset).
|
|
2417
|
+
*
|
|
2418
|
+
* A hairline divider (color.divider) sits between two rows that follow each other, as siblings
|
|
2419
|
+
* or each as the direct child of its own <li>; a wrapper element in between drops it. The app
|
|
2420
|
+
* resets the list (margin, padding, list-style) and adds role="list", which Safari needs to keep
|
|
2421
|
+
* announcing a list without bullets. The height is --plinth-list-row-height (44px); a description makes the
|
|
2422
|
+
* row taller. Long titles and descriptions wrap; the trailing slot keeps its width up to half the
|
|
2423
|
+
* row, and only a value longer than that wraps.
|
|
2424
|
+
*/
|
|
2425
|
+
|
|
2426
|
+
@layer plinth {
|
|
2427
|
+
.plinth-list-row {
|
|
2428
|
+
box-sizing: border-box;
|
|
2429
|
+
display: flex;
|
|
2430
|
+
gap: var(--plinth-space-3);
|
|
2431
|
+
align-items: center;
|
|
2432
|
+
inline-size: 100%;
|
|
2433
|
+
min-block-size: var(--plinth-list-row-height);
|
|
2434
|
+
padding-block: var(--plinth-space-2);
|
|
2435
|
+
padding-inline: var(--plinth-space-4);
|
|
2436
|
+
margin: 0;
|
|
2437
|
+
font-family: var(--plinth-font-family-body);
|
|
2438
|
+
font-size: var(--plinth-font-size-md);
|
|
2439
|
+
font-weight: var(--plinth-font-weight-regular);
|
|
2440
|
+
line-height: var(--plinth-font-leading-tight);
|
|
2441
|
+
color: var(--plinth-color-text);
|
|
2442
|
+
text-align: start;
|
|
2443
|
+
text-decoration: none;
|
|
2444
|
+
background-color: transparent;
|
|
2445
|
+
border: 0;
|
|
2446
|
+
transition: background-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
:is(a, button).plinth-list-row {
|
|
2450
|
+
cursor: pointer;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
li + li > .plinth-list-row {
|
|
2454
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
.plinth-list-row + .plinth-list-row {
|
|
2458
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
@media (hover: hover) {
|
|
2462
|
+
a.plinth-list-row[href]:not([aria-current='page']):hover,
|
|
2463
|
+
button.plinth-list-row:enabled:not([aria-current='page']):hover {
|
|
2464
|
+
background-color: var(--plinth-color-surface-hover);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
a.plinth-list-row[href]:not([aria-current='page']):active,
|
|
2469
|
+
button.plinth-list-row:enabled:not([aria-current='page']):active {
|
|
2470
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
.plinth-list-row[aria-current='page'] {
|
|
2474
|
+
color: var(--plinth-color-accent-text);
|
|
2475
|
+
background-color: var(--plinth-color-accent-subtle);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
.plinth-list-row:disabled,
|
|
2479
|
+
.plinth-list-row[aria-disabled='true'] {
|
|
2480
|
+
cursor: not-allowed;
|
|
2481
|
+
opacity: var(--plinth-opacity-disabled);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
/* Inside the row, so a scrolling sheet or a card doesn't cut the ring off at the sides. */
|
|
2485
|
+
.plinth-list-row:focus-visible {
|
|
2486
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
2487
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
.plinth-list-row-leading {
|
|
2491
|
+
display: inline-flex;
|
|
2492
|
+
flex: none;
|
|
2493
|
+
align-items: center;
|
|
2494
|
+
color: var(--plinth-color-text-muted);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
.plinth-list-row-content {
|
|
2498
|
+
display: flex;
|
|
2499
|
+
flex: 1 1 auto;
|
|
2500
|
+
flex-direction: column;
|
|
2501
|
+
gap: var(--plinth-space-0-5);
|
|
2502
|
+
min-inline-size: 0;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.plinth-list-row-title {
|
|
2506
|
+
overflow-wrap: anywhere;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
.plinth-list-row-description {
|
|
2510
|
+
font-size: var(--plinth-font-size-sm);
|
|
2511
|
+
line-height: var(--plinth-font-leading-normal);
|
|
2512
|
+
color: var(--plinth-color-text-muted);
|
|
2513
|
+
overflow-wrap: anywhere;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
/* Keeps its width up to half the row, so a long title wraps before a short value does. */
|
|
2517
|
+
.plinth-list-row-trailing {
|
|
2518
|
+
display: inline-flex;
|
|
2519
|
+
flex: none;
|
|
2520
|
+
gap: var(--plinth-space-2);
|
|
2521
|
+
align-items: center;
|
|
2522
|
+
max-inline-size: 50%;
|
|
2523
|
+
color: var(--plinth-color-text-muted);
|
|
2524
|
+
text-align: end;
|
|
2525
|
+
overflow-wrap: anywhere;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
.plinth-list-row-chevron {
|
|
2529
|
+
color: var(--plinth-color-text-subtle);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
.plinth-list-row[aria-current='page'] .plinth-list-row-title {
|
|
2533
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
@media (forced-colors: active) {
|
|
2537
|
+
.plinth-list-row[aria-current='page'] {
|
|
2538
|
+
color: HighlightText;
|
|
2539
|
+
background-color: Highlight;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
.plinth-list-row:disabled,
|
|
2543
|
+
.plinth-list-row[aria-disabled='true'] {
|
|
2544
|
+
color: GrayText;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
/* menu-sheet.css */
|
|
2550
|
+
/*
|
|
2551
|
+
* Menu sheet: the app shell's menu, a sheet with the signed-in user, the navigation and the
|
|
2552
|
+
* account actions. A side panel from 768px up, a bottom sheet on a phone.
|
|
2553
|
+
*
|
|
2554
|
+
* <dialog id="menu" class="plinth-sheet plinth-menu-sheet" data-side="end"
|
|
2555
|
+
* data-side-phone="bottom" closedby="any" aria-labelledby="menu-title">
|
|
2556
|
+
* <div class="plinth-sheet-panel">
|
|
2557
|
+
* <div class="plinth-sheet-handle" aria-hidden="true"></div>
|
|
2558
|
+
* <div class="plinth-sheet-header">
|
|
2559
|
+
* <h2 id="menu-title" class="plinth-sheet-title">Menu</h2>
|
|
2560
|
+
* <button … class="plinth-button plinth-icon-button plinth-sheet-close" commandfor="menu"
|
|
2561
|
+
* command="close">…</button>
|
|
2562
|
+
* </div>
|
|
2563
|
+
* <div class="plinth-sheet-body">
|
|
2564
|
+
* <a class="plinth-list-row plinth-menu-sheet-user" href="/profile">
|
|
2565
|
+
* <span class="plinth-list-row-leading">
|
|
2566
|
+
* <span class="plinth-avatar" data-size="lg" aria-hidden="true">…</span>
|
|
2567
|
+
* </span>
|
|
2568
|
+
* <span class="plinth-list-row-content">
|
|
2569
|
+
* <span class="plinth-list-row-title">Sam Lee</span>
|
|
2570
|
+
* <span class="plinth-list-row-description">sam@example.com</span>
|
|
2571
|
+
* </span>
|
|
2572
|
+
* <span class="plinth-list-row-trailing">…chevron…</span>
|
|
2573
|
+
* </a>
|
|
2574
|
+
* <nav class="plinth-menu-sheet-nav" aria-labelledby="menu-title">
|
|
2575
|
+
* <div class="plinth-list-card">…link rows, one with aria-current="page"…</div>
|
|
2576
|
+
* </nav>
|
|
2577
|
+
* <div class="plinth-menu-sheet-account">
|
|
2578
|
+
* <div class="plinth-list-card">…Settings, Sign out…</div>
|
|
2579
|
+
* </div>
|
|
2580
|
+
* </div>
|
|
2581
|
+
* </div>
|
|
2582
|
+
* </dialog>
|
|
2583
|
+
*
|
|
2584
|
+
* It is a sheet (sheet.css), always data-side="end" data-side-phone="bottom", with no footer;
|
|
2585
|
+
* everything the sheet's contract says holds, and it needs modal.css, sheet.css, list-row.css, list-card.css, avatar.css,
|
|
2586
|
+
* button.css and icon.css. The root's id is required in practice: the header's menu button opens
|
|
2587
|
+
* it with <button commandfor="menu" command="show-modal">, the close button points at it too, and
|
|
2588
|
+
* the dialog and the nav are named by the title, whose id is the root's id with -title.
|
|
2589
|
+
*
|
|
2590
|
+
* The parts, in the body:
|
|
2591
|
+
* - the user row (optional): a list row with a decorative avatar (lg), the name and an optional
|
|
2592
|
+
* detail (an email or a role); a static <div>, or a link to the profile with a chevron when it
|
|
2593
|
+
* has an href. The avatar is decorative because the name is printed next to it.
|
|
2594
|
+
* - the nav (always there): a <nav> named by the sheet's title (aria-labelledby), holding list
|
|
2595
|
+
* cards of link rows, their headings <h3> under the sheet's <h2> title. The app marks the row
|
|
2596
|
+
* of the page being shown with aria-current="page" (current on the list row) and keeps it up
|
|
2597
|
+
* to date; the menu remembers nothing.
|
|
2598
|
+
* - the account (optional): a list card of account actions, such as Settings and Sign out (a
|
|
2599
|
+
* button row; for a form post, a button row with type="submit" and form="…").
|
|
2600
|
+
*
|
|
2601
|
+
* The parts stack with a gap between them. In the full-height side panel the account sits on the
|
|
2602
|
+
* bottom edge (margin-block-start: auto) and rises with the content when the nav is long; the
|
|
2603
|
+
* body scrolls as one.
|
|
2604
|
+
*/
|
|
2605
|
+
|
|
2606
|
+
@layer plinth {
|
|
2607
|
+
.plinth-menu-sheet .plinth-sheet-body {
|
|
2608
|
+
display: flex;
|
|
2609
|
+
flex-direction: column;
|
|
2610
|
+
gap: var(--plinth-space-5);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
.plinth-menu-sheet-nav,
|
|
2614
|
+
.plinth-menu-sheet-account {
|
|
2615
|
+
display: flex;
|
|
2616
|
+
flex-direction: column;
|
|
2617
|
+
gap: var(--plinth-space-5);
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
.plinth-menu-sheet-account {
|
|
2621
|
+
margin-block-start: auto;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
/* The name reads as the row's heading. */
|
|
2625
|
+
.plinth-menu-sheet-user .plinth-list-row-title {
|
|
2626
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/* A link to the profile rounds its hover tint like the list cards' corners below it. */
|
|
2630
|
+
a.plinth-menu-sheet-user {
|
|
2631
|
+
border-radius: var(--plinth-radius-container);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/* menu.css */
|
|
2636
|
+
/*
|
|
2637
|
+
* Menu: a short list of actions that opens from a button, in a popover anchored to it.
|
|
2638
|
+
*
|
|
2639
|
+
* <button type="button" class="plinth-button plinth-icon-button" popovertarget="ticket-actions"
|
|
2640
|
+
* aria-label="More actions">…</button>
|
|
2641
|
+
* <div class="plinth-menu" id="ticket-actions" popover="auto">
|
|
2642
|
+
* <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
|
|
2643
|
+
* command="hide-popover">
|
|
2644
|
+
* <span class="plinth-menu-icon" aria-hidden="true"><svg>…</svg></span>
|
|
2645
|
+
* <span class="plinth-menu-label">Assign to me</span>
|
|
2646
|
+
* <kbd class="plinth-menu-shortcut" aria-hidden="true">A</kbd>
|
|
2647
|
+
* </button>
|
|
2648
|
+
* <a class="plinth-menu-item" href="/tickets/42/print">
|
|
2649
|
+
* <span class="plinth-menu-label">Print</span>
|
|
2650
|
+
* </a>
|
|
2651
|
+
* <hr class="plinth-menu-separator">
|
|
2652
|
+
* <div class="plinth-menu-group" role="group" aria-label="Move to">
|
|
2653
|
+
* <div class="plinth-menu-heading" aria-hidden="true">Move to</div>
|
|
2654
|
+
* <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
|
|
2655
|
+
* command="hide-popover"><span class="plinth-menu-label">Archive</span></button>
|
|
2656
|
+
* </div>
|
|
2657
|
+
* <hr class="plinth-menu-separator">
|
|
2658
|
+
* <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
|
|
2659
|
+
* command="hide-popover" data-tone="danger">
|
|
2660
|
+
* <span class="plinth-menu-label">Delete</span>
|
|
2661
|
+
* </button>
|
|
2662
|
+
* </div>
|
|
2663
|
+
*
|
|
2664
|
+
* The root is a <div popover="auto"> with an id (required in Blade, Vue and React), which the
|
|
2665
|
+
* app's button names in popovertarget (or commandfor with command="toggle-popover"). The browser
|
|
2666
|
+
* opens and closes it (ADR 2): the top layer, a click outside or Escape closes it, focus goes back
|
|
2667
|
+
* to the button, and the browser tells assistive technology whether the button's menu is
|
|
2668
|
+
* expanded. The button is the app's own (an icon button or a button); it is not part of the menu.
|
|
2669
|
+
*
|
|
2670
|
+
* It is not role="menu". The ARIA menu pattern needs arrow keys and a roving focus, which would be
|
|
2671
|
+
* script in every layer; this is a list of buttons and links, reached with Tab, as ADR 2 decides
|
|
2672
|
+
* for menus of actions. So the button has no aria-haspopup, and the items are plain buttons and
|
|
2673
|
+
* links that read as such.
|
|
2674
|
+
*
|
|
2675
|
+
* Parts, each its own component in Blade, Vue and React:
|
|
2676
|
+
* - item: a <button type="button">, or an <a href> when it navigates. A button item closes its
|
|
2677
|
+
* menu when pressed: commandfor="<the menu's id>" command="hide-popover", which the layers take
|
|
2678
|
+
* from the menu around the item. A link closes the menu by leaving the page; an app that routes
|
|
2679
|
+
* without loading a page closes it with hidePopover(). The icon is the project's SVG, decorative;
|
|
2680
|
+
* the label is the item's name. The shortcut is a hint for the eye, aria-hidden; the app that
|
|
2681
|
+
* has the shortcut adds aria-keyshortcuts to the item.
|
|
2682
|
+
* - separator: an <hr> between items.
|
|
2683
|
+
* - group: a <div role="group"> named by its heading (aria-label), with the heading shown above
|
|
2684
|
+
* its items and hidden from assistive technology, which reads the group's name instead.
|
|
2685
|
+
*
|
|
2686
|
+
* Variants and states:
|
|
2687
|
+
* - data-tone="danger" on an item that deletes or removes something: the danger text colour and a
|
|
2688
|
+
* danger tint on hover. The label still says what it does; the colour is not the only signal.
|
|
2689
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
2690
|
+
* - hover only where the device can hover; pressed takes the sunken surface; the focus ring on
|
|
2691
|
+
* :focus-visible, drawn inside the item (focus-ring.offset-inset).
|
|
2692
|
+
* - open is the native one (:popover-open), never a class.
|
|
2693
|
+
*
|
|
2694
|
+
* Placement: below the button, starting at its start edge, through CSS anchor positioning (the
|
|
2695
|
+
* popover's button is its implicit anchor). It takes the side of the button with more room, above
|
|
2696
|
+
* or below, and is never taller than that room: a long menu scrolls. When it would leave the
|
|
2697
|
+
* screen sideways it flips to the button's end edge. It is the same at every width; a phone gets no bottom sheet,
|
|
2698
|
+
* since a menu is a few actions next to the button that opened them (a long list belongs in a
|
|
2699
|
+
* sheet). A browser without anchor positioning shows the menu in the middle of the screen, where
|
|
2700
|
+
* it still works. Script opens it with showPopover({ source: button }), so it has its anchor;
|
|
2701
|
+
* without a source it has none, and sits in the screen's start corner.
|
|
2702
|
+
*
|
|
2703
|
+
* Size: --plinth-menu-min-width (192px) to --plinth-menu-max-width (320px), never wider than the
|
|
2704
|
+
* screen less a margin, and never taller than the room it has. Items are --plinth-touch-target
|
|
2705
|
+
* (40px) tall; with a mouse from 768px up, --plinth-control-md (32px). A long label wraps.
|
|
2706
|
+
* It fades in and out on the motion tokens, which drop to 0ms under prefers-reduced-motion.
|
|
2707
|
+
*/
|
|
2708
|
+
|
|
2709
|
+
@layer plinth {
|
|
2710
|
+
.plinth-menu {
|
|
2711
|
+
/* The browser's popover rules centre it on the screen: the fallback without anchoring. */
|
|
2712
|
+
box-sizing: border-box;
|
|
2713
|
+
min-inline-size: min(var(--plinth-menu-min-width), 100vi - 2 * var(--plinth-space-2));
|
|
2714
|
+
max-inline-size: min(var(--plinth-menu-max-width), 100vi - 2 * var(--plinth-space-2));
|
|
2715
|
+
max-block-size: calc(100dvb - 2 * var(--plinth-space-2));
|
|
2716
|
+
padding: var(--plinth-space-1);
|
|
2717
|
+
overflow: hidden auto;
|
|
2718
|
+
overscroll-behavior: contain;
|
|
2719
|
+
font-family: var(--plinth-font-family-body);
|
|
2720
|
+
font-size: var(--plinth-font-size-md);
|
|
2721
|
+
line-height: var(--plinth-font-leading-tight);
|
|
2722
|
+
color: var(--plinth-color-text);
|
|
2723
|
+
background-color: var(--plinth-color-surface-raised);
|
|
2724
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
2725
|
+
border-radius: var(--plinth-radius-container);
|
|
2726
|
+
box-shadow: var(--plinth-elevation-2);
|
|
2727
|
+
opacity: 0;
|
|
2728
|
+
transition:
|
|
2729
|
+
opacity var(--plinth-duration-fast) var(--plinth-easing-exit),
|
|
2730
|
+
overlay var(--plinth-duration-fast) allow-discrete,
|
|
2731
|
+
display var(--plinth-duration-fast) allow-discrete;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
.plinth-menu:popover-open {
|
|
2735
|
+
opacity: 1;
|
|
2736
|
+
transition-timing-function: var(--plinth-easing-enter);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
@starting-style {
|
|
2740
|
+
.plinth-menu:popover-open {
|
|
2741
|
+
opacity: 0;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
@supports (position-area: block-end) {
|
|
2746
|
+
.plinth-menu {
|
|
2747
|
+
inset: auto;
|
|
2748
|
+
max-block-size: calc(100% - 2 * var(--plinth-space-1));
|
|
2749
|
+
margin-block: var(--plinth-space-1);
|
|
2750
|
+
margin-inline: 0;
|
|
2751
|
+
position-area: block-end span-inline-end;
|
|
2752
|
+
position-try-order: most-block-size;
|
|
2753
|
+
position-try-fallbacks:
|
|
2754
|
+
flip-block,
|
|
2755
|
+
flip-inline,
|
|
2756
|
+
flip-block flip-inline;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
.plinth-menu-item {
|
|
2761
|
+
box-sizing: border-box;
|
|
2762
|
+
display: flex;
|
|
2763
|
+
gap: var(--plinth-space-2);
|
|
2764
|
+
align-items: center;
|
|
2765
|
+
inline-size: 100%;
|
|
2766
|
+
min-block-size: var(--plinth-touch-target);
|
|
2767
|
+
padding-block: var(--plinth-space-1);
|
|
2768
|
+
padding-inline: var(--plinth-space-2);
|
|
2769
|
+
margin: 0;
|
|
2770
|
+
font: inherit;
|
|
2771
|
+
color: var(--plinth-color-text);
|
|
2772
|
+
text-align: start;
|
|
2773
|
+
text-decoration: none;
|
|
2774
|
+
cursor: pointer;
|
|
2775
|
+
user-select: none;
|
|
2776
|
+
background-color: transparent;
|
|
2777
|
+
border: 0;
|
|
2778
|
+
border-radius: var(--plinth-radius-control);
|
|
2779
|
+
transition:
|
|
2780
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
2781
|
+
color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
.plinth-menu-item[data-tone='danger'] {
|
|
2785
|
+
color: var(--plinth-color-danger-text);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
@media (hover: hover) {
|
|
2789
|
+
a.plinth-menu-item[href]:hover,
|
|
2790
|
+
button.plinth-menu-item:enabled:hover {
|
|
2791
|
+
background-color: var(--plinth-color-surface-hover);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
a.plinth-menu-item[href][data-tone='danger']:hover,
|
|
2795
|
+
button.plinth-menu-item:enabled[data-tone='danger']:hover {
|
|
2796
|
+
background-color: var(--plinth-color-danger-surface);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
a.plinth-menu-item[href]:active,
|
|
2801
|
+
button.plinth-menu-item:enabled:active {
|
|
2802
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
.plinth-menu-item:focus-visible {
|
|
2806
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
2807
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
.plinth-menu-item:disabled,
|
|
2811
|
+
.plinth-menu-item[aria-disabled='true'] {
|
|
2812
|
+
cursor: not-allowed;
|
|
2813
|
+
opacity: var(--plinth-opacity-disabled);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.plinth-menu-icon {
|
|
2817
|
+
display: inline-flex;
|
|
2818
|
+
flex: none;
|
|
2819
|
+
inline-size: var(--plinth-size-icon-sm);
|
|
2820
|
+
block-size: var(--plinth-size-icon-sm);
|
|
2821
|
+
color: var(--plinth-color-text-muted);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
.plinth-menu-item[data-tone='danger'] .plinth-menu-icon {
|
|
2825
|
+
color: inherit;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
.plinth-menu-icon > svg {
|
|
2829
|
+
inline-size: 100%;
|
|
2830
|
+
block-size: 100%;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
.plinth-menu-label {
|
|
2834
|
+
flex: 1 1 auto;
|
|
2835
|
+
min-inline-size: 0;
|
|
2836
|
+
overflow-wrap: anywhere;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
.plinth-menu-shortcut {
|
|
2840
|
+
flex: none;
|
|
2841
|
+
padding-inline-start: var(--plinth-space-4);
|
|
2842
|
+
margin-inline-start: auto;
|
|
2843
|
+
font-family: var(--plinth-font-family-mono);
|
|
2844
|
+
font-size: var(--plinth-font-size-xs);
|
|
2845
|
+
color: var(--plinth-color-text-subtle);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
.plinth-menu-separator {
|
|
2849
|
+
block-size: 0;
|
|
2850
|
+
margin-block: var(--plinth-space-1);
|
|
2851
|
+
margin-inline: calc(-1 * var(--plinth-space-1));
|
|
2852
|
+
border: 0;
|
|
2853
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
.plinth-menu-group {
|
|
2857
|
+
margin: 0;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
.plinth-menu-heading {
|
|
2861
|
+
padding-block: var(--plinth-space-2) var(--plinth-space-1);
|
|
2862
|
+
padding-inline: var(--plinth-space-2);
|
|
2863
|
+
font-size: var(--plinth-font-size-xs);
|
|
2864
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
2865
|
+
color: var(--plinth-color-text-subtle);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
@media (min-width: 768px) and (pointer: fine) {
|
|
2869
|
+
.plinth-menu-item {
|
|
2870
|
+
min-block-size: var(--plinth-control-md);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
@media (forced-colors: active) {
|
|
2875
|
+
.plinth-menu-item:disabled,
|
|
2876
|
+
.plinth-menu-item[aria-disabled='true'] {
|
|
2877
|
+
color: GrayText;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/* modal.css */
|
|
2883
|
+
/*
|
|
2884
|
+
* Modal base: what the dialog (dialog.css), the sheet (sheet.css) and the drawer (drawer.css)
|
|
2885
|
+
* share, written once so they can't drift apart (docs/adr/0005): the root's surface and motion,
|
|
2886
|
+
* the backdrop, the scroll lock, the safe areas, the panel and the grab handle, and the header,
|
|
2887
|
+
* title, description, close button, body and footer. It has no markup of its own and no class:
|
|
2888
|
+
* each component keeps its own root and part classes (plinth-dialog, plinth-dialog-panel, …;
|
|
2889
|
+
* plinth-sheet, plinth-sheet-panel, …), listed here in :is(). A page that loads the components
|
|
2890
|
+
* one by one loads modal.css with them.
|
|
2891
|
+
*
|
|
2892
|
+
* The parts sit in a panel, the root's only child:
|
|
2893
|
+
* <dialog class="plinth-sheet" …>
|
|
2894
|
+
* <div class="plinth-sheet-panel">
|
|
2895
|
+
* <div class="plinth-sheet-handle" aria-hidden="true"></div> (a bottom panel on a phone)
|
|
2896
|
+
* <div class="plinth-sheet-header">…</div>
|
|
2897
|
+
* <div class="plinth-sheet-body">…</div>
|
|
2898
|
+
* <div class="plinth-sheet-footer">…</div> (optional)
|
|
2899
|
+
* </div>
|
|
2900
|
+
* </dialog>
|
|
2901
|
+
*
|
|
2902
|
+
* The rules sit in a layer inside plinth (plinth.modal), so a component's own rules win over them
|
|
2903
|
+
* whatever the order the files load in, and a project's utilities win over both.
|
|
2904
|
+
*
|
|
2905
|
+
* Each component sets where its root sits and how big it is, and tunes the motion and the safe
|
|
2906
|
+
* area through three custom properties on the root; it never sets translate or opacity itself:
|
|
2907
|
+
* - --plinth-modal-offset: where the root slides in from and back out to (a translate value;
|
|
2908
|
+
* 0 0, the default, is no slide);
|
|
2909
|
+
* - --plinth-modal-closed-opacity: its opacity while it opens and closes (0 fades it, 1, the
|
|
2910
|
+
* default, doesn't);
|
|
2911
|
+
* - --plinth-modal-safe-area-end: env(safe-area-inset-bottom) when the root reaches the bottom
|
|
2912
|
+
* edge of the screen, so its last part clears the home indicator; initial (the default) when
|
|
2913
|
+
* it doesn't.
|
|
2914
|
+
*
|
|
2915
|
+
* The motion runs on the motion tokens, which drop to 0ms under prefers-reduced-motion. While a
|
|
2916
|
+
* modal is open the page under it doesn't scroll (html:has(…:modal)), and it has no scrollbar
|
|
2917
|
+
* gutter (scrollbar-gutter: auto, over a project's stable in an earlier layer): the top layer is
|
|
2918
|
+
* never drawn in the gutter, so a gutter would leave a strip of the page beside a sheet on the end
|
|
2919
|
+
* edge or the full width. Where the scrollbar takes space the page shifts by its width under the
|
|
2920
|
+
* backdrop; overlay scrollbars take no space and nothing shifts. The body scrolls, with
|
|
2921
|
+
* overscroll-behavior: contain; the header and footer stay put; the margins of the body's first and
|
|
2922
|
+
* last child are reset, so its padding is its edge. On a phone the footer's actions share its
|
|
2923
|
+
* width; from 768px up they sit at its end.
|
|
2924
|
+
*
|
|
2925
|
+
* A bottom panel on a phone (a sheet whose side below 768px is the bottom, a dialog with
|
|
2926
|
+
* data-side-phone="bottom" that can be dismissed, and the drawer) has a grab handle, and the
|
|
2927
|
+
* person drags it down to close it. The layers print the handle only there; its presence is what
|
|
2928
|
+
* turns the dragging on, below 768px. Dragging is native scrolling: the root fills the screen and
|
|
2929
|
+
* is a scroll-snap container, see-through, with a spacer as tall as the screen above the panel,
|
|
2930
|
+
* so the browser gives the momentum and the snapping. Without script the handle shows and the
|
|
2931
|
+
* close button and Escape close the panel. What the platform can't do, the layers add (a few
|
|
2932
|
+
* lines, docs/adr/0002 section 6: x-init in Blade, useSwipeToClose in Vue and React):
|
|
2933
|
+
* - on scrollend (scroll where it is missing), when the panel has gone off the bottom edge, they
|
|
2934
|
+
* call requestClose() (close() where it is missing), so cancel fires first, as for Escape; when
|
|
2935
|
+
* the app prevents it, the panel goes back to where it opened;
|
|
2936
|
+
* - a tap on the root itself, above the panel, closes it when closedby="any", since the browser
|
|
2937
|
+
* reads that tap as one on the dialog, not its backdrop;
|
|
2938
|
+
* - on toggle to open, a drawer with data-snap scrolls to that snap point (a browser that fires no
|
|
2939
|
+
* toggle on a dialog opens it at its largest).
|
|
2940
|
+
* The snapping is the browser's own scrolling, and the layers' scroll is instant.
|
|
2941
|
+
*/
|
|
2942
|
+
|
|
2943
|
+
@layer plinth {
|
|
2944
|
+
@layer modal {
|
|
2945
|
+
html:has(:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):modal) {
|
|
2946
|
+
overflow: hidden;
|
|
2947
|
+
scrollbar-gutter: auto;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer) {
|
|
2951
|
+
--plinth-modal-offset: 0 0;
|
|
2952
|
+
--plinth-modal-closed-opacity: 1;
|
|
2953
|
+
--plinth-modal-safe-area-end: initial;
|
|
2954
|
+
|
|
2955
|
+
position: fixed;
|
|
2956
|
+
box-sizing: border-box;
|
|
2957
|
+
flex-direction: column;
|
|
2958
|
+
max-inline-size: 100%;
|
|
2959
|
+
padding: 0;
|
|
2960
|
+
overflow: hidden;
|
|
2961
|
+
font-family: var(--plinth-font-family-body);
|
|
2962
|
+
color: var(--plinth-color-text);
|
|
2963
|
+
background-color: var(--plinth-color-surface-raised);
|
|
2964
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
2965
|
+
border-radius: var(--plinth-radius-container);
|
|
2966
|
+
box-shadow: var(--plinth-elevation-3);
|
|
2967
|
+
opacity: var(--plinth-modal-closed-opacity);
|
|
2968
|
+
translate: var(--plinth-modal-offset);
|
|
2969
|
+
transition:
|
|
2970
|
+
translate var(--plinth-duration-normal) var(--plinth-easing-exit),
|
|
2971
|
+
opacity var(--plinth-duration-normal) var(--plinth-easing-exit),
|
|
2972
|
+
overlay var(--plinth-duration-normal) allow-discrete,
|
|
2973
|
+
display var(--plinth-duration-normal) allow-discrete;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open] {
|
|
2977
|
+
display: flex;
|
|
2978
|
+
opacity: 1;
|
|
2979
|
+
translate: 0 0;
|
|
2980
|
+
transition-timing-function: var(--plinth-easing-enter);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
@starting-style {
|
|
2984
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open] {
|
|
2985
|
+
opacity: var(--plinth-modal-closed-opacity);
|
|
2986
|
+
translate: var(--plinth-modal-offset);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)::backdrop {
|
|
2991
|
+
background-color: var(--plinth-color-overlay);
|
|
2992
|
+
opacity: 0;
|
|
2993
|
+
transition:
|
|
2994
|
+
opacity var(--plinth-duration-normal) var(--plinth-easing-standard),
|
|
2995
|
+
overlay var(--plinth-duration-normal) allow-discrete,
|
|
2996
|
+
display var(--plinth-duration-normal) allow-discrete;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open]::backdrop {
|
|
3000
|
+
opacity: 1;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
@starting-style {
|
|
3004
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open]::backdrop {
|
|
3005
|
+
opacity: 0;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/*
|
|
3010
|
+
* The panel holds the parts, the root's only child. It is the column the body scrolls in;
|
|
3011
|
+
* the root is the surface, except for a bottom panel on a phone (below).
|
|
3012
|
+
*/
|
|
3013
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel) {
|
|
3014
|
+
display: flex;
|
|
3015
|
+
flex: 1 1 auto;
|
|
3016
|
+
flex-direction: column;
|
|
3017
|
+
min-block-size: 0;
|
|
3018
|
+
margin: 0;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
:is(.plinth-dialog-header, .plinth-sheet-header, .plinth-drawer-header) {
|
|
3022
|
+
display: grid;
|
|
3023
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
3024
|
+
gap: var(--plinth-space-1) var(--plinth-space-3);
|
|
3025
|
+
align-items: center;
|
|
3026
|
+
padding-block: var(--plinth-space-3);
|
|
3027
|
+
padding-inline: var(--plinth-space-4) var(--plinth-space-3);
|
|
3028
|
+
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/* With no close button, the header's padding is even. */
|
|
3032
|
+
:is(.plinth-dialog-header, .plinth-sheet-header, .plinth-drawer-header):not(
|
|
3033
|
+
:has(> :is(.plinth-dialog-close, .plinth-sheet-close, .plinth-drawer-close))
|
|
3034
|
+
) {
|
|
3035
|
+
padding-inline-end: var(--plinth-space-4);
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
:is(.plinth-dialog-title, .plinth-sheet-title, .plinth-drawer-title) {
|
|
3039
|
+
grid-column: 1;
|
|
3040
|
+
margin: 0;
|
|
3041
|
+
font-size: var(--plinth-font-size-lg);
|
|
3042
|
+
font-weight: var(--plinth-font-weight-semibold);
|
|
3043
|
+
line-height: var(--plinth-font-leading-tight);
|
|
3044
|
+
letter-spacing: var(--plinth-font-tracking-tight);
|
|
3045
|
+
overflow-wrap: anywhere;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
:is(.plinth-dialog-description, .plinth-sheet-description, .plinth-drawer-description) {
|
|
3049
|
+
grid-column: 1;
|
|
3050
|
+
margin: 0;
|
|
3051
|
+
font-size: var(--plinth-font-size-sm);
|
|
3052
|
+
line-height: var(--plinth-font-leading-normal);
|
|
3053
|
+
color: var(--plinth-color-text-muted);
|
|
3054
|
+
overflow-wrap: anywhere;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
:is(.plinth-dialog-close, .plinth-sheet-close, .plinth-drawer-close) {
|
|
3058
|
+
grid-row: 1;
|
|
3059
|
+
grid-column: 2;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
|
|
3063
|
+
flex: 1 1 auto;
|
|
3064
|
+
min-block-size: 0;
|
|
3065
|
+
padding: var(--plinth-space-4);
|
|
3066
|
+
overflow-y: auto;
|
|
3067
|
+
overscroll-behavior: contain;
|
|
3068
|
+
font-size: var(--plinth-font-size-md);
|
|
3069
|
+
line-height: var(--plinth-font-leading-normal);
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
/* The body's padding is its edge: the first and last child's margins don't add to it. */
|
|
3073
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) > :first-child {
|
|
3074
|
+
margin-block-start: 0;
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) > :last-child {
|
|
3078
|
+
margin-block-end: 0;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/*
|
|
3082
|
+
* A browser can focus a scrolling area that has nothing focusable in it (Chrome and Firefox
|
|
3083
|
+
* do, Safari doesn't yet), so a long body of text only is better with a link or a control in
|
|
3084
|
+
* it.
|
|
3085
|
+
*/
|
|
3086
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body):focus-visible {
|
|
3087
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
3088
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
/* The last part clears the home indicator when the root reaches the bottom edge. */
|
|
3092
|
+
:is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body):last-child {
|
|
3093
|
+
padding-block-end: max(
|
|
3094
|
+
var(--plinth-space-4),
|
|
3095
|
+
var(--plinth-modal-safe-area-end, var(--plinth-space-4))
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
:is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) {
|
|
3100
|
+
display: flex;
|
|
3101
|
+
flex-wrap: wrap;
|
|
3102
|
+
gap: var(--plinth-space-2);
|
|
3103
|
+
justify-content: flex-end;
|
|
3104
|
+
padding-block: var(--plinth-space-3)
|
|
3105
|
+
max(var(--plinth-space-3), var(--plinth-modal-safe-area-end, var(--plinth-space-3)));
|
|
3106
|
+
padding-inline: var(--plinth-space-4);
|
|
3107
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
/* On a phone the footer's actions share its width. */
|
|
3111
|
+
:is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) > * {
|
|
3112
|
+
flex: 1 1 0;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
/*
|
|
3116
|
+
* A bottom panel on a phone, dragged down to close: a root whose panel holds a handle (the
|
|
3117
|
+
* layers print one only where the panel is on the bottom edge below 768px, and can be
|
|
3118
|
+
* dismissed). The root fills the screen and is a scroll-snap container, see-through; its
|
|
3119
|
+
* ::before is a spacer as tall as the screen above the panel, so scrolling it down is the
|
|
3120
|
+
* panel going off the bottom edge. The flex column runs bottom up (column-reverse), so the
|
|
3121
|
+
* root opens scrolled to its end, with the panel showing. Two snap positions: the panel's
|
|
3122
|
+
* end on the screen's bottom edge (open), and the spacer filling the screen (closed); the
|
|
3123
|
+
* layers close the dialog when the scrolling settles there. The panel is the surface, and the
|
|
3124
|
+
* body hands a swipe on to the root once it is scrolled to its top.
|
|
3125
|
+
*/
|
|
3126
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
3127
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3128
|
+
) {
|
|
3129
|
+
flex-direction: column-reverse;
|
|
3130
|
+
overflow-y: auto;
|
|
3131
|
+
overscroll-behavior: contain;
|
|
3132
|
+
scroll-snap-type: y mandatory;
|
|
3133
|
+
scrollbar-width: none;
|
|
3134
|
+
background-color: transparent;
|
|
3135
|
+
box-shadow: none;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
/* As a scrolling area the root can take focus (Chrome and Firefox), so it shows the ring. */
|
|
3139
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
3140
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3141
|
+
):focus-visible {
|
|
3142
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
3143
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
3147
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3148
|
+
)::before {
|
|
3149
|
+
flex: none;
|
|
3150
|
+
order: 1;
|
|
3151
|
+
block-size: 100%;
|
|
3152
|
+
content: '';
|
|
3153
|
+
scroll-snap-align: start;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
3157
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3158
|
+
) {
|
|
3159
|
+
flex: none;
|
|
3160
|
+
max-block-size: calc(
|
|
3161
|
+
100% - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
|
|
3162
|
+
);
|
|
3163
|
+
padding-inline: max(env(safe-area-inset-left), env(safe-area-inset-right));
|
|
3164
|
+
scroll-snap-align: end;
|
|
3165
|
+
background-color: var(--plinth-color-surface-raised);
|
|
3166
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
3167
|
+
border-start-start-radius: var(--plinth-radius-container);
|
|
3168
|
+
border-start-end-radius: var(--plinth-radius-container);
|
|
3169
|
+
box-shadow: var(--plinth-elevation-3);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
/*
|
|
3173
|
+
* The grab handle, a short pill centred at the top; decorative, since Escape and the close
|
|
3174
|
+
* button close the panel too. From 768px up it is hidden.
|
|
3175
|
+
*/
|
|
3176
|
+
:is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
|
|
3177
|
+
display: block;
|
|
3178
|
+
flex: none;
|
|
3179
|
+
align-self: center;
|
|
3180
|
+
inline-size: var(--plinth-space-10);
|
|
3181
|
+
block-size: var(--plinth-space-1);
|
|
3182
|
+
margin-block-start: var(--plinth-space-2);
|
|
3183
|
+
background-color: var(--plinth-color-border-strong);
|
|
3184
|
+
border-radius: var(--plinth-radius-pill);
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
3188
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3189
|
+
)
|
|
3190
|
+
> :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
|
|
3191
|
+
overscroll-behavior: auto;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
@media (forced-colors: active) {
|
|
3195
|
+
:is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
|
|
3196
|
+
background-color: CanvasText;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
@media (min-width: 768px) {
|
|
3201
|
+
:is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) > * {
|
|
3202
|
+
flex: none;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
/* From 768px up no panel is dragged: the root is the surface again. */
|
|
3206
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
3207
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3208
|
+
) {
|
|
3209
|
+
flex-direction: column;
|
|
3210
|
+
overflow: hidden;
|
|
3211
|
+
overscroll-behavior: auto;
|
|
3212
|
+
scroll-snap-type: none;
|
|
3213
|
+
scrollbar-width: auto;
|
|
3214
|
+
background-color: var(--plinth-color-surface-raised);
|
|
3215
|
+
box-shadow: var(--plinth-elevation-3);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
|
|
3219
|
+
> * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3220
|
+
)::before {
|
|
3221
|
+
content: none;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
3225
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3226
|
+
) {
|
|
3227
|
+
flex: 1 1 auto;
|
|
3228
|
+
max-block-size: none;
|
|
3229
|
+
padding-inline: 0;
|
|
3230
|
+
scroll-snap-align: none;
|
|
3231
|
+
background-color: transparent;
|
|
3232
|
+
border-block-start: 0;
|
|
3233
|
+
border-radius: 0;
|
|
3234
|
+
box-shadow: none;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
:is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
|
|
3238
|
+
display: none;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
:is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
|
|
3242
|
+
> :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
|
|
3243
|
+
)
|
|
3244
|
+
> :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
|
|
3245
|
+
overscroll-behavior: contain;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
/* page-header.css */
|
|
3252
|
+
/*
|
|
3253
|
+
* Page header: the bar at the top of a page, with a leading action, the page's heading and
|
|
3254
|
+
* trailing actions. On a phone it is the screen's top bar; in an app shell it heads the page beside
|
|
3255
|
+
* the sidebar, under the app bar. It sticks to the top as the content scrolls under it.
|
|
3256
|
+
*
|
|
3257
|
+
* <header class="plinth-page-header" data-top>
|
|
3258
|
+
* <div class="plinth-page-header-leading">
|
|
3259
|
+
* <a class="plinth-button plinth-icon-button" href="/inbox" data-variant="ghost"
|
|
3260
|
+
* data-size="md" aria-label="Back">
|
|
3261
|
+
* <span class="plinth-icon" data-size="md" data-mirror aria-hidden="true"><svg>…</svg></span>
|
|
3262
|
+
* </a>
|
|
3263
|
+
* </div>
|
|
3264
|
+
* <h1 class="plinth-page-header-heading">Inbox</h1>
|
|
3265
|
+
* <div class="plinth-page-header-actions">
|
|
3266
|
+
* <button type="button" class="plinth-button plinth-icon-button" data-variant="ghost"
|
|
3267
|
+
* data-size="md" aria-label="Search">…</button>
|
|
3268
|
+
* </div>
|
|
3269
|
+
* </header>
|
|
3270
|
+
*
|
|
3271
|
+
* The root is a <header>. At the top of the page (not inside <main>, <article>, <section>, <aside>
|
|
3272
|
+
* or <nav>) it is the page's banner landmark, so a page has one there; inside <main>, as in an app
|
|
3273
|
+
* shell, it is no landmark, only the page's header. The heading is required: an <h1> by default, or
|
|
3274
|
+
* <h2> to <h6> (level) when the page sits beside another page's <h1>, such as an open ticket beside
|
|
3275
|
+
* the list on a wide screen. The level doesn't change the look. The heading stays on one line and
|
|
3276
|
+
* ends in an ellipsis when it doesn't fit.
|
|
3277
|
+
*
|
|
3278
|
+
* The leading part holds one action, usually an icon button: a menu button that opens a sheet
|
|
3279
|
+
* (commandfor and command="show-modal") on a top-level page, or a back link (with data-mirror on
|
|
3280
|
+
* its icon so it points the other way in right-to-left text) on a page under it. The actions part
|
|
3281
|
+
* holds the trailing actions: at most three icon buttons (put the rest in a menu), or a primary
|
|
3282
|
+
* button such as New ticket. Both are optional, and printed only when they have content. Every
|
|
3283
|
+
* icon button needs its own name (label). The page header needs button.css and icon.css for them.
|
|
3284
|
+
*
|
|
3285
|
+
* Variant (the app sets it, the page header doesn't guess):
|
|
3286
|
+
* - data-top: the page header is the screen's top bar, at the very top of a phone's screen. It
|
|
3287
|
+
* adds env(safe-area-inset-top) above its height, which pads it below the status bar and the
|
|
3288
|
+
* notch on a phone that draws under them (viewport-fit=cover). Leave it off under an app bar.
|
|
3289
|
+
*
|
|
3290
|
+
* Its height is the page-header.height component token (56px). The inline padding clears the
|
|
3291
|
+
* notch on a phone held sideways. It is sticky on the layer.sticky z-index, so it sits above the
|
|
3292
|
+
* page and below sheets and menus (the top layer), and it sticks --plinth-page-header-offset from
|
|
3293
|
+
* the top: 0 by default, or the app bar's height in a shell that has one. Content scrolls under
|
|
3294
|
+
* it, so the app sets scroll-padding-block-start on its scrolling element (the offset plus the
|
|
3295
|
+
* height, plus env(safe-area-inset-top) with data-top) to keep a focused element or an anchor's
|
|
3296
|
+
* target from landing under the page header.
|
|
3297
|
+
*
|
|
3298
|
+
* The actions sit var(--plinth-space-2) apart, so on a touch screen the 40px hit areas of 32px
|
|
3299
|
+
* icon buttons don't overlap.
|
|
3300
|
+
*
|
|
3301
|
+
* The app owns the state: the page header has no states of its own. What the actions do is the
|
|
3302
|
+
* app's (a link, a commandfor, wire:click, @click on the icon button).
|
|
3303
|
+
*/
|
|
3304
|
+
|
|
3305
|
+
@layer plinth {
|
|
3306
|
+
.plinth-page-header {
|
|
3307
|
+
position: sticky;
|
|
3308
|
+
inset-block-start: var(--plinth-page-header-offset);
|
|
3309
|
+
z-index: var(--plinth-layer-sticky);
|
|
3310
|
+
box-sizing: border-box;
|
|
3311
|
+
display: flex;
|
|
3312
|
+
gap: var(--plinth-space-2);
|
|
3313
|
+
align-items: center;
|
|
3314
|
+
min-block-size: var(--plinth-page-header-height);
|
|
3315
|
+
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
|
|
3316
|
+
max(var(--plinth-space-2), env(safe-area-inset-right));
|
|
3317
|
+
font-family: var(--plinth-font-family-body);
|
|
3318
|
+
color: var(--plinth-color-text);
|
|
3319
|
+
background-color: var(--plinth-color-surface);
|
|
3320
|
+
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
.plinth-page-header:dir(rtl) {
|
|
3324
|
+
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
|
|
3325
|
+
max(var(--plinth-space-2), env(safe-area-inset-left));
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
/* The screen's top bar clears the status bar and the notch. */
|
|
3329
|
+
.plinth-page-header[data-top] {
|
|
3330
|
+
min-block-size: calc(var(--plinth-page-header-height) + env(safe-area-inset-top));
|
|
3331
|
+
padding-block-start: env(safe-area-inset-top);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
.plinth-page-header-leading,
|
|
3335
|
+
.plinth-page-header-actions {
|
|
3336
|
+
display: flex;
|
|
3337
|
+
flex: none;
|
|
3338
|
+
gap: var(--plinth-space-2);
|
|
3339
|
+
align-items: center;
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
.plinth-page-header-heading {
|
|
3343
|
+
flex: 1 1 auto;
|
|
3344
|
+
min-inline-size: 0;
|
|
3345
|
+
margin: 0;
|
|
3346
|
+
overflow: hidden;
|
|
3347
|
+
text-overflow: ellipsis;
|
|
3348
|
+
font-size: var(--plinth-font-size-lg);
|
|
3349
|
+
font-weight: var(--plinth-font-weight-semibold);
|
|
3350
|
+
line-height: var(--plinth-font-leading-tight);
|
|
3351
|
+
letter-spacing: var(--plinth-font-tracking-tight);
|
|
3352
|
+
white-space: nowrap;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
/* Without a leading action or trailing actions, the heading keeps the text's distance from the edge. */
|
|
3356
|
+
.plinth-page-header-heading:first-child {
|
|
3357
|
+
margin-inline-start: var(--plinth-space-2);
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
.plinth-page-header-heading:last-child {
|
|
3361
|
+
margin-inline-end: var(--plinth-space-2);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
@media (min-width: 768px) {
|
|
3365
|
+
.plinth-page-header {
|
|
3366
|
+
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
|
|
3367
|
+
max(var(--plinth-space-4), env(safe-area-inset-right));
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
.plinth-page-header:dir(rtl) {
|
|
3371
|
+
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
|
|
3372
|
+
max(var(--plinth-space-4), env(safe-area-inset-left));
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
/* pill-row.css */
|
|
3378
|
+
/*
|
|
3379
|
+
* Pill row: a row of pills for filters, segments or sub-pages.
|
|
3380
|
+
*
|
|
3381
|
+
* <div class="plinth-pill-row" role="group" aria-label="Status">
|
|
3382
|
+
* <button type="button" class="plinth-pill" aria-pressed="true">
|
|
3383
|
+
* <span class="plinth-pill-label">Open</span>
|
|
3384
|
+
* <span class="plinth-pill-count">12</span>
|
|
3385
|
+
* </button>
|
|
3386
|
+
* <button type="button" class="plinth-pill" aria-pressed="false">
|
|
3387
|
+
* <span class="plinth-pill-label">Closed</span>
|
|
3388
|
+
* </button>
|
|
3389
|
+
* </div>
|
|
3390
|
+
*
|
|
3391
|
+
* The root is a <div role="group">, named by aria-label (label in Blade and Vue) or by the app's
|
|
3392
|
+
* own aria-labelledby. A row of links that navigates is <nav class="plinth-pill-row"> instead,
|
|
3393
|
+
* with no role (nav in Blade and Vue), and its pills are links:
|
|
3394
|
+
*
|
|
3395
|
+
* <a class="plinth-pill" href="…" aria-current="page"><span class="plinth-pill-label">…</span></a>
|
|
3396
|
+
*
|
|
3397
|
+
* Each pill is its own component (<x-plinth::pill>, PlinthPill), so the app's attributes and
|
|
3398
|
+
* listeners land on the pill (ADR 2). The count is optional. plinth-pill-label has no rule of its
|
|
3399
|
+
* own: it is a hook for a project's styles.
|
|
3400
|
+
*
|
|
3401
|
+
* States, set by the app:
|
|
3402
|
+
* - selected: a button pill always prints aria-pressed, "true" or "false"; a link pill prints
|
|
3403
|
+
* aria-current="page" for the page being shown. Selected takes the selection tint
|
|
3404
|
+
* (accent-subtle, accent-text) inside a 2px accent edge (border-width.thick), where the others
|
|
3405
|
+
* have a 1px neutral one, so colour is not its only cue. A pressed button still answers hover
|
|
3406
|
+
* and press with a step on the tint (accent-subtle-hover, accent-subtle-active).
|
|
3407
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
3408
|
+
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
3409
|
+
*
|
|
3410
|
+
* Layout: on a phone the row scrolls sideways, with no visible scrollbar, and snaps to the pills;
|
|
3411
|
+
* from 768px it wraps. The row's padding keeps the focus ring and the 40px touch target of the
|
|
3412
|
+
* pills inside the scrolling area, whatever --plinth-pill-height is (32px by default). A pill is
|
|
3413
|
+
* at least 40px wide.
|
|
3414
|
+
*/
|
|
3415
|
+
|
|
3416
|
+
@layer plinth {
|
|
3417
|
+
.plinth-pill-row {
|
|
3418
|
+
box-sizing: border-box;
|
|
3419
|
+
display: flex;
|
|
3420
|
+
gap: var(--plinth-space-2);
|
|
3421
|
+
align-items: center;
|
|
3422
|
+
max-inline-size: 100%;
|
|
3423
|
+
padding-block: max(
|
|
3424
|
+
var(--plinth-space-1),
|
|
3425
|
+
(var(--plinth-touch-target) - var(--plinth-pill-height)) / 2
|
|
3426
|
+
);
|
|
3427
|
+
padding-inline: var(--plinth-space-1);
|
|
3428
|
+
margin: 0;
|
|
3429
|
+
overflow-x: auto;
|
|
3430
|
+
overscroll-behavior-inline: contain;
|
|
3431
|
+
scroll-snap-type: inline proximity;
|
|
3432
|
+
scroll-padding-inline: var(--plinth-space-1);
|
|
3433
|
+
scrollbar-width: none;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
.plinth-pill {
|
|
3437
|
+
position: relative;
|
|
3438
|
+
box-sizing: border-box;
|
|
3439
|
+
display: inline-flex;
|
|
3440
|
+
flex: none;
|
|
3441
|
+
gap: var(--plinth-space-2);
|
|
3442
|
+
align-items: center;
|
|
3443
|
+
justify-content: center;
|
|
3444
|
+
min-inline-size: var(--plinth-touch-target);
|
|
3445
|
+
min-block-size: var(--plinth-pill-height);
|
|
3446
|
+
padding-block: 0;
|
|
3447
|
+
padding-inline: var(--plinth-space-3);
|
|
3448
|
+
margin: 0;
|
|
3449
|
+
font-family: var(--plinth-font-family-body);
|
|
3450
|
+
font-size: var(--plinth-font-size-sm);
|
|
3451
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
3452
|
+
line-height: var(--plinth-font-leading-tight);
|
|
3453
|
+
color: var(--plinth-color-text);
|
|
3454
|
+
white-space: nowrap;
|
|
3455
|
+
text-decoration: none;
|
|
3456
|
+
cursor: pointer;
|
|
3457
|
+
user-select: none;
|
|
3458
|
+
scroll-snap-align: start;
|
|
3459
|
+
background-color: transparent;
|
|
3460
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
3461
|
+
border-radius: var(--plinth-radius-pill);
|
|
3462
|
+
transition:
|
|
3463
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
3464
|
+
border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
3465
|
+
box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
3466
|
+
color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
/* A 40px touch target through an invisible hit area, as on the button (ADR 3). */
|
|
3470
|
+
@media (pointer: coarse) {
|
|
3471
|
+
.plinth-pill::before {
|
|
3472
|
+
position: absolute;
|
|
3473
|
+
inset-block-start: 50%;
|
|
3474
|
+
inset-inline: 0;
|
|
3475
|
+
block-size: max(100%, var(--plinth-touch-target));
|
|
3476
|
+
content: '';
|
|
3477
|
+
translate: 0 -50%;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
@media (hover: hover) {
|
|
3482
|
+
a.plinth-pill[href]:not([aria-current='page']):hover,
|
|
3483
|
+
button.plinth-pill:enabled:not([aria-pressed='true']):hover {
|
|
3484
|
+
background-color: var(--plinth-color-surface-hover);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
a.plinth-pill[href]:not([aria-current='page']):active,
|
|
3489
|
+
button.plinth-pill:enabled:not([aria-pressed='true']):active {
|
|
3490
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
/* The border and an inset ring make the 2px edge, so selecting doesn't move the label. */
|
|
3494
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) {
|
|
3495
|
+
color: var(--plinth-color-accent-text);
|
|
3496
|
+
background-color: var(--plinth-color-accent-subtle);
|
|
3497
|
+
border-color: var(--plinth-color-accent);
|
|
3498
|
+
box-shadow: inset 0 0 0 calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
|
|
3499
|
+
var(--plinth-color-accent);
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
/*
|
|
3503
|
+
* A pressed button can be pressed again, so it answers hover and press with a step on the tint.
|
|
3504
|
+
* Not in forced colours, where the tint would replace the system highlight under the label.
|
|
3505
|
+
*/
|
|
3506
|
+
@media (hover: hover) and (forced-colors: none) {
|
|
3507
|
+
button.plinth-pill:enabled[aria-pressed='true']:hover {
|
|
3508
|
+
background-color: var(--plinth-color-accent-subtle-hover);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
@media (forced-colors: none) {
|
|
3513
|
+
button.plinth-pill:enabled[aria-pressed='true']:active {
|
|
3514
|
+
background-color: var(--plinth-color-accent-subtle-active);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
.plinth-pill:disabled,
|
|
3519
|
+
.plinth-pill[aria-disabled='true'] {
|
|
3520
|
+
cursor: not-allowed;
|
|
3521
|
+
opacity: var(--plinth-opacity-disabled);
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
.plinth-pill:focus-visible {
|
|
3525
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
3526
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
/* A gap between the ring and the accent edge, so focus and selection read apart. */
|
|
3530
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']):focus-visible {
|
|
3531
|
+
outline-offset: var(--plinth-focus-ring-offset-filled);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
.plinth-pill-count {
|
|
3535
|
+
font-weight: var(--plinth-font-weight-regular);
|
|
3536
|
+
font-variant-numeric: tabular-nums;
|
|
3537
|
+
color: var(--plinth-color-text-muted);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) .plinth-pill-count {
|
|
3541
|
+
color: inherit;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
/* Wider screens have room: the pills wrap onto more lines instead of scrolling. */
|
|
3545
|
+
@media (min-width: 768px) {
|
|
3546
|
+
.plinth-pill-row {
|
|
3547
|
+
flex-wrap: wrap;
|
|
3548
|
+
overflow-x: visible;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
@media (forced-colors: active) {
|
|
3553
|
+
.plinth-pill {
|
|
3554
|
+
border-color: ButtonText;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) {
|
|
3558
|
+
color: HighlightText;
|
|
3559
|
+
background-color: Highlight;
|
|
3560
|
+
border-color: Highlight;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
.plinth-pill:is([aria-pressed='true'], [aria-current='page']) .plinth-pill-count {
|
|
3564
|
+
color: HighlightText;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
.plinth-pill:disabled,
|
|
3568
|
+
.plinth-pill[aria-disabled='true'] {
|
|
3569
|
+
color: GrayText;
|
|
3570
|
+
background-color: Canvas;
|
|
3571
|
+
border-color: GrayText;
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
/* section-menu.css */
|
|
3577
|
+
/*
|
|
3578
|
+
* Section menu: the row under the page header that switches between the sections of a page.
|
|
3579
|
+
*
|
|
3580
|
+
* <nav class="plinth-section-menu" aria-label="Project">
|
|
3581
|
+
* <a class="plinth-section-menu-item" href="/overview" aria-current="page">
|
|
3582
|
+
* <span class="plinth-section-menu-label">Overview</span>
|
|
3583
|
+
* </a>
|
|
3584
|
+
* <a class="plinth-section-menu-item" href="/tasks">
|
|
3585
|
+
* <span class="plinth-section-menu-label">Tasks</span>
|
|
3586
|
+
* <span class="plinth-section-menu-count">8</span>
|
|
3587
|
+
* </a>
|
|
3588
|
+
* </nav>
|
|
3589
|
+
*
|
|
3590
|
+
* The root is a <nav>, named by aria-label (label in Blade and Vue) or by the app's own
|
|
3591
|
+
* aria-labelledby, so it reads apart from the page's other navigation. Its items are links. A
|
|
3592
|
+
* menu that switches views in place, without navigating, has buttons instead, and its root is
|
|
3593
|
+
* <div class="plinth-section-menu" role="group"> (nav set to false in Blade and Vue):
|
|
3594
|
+
*
|
|
3595
|
+
* <button type="button" class="plinth-section-menu-item" aria-pressed="true">…</button>
|
|
3596
|
+
*
|
|
3597
|
+
* Each item is its own component (<x-plinth::section-menu-item>, PlinthSectionMenuItem), so the
|
|
3598
|
+
* app's attributes and listeners land on the item (ADR 2). The count is optional.
|
|
3599
|
+
* plinth-section-menu-label has no rule of its own: it is a hook for a project's styles.
|
|
3600
|
+
*
|
|
3601
|
+
* States, set by the app:
|
|
3602
|
+
* - current: a link prints aria-current="page" for the section being shown; a button always
|
|
3603
|
+
* prints aria-pressed, "true" or "false". The current item takes the selection tint
|
|
3604
|
+
* (accent-subtle, accent-text) inside a 2px accent edge (border-width.thick), so colour is not
|
|
3605
|
+
* its only cue; the others are muted text on no surface, like a ghost button. A pressed button
|
|
3606
|
+
* still answers hover and press with a step on the tint (accent-subtle-hover,
|
|
3607
|
+
* accent-subtle-active).
|
|
3608
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
3609
|
+
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
3610
|
+
*
|
|
3611
|
+
* Layout: a row --plinth-section-menu-height (56px) tall, on the surface with a divider below,
|
|
3612
|
+
* the same inline padding as the page header and clear of the notch on a phone held sideways. Its
|
|
3613
|
+
* items are --plinth-section-menu-item-height (40px) tall, never less than the 40px touch
|
|
3614
|
+
* target, and at least 40px wide. The row never wraps: when the items don't fit it scrolls
|
|
3615
|
+
* sideways and snaps to them, with a thin scrollbar for a mouse and none on a touch screen; the
|
|
3616
|
+
* row's block padding keeps the focus ring inside it. The app scrolls a current item that starts
|
|
3617
|
+
* out of view into view.
|
|
3618
|
+
* Placement is the app's: put it under the page header, or make it sticky below it with
|
|
3619
|
+
* position: sticky, inset-block-start: calc(var(--plinth-page-header-offset) +
|
|
3620
|
+
* var(--plinth-page-header-height)), plus env(safe-area-inset-top) under the screen's top bar,
|
|
3621
|
+
* and z-index: var(--plinth-layer-sticky).
|
|
3622
|
+
*/
|
|
3623
|
+
|
|
3624
|
+
@layer plinth {
|
|
3625
|
+
.plinth-section-menu {
|
|
3626
|
+
box-sizing: border-box;
|
|
3627
|
+
display: flex;
|
|
3628
|
+
gap: var(--plinth-space-1);
|
|
3629
|
+
align-items: center;
|
|
3630
|
+
min-block-size: var(--plinth-section-menu-height);
|
|
3631
|
+
|
|
3632
|
+
/* What the item and the divider leave of the row's height, so the row is exactly its token. */
|
|
3633
|
+
padding-block: calc(
|
|
3634
|
+
(
|
|
3635
|
+
var(--plinth-section-menu-height) - var(--plinth-section-menu-item-height) -
|
|
3636
|
+
var(--plinth-border-width-thin)
|
|
3637
|
+
) /
|
|
3638
|
+
2
|
|
3639
|
+
);
|
|
3640
|
+
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
|
|
3641
|
+
max(var(--plinth-space-2), env(safe-area-inset-right));
|
|
3642
|
+
margin: 0;
|
|
3643
|
+
overflow-x: auto;
|
|
3644
|
+
overscroll-behavior-inline: contain;
|
|
3645
|
+
font-family: var(--plinth-font-family-body);
|
|
3646
|
+
scroll-snap-type: inline proximity;
|
|
3647
|
+
scroll-padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
|
|
3648
|
+
max(var(--plinth-space-2), env(safe-area-inset-right));
|
|
3649
|
+
scrollbar-width: none;
|
|
3650
|
+
background-color: var(--plinth-color-surface);
|
|
3651
|
+
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
.plinth-section-menu:dir(rtl) {
|
|
3655
|
+
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
|
|
3656
|
+
max(var(--plinth-space-2), env(safe-area-inset-left));
|
|
3657
|
+
scroll-padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
|
|
3658
|
+
max(var(--plinth-space-2), env(safe-area-inset-left));
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
/* A mouse without a sideways wheel needs a scrollbar to reach the items that don't fit. */
|
|
3662
|
+
@media (hover: hover) and (pointer: fine) {
|
|
3663
|
+
.plinth-section-menu {
|
|
3664
|
+
scrollbar-width: thin;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
.plinth-section-menu-item {
|
|
3669
|
+
box-sizing: border-box;
|
|
3670
|
+
display: inline-flex;
|
|
3671
|
+
flex: none;
|
|
3672
|
+
gap: var(--plinth-space-2);
|
|
3673
|
+
align-items: center;
|
|
3674
|
+
justify-content: center;
|
|
3675
|
+
min-inline-size: var(--plinth-touch-target);
|
|
3676
|
+
min-block-size: max(var(--plinth-section-menu-item-height), var(--plinth-touch-target));
|
|
3677
|
+
padding-block: 0;
|
|
3678
|
+
padding-inline: var(--plinth-space-3);
|
|
3679
|
+
margin: 0;
|
|
3680
|
+
font-family: inherit;
|
|
3681
|
+
font-size: var(--plinth-font-size-md);
|
|
3682
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
3683
|
+
line-height: var(--plinth-font-leading-tight);
|
|
3684
|
+
color: var(--plinth-color-text-muted);
|
|
3685
|
+
white-space: nowrap;
|
|
3686
|
+
text-decoration: none;
|
|
3687
|
+
cursor: pointer;
|
|
3688
|
+
user-select: none;
|
|
3689
|
+
scroll-snap-align: start;
|
|
3690
|
+
background-color: transparent;
|
|
3691
|
+
border: var(--plinth-border-width-thin) solid transparent;
|
|
3692
|
+
border-radius: var(--plinth-radius-control);
|
|
3693
|
+
transition:
|
|
3694
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
3695
|
+
border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
3696
|
+
box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
3697
|
+
color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
@media (hover: hover) {
|
|
3701
|
+
a.plinth-section-menu-item[href]:not([aria-current='page']):hover,
|
|
3702
|
+
button.plinth-section-menu-item:enabled:not([aria-pressed='true']):hover {
|
|
3703
|
+
color: var(--plinth-color-text);
|
|
3704
|
+
background-color: var(--plinth-color-surface-hover);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
a.plinth-section-menu-item[href]:not([aria-current='page']):active,
|
|
3709
|
+
button.plinth-section-menu-item:enabled:not([aria-pressed='true']):active {
|
|
3710
|
+
color: var(--plinth-color-text);
|
|
3711
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
/* The border and an inset ring make the 2px edge, so selecting doesn't move the label. */
|
|
3715
|
+
.plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']) {
|
|
3716
|
+
color: var(--plinth-color-accent-text);
|
|
3717
|
+
background-color: var(--plinth-color-accent-subtle);
|
|
3718
|
+
border-color: var(--plinth-color-accent);
|
|
3719
|
+
box-shadow: inset 0 0 0 calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
|
|
3720
|
+
var(--plinth-color-accent);
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
/*
|
|
3724
|
+
* A pressed button can be pressed again, so it answers hover and press with a step on the tint.
|
|
3725
|
+
* Not in forced colours, where the tint would replace the system highlight under the label.
|
|
3726
|
+
*/
|
|
3727
|
+
@media (hover: hover) and (forced-colors: none) {
|
|
3728
|
+
button.plinth-section-menu-item:enabled[aria-pressed='true']:hover {
|
|
3729
|
+
background-color: var(--plinth-color-accent-subtle-hover);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
@media (forced-colors: none) {
|
|
3734
|
+
button.plinth-section-menu-item:enabled[aria-pressed='true']:active {
|
|
3735
|
+
background-color: var(--plinth-color-accent-subtle-active);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
.plinth-section-menu-item:disabled,
|
|
3740
|
+
.plinth-section-menu-item[aria-disabled='true'] {
|
|
3741
|
+
cursor: not-allowed;
|
|
3742
|
+
opacity: var(--plinth-opacity-disabled);
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
.plinth-section-menu-item:focus-visible {
|
|
3746
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
3747
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
/* A gap between the ring and the accent edge, so focus and selection read apart. */
|
|
3751
|
+
.plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']):focus-visible {
|
|
3752
|
+
outline-offset: var(--plinth-focus-ring-offset-filled);
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
.plinth-section-menu-count {
|
|
3756
|
+
font-family: var(--plinth-font-family-mono);
|
|
3757
|
+
font-size: var(--plinth-font-size-xs);
|
|
3758
|
+
font-weight: var(--plinth-font-weight-regular);
|
|
3759
|
+
font-variant-numeric: tabular-nums;
|
|
3760
|
+
color: var(--plinth-color-text-muted);
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
.plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page'])
|
|
3764
|
+
.plinth-section-menu-count {
|
|
3765
|
+
color: inherit;
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
@media (min-width: 768px) {
|
|
3769
|
+
.plinth-section-menu {
|
|
3770
|
+
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
|
|
3771
|
+
max(var(--plinth-space-4), env(safe-area-inset-right));
|
|
3772
|
+
scroll-padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
|
|
3773
|
+
max(var(--plinth-space-4), env(safe-area-inset-right));
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
.plinth-section-menu:dir(rtl) {
|
|
3777
|
+
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
|
|
3778
|
+
max(var(--plinth-space-4), env(safe-area-inset-left));
|
|
3779
|
+
scroll-padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
|
|
3780
|
+
max(var(--plinth-space-4), env(safe-area-inset-left));
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
/* The current item is only a surface tint: in forced colours it takes the system highlight. */
|
|
3785
|
+
@media (forced-colors: active) {
|
|
3786
|
+
.plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']) {
|
|
3787
|
+
color: HighlightText;
|
|
3788
|
+
background-color: Highlight;
|
|
3789
|
+
border-color: Highlight;
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
.plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page'])
|
|
3793
|
+
.plinth-section-menu-count {
|
|
3794
|
+
color: HighlightText;
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
.plinth-section-menu-item:disabled,
|
|
3798
|
+
.plinth-section-menu-item[aria-disabled='true'] {
|
|
3799
|
+
color: GrayText;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
/* sheet.css */
|
|
3805
|
+
/*
|
|
3806
|
+
* Sheet: a modal panel on the edge of the screen the developer picks, for filters, side
|
|
3807
|
+
* information or navigation. Optionally on another edge below 768px.
|
|
3808
|
+
*
|
|
3809
|
+
* <dialog id="filters" class="plinth-sheet" data-side="end" data-side-phone="bottom"
|
|
3810
|
+
* closedby="any" aria-labelledby="filters-title" aria-describedby="filters-description">
|
|
3811
|
+
* <div class="plinth-sheet-panel">
|
|
3812
|
+
* <div class="plinth-sheet-handle" aria-hidden="true"></div>
|
|
3813
|
+
* <div class="plinth-sheet-header">
|
|
3814
|
+
* <h2 id="filters-title" class="plinth-sheet-title">Filters</h2>
|
|
3815
|
+
* <p id="filters-description" class="plinth-sheet-description">Narrow the list down.</p>
|
|
3816
|
+
* <button type="button" class="plinth-button plinth-icon-button plinth-sheet-close"
|
|
3817
|
+
* data-variant="ghost" data-size="md" aria-label="Close" commandfor="filters"
|
|
3818
|
+
* command="close">
|
|
3819
|
+
* <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
|
|
3820
|
+
* </button>
|
|
3821
|
+
* </div>
|
|
3822
|
+
* <div class="plinth-sheet-body">…</div>
|
|
3823
|
+
* <div class="plinth-sheet-footer">…</div>
|
|
3824
|
+
* </div>
|
|
3825
|
+
* </dialog>
|
|
3826
|
+
*
|
|
3827
|
+
* The root is the <dialog>, and it needs an id: commandfor and aria-labelledby point at it, and
|
|
3828
|
+
* the title's and description's ids are the root's id with -title and -description. The panel is
|
|
3829
|
+
* the root's only child and holds the parts. The handle is there only where the sheet's side on a
|
|
3830
|
+
* phone is the bottom (data-side="bottom" with no data-side-phone, or data-side-phone="bottom"):
|
|
3831
|
+
* decorative (aria-hidden), shown below 768px, where the sheet is dragged down to close
|
|
3832
|
+
* (modal.css). The title is
|
|
3833
|
+
* required; the description (and its aria-describedby) and the footer are optional. The close
|
|
3834
|
+
* button is an icon button (button.css and icon.css) and is always there: closedby="any" (tap the
|
|
3835
|
+
* backdrop) is an enhancement Safari doesn't have yet. A project may set closedby="closerequest"
|
|
3836
|
+
* on a sheet that must not close on a stray tap.
|
|
3837
|
+
*
|
|
3838
|
+
* Open it with showModal() or a <button commandfor="filters" command="show-modal">. Open is the
|
|
3839
|
+
* native state, [open]; the browser puts the sheet in the top layer, makes the page inert, traps
|
|
3840
|
+
* focus, closes on Escape and returns focus. The dialog fires close when it closes. For a modal
|
|
3841
|
+
* centred on the screen, use the dialog (dialog.css).
|
|
3842
|
+
*
|
|
3843
|
+
* Side (data-side), at every width: end (default, also when missing or unknown) and start are
|
|
3844
|
+
* full-height panels on the inline-end and inline-start edges, sheet.side-width wide and at
|
|
3845
|
+
* most the screen's width less a gap, so the page shows beside them; top and bottom are the
|
|
3846
|
+
* screen's width on the top and bottom edges, as tall as their content up to the screen's height
|
|
3847
|
+
* less a gap that also clears the safe area at the other edge. The sides are logical: in
|
|
3848
|
+
* right-to-left text end is the left edge and start the right.
|
|
3849
|
+
*
|
|
3850
|
+
* Side on a phone (data-side-phone, optional): the side below 768px, one of the same four; from
|
|
3851
|
+
* 768px up data-side holds. data-side="end" data-side-phone="bottom" is a side panel on a tablet
|
|
3852
|
+
* or desktop and a bottom sheet on a phone. Leave it out to keep data-side at every width. It
|
|
3853
|
+
* takes only those four values: any other makes the phone's side end (the layers never print
|
|
3854
|
+
* one, and leave the attribute out instead).
|
|
3855
|
+
*
|
|
3856
|
+
* The edge the sheet sits on has no border or radius; the edge facing the page has a hairline
|
|
3857
|
+
* (in forced colours too), and a top or bottom sheet rounds the corners facing the page. Each
|
|
3858
|
+
* side clears the safe areas it touches (the notch, the status bar and the home indicator, with
|
|
3859
|
+
* viewport-fit=cover).
|
|
3860
|
+
*
|
|
3861
|
+
* The description is a secondary line, at the size of a list row's description.
|
|
3862
|
+
*
|
|
3863
|
+
* The surface, the backdrop, the scroll lock, the motion, the safe areas and the parts come from
|
|
3864
|
+
* the modal base (modal.css), which the dialog shares; load it with this file. The body scrolls,
|
|
3865
|
+
* the header and footer stay put, and the page under an open sheet doesn't scroll. The sheet
|
|
3866
|
+
* slides in from its edge and back out on the motion tokens, which drop to 0ms under
|
|
3867
|
+
* prefers-reduced-motion. This file sets only where the sheet sits and how big it is.
|
|
3868
|
+
*
|
|
3869
|
+
* A sheet on the bottom edge has its height capped by 100dvh, which clears the home indicator
|
|
3870
|
+
* (env(safe-area-inset-bottom)) but not, on its own, the on-screen keyboard: iOS lays the
|
|
3871
|
+
* keyboard over the page. A page with a form in a sheet sets
|
|
3872
|
+
* <meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
|
|
3873
|
+
* where the browser supports it, and keeps the sheet's fields near its top.
|
|
3874
|
+
*
|
|
3875
|
+
* In Livewire, a re-render would morph the open dialog back to the server's markup and drop its
|
|
3876
|
+
* open attribute: put wire:ignore.self on the sheet (or keep it outside the component that
|
|
3877
|
+
* re-renders), and always pass an id, since a generated one changes on every render.
|
|
3878
|
+
*/
|
|
3879
|
+
|
|
3880
|
+
@layer plinth {
|
|
3881
|
+
/*
|
|
3882
|
+
* The end side, the default. The sides are written twice: as the phone's side (data-side-phone,
|
|
3883
|
+
* or data-side without it), and from 768px up as data-side again for a sheet with
|
|
3884
|
+
* data-side-phone. The phone's rules use :where() so the later ones win on order alone.
|
|
3885
|
+
*/
|
|
3886
|
+
.plinth-sheet {
|
|
3887
|
+
/* Translate isn't logical: -1 (under :dir(rtl), below) turns the slide around. */
|
|
3888
|
+
--plinth-sheet-direction: 1;
|
|
3889
|
+
--plinth-sheet-safe-area-inline-start: env(safe-area-inset-left);
|
|
3890
|
+
--plinth-sheet-safe-area-inline-end: env(safe-area-inset-right);
|
|
3891
|
+
--plinth-modal-offset: calc(100% * var(--plinth-sheet-direction)) 0;
|
|
3892
|
+
--plinth-modal-safe-area-end: env(safe-area-inset-bottom);
|
|
3893
|
+
|
|
3894
|
+
inset-block: 0;
|
|
3895
|
+
inset-inline: auto 0;
|
|
3896
|
+
inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
|
|
3897
|
+
block-size: auto;
|
|
3898
|
+
max-block-size: none;
|
|
3899
|
+
padding-block: env(safe-area-inset-top) 0;
|
|
3900
|
+
padding-inline: 0 var(--plinth-sheet-safe-area-inline-end);
|
|
3901
|
+
margin: 0;
|
|
3902
|
+
border-width: 0;
|
|
3903
|
+
border-inline-start-width: var(--plinth-border-width-thin);
|
|
3904
|
+
border-radius: 0;
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
.plinth-sheet:where([data-side='start']:not([data-side-phone]), [data-side-phone='start']) {
|
|
3908
|
+
--plinth-modal-offset: calc(-100% * var(--plinth-sheet-direction)) 0;
|
|
3909
|
+
|
|
3910
|
+
inset-block: 0;
|
|
3911
|
+
inset-inline: 0 auto;
|
|
3912
|
+
inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
|
|
3913
|
+
max-block-size: none;
|
|
3914
|
+
padding-block: env(safe-area-inset-top) 0;
|
|
3915
|
+
padding-inline: var(--plinth-sheet-safe-area-inline-start) 0;
|
|
3916
|
+
border-width: 0;
|
|
3917
|
+
border-inline-end-width: var(--plinth-border-width-thin);
|
|
3918
|
+
border-radius: 0;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
.plinth-sheet:where([data-side='top']:not([data-side-phone]), [data-side-phone='top']) {
|
|
3922
|
+
--plinth-modal-offset: 0 -100%;
|
|
3923
|
+
--plinth-modal-safe-area-end: initial;
|
|
3924
|
+
|
|
3925
|
+
inset-block: 0 auto;
|
|
3926
|
+
inset-inline: 0;
|
|
3927
|
+
inline-size: 100%;
|
|
3928
|
+
max-block-size: calc(
|
|
3929
|
+
100dvh - max(var(--plinth-space-12), env(safe-area-inset-bottom) + var(--plinth-space-4))
|
|
3930
|
+
);
|
|
3931
|
+
padding-block: env(safe-area-inset-top) 0;
|
|
3932
|
+
padding-inline: var(--plinth-sheet-safe-area-inline-start)
|
|
3933
|
+
var(--plinth-sheet-safe-area-inline-end);
|
|
3934
|
+
border-width: 0;
|
|
3935
|
+
border-block-end-width: var(--plinth-border-width-thin);
|
|
3936
|
+
border-radius: 0;
|
|
3937
|
+
border-end-start-radius: var(--plinth-radius-container);
|
|
3938
|
+
border-end-end-radius: var(--plinth-radius-container);
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
.plinth-sheet:where([data-side='bottom']:not([data-side-phone]), [data-side-phone='bottom']) {
|
|
3942
|
+
--plinth-modal-offset: 0 100%;
|
|
3943
|
+
|
|
3944
|
+
inset-block: auto 0;
|
|
3945
|
+
inset-inline: 0;
|
|
3946
|
+
inline-size: 100%;
|
|
3947
|
+
max-block-size: calc(
|
|
3948
|
+
100dvh - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
|
|
3949
|
+
);
|
|
3950
|
+
padding-block: 0;
|
|
3951
|
+
padding-inline: var(--plinth-sheet-safe-area-inline-start)
|
|
3952
|
+
var(--plinth-sheet-safe-area-inline-end);
|
|
3953
|
+
border-width: 0;
|
|
3954
|
+
border-block-start-width: var(--plinth-border-width-thin);
|
|
3955
|
+
border-radius: 0;
|
|
3956
|
+
border-start-start-radius: var(--plinth-radius-container);
|
|
3957
|
+
border-start-end-radius: var(--plinth-radius-container);
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
/*
|
|
3961
|
+
* With its grab handle, a bottom sheet on a phone is dragged down to close (modal.css): the root
|
|
3962
|
+
* fills the screen, see-through, and its panel is the surface, as tall as its content up to the
|
|
3963
|
+
* screen's height less a gap.
|
|
3964
|
+
*/
|
|
3965
|
+
.plinth-sheet:where([data-side='bottom']:not([data-side-phone]), [data-side-phone='bottom']):has(
|
|
3966
|
+
> * > .plinth-sheet-handle
|
|
3967
|
+
) {
|
|
3968
|
+
inset: 0;
|
|
3969
|
+
max-block-size: none;
|
|
3970
|
+
padding: 0;
|
|
3971
|
+
border-width: 0;
|
|
3972
|
+
border-radius: 0;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
/* Right-to-left text turns the slide around and swaps the safe areas. */
|
|
3976
|
+
.plinth-sheet:dir(rtl) {
|
|
3977
|
+
--plinth-sheet-direction: -1;
|
|
3978
|
+
--plinth-sheet-safe-area-inline-start: env(safe-area-inset-right);
|
|
3979
|
+
--plinth-sheet-safe-area-inline-end: env(safe-area-inset-left);
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
@media (min-width: 768px) {
|
|
3983
|
+
.plinth-sheet[data-side-phone] {
|
|
3984
|
+
--plinth-modal-offset: calc(100% * var(--plinth-sheet-direction)) 0;
|
|
3985
|
+
--plinth-modal-safe-area-end: env(safe-area-inset-bottom);
|
|
3986
|
+
|
|
3987
|
+
inset-block: 0;
|
|
3988
|
+
inset-inline: auto 0;
|
|
3989
|
+
inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
|
|
3990
|
+
max-block-size: none;
|
|
3991
|
+
padding-block: env(safe-area-inset-top) 0;
|
|
3992
|
+
padding-inline: 0 var(--plinth-sheet-safe-area-inline-end);
|
|
3993
|
+
border-width: 0;
|
|
3994
|
+
border-inline-start-width: var(--plinth-border-width-thin);
|
|
3995
|
+
border-radius: 0;
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
.plinth-sheet[data-side-phone][data-side='start'] {
|
|
3999
|
+
--plinth-modal-offset: calc(-100% * var(--plinth-sheet-direction)) 0;
|
|
4000
|
+
|
|
4001
|
+
inset-inline: 0 auto;
|
|
4002
|
+
padding-inline: var(--plinth-sheet-safe-area-inline-start) 0;
|
|
4003
|
+
border-inline-start-width: 0;
|
|
4004
|
+
border-inline-end-width: var(--plinth-border-width-thin);
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
.plinth-sheet[data-side-phone][data-side='top'] {
|
|
4008
|
+
--plinth-modal-offset: 0 -100%;
|
|
4009
|
+
--plinth-modal-safe-area-end: initial;
|
|
4010
|
+
|
|
4011
|
+
inset-block: 0 auto;
|
|
4012
|
+
inset-inline: 0;
|
|
4013
|
+
inline-size: 100%;
|
|
4014
|
+
max-block-size: calc(
|
|
4015
|
+
100dvh - max(var(--plinth-space-12), env(safe-area-inset-bottom) + var(--plinth-space-4))
|
|
4016
|
+
);
|
|
4017
|
+
padding-inline: var(--plinth-sheet-safe-area-inline-start)
|
|
4018
|
+
var(--plinth-sheet-safe-area-inline-end);
|
|
4019
|
+
border-block-end-width: var(--plinth-border-width-thin);
|
|
4020
|
+
border-inline-start-width: 0;
|
|
4021
|
+
border-end-start-radius: var(--plinth-radius-container);
|
|
4022
|
+
border-end-end-radius: var(--plinth-radius-container);
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
.plinth-sheet[data-side='bottom'] {
|
|
4026
|
+
--plinth-modal-offset: 0 100%;
|
|
4027
|
+
|
|
4028
|
+
inset-block: auto 0;
|
|
4029
|
+
inset-inline: 0;
|
|
4030
|
+
inline-size: 100%;
|
|
4031
|
+
max-block-size: calc(
|
|
4032
|
+
100dvh - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
|
|
4033
|
+
);
|
|
4034
|
+
padding-block: 0;
|
|
4035
|
+
padding-inline: var(--plinth-sheet-safe-area-inline-start)
|
|
4036
|
+
var(--plinth-sheet-safe-area-inline-end);
|
|
4037
|
+
border-block-start-width: var(--plinth-border-width-thin);
|
|
4038
|
+
border-inline-start-width: 0;
|
|
4039
|
+
border-start-start-radius: var(--plinth-radius-container);
|
|
4040
|
+
border-start-end-radius: var(--plinth-radius-container);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
/* text-field.css */
|
|
4046
|
+
/*
|
|
4047
|
+
* Text field: one line of text, with its label, an optional description and an error.
|
|
4048
|
+
*
|
|
4049
|
+
* <div class="plinth-text-field" data-size="md">
|
|
4050
|
+
* <label class="plinth-text-field-label" for="email">Email</label>
|
|
4051
|
+
* <div class="plinth-text-field-control">
|
|
4052
|
+
* <span class="plinth-icon" data-size="sm" aria-hidden="true"><svg>…</svg></span>
|
|
4053
|
+
* <input type="email" class="plinth-text-field-input" id="email"
|
|
4054
|
+
* aria-describedby="email-description email-error" aria-invalid="true">
|
|
4055
|
+
* <span class="plinth-text-field-trailing">…</span>
|
|
4056
|
+
* </div>
|
|
4057
|
+
* <p class="plinth-text-field-description" id="email-description">For receipts only.</p>
|
|
4058
|
+
* <p class="plinth-text-field-error" id="email-error">Enter an email address.</p>
|
|
4059
|
+
* </div>
|
|
4060
|
+
*
|
|
4061
|
+
* The input needs an id: the label points at it with for, and the description's and error's ids
|
|
4062
|
+
* are the input's id with -description and -error. The icon, the trailing slot, the description
|
|
4063
|
+
* and the error are optional; aria-describedby lists the description, then the error, and is left
|
|
4064
|
+
* out when there is neither.
|
|
4065
|
+
*
|
|
4066
|
+
* Attributes go to the input, not the root (name, value, placeholder, autocomplete, required,
|
|
4067
|
+
* wire:model, x-model, data-*, aria-*), so the app talks to the input; only a project's class is
|
|
4068
|
+
* added to the root's.
|
|
4069
|
+
*
|
|
4070
|
+
* - type (on the input): text by default; email, url, tel, password, number or search. A search
|
|
4071
|
+
* field takes a search icon and, in the trailing slot, a clear button (a small ghost icon
|
|
4072
|
+
* button) that the app shows when there is text and wires up. With a trailing slot, the
|
|
4073
|
+
* browser's own clear button is hidden, so there is only one.
|
|
4074
|
+
* - the leading icon is 16px and decorative (aria-hidden); the label names the field.
|
|
4075
|
+
* - the trailing slot holds one small control or hint, in the room the field keeps at its end
|
|
4076
|
+
* (--plinth-text-field-trailing-size, 32px): a small icon button, or a keyboard hint as <kbd>.
|
|
4077
|
+
* - data-label="hidden" on the root hides the label visually; screen readers still read it. Use it
|
|
4078
|
+
* only where the field's purpose is plain without it, such as a search box with its icon.
|
|
4079
|
+
*
|
|
4080
|
+
* Sizes (data-size): md 32px (default, also when missing) and sm 28px. On a touch screen both are
|
|
4081
|
+
* 40px tall. The text is 16px on a phone, so iOS doesn't zoom into the field, and from 768px
|
|
4082
|
+
* up it is 14px (md) or 13px (sm).
|
|
4083
|
+
*
|
|
4084
|
+
* States:
|
|
4085
|
+
* - error: aria-invalid="true" on the input and the error message under it; the edge turns
|
|
4086
|
+
* danger and thick, so it isn't only a colour, and the message says what to fix.
|
|
4087
|
+
* - disabled: `disabled` on the input; the field is faded and can't be focused.
|
|
4088
|
+
* - read-only: `readonly` on the input; it keeps its text and focus, without the field's fill
|
|
4089
|
+
* and with a dashed edge.
|
|
4090
|
+
* - the focus ring on the input's :focus-visible.
|
|
4091
|
+
*/
|
|
4092
|
+
|
|
4093
|
+
@layer plinth {
|
|
4094
|
+
.plinth-text-field {
|
|
4095
|
+
--plinth-text-field-height: var(--plinth-control-md);
|
|
4096
|
+
--plinth-text-field-text: var(--plinth-font-size-md);
|
|
4097
|
+
|
|
4098
|
+
display: grid;
|
|
4099
|
+
gap: var(--plinth-space-2);
|
|
4100
|
+
min-inline-size: 0;
|
|
4101
|
+
font-family: var(--plinth-font-family-body);
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
.plinth-text-field[data-size='sm'] {
|
|
4105
|
+
--plinth-text-field-height: var(--plinth-control-sm);
|
|
4106
|
+
--plinth-text-field-text: var(--plinth-font-size-sm);
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
.plinth-text-field-label {
|
|
4110
|
+
font-size: var(--plinth-font-size-sm);
|
|
4111
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
4112
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4113
|
+
color: var(--plinth-color-text-muted);
|
|
4114
|
+
overflow-wrap: anywhere;
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
/* Hidden from sight, still read by screen readers. */
|
|
4118
|
+
.plinth-text-field[data-label='hidden'] .plinth-text-field-label {
|
|
4119
|
+
position: absolute;
|
|
4120
|
+
inline-size: 1px;
|
|
4121
|
+
block-size: 1px;
|
|
4122
|
+
overflow: hidden;
|
|
4123
|
+
white-space: nowrap;
|
|
4124
|
+
clip-path: inset(50%);
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
.plinth-text-field-control {
|
|
4128
|
+
position: relative;
|
|
4129
|
+
display: flex;
|
|
4130
|
+
align-items: center;
|
|
4131
|
+
min-inline-size: 0;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
.plinth-text-field-input {
|
|
4135
|
+
box-sizing: border-box;
|
|
4136
|
+
flex: 1;
|
|
4137
|
+
min-inline-size: 0;
|
|
4138
|
+
min-block-size: var(--plinth-text-field-height);
|
|
4139
|
+
padding-block: 0;
|
|
4140
|
+
padding-inline: var(--plinth-space-3);
|
|
4141
|
+
margin: 0;
|
|
4142
|
+
font-family: inherit;
|
|
4143
|
+
font-size: var(--plinth-font-size-field);
|
|
4144
|
+
line-height: var(--plinth-font-leading-tight);
|
|
4145
|
+
color: var(--plinth-color-text);
|
|
4146
|
+
appearance: none;
|
|
4147
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
4148
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
4149
|
+
border-radius: var(--plinth-radius-control);
|
|
4150
|
+
transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
.plinth-text-field-input::placeholder {
|
|
4154
|
+
color: var(--plinth-color-text-subtle);
|
|
4155
|
+
opacity: 1;
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
@media (hover: hover) {
|
|
4159
|
+
.plinth-text-field-input:hover:not(:disabled, :read-only, [aria-invalid='true']) {
|
|
4160
|
+
border-color: var(--plinth-color-text-subtle);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
.plinth-text-field-input:focus-visible {
|
|
4165
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
4166
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
.plinth-text-field-input[aria-invalid='true'] {
|
|
4170
|
+
border-color: var(--plinth-color-danger);
|
|
4171
|
+
border-width: var(--plinth-border-width-thick);
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
.plinth-text-field-input:disabled {
|
|
4175
|
+
cursor: not-allowed;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
/* Without the fill, and with a dashed edge, so it doesn't read as a field to type in. */
|
|
4179
|
+
.plinth-text-field-input:read-only:not(:disabled) {
|
|
4180
|
+
background-color: transparent;
|
|
4181
|
+
border-style: dashed;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
.plinth-text-field-control:has(.plinth-text-field-input:disabled) {
|
|
4185
|
+
opacity: var(--plinth-opacity-disabled);
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
/* The leading icon sits over the input's start, which keeps room for it. */
|
|
4189
|
+
.plinth-text-field-control > .plinth-icon {
|
|
4190
|
+
position: absolute;
|
|
4191
|
+
inset-inline-start: var(--plinth-space-3);
|
|
4192
|
+
color: var(--plinth-color-text-subtle);
|
|
4193
|
+
pointer-events: none;
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
.plinth-text-field-control:has(> .plinth-icon) > .plinth-text-field-input {
|
|
4197
|
+
padding-inline-start: calc(
|
|
4198
|
+
var(--plinth-space-3) + var(--plinth-size-icon-sm) + var(--plinth-space-2)
|
|
4199
|
+
);
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
.plinth-text-field-trailing {
|
|
4203
|
+
position: absolute;
|
|
4204
|
+
inset-inline-end: var(--plinth-space-1);
|
|
4205
|
+
display: flex;
|
|
4206
|
+
gap: var(--plinth-space-1);
|
|
4207
|
+
align-items: center;
|
|
4208
|
+
justify-content: center;
|
|
4209
|
+
min-inline-size: var(--plinth-text-field-trailing-size);
|
|
4210
|
+
color: var(--plinth-color-text-subtle);
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
.plinth-text-field-control:has(> .plinth-text-field-trailing) > .plinth-text-field-input {
|
|
4214
|
+
padding-inline-end: calc(var(--plinth-space-1) + var(--plinth-text-field-trailing-size));
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
/* With its own clear button in the trailing slot, the browser's is one too many. */
|
|
4218
|
+
.plinth-text-field-control:has(> .plinth-text-field-trailing)
|
|
4219
|
+
> .plinth-text-field-input::-webkit-search-cancel-button {
|
|
4220
|
+
display: none;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
.plinth-text-field-trailing kbd {
|
|
4224
|
+
box-sizing: border-box;
|
|
4225
|
+
min-inline-size: var(--plinth-size-icon-md);
|
|
4226
|
+
padding-block: 0;
|
|
4227
|
+
padding-inline: var(--plinth-space-1);
|
|
4228
|
+
font-family: var(--plinth-font-family-mono);
|
|
4229
|
+
font-size: var(--plinth-font-size-xs);
|
|
4230
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4231
|
+
color: var(--plinth-color-text-muted);
|
|
4232
|
+
text-align: center;
|
|
4233
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
4234
|
+
border-radius: var(--plinth-radius-control);
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
.plinth-text-field-description,
|
|
4238
|
+
.plinth-text-field-error {
|
|
4239
|
+
margin: 0;
|
|
4240
|
+
font-size: var(--plinth-font-size-sm);
|
|
4241
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4242
|
+
overflow-wrap: anywhere;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
.plinth-text-field-description {
|
|
4246
|
+
color: var(--plinth-color-text-muted);
|
|
4247
|
+
}
|
|
4248
|
+
|
|
4249
|
+
.plinth-text-field-error {
|
|
4250
|
+
color: var(--plinth-color-danger-text);
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
@media (pointer: coarse) {
|
|
4254
|
+
.plinth-text-field-input {
|
|
4255
|
+
min-block-size: var(--plinth-touch-target);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
@media (min-width: 768px) {
|
|
4260
|
+
.plinth-text-field-input {
|
|
4261
|
+
font-size: var(--plinth-text-field-text);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
@media (forced-colors: active) {
|
|
4266
|
+
.plinth-text-field-control:has(.plinth-text-field-input:disabled) {
|
|
4267
|
+
color: GrayText;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
.plinth-text-field-input:disabled {
|
|
4271
|
+
color: GrayText;
|
|
4272
|
+
border-color: GrayText;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
/* textarea.css */
|
|
4278
|
+
/*
|
|
4279
|
+
* Textarea: several lines of text, with its label, an optional description and an error. With
|
|
4280
|
+
* grow, it grows with its content.
|
|
4281
|
+
*
|
|
4282
|
+
* <div class="plinth-textarea">
|
|
4283
|
+
* <label class="plinth-textarea-label" for="reply">Reply</label>
|
|
4284
|
+
* <textarea class="plinth-textarea-input" id="reply" rows="3"
|
|
4285
|
+
* aria-describedby="reply-description reply-error" aria-invalid="true"></textarea>
|
|
4286
|
+
* <p class="plinth-textarea-description" id="reply-description">The customer sees it.</p>
|
|
4287
|
+
* <p class="plinth-textarea-error" id="reply-error">Write a reply first.</p>
|
|
4288
|
+
* </div>
|
|
4289
|
+
*
|
|
4290
|
+
* The textarea needs an id: the label points at it with for, and the description's and error's
|
|
4291
|
+
* ids are the textarea's id with -description and -error. The description and the error are
|
|
4292
|
+
* optional; aria-describedby lists the description, then the error, and is left out when there is
|
|
4293
|
+
* neither. rows is 3 by default.
|
|
4294
|
+
*
|
|
4295
|
+
* Attributes go to the textarea, not the root (name, placeholder, maxlength, required,
|
|
4296
|
+
* wire:model, x-model, data-*, aria-*), so the app talks to the textarea; only a project's class
|
|
4297
|
+
* is added to the root's.
|
|
4298
|
+
*
|
|
4299
|
+
* - grow: data-grow on the textarea, with its rows and maximum as custom properties:
|
|
4300
|
+
* <textarea class="plinth-textarea-input" id="reply" rows="3" data-grow
|
|
4301
|
+
* style="--plinth-textarea-rows: 3; --plinth-textarea-max-rows: 8"></textarea>
|
|
4302
|
+
* It starts at rows lines and grows with its content (field-sizing: content) up to max-rows
|
|
4303
|
+
* (8 by default), then scrolls. Where the browser has no field-sizing it keeps rows lines and
|
|
4304
|
+
* the person can resize it, as without grow.
|
|
4305
|
+
* - data-label="hidden" on the root hides the label visually; screen readers still read it. Use it
|
|
4306
|
+
* only where the field's purpose is plain without it, such as a reply box under a thread.
|
|
4307
|
+
*
|
|
4308
|
+
* The text is 16px on a phone, so iOS doesn't zoom into the field, and 14px from 768px up.
|
|
4309
|
+
*
|
|
4310
|
+
* States:
|
|
4311
|
+
* - error: aria-invalid="true" on the textarea and the error message under it; the edge turns
|
|
4312
|
+
* danger and thick, so it isn't only a colour, and the message says what to fix.
|
|
4313
|
+
* - disabled: `disabled` on the textarea; the field is faded and can't be focused.
|
|
4314
|
+
* - read-only: `readonly` on the textarea; it keeps its text and focus, without the field's fill
|
|
4315
|
+
* and with a dashed edge.
|
|
4316
|
+
* - the focus ring on the textarea's :focus-visible.
|
|
4317
|
+
*/
|
|
4318
|
+
|
|
4319
|
+
@layer plinth {
|
|
4320
|
+
.plinth-textarea {
|
|
4321
|
+
display: grid;
|
|
4322
|
+
gap: var(--plinth-space-2);
|
|
4323
|
+
min-inline-size: 0;
|
|
4324
|
+
font-family: var(--plinth-font-family-body);
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
.plinth-textarea-label {
|
|
4328
|
+
font-size: var(--plinth-font-size-sm);
|
|
4329
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
4330
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4331
|
+
color: var(--plinth-color-text-muted);
|
|
4332
|
+
overflow-wrap: anywhere;
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
/* Hidden from sight, still read by screen readers. */
|
|
4336
|
+
.plinth-textarea[data-label='hidden'] .plinth-textarea-label {
|
|
4337
|
+
position: absolute;
|
|
4338
|
+
inline-size: 1px;
|
|
4339
|
+
block-size: 1px;
|
|
4340
|
+
overflow: hidden;
|
|
4341
|
+
white-space: nowrap;
|
|
4342
|
+
clip-path: inset(50%);
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
.plinth-textarea-input {
|
|
4346
|
+
box-sizing: border-box;
|
|
4347
|
+
inline-size: 100%;
|
|
4348
|
+
min-block-size: var(--plinth-touch-target);
|
|
4349
|
+
padding-block: var(--plinth-space-2);
|
|
4350
|
+
padding-inline: var(--plinth-space-3);
|
|
4351
|
+
margin: 0;
|
|
4352
|
+
font-family: inherit;
|
|
4353
|
+
font-size: var(--plinth-font-size-field);
|
|
4354
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4355
|
+
color: var(--plinth-color-text);
|
|
4356
|
+
resize: vertical;
|
|
4357
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
4358
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
4359
|
+
border-radius: var(--plinth-radius-control);
|
|
4360
|
+
transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
.plinth-textarea-input::placeholder {
|
|
4364
|
+
color: var(--plinth-color-text-subtle);
|
|
4365
|
+
opacity: 1;
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
@media (hover: hover) {
|
|
4369
|
+
.plinth-textarea-input:hover:not(:disabled, :read-only, [aria-invalid='true']) {
|
|
4370
|
+
border-color: var(--plinth-color-text-subtle);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
.plinth-textarea-input:focus-visible {
|
|
4375
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
4376
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
.plinth-textarea-input[aria-invalid='true'] {
|
|
4380
|
+
border-color: var(--plinth-color-danger);
|
|
4381
|
+
border-width: var(--plinth-border-width-thick);
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
.plinth-textarea-input:disabled {
|
|
4385
|
+
cursor: not-allowed;
|
|
4386
|
+
resize: none;
|
|
4387
|
+
opacity: var(--plinth-opacity-disabled);
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
/* Without the fill, and with a dashed edge, so it doesn't read as a field to type in. */
|
|
4391
|
+
.plinth-textarea-input:read-only:not(:disabled) {
|
|
4392
|
+
background-color: transparent;
|
|
4393
|
+
border-style: dashed;
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
/*
|
|
4397
|
+
* Grow: from rows lines to max-rows lines (in the textarea's own line height, with its padding
|
|
4398
|
+
* and edge), then it scrolls.
|
|
4399
|
+
*/
|
|
4400
|
+
@supports (field-sizing: content) {
|
|
4401
|
+
.plinth-textarea-input[data-grow] {
|
|
4402
|
+
field-sizing: content;
|
|
4403
|
+
min-block-size: max(
|
|
4404
|
+
var(--plinth-touch-target),
|
|
4405
|
+
calc(
|
|
4406
|
+
var(--plinth-textarea-rows, 3) * 1lh + 2 * var(--plinth-space-2) + 2 *
|
|
4407
|
+
var(--plinth-border-width-thin)
|
|
4408
|
+
)
|
|
4409
|
+
);
|
|
4410
|
+
max-block-size: calc(
|
|
4411
|
+
var(--plinth-textarea-max-rows, 8) * 1lh + 2 * var(--plinth-space-2) + 2 *
|
|
4412
|
+
var(--plinth-border-width-thin)
|
|
4413
|
+
);
|
|
4414
|
+
resize: none;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
.plinth-textarea-description,
|
|
4419
|
+
.plinth-textarea-error {
|
|
4420
|
+
margin: 0;
|
|
4421
|
+
font-size: var(--plinth-font-size-sm);
|
|
4422
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4423
|
+
overflow-wrap: anywhere;
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
.plinth-textarea-description {
|
|
4427
|
+
color: var(--plinth-color-text-muted);
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
.plinth-textarea-error {
|
|
4431
|
+
color: var(--plinth-color-danger-text);
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
@media (min-width: 768px) {
|
|
4435
|
+
.plinth-textarea-input {
|
|
4436
|
+
font-size: var(--plinth-font-size-md);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
@media (forced-colors: active) {
|
|
4441
|
+
.plinth-textarea-input:disabled {
|
|
4442
|
+
color: GrayText;
|
|
4443
|
+
border-color: GrayText;
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
}
|