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