@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,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
|
+
}
|