@ptbdynamic/plinth 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) 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/modal.css +367 -0
  33. package/packages/css/dist/pill-row.css +197 -0
  34. package/packages/css/dist/plinth.css +3773 -0
  35. package/packages/css/dist/section-menu.css +225 -0
  36. package/packages/css/dist/sheet.css +239 -0
  37. package/packages/css/dist/tokens.css +405 -0
  38. package/packages/react/dist/PlinthAvatar.d.ts +17 -0
  39. package/packages/react/dist/PlinthBadge.d.ts +7 -0
  40. package/packages/react/dist/PlinthButton.d.ts +29 -0
  41. package/packages/react/dist/PlinthCallout.d.ts +16 -0
  42. package/packages/react/dist/PlinthCodeBlock.d.ts +24 -0
  43. package/packages/react/dist/PlinthConfirmDialog.d.ts +38 -0
  44. package/packages/react/dist/PlinthDataTable.d.ts +11 -0
  45. package/packages/react/dist/PlinthDataTableCell.d.ts +15 -0
  46. package/packages/react/dist/PlinthDataTableColumn.d.ts +13 -0
  47. package/packages/react/dist/PlinthDataTableEmpty.d.ts +9 -0
  48. package/packages/react/dist/PlinthDataTableRow.d.ts +9 -0
  49. package/packages/react/dist/PlinthDataTableSort.d.ts +4 -0
  50. package/packages/react/dist/PlinthDialog.d.ts +26 -0
  51. package/packages/react/dist/PlinthDrawer.d.ts +29 -0
  52. package/packages/react/dist/PlinthHeader.d.ts +12 -0
  53. package/packages/react/dist/PlinthIcon.d.ts +10 -0
  54. package/packages/react/dist/PlinthIconButton.d.ts +26 -0
  55. package/packages/react/dist/PlinthListCard.d.ts +14 -0
  56. package/packages/react/dist/PlinthListRow.d.ts +29 -0
  57. package/packages/react/dist/PlinthMenuSheet.d.ts +24 -0
  58. package/packages/react/dist/PlinthPill.d.ts +17 -0
  59. package/packages/react/dist/PlinthPillRow.d.ts +13 -0
  60. package/packages/react/dist/PlinthSectionMenu.d.ts +11 -0
  61. package/packages/react/dist/PlinthSectionMenuItem.d.ts +20 -0
  62. package/packages/react/dist/PlinthSheet.d.ts +26 -0
  63. package/packages/react/dist/avatar.d.ts +12 -0
  64. package/packages/react/dist/button.d.ts +21 -0
  65. package/packages/react/dist/callout.d.ts +19 -0
  66. package/packages/react/dist/code-block.d.ts +16 -0
  67. package/packages/react/dist/confirm-dialog.d.ts +23 -0
  68. package/packages/react/dist/data-table.d.ts +15 -0
  69. package/packages/react/dist/dialog.d.ts +10 -0
  70. package/packages/react/dist/drawer.d.ts +20 -0
  71. package/packages/react/dist/heading.d.ts +5 -0
  72. package/packages/react/dist/icon.d.ts +5 -0
  73. package/packages/react/dist/index.d.ts +40 -0
  74. package/packages/react/dist/index.js +1127 -0
  75. package/packages/react/dist/list-row.d.ts +5 -0
  76. package/packages/react/dist/markup.d.ts +9 -0
  77. package/packages/react/dist/sheet.d.ts +12 -0
  78. package/packages/react/dist/size.d.ts +13 -0
  79. package/packages/react/dist/swipe.d.ts +15 -0
  80. package/packages/react/dist/tones.d.ts +5 -0
  81. package/packages/tokens/dist/index.d.ts +218 -0
  82. package/packages/tokens/dist/index.js +3966 -0
  83. package/packages/tokens/dist/tailwind-theme.css +80 -0
  84. package/packages/tokens/dist/tokens.css +405 -0
  85. package/packages/tokens/dist/tokens.json +3762 -0
  86. package/packages/vue/dist/PlinthAvatar.vue.d.ts +24 -0
  87. package/packages/vue/dist/PlinthBadge.vue.d.ts +21 -0
  88. package/packages/vue/dist/PlinthButton.vue.d.ts +42 -0
  89. package/packages/vue/dist/PlinthCallout.vue.d.ts +31 -0
  90. package/packages/vue/dist/PlinthCodeBlock.vue.d.ts +49 -0
  91. package/packages/vue/dist/PlinthConfirmDialog.vue.d.ts +51 -0
  92. package/packages/vue/dist/PlinthDataTable.vue.d.ts +21 -0
  93. package/packages/vue/dist/PlinthDataTableCell.vue.d.ts +29 -0
  94. package/packages/vue/dist/PlinthDataTableColumn.vue.d.ts +26 -0
  95. package/packages/vue/dist/PlinthDataTableEmpty.vue.d.ts +19 -0
  96. package/packages/vue/dist/PlinthDataTableRow.vue.d.ts +19 -0
  97. package/packages/vue/dist/PlinthDataTableSort.vue.d.ts +13 -0
  98. package/packages/vue/dist/PlinthDialog.vue.d.ts +45 -0
  99. package/packages/vue/dist/PlinthDrawer.vue.d.ts +48 -0
  100. package/packages/vue/dist/PlinthHeader.vue.d.ts +19 -0
  101. package/packages/vue/dist/PlinthIcon.vue.d.ts +23 -0
  102. package/packages/vue/dist/PlinthIconButton.vue.d.ts +37 -0
  103. package/packages/vue/dist/PlinthListCard.vue.d.ts +26 -0
  104. package/packages/vue/dist/PlinthListRow.vue.d.ts +38 -0
  105. package/packages/vue/dist/PlinthMenuSheet.vue.d.ts +48 -0
  106. package/packages/vue/dist/PlinthPill.vue.d.ts +28 -0
  107. package/packages/vue/dist/PlinthPillRow.vue.d.ts +22 -0
  108. package/packages/vue/dist/PlinthSectionMenu.vue.d.ts +22 -0
  109. package/packages/vue/dist/PlinthSectionMenuItem.vue.d.ts +28 -0
  110. package/packages/vue/dist/PlinthSheet.vue.d.ts +45 -0
  111. package/packages/vue/dist/avatar.d.ts +12 -0
  112. package/packages/vue/dist/button.d.ts +5 -0
  113. package/packages/vue/dist/callout.d.ts +19 -0
  114. package/packages/vue/dist/code-block.d.ts +16 -0
  115. package/packages/vue/dist/confirm-dialog.d.ts +23 -0
  116. package/packages/vue/dist/data-table.d.ts +15 -0
  117. package/packages/vue/dist/dialog.d.ts +10 -0
  118. package/packages/vue/dist/drawer.d.ts +20 -0
  119. package/packages/vue/dist/heading.d.ts +5 -0
  120. package/packages/vue/dist/icon.d.ts +5 -0
  121. package/packages/vue/dist/index.d.ts +44 -0
  122. package/packages/vue/dist/index.js +1439 -0
  123. package/packages/vue/dist/list-row.d.ts +5 -0
  124. package/packages/vue/dist/sheet.d.ts +12 -0
  125. package/packages/vue/dist/size.d.ts +13 -0
  126. package/packages/vue/dist/slots.d.ts +3 -0
  127. package/packages/vue/dist/swipe.d.ts +12 -0
  128. package/packages/vue/dist/tones.d.ts +5 -0
@@ -0,0 +1,225 @@
1
+ /*
2
+ * Section menu: the row under the header that switches between the sections of a page.
3
+ *
4
+ * <nav class="plinth-section-menu" aria-label="Project">
5
+ * <a class="plinth-section-menu-item" href="/overview" aria-current="page">
6
+ * <span class="plinth-section-menu-label">Overview</span>
7
+ * </a>
8
+ * <a class="plinth-section-menu-item" href="/tasks">
9
+ * <span class="plinth-section-menu-label">Tasks</span>
10
+ * <span class="plinth-section-menu-count">8</span>
11
+ * </a>
12
+ * </nav>
13
+ *
14
+ * The root is a <nav>, named by aria-label (label in Blade and Vue) or by the app's own
15
+ * aria-labelledby, so it reads apart from the page's other navigation. Its items are links. A
16
+ * menu that switches views in place, without navigating, has buttons instead, and its root is
17
+ * <div class="plinth-section-menu" role="group"> (nav set to false in Blade and Vue):
18
+ *
19
+ * <button type="button" class="plinth-section-menu-item" aria-pressed="true">…</button>
20
+ *
21
+ * Each item is its own component (<x-plinth::section-menu-item>, PlinthSectionMenuItem), so the
22
+ * app's attributes and listeners land on the item (ADR 2). The count is optional.
23
+ * plinth-section-menu-label has no rule of its own: it is a hook for a project's styles.
24
+ *
25
+ * States, set by the app:
26
+ * - current: a link prints aria-current="page" for the section being shown; a button always
27
+ * prints aria-pressed, "true" or "false". The current item takes the selection tint
28
+ * (accent-subtle, accent-text) inside a 2px accent edge (border-width.thick), so colour is not
29
+ * its only cue; the others are muted text on no surface, like a ghost button. A pressed button
30
+ * still answers hover and press with a step on the tint (accent-subtle-hover,
31
+ * accent-subtle-active).
32
+ * - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
33
+ * - hover only where the device can hover; the focus ring on :focus-visible.
34
+ *
35
+ * Layout: a row --plinth-section-menu-height (56px) tall, on the surface with a divider below,
36
+ * the same inline padding as the header and clear of the notch on a phone held sideways. Its
37
+ * items are --plinth-section-menu-item-height (40px) tall, never less than the 40px touch
38
+ * target, and at least 40px wide. The row never wraps: when the items don't fit it scrolls
39
+ * sideways and snaps to them, with a thin scrollbar for a mouse and none on a touch screen; the
40
+ * row's block padding keeps the focus ring inside it. The app scrolls a current item that starts
41
+ * out of view into view.
42
+ * Placement is the app's: put it under the header, or make it sticky below it with
43
+ * position: sticky, inset-block-start: calc(var(--plinth-header-height) +
44
+ * env(safe-area-inset-top)) and z-index: var(--plinth-layer-sticky).
45
+ */
46
+
47
+ @layer plinth {
48
+ .plinth-section-menu {
49
+ box-sizing: border-box;
50
+ display: flex;
51
+ gap: var(--plinth-space-1);
52
+ align-items: center;
53
+ min-block-size: var(--plinth-section-menu-height);
54
+
55
+ /* What the item and the divider leave of the row's height, so the row is exactly its token. */
56
+ padding-block: calc(
57
+ (
58
+ var(--plinth-section-menu-height) - var(--plinth-section-menu-item-height) -
59
+ var(--plinth-border-width-thin)
60
+ ) /
61
+ 2
62
+ );
63
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
64
+ max(var(--plinth-space-2), env(safe-area-inset-right));
65
+ margin: 0;
66
+ overflow-x: auto;
67
+ overscroll-behavior-inline: contain;
68
+ font-family: var(--plinth-font-family-body);
69
+ scroll-snap-type: inline proximity;
70
+ scroll-padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
71
+ max(var(--plinth-space-2), env(safe-area-inset-right));
72
+ scrollbar-width: none;
73
+ background-color: var(--plinth-color-surface);
74
+ border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
75
+ }
76
+
77
+ .plinth-section-menu:dir(rtl) {
78
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
79
+ max(var(--plinth-space-2), env(safe-area-inset-left));
80
+ scroll-padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
81
+ max(var(--plinth-space-2), env(safe-area-inset-left));
82
+ }
83
+
84
+ /* A mouse without a sideways wheel needs a scrollbar to reach the items that don't fit. */
85
+ @media (hover: hover) and (pointer: fine) {
86
+ .plinth-section-menu {
87
+ scrollbar-width: thin;
88
+ }
89
+ }
90
+
91
+ .plinth-section-menu-item {
92
+ box-sizing: border-box;
93
+ display: inline-flex;
94
+ flex: none;
95
+ gap: var(--plinth-space-2);
96
+ align-items: center;
97
+ justify-content: center;
98
+ min-inline-size: var(--plinth-touch-target);
99
+ min-block-size: max(var(--plinth-section-menu-item-height), var(--plinth-touch-target));
100
+ padding-block: 0;
101
+ padding-inline: var(--plinth-space-3);
102
+ margin: 0;
103
+ font-family: inherit;
104
+ font-size: var(--plinth-font-size-md);
105
+ font-weight: var(--plinth-font-weight-medium);
106
+ line-height: var(--plinth-font-leading-tight);
107
+ color: var(--plinth-color-text-muted);
108
+ white-space: nowrap;
109
+ text-decoration: none;
110
+ cursor: pointer;
111
+ user-select: none;
112
+ scroll-snap-align: start;
113
+ background-color: transparent;
114
+ border: var(--plinth-border-width-thin) solid transparent;
115
+ border-radius: var(--plinth-radius-control);
116
+ transition:
117
+ background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
118
+ border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
119
+ box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard),
120
+ color var(--plinth-duration-fast) var(--plinth-easing-standard);
121
+ }
122
+
123
+ @media (hover: hover) {
124
+ a.plinth-section-menu-item[href]:not([aria-current='page']):hover,
125
+ button.plinth-section-menu-item:enabled:not([aria-pressed='true']):hover {
126
+ color: var(--plinth-color-text);
127
+ background-color: var(--plinth-color-surface-hover);
128
+ }
129
+ }
130
+
131
+ a.plinth-section-menu-item[href]:not([aria-current='page']):active,
132
+ button.plinth-section-menu-item:enabled:not([aria-pressed='true']):active {
133
+ color: var(--plinth-color-text);
134
+ background-color: var(--plinth-color-surface-sunken);
135
+ }
136
+
137
+ /* The border and an inset ring make the 2px edge, so selecting doesn't move the label. */
138
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']) {
139
+ color: var(--plinth-color-accent-text);
140
+ background-color: var(--plinth-color-accent-subtle);
141
+ border-color: var(--plinth-color-accent);
142
+ box-shadow: inset 0 0 0 calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
143
+ var(--plinth-color-accent);
144
+ }
145
+
146
+ /*
147
+ * A pressed button can be pressed again, so it answers hover and press with a step on the tint.
148
+ * Not in forced colours, where the tint would replace the system highlight under the label.
149
+ */
150
+ @media (hover: hover) and (forced-colors: none) {
151
+ button.plinth-section-menu-item:enabled[aria-pressed='true']:hover {
152
+ background-color: var(--plinth-color-accent-subtle-hover);
153
+ }
154
+ }
155
+
156
+ @media (forced-colors: none) {
157
+ button.plinth-section-menu-item:enabled[aria-pressed='true']:active {
158
+ background-color: var(--plinth-color-accent-subtle-active);
159
+ }
160
+ }
161
+
162
+ .plinth-section-menu-item:disabled,
163
+ .plinth-section-menu-item[aria-disabled='true'] {
164
+ cursor: not-allowed;
165
+ opacity: var(--plinth-opacity-disabled);
166
+ }
167
+
168
+ .plinth-section-menu-item:focus-visible {
169
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
170
+ outline-offset: var(--plinth-focus-ring-offset);
171
+ }
172
+
173
+ /* A gap between the ring and the accent edge, so focus and selection read apart. */
174
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']):focus-visible {
175
+ outline-offset: var(--plinth-focus-ring-offset-filled);
176
+ }
177
+
178
+ .plinth-section-menu-count {
179
+ font-family: var(--plinth-font-family-mono);
180
+ font-size: var(--plinth-font-size-xs);
181
+ font-weight: var(--plinth-font-weight-regular);
182
+ font-variant-numeric: tabular-nums;
183
+ color: var(--plinth-color-text-muted);
184
+ }
185
+
186
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page'])
187
+ .plinth-section-menu-count {
188
+ color: inherit;
189
+ }
190
+
191
+ @media (min-width: 768px) {
192
+ .plinth-section-menu {
193
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
194
+ max(var(--plinth-space-4), env(safe-area-inset-right));
195
+ scroll-padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
196
+ max(var(--plinth-space-4), env(safe-area-inset-right));
197
+ }
198
+
199
+ .plinth-section-menu:dir(rtl) {
200
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
201
+ max(var(--plinth-space-4), env(safe-area-inset-left));
202
+ scroll-padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
203
+ max(var(--plinth-space-4), env(safe-area-inset-left));
204
+ }
205
+ }
206
+
207
+ /* The current item is only a surface tint: in forced colours it takes the system highlight. */
208
+ @media (forced-colors: active) {
209
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']) {
210
+ color: HighlightText;
211
+ background-color: Highlight;
212
+ border-color: Highlight;
213
+ }
214
+
215
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page'])
216
+ .plinth-section-menu-count {
217
+ color: HighlightText;
218
+ }
219
+
220
+ .plinth-section-menu-item:disabled,
221
+ .plinth-section-menu-item[aria-disabled='true'] {
222
+ color: GrayText;
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,239 @@
1
+ /*
2
+ * Sheet: a modal panel on the edge of the screen the developer picks, for filters, side
3
+ * information or navigation. Optionally on another edge below 768px.
4
+ *
5
+ * <dialog id="filters" class="plinth-sheet" data-side="end" data-side-phone="bottom"
6
+ * closedby="any" aria-labelledby="filters-title" aria-describedby="filters-description">
7
+ * <div class="plinth-sheet-panel">
8
+ * <div class="plinth-sheet-handle" aria-hidden="true"></div>
9
+ * <div class="plinth-sheet-header">
10
+ * <h2 id="filters-title" class="plinth-sheet-title">Filters</h2>
11
+ * <p id="filters-description" class="plinth-sheet-description">Narrow the list down.</p>
12
+ * <button type="button" class="plinth-button plinth-icon-button plinth-sheet-close"
13
+ * data-variant="ghost" data-size="md" aria-label="Close" commandfor="filters"
14
+ * command="close">
15
+ * <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
16
+ * </button>
17
+ * </div>
18
+ * <div class="plinth-sheet-body">…</div>
19
+ * <div class="plinth-sheet-footer">…</div>
20
+ * </div>
21
+ * </dialog>
22
+ *
23
+ * The root is the <dialog>, and it needs an id: commandfor and aria-labelledby point at it, and
24
+ * the title's and description's ids are the root's id with -title and -description. The panel is
25
+ * the root's only child and holds the parts. The handle is there only where the sheet's side on a
26
+ * phone is the bottom (data-side="bottom" with no data-side-phone, or data-side-phone="bottom"):
27
+ * decorative (aria-hidden), shown below 768px, where the sheet is dragged down to close
28
+ * (modal.css). The title is
29
+ * required; the description (and its aria-describedby) and the footer are optional. The close
30
+ * button is an icon button (button.css and icon.css) and is always there: closedby="any" (tap the
31
+ * backdrop) is an enhancement Safari doesn't have yet. A project may set closedby="closerequest"
32
+ * on a sheet that must not close on a stray tap.
33
+ *
34
+ * Open it with showModal() or a <button commandfor="filters" command="show-modal">. Open is the
35
+ * native state, [open]; the browser puts the sheet in the top layer, makes the page inert, traps
36
+ * focus, closes on Escape and returns focus. The dialog fires close when it closes. For a modal
37
+ * centred on the screen, use the dialog (dialog.css).
38
+ *
39
+ * Side (data-side), at every width: end (default, also when missing or unknown) and start are
40
+ * full-height panels on the inline-end and inline-start edges, sheet.side-width wide and at
41
+ * most the screen's width less a gap, so the page shows beside them; top and bottom are the
42
+ * screen's width on the top and bottom edges, as tall as their content up to the screen's height
43
+ * less a gap that also clears the safe area at the other edge. The sides are logical: in
44
+ * right-to-left text end is the left edge and start the right.
45
+ *
46
+ * Side on a phone (data-side-phone, optional): the side below 768px, one of the same four; from
47
+ * 768px up data-side holds. data-side="end" data-side-phone="bottom" is a side panel on a tablet
48
+ * or desktop and a bottom sheet on a phone. Leave it out to keep data-side at every width. It
49
+ * takes only those four values: any other makes the phone's side end (the layers never print
50
+ * one, and leave the attribute out instead).
51
+ *
52
+ * The edge the sheet sits on has no border or radius; the edge facing the page has a hairline
53
+ * (in forced colours too), and a top or bottom sheet rounds the corners facing the page. Each
54
+ * side clears the safe areas it touches (the notch, the status bar and the home indicator, with
55
+ * viewport-fit=cover).
56
+ *
57
+ * The description is a secondary line, at the size of a list row's description.
58
+ *
59
+ * The surface, the backdrop, the scroll lock, the motion, the safe areas and the parts come from
60
+ * the modal base (modal.css), which the dialog shares; load it with this file. The body scrolls,
61
+ * the header and footer stay put, and the page under an open sheet doesn't scroll. The sheet
62
+ * slides in from its edge and back out on the motion tokens, which drop to 0ms under
63
+ * prefers-reduced-motion. This file sets only where the sheet sits and how big it is.
64
+ *
65
+ * A sheet on the bottom edge has its height capped by 100dvh, which clears the home indicator
66
+ * (env(safe-area-inset-bottom)) but not, on its own, the on-screen keyboard: iOS lays the
67
+ * keyboard over the page. A page with a form in a sheet sets
68
+ * <meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
69
+ * where the browser supports it, and keeps the sheet's fields near its top.
70
+ *
71
+ * In Livewire, a re-render would morph the open dialog back to the server's markup and drop its
72
+ * open attribute: put wire:ignore.self on the sheet (or keep it outside the component that
73
+ * re-renders), and always pass an id, since a generated one changes on every render.
74
+ */
75
+
76
+ @layer plinth {
77
+ /*
78
+ * The end side, the default. The sides are written twice: as the phone's side (data-side-phone,
79
+ * or data-side without it), and from 768px up as data-side again for a sheet with
80
+ * data-side-phone. The phone's rules use :where() so the later ones win on order alone.
81
+ */
82
+ .plinth-sheet {
83
+ /* Translate isn't logical: -1 (under :dir(rtl), below) turns the slide around. */
84
+ --plinth-sheet-direction: 1;
85
+ --plinth-sheet-safe-area-inline-start: env(safe-area-inset-left);
86
+ --plinth-sheet-safe-area-inline-end: env(safe-area-inset-right);
87
+ --plinth-modal-offset: calc(100% * var(--plinth-sheet-direction)) 0;
88
+ --plinth-modal-safe-area-end: env(safe-area-inset-bottom);
89
+
90
+ inset-block: 0;
91
+ inset-inline: auto 0;
92
+ inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
93
+ block-size: auto;
94
+ max-block-size: none;
95
+ padding-block: env(safe-area-inset-top) 0;
96
+ padding-inline: 0 var(--plinth-sheet-safe-area-inline-end);
97
+ margin: 0;
98
+ border-width: 0;
99
+ border-inline-start-width: var(--plinth-border-width-thin);
100
+ border-radius: 0;
101
+ }
102
+
103
+ .plinth-sheet:where([data-side='start']:not([data-side-phone]), [data-side-phone='start']) {
104
+ --plinth-modal-offset: calc(-100% * var(--plinth-sheet-direction)) 0;
105
+
106
+ inset-block: 0;
107
+ inset-inline: 0 auto;
108
+ inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
109
+ max-block-size: none;
110
+ padding-block: env(safe-area-inset-top) 0;
111
+ padding-inline: var(--plinth-sheet-safe-area-inline-start) 0;
112
+ border-width: 0;
113
+ border-inline-end-width: var(--plinth-border-width-thin);
114
+ border-radius: 0;
115
+ }
116
+
117
+ .plinth-sheet:where([data-side='top']:not([data-side-phone]), [data-side-phone='top']) {
118
+ --plinth-modal-offset: 0 -100%;
119
+ --plinth-modal-safe-area-end: initial;
120
+
121
+ inset-block: 0 auto;
122
+ inset-inline: 0;
123
+ inline-size: 100%;
124
+ max-block-size: calc(
125
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-bottom) + var(--plinth-space-4))
126
+ );
127
+ padding-block: env(safe-area-inset-top) 0;
128
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
129
+ var(--plinth-sheet-safe-area-inline-end);
130
+ border-width: 0;
131
+ border-block-end-width: var(--plinth-border-width-thin);
132
+ border-radius: 0;
133
+ border-end-start-radius: var(--plinth-radius-container);
134
+ border-end-end-radius: var(--plinth-radius-container);
135
+ }
136
+
137
+ .plinth-sheet:where([data-side='bottom']:not([data-side-phone]), [data-side-phone='bottom']) {
138
+ --plinth-modal-offset: 0 100%;
139
+
140
+ inset-block: auto 0;
141
+ inset-inline: 0;
142
+ inline-size: 100%;
143
+ max-block-size: calc(
144
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
145
+ );
146
+ padding-block: 0;
147
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
148
+ var(--plinth-sheet-safe-area-inline-end);
149
+ border-width: 0;
150
+ border-block-start-width: var(--plinth-border-width-thin);
151
+ border-radius: 0;
152
+ border-start-start-radius: var(--plinth-radius-container);
153
+ border-start-end-radius: var(--plinth-radius-container);
154
+ }
155
+
156
+ /*
157
+ * With its grab handle, a bottom sheet on a phone is dragged down to close (modal.css): the root
158
+ * fills the screen, see-through, and its panel is the surface, as tall as its content up to the
159
+ * screen's height less a gap.
160
+ */
161
+ .plinth-sheet:where([data-side='bottom']:not([data-side-phone]), [data-side-phone='bottom']):has(
162
+ > * > .plinth-sheet-handle
163
+ ) {
164
+ inset: 0;
165
+ max-block-size: none;
166
+ padding: 0;
167
+ border-width: 0;
168
+ border-radius: 0;
169
+ }
170
+
171
+ /* Right-to-left text turns the slide around and swaps the safe areas. */
172
+ .plinth-sheet:dir(rtl) {
173
+ --plinth-sheet-direction: -1;
174
+ --plinth-sheet-safe-area-inline-start: env(safe-area-inset-right);
175
+ --plinth-sheet-safe-area-inline-end: env(safe-area-inset-left);
176
+ }
177
+
178
+ @media (min-width: 768px) {
179
+ .plinth-sheet[data-side-phone] {
180
+ --plinth-modal-offset: calc(100% * var(--plinth-sheet-direction)) 0;
181
+ --plinth-modal-safe-area-end: env(safe-area-inset-bottom);
182
+
183
+ inset-block: 0;
184
+ inset-inline: auto 0;
185
+ inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
186
+ max-block-size: none;
187
+ padding-block: env(safe-area-inset-top) 0;
188
+ padding-inline: 0 var(--plinth-sheet-safe-area-inline-end);
189
+ border-width: 0;
190
+ border-inline-start-width: var(--plinth-border-width-thin);
191
+ border-radius: 0;
192
+ }
193
+
194
+ .plinth-sheet[data-side-phone][data-side='start'] {
195
+ --plinth-modal-offset: calc(-100% * var(--plinth-sheet-direction)) 0;
196
+
197
+ inset-inline: 0 auto;
198
+ padding-inline: var(--plinth-sheet-safe-area-inline-start) 0;
199
+ border-inline-start-width: 0;
200
+ border-inline-end-width: var(--plinth-border-width-thin);
201
+ }
202
+
203
+ .plinth-sheet[data-side-phone][data-side='top'] {
204
+ --plinth-modal-offset: 0 -100%;
205
+ --plinth-modal-safe-area-end: initial;
206
+
207
+ inset-block: 0 auto;
208
+ inset-inline: 0;
209
+ inline-size: 100%;
210
+ max-block-size: calc(
211
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-bottom) + var(--plinth-space-4))
212
+ );
213
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
214
+ var(--plinth-sheet-safe-area-inline-end);
215
+ border-block-end-width: var(--plinth-border-width-thin);
216
+ border-inline-start-width: 0;
217
+ border-end-start-radius: var(--plinth-radius-container);
218
+ border-end-end-radius: var(--plinth-radius-container);
219
+ }
220
+
221
+ .plinth-sheet[data-side='bottom'] {
222
+ --plinth-modal-offset: 0 100%;
223
+
224
+ inset-block: auto 0;
225
+ inset-inline: 0;
226
+ inline-size: 100%;
227
+ max-block-size: calc(
228
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
229
+ );
230
+ padding-block: 0;
231
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
232
+ var(--plinth-sheet-safe-area-inline-end);
233
+ border-block-start-width: var(--plinth-border-width-thin);
234
+ border-inline-start-width: 0;
235
+ border-start-start-radius: var(--plinth-radius-container);
236
+ border-start-end-radius: var(--plinth-radius-container);
237
+ }
238
+ }
239
+ }