@ptbdynamic/plinth 0.1.1 → 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/README.md +1 -1
- package/package.json +1 -1
- package/packages/css/dist/menu.css +245 -0
- package/packages/css/dist/page-header.css +124 -0
- package/packages/css/dist/plinth.css +833 -160
- package/packages/css/dist/section-menu.css +6 -5
- package/packages/css/dist/text-field.css +230 -0
- package/packages/css/dist/textarea.css +169 -0
- package/packages/css/dist/tokens.css +9 -1
- 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/PlinthPageHeader.d.ts +17 -0
- package/packages/react/dist/PlinthTextField.d.ts +24 -0
- package/packages/react/dist/PlinthTextarea.d.ts +24 -0
- package/packages/react/dist/index.d.ts +10 -1
- package/packages/react/dist/index.js +565 -366
- package/packages/react/dist/menu.d.ts +7 -0
- package/packages/react/dist/page-header.d.ts +5 -0
- package/packages/react/dist/text-field.d.ts +10 -0
- package/packages/tokens/dist/index.d.ts +13 -1
- package/packages/tokens/dist/index.js +131 -6
- package/packages/tokens/dist/tailwind-theme.css +2 -2
- package/packages/tokens/dist/tokens.css +9 -1
- package/packages/tokens/dist/tokens.json +116 -3
- package/packages/vue/dist/PlinthMenu.vue.d.ts +24 -0
- package/packages/vue/dist/{PlinthHeader.vue.d.ts → PlinthMenuGroup.vue.d.ts} +5 -7
- package/packages/vue/dist/PlinthMenuItem.vue.d.ts +34 -0
- package/packages/vue/dist/PlinthMenuSeparator.vue.d.ts +3 -0
- package/packages/vue/dist/PlinthPageHeader.vue.d.ts +27 -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/index.d.ts +11 -2
- package/packages/vue/dist/index.js +661 -368
- package/packages/vue/dist/menu.d.ts +8 -0
- package/packages/vue/dist/page-header.d.ts +5 -0
- package/packages/vue/dist/text-field.d.ts +10 -0
- package/packages/react/dist/PlinthHeader.d.ts +0 -12
|
@@ -87,6 +87,8 @@
|
|
|
87
87
|
--plinth-ref-size-44: 2.75rem;
|
|
88
88
|
--plinth-ref-size-56: 3.5rem;
|
|
89
89
|
--plinth-ref-size-160: 10rem;
|
|
90
|
+
--plinth-ref-size-192: 12rem;
|
|
91
|
+
--plinth-ref-size-320: 20rem;
|
|
90
92
|
--plinth-ref-size-400: 25rem;
|
|
91
93
|
--plinth-ref-size-480: 30rem;
|
|
92
94
|
--plinth-ref-radius-0: 0;
|
|
@@ -199,6 +201,8 @@
|
|
|
199
201
|
--plinth-size-dialog: var(--plinth-ref-size-480);
|
|
200
202
|
--plinth-size-panel: var(--plinth-ref-size-400);
|
|
201
203
|
--plinth-size-peek: var(--plinth-ref-size-160);
|
|
204
|
+
--plinth-size-menu-min: var(--plinth-ref-size-192);
|
|
205
|
+
--plinth-size-menu-max: var(--plinth-ref-size-320);
|
|
202
206
|
--plinth-layer-base: 0;
|
|
203
207
|
--plinth-layer-sticky: 100;
|
|
204
208
|
--plinth-layer-overlay: 200;
|
|
@@ -206,7 +210,8 @@
|
|
|
206
210
|
--plinth-layer-toast: 400;
|
|
207
211
|
--plinth-breakpoint-md: 768px;
|
|
208
212
|
--plinth-breakpoint-lg: 1280px;
|
|
209
|
-
--plinth-header-height: var(--plinth-space-14);
|
|
213
|
+
--plinth-page-header-height: var(--plinth-space-14);
|
|
214
|
+
--plinth-page-header-offset: var(--plinth-space-0);
|
|
210
215
|
--plinth-data-table-row-height: var(--plinth-size-row);
|
|
211
216
|
--plinth-list-row-height: var(--plinth-size-row);
|
|
212
217
|
--plinth-pill-height: var(--plinth-control-md);
|
|
@@ -215,6 +220,9 @@
|
|
|
215
220
|
--plinth-drawer-peek-height: var(--plinth-size-peek);
|
|
216
221
|
--plinth-section-menu-height: var(--plinth-space-14);
|
|
217
222
|
--plinth-section-menu-item-height: var(--plinth-control-lg);
|
|
223
|
+
--plinth-menu-min-width: var(--plinth-size-menu-min);
|
|
224
|
+
--plinth-menu-max-width: var(--plinth-size-menu-max);
|
|
225
|
+
--plinth-text-field-trailing-size: var(--plinth-control-md);
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
:root,
|
|
@@ -2241,118 +2249,6 @@
|
|
|
2241
2249
|
}
|
|
2242
2250
|
}
|
|
2243
2251
|
|
|
2244
|
-
/* header.css */
|
|
2245
|
-
/*
|
|
2246
|
-
* Header: the top bar of the phone shell, with a leading action, the page's title and trailing
|
|
2247
|
-
* actions. It sticks to the top of the page as the content scrolls under it.
|
|
2248
|
-
*
|
|
2249
|
-
* <header class="plinth-header">
|
|
2250
|
-
* <div class="plinth-header-leading">
|
|
2251
|
-
* <a class="plinth-button plinth-icon-button" href="/inbox" data-variant="ghost"
|
|
2252
|
-
* data-size="md" aria-label="Back">
|
|
2253
|
-
* <span class="plinth-icon" data-size="md" data-mirror aria-hidden="true"><svg>…</svg></span>
|
|
2254
|
-
* </a>
|
|
2255
|
-
* </div>
|
|
2256
|
-
* <h1 class="plinth-header-title">Inbox</h1>
|
|
2257
|
-
* <div class="plinth-header-actions">
|
|
2258
|
-
* <button type="button" class="plinth-button plinth-icon-button" data-variant="ghost"
|
|
2259
|
-
* data-size="md" aria-label="Search">…</button>
|
|
2260
|
-
* </div>
|
|
2261
|
-
* </header>
|
|
2262
|
-
*
|
|
2263
|
-
* The root is a <header>: at the top of the page (not inside <main>, <article> or <section>) it is
|
|
2264
|
-
* the page's banner landmark, so a page has one. The title is an <h1>, the page's heading, and is
|
|
2265
|
-
* required. It stays on one line and ends in an ellipsis when it doesn't fit.
|
|
2266
|
-
*
|
|
2267
|
-
* The leading part holds one action, usually an icon button: back (a link, with data-mirror on
|
|
2268
|
-
* its icon so it points the other way in right-to-left text) or a menu button that opens a sheet
|
|
2269
|
-
* (commandfor and command="show-modal"). The actions part holds the trailing actions, at most two
|
|
2270
|
-
* or three icon buttons; put the rest in a menu. Both are optional, and printed only when they
|
|
2271
|
-
* have content. Every icon button needs its own name (label). The header needs button.css and
|
|
2272
|
-
* icon.css for them.
|
|
2273
|
-
*
|
|
2274
|
-
* Its height is the header.height component token (56px), plus env(safe-area-inset-top), which
|
|
2275
|
-
* pads it below the status bar and the notch on a phone that draws under them (viewport-fit=cover);
|
|
2276
|
-
* the inline padding clears the notch on a phone held sideways. It is sticky on the
|
|
2277
|
-
* layer.sticky z-index, so it sits above the page and below sheets and menus (the top layer).
|
|
2278
|
-
* Content scrolls under it, so the app sets scroll-padding-block-start on its scrolling element
|
|
2279
|
-
* (calc(var(--plinth-header-height) + env(safe-area-inset-top))) to keep a focused element or an
|
|
2280
|
-
* anchor's target from landing under the header.
|
|
2281
|
-
*
|
|
2282
|
-
* The actions sit var(--plinth-space-2) apart, so on a touch screen the 40px hit areas of 32px
|
|
2283
|
-
* icon buttons don't overlap.
|
|
2284
|
-
*
|
|
2285
|
-
* The app owns the state: the header has no variants or states of its own. What the actions do is
|
|
2286
|
-
* the app's (a link, a commandfor, wire:click, @click on the icon button).
|
|
2287
|
-
*/
|
|
2288
|
-
|
|
2289
|
-
@layer plinth {
|
|
2290
|
-
.plinth-header {
|
|
2291
|
-
position: sticky;
|
|
2292
|
-
inset-block-start: 0;
|
|
2293
|
-
z-index: var(--plinth-layer-sticky);
|
|
2294
|
-
box-sizing: border-box;
|
|
2295
|
-
display: flex;
|
|
2296
|
-
gap: var(--plinth-space-2);
|
|
2297
|
-
align-items: center;
|
|
2298
|
-
min-block-size: calc(var(--plinth-header-height) + env(safe-area-inset-top));
|
|
2299
|
-
padding-block-start: env(safe-area-inset-top);
|
|
2300
|
-
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
|
|
2301
|
-
max(var(--plinth-space-2), env(safe-area-inset-right));
|
|
2302
|
-
font-family: var(--plinth-font-family-body);
|
|
2303
|
-
color: var(--plinth-color-text);
|
|
2304
|
-
background-color: var(--plinth-color-surface);
|
|
2305
|
-
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
.plinth-header:dir(rtl) {
|
|
2309
|
-
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
|
|
2310
|
-
max(var(--plinth-space-2), env(safe-area-inset-left));
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
.plinth-header-leading,
|
|
2314
|
-
.plinth-header-actions {
|
|
2315
|
-
display: flex;
|
|
2316
|
-
flex: none;
|
|
2317
|
-
gap: var(--plinth-space-2);
|
|
2318
|
-
align-items: center;
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
.plinth-header-title {
|
|
2322
|
-
flex: 1 1 auto;
|
|
2323
|
-
min-inline-size: 0;
|
|
2324
|
-
margin: 0;
|
|
2325
|
-
overflow: hidden;
|
|
2326
|
-
text-overflow: ellipsis;
|
|
2327
|
-
font-size: var(--plinth-font-size-lg);
|
|
2328
|
-
font-weight: var(--plinth-font-weight-semibold);
|
|
2329
|
-
line-height: var(--plinth-font-leading-tight);
|
|
2330
|
-
letter-spacing: var(--plinth-font-tracking-tight);
|
|
2331
|
-
white-space: nowrap;
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
/* Without a leading action or trailing actions, the title keeps the text's distance from the edge. */
|
|
2335
|
-
.plinth-header-title:first-child {
|
|
2336
|
-
margin-inline-start: var(--plinth-space-2);
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
.plinth-header-title:last-child {
|
|
2340
|
-
margin-inline-end: var(--plinth-space-2);
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
@media (min-width: 768px) {
|
|
2344
|
-
.plinth-header {
|
|
2345
|
-
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
|
|
2346
|
-
max(var(--plinth-space-4), env(safe-area-inset-right));
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
.plinth-header:dir(rtl) {
|
|
2350
|
-
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
|
|
2351
|
-
max(var(--plinth-space-4), env(safe-area-inset-left));
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
2252
|
/* icon.css */
|
|
2357
2253
|
/*
|
|
2358
2254
|
* Icon: a project's own SVG, sized by Plinth.
|
|
@@ -2736,6 +2632,253 @@
|
|
|
2736
2632
|
}
|
|
2737
2633
|
}
|
|
2738
2634
|
|
|
2635
|
+
/* menu.css */
|
|
2636
|
+
/*
|
|
2637
|
+
* Menu: a short list of actions that opens from a button, in a popover anchored to it.
|
|
2638
|
+
*
|
|
2639
|
+
* <button type="button" class="plinth-button plinth-icon-button" popovertarget="ticket-actions"
|
|
2640
|
+
* aria-label="More actions">…</button>
|
|
2641
|
+
* <div class="plinth-menu" id="ticket-actions" popover="auto">
|
|
2642
|
+
* <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
|
|
2643
|
+
* command="hide-popover">
|
|
2644
|
+
* <span class="plinth-menu-icon" aria-hidden="true"><svg>…</svg></span>
|
|
2645
|
+
* <span class="plinth-menu-label">Assign to me</span>
|
|
2646
|
+
* <kbd class="plinth-menu-shortcut" aria-hidden="true">A</kbd>
|
|
2647
|
+
* </button>
|
|
2648
|
+
* <a class="plinth-menu-item" href="/tickets/42/print">
|
|
2649
|
+
* <span class="plinth-menu-label">Print</span>
|
|
2650
|
+
* </a>
|
|
2651
|
+
* <hr class="plinth-menu-separator">
|
|
2652
|
+
* <div class="plinth-menu-group" role="group" aria-label="Move to">
|
|
2653
|
+
* <div class="plinth-menu-heading" aria-hidden="true">Move to</div>
|
|
2654
|
+
* <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
|
|
2655
|
+
* command="hide-popover"><span class="plinth-menu-label">Archive</span></button>
|
|
2656
|
+
* </div>
|
|
2657
|
+
* <hr class="plinth-menu-separator">
|
|
2658
|
+
* <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
|
|
2659
|
+
* command="hide-popover" data-tone="danger">
|
|
2660
|
+
* <span class="plinth-menu-label">Delete</span>
|
|
2661
|
+
* </button>
|
|
2662
|
+
* </div>
|
|
2663
|
+
*
|
|
2664
|
+
* The root is a <div popover="auto"> with an id (required in Blade, Vue and React), which the
|
|
2665
|
+
* app's button names in popovertarget (or commandfor with command="toggle-popover"). The browser
|
|
2666
|
+
* opens and closes it (ADR 2): the top layer, a click outside or Escape closes it, focus goes back
|
|
2667
|
+
* to the button, and the browser tells assistive technology whether the button's menu is
|
|
2668
|
+
* expanded. The button is the app's own (an icon button or a button); it is not part of the menu.
|
|
2669
|
+
*
|
|
2670
|
+
* It is not role="menu". The ARIA menu pattern needs arrow keys and a roving focus, which would be
|
|
2671
|
+
* script in every layer; this is a list of buttons and links, reached with Tab, as ADR 2 decides
|
|
2672
|
+
* for menus of actions. So the button has no aria-haspopup, and the items are plain buttons and
|
|
2673
|
+
* links that read as such.
|
|
2674
|
+
*
|
|
2675
|
+
* Parts, each its own component in Blade, Vue and React:
|
|
2676
|
+
* - item: a <button type="button">, or an <a href> when it navigates. A button item closes its
|
|
2677
|
+
* menu when pressed: commandfor="<the menu's id>" command="hide-popover", which the layers take
|
|
2678
|
+
* from the menu around the item. A link closes the menu by leaving the page; an app that routes
|
|
2679
|
+
* without loading a page closes it with hidePopover(). The icon is the project's SVG, decorative;
|
|
2680
|
+
* the label is the item's name. The shortcut is a hint for the eye, aria-hidden; the app that
|
|
2681
|
+
* has the shortcut adds aria-keyshortcuts to the item.
|
|
2682
|
+
* - separator: an <hr> between items.
|
|
2683
|
+
* - group: a <div role="group"> named by its heading (aria-label), with the heading shown above
|
|
2684
|
+
* its items and hidden from assistive technology, which reads the group's name instead.
|
|
2685
|
+
*
|
|
2686
|
+
* Variants and states:
|
|
2687
|
+
* - data-tone="danger" on an item that deletes or removes something: the danger text colour and a
|
|
2688
|
+
* danger tint on hover. The label still says what it does; the colour is not the only signal.
|
|
2689
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
2690
|
+
* - hover only where the device can hover; pressed takes the sunken surface; the focus ring on
|
|
2691
|
+
* :focus-visible, drawn inside the item (focus-ring.offset-inset).
|
|
2692
|
+
* - open is the native one (:popover-open), never a class.
|
|
2693
|
+
*
|
|
2694
|
+
* Placement: below the button, starting at its start edge, through CSS anchor positioning (the
|
|
2695
|
+
* popover's button is its implicit anchor). It takes the side of the button with more room, above
|
|
2696
|
+
* or below, and is never taller than that room: a long menu scrolls. When it would leave the
|
|
2697
|
+
* screen sideways it flips to the button's end edge. It is the same at every width; a phone gets no bottom sheet,
|
|
2698
|
+
* since a menu is a few actions next to the button that opened them (a long list belongs in a
|
|
2699
|
+
* sheet). A browser without anchor positioning shows the menu in the middle of the screen, where
|
|
2700
|
+
* it still works. Script opens it with showPopover({ source: button }), so it has its anchor;
|
|
2701
|
+
* without a source it has none, and sits in the screen's start corner.
|
|
2702
|
+
*
|
|
2703
|
+
* Size: --plinth-menu-min-width (192px) to --plinth-menu-max-width (320px), never wider than the
|
|
2704
|
+
* screen less a margin, and never taller than the room it has. Items are --plinth-touch-target
|
|
2705
|
+
* (40px) tall; with a mouse from 768px up, --plinth-control-md (32px). A long label wraps.
|
|
2706
|
+
* It fades in and out on the motion tokens, which drop to 0ms under prefers-reduced-motion.
|
|
2707
|
+
*/
|
|
2708
|
+
|
|
2709
|
+
@layer plinth {
|
|
2710
|
+
.plinth-menu {
|
|
2711
|
+
/* The browser's popover rules centre it on the screen: the fallback without anchoring. */
|
|
2712
|
+
box-sizing: border-box;
|
|
2713
|
+
min-inline-size: min(var(--plinth-menu-min-width), 100vi - 2 * var(--plinth-space-2));
|
|
2714
|
+
max-inline-size: min(var(--plinth-menu-max-width), 100vi - 2 * var(--plinth-space-2));
|
|
2715
|
+
max-block-size: calc(100dvb - 2 * var(--plinth-space-2));
|
|
2716
|
+
padding: var(--plinth-space-1);
|
|
2717
|
+
overflow: hidden auto;
|
|
2718
|
+
overscroll-behavior: contain;
|
|
2719
|
+
font-family: var(--plinth-font-family-body);
|
|
2720
|
+
font-size: var(--plinth-font-size-md);
|
|
2721
|
+
line-height: var(--plinth-font-leading-tight);
|
|
2722
|
+
color: var(--plinth-color-text);
|
|
2723
|
+
background-color: var(--plinth-color-surface-raised);
|
|
2724
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
|
|
2725
|
+
border-radius: var(--plinth-radius-container);
|
|
2726
|
+
box-shadow: var(--plinth-elevation-2);
|
|
2727
|
+
opacity: 0;
|
|
2728
|
+
transition:
|
|
2729
|
+
opacity var(--plinth-duration-fast) var(--plinth-easing-exit),
|
|
2730
|
+
overlay var(--plinth-duration-fast) allow-discrete,
|
|
2731
|
+
display var(--plinth-duration-fast) allow-discrete;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
.plinth-menu:popover-open {
|
|
2735
|
+
opacity: 1;
|
|
2736
|
+
transition-timing-function: var(--plinth-easing-enter);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
@starting-style {
|
|
2740
|
+
.plinth-menu:popover-open {
|
|
2741
|
+
opacity: 0;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
@supports (position-area: block-end) {
|
|
2746
|
+
.plinth-menu {
|
|
2747
|
+
inset: auto;
|
|
2748
|
+
max-block-size: calc(100% - 2 * var(--plinth-space-1));
|
|
2749
|
+
margin-block: var(--plinth-space-1);
|
|
2750
|
+
margin-inline: 0;
|
|
2751
|
+
position-area: block-end span-inline-end;
|
|
2752
|
+
position-try-order: most-block-size;
|
|
2753
|
+
position-try-fallbacks:
|
|
2754
|
+
flip-block,
|
|
2755
|
+
flip-inline,
|
|
2756
|
+
flip-block flip-inline;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
.plinth-menu-item {
|
|
2761
|
+
box-sizing: border-box;
|
|
2762
|
+
display: flex;
|
|
2763
|
+
gap: var(--plinth-space-2);
|
|
2764
|
+
align-items: center;
|
|
2765
|
+
inline-size: 100%;
|
|
2766
|
+
min-block-size: var(--plinth-touch-target);
|
|
2767
|
+
padding-block: var(--plinth-space-1);
|
|
2768
|
+
padding-inline: var(--plinth-space-2);
|
|
2769
|
+
margin: 0;
|
|
2770
|
+
font: inherit;
|
|
2771
|
+
color: var(--plinth-color-text);
|
|
2772
|
+
text-align: start;
|
|
2773
|
+
text-decoration: none;
|
|
2774
|
+
cursor: pointer;
|
|
2775
|
+
user-select: none;
|
|
2776
|
+
background-color: transparent;
|
|
2777
|
+
border: 0;
|
|
2778
|
+
border-radius: var(--plinth-radius-control);
|
|
2779
|
+
transition:
|
|
2780
|
+
background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
|
|
2781
|
+
color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
.plinth-menu-item[data-tone='danger'] {
|
|
2785
|
+
color: var(--plinth-color-danger-text);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
@media (hover: hover) {
|
|
2789
|
+
a.plinth-menu-item[href]:hover,
|
|
2790
|
+
button.plinth-menu-item:enabled:hover {
|
|
2791
|
+
background-color: var(--plinth-color-surface-hover);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
a.plinth-menu-item[href][data-tone='danger']:hover,
|
|
2795
|
+
button.plinth-menu-item:enabled[data-tone='danger']:hover {
|
|
2796
|
+
background-color: var(--plinth-color-danger-surface);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
a.plinth-menu-item[href]:active,
|
|
2801
|
+
button.plinth-menu-item:enabled:active {
|
|
2802
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
.plinth-menu-item:focus-visible {
|
|
2806
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
2807
|
+
outline-offset: var(--plinth-focus-ring-offset-inset);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
.plinth-menu-item:disabled,
|
|
2811
|
+
.plinth-menu-item[aria-disabled='true'] {
|
|
2812
|
+
cursor: not-allowed;
|
|
2813
|
+
opacity: var(--plinth-opacity-disabled);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.plinth-menu-icon {
|
|
2817
|
+
display: inline-flex;
|
|
2818
|
+
flex: none;
|
|
2819
|
+
inline-size: var(--plinth-size-icon-sm);
|
|
2820
|
+
block-size: var(--plinth-size-icon-sm);
|
|
2821
|
+
color: var(--plinth-color-text-muted);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
.plinth-menu-item[data-tone='danger'] .plinth-menu-icon {
|
|
2825
|
+
color: inherit;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
.plinth-menu-icon > svg {
|
|
2829
|
+
inline-size: 100%;
|
|
2830
|
+
block-size: 100%;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
.plinth-menu-label {
|
|
2834
|
+
flex: 1 1 auto;
|
|
2835
|
+
min-inline-size: 0;
|
|
2836
|
+
overflow-wrap: anywhere;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
.plinth-menu-shortcut {
|
|
2840
|
+
flex: none;
|
|
2841
|
+
padding-inline-start: var(--plinth-space-4);
|
|
2842
|
+
margin-inline-start: auto;
|
|
2843
|
+
font-family: var(--plinth-font-family-mono);
|
|
2844
|
+
font-size: var(--plinth-font-size-xs);
|
|
2845
|
+
color: var(--plinth-color-text-subtle);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
.plinth-menu-separator {
|
|
2849
|
+
block-size: 0;
|
|
2850
|
+
margin-block: var(--plinth-space-1);
|
|
2851
|
+
margin-inline: calc(-1 * var(--plinth-space-1));
|
|
2852
|
+
border: 0;
|
|
2853
|
+
border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
.plinth-menu-group {
|
|
2857
|
+
margin: 0;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
.plinth-menu-heading {
|
|
2861
|
+
padding-block: var(--plinth-space-2) var(--plinth-space-1);
|
|
2862
|
+
padding-inline: var(--plinth-space-2);
|
|
2863
|
+
font-size: var(--plinth-font-size-xs);
|
|
2864
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
2865
|
+
color: var(--plinth-color-text-subtle);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
@media (min-width: 768px) and (pointer: fine) {
|
|
2869
|
+
.plinth-menu-item {
|
|
2870
|
+
min-block-size: var(--plinth-control-md);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
@media (forced-colors: active) {
|
|
2875
|
+
.plinth-menu-item:disabled,
|
|
2876
|
+
.plinth-menu-item[aria-disabled='true'] {
|
|
2877
|
+
color: GrayText;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2739
2882
|
/* modal.css */
|
|
2740
2883
|
/*
|
|
2741
2884
|
* Modal base: what the dialog (dialog.css), the sheet (sheet.css) and the drawer (drawer.css)
|
|
@@ -3105,62 +3248,188 @@
|
|
|
3105
3248
|
}
|
|
3106
3249
|
}
|
|
3107
3250
|
|
|
3108
|
-
/*
|
|
3251
|
+
/* page-header.css */
|
|
3109
3252
|
/*
|
|
3110
|
-
*
|
|
3253
|
+
* Page header: the bar at the top of a page, with a leading action, the page's heading and
|
|
3254
|
+
* trailing actions. On a phone it is the screen's top bar; in an app shell it heads the page beside
|
|
3255
|
+
* the sidebar, under the app bar. It sticks to the top as the content scrolls under it.
|
|
3111
3256
|
*
|
|
3112
|
-
* <
|
|
3113
|
-
* <
|
|
3114
|
-
* <
|
|
3115
|
-
*
|
|
3116
|
-
*
|
|
3117
|
-
*
|
|
3118
|
-
*
|
|
3119
|
-
* </
|
|
3120
|
-
*
|
|
3257
|
+
* <header class="plinth-page-header" data-top>
|
|
3258
|
+
* <div class="plinth-page-header-leading">
|
|
3259
|
+
* <a class="plinth-button plinth-icon-button" href="/inbox" data-variant="ghost"
|
|
3260
|
+
* data-size="md" aria-label="Back">
|
|
3261
|
+
* <span class="plinth-icon" data-size="md" data-mirror aria-hidden="true"><svg>…</svg></span>
|
|
3262
|
+
* </a>
|
|
3263
|
+
* </div>
|
|
3264
|
+
* <h1 class="plinth-page-header-heading">Inbox</h1>
|
|
3265
|
+
* <div class="plinth-page-header-actions">
|
|
3266
|
+
* <button type="button" class="plinth-button plinth-icon-button" data-variant="ghost"
|
|
3267
|
+
* data-size="md" aria-label="Search">…</button>
|
|
3268
|
+
* </div>
|
|
3269
|
+
* </header>
|
|
3121
3270
|
*
|
|
3122
|
-
* The root is a <
|
|
3123
|
-
*
|
|
3124
|
-
*
|
|
3271
|
+
* The root is a <header>. At the top of the page (not inside <main>, <article>, <section>, <aside>
|
|
3272
|
+
* or <nav>) it is the page's banner landmark, so a page has one there; inside <main>, as in an app
|
|
3273
|
+
* shell, it is no landmark, only the page's header. The heading is required: an <h1> by default, or
|
|
3274
|
+
* <h2> to <h6> (level) when the page sits beside another page's <h1>, such as an open ticket beside
|
|
3275
|
+
* the list on a wide screen. The level doesn't change the look. The heading stays on one line and
|
|
3276
|
+
* ends in an ellipsis when it doesn't fit.
|
|
3125
3277
|
*
|
|
3126
|
-
*
|
|
3278
|
+
* The leading part holds one action, usually an icon button: a menu button that opens a sheet
|
|
3279
|
+
* (commandfor and command="show-modal") on a top-level page, or a back link (with data-mirror on
|
|
3280
|
+
* its icon so it points the other way in right-to-left text) on a page under it. The actions part
|
|
3281
|
+
* holds the trailing actions: at most three icon buttons (put the rest in a menu), or a primary
|
|
3282
|
+
* button such as New ticket. Both are optional, and printed only when they have content. Every
|
|
3283
|
+
* icon button needs its own name (label). The page header needs button.css and icon.css for them.
|
|
3127
3284
|
*
|
|
3128
|
-
*
|
|
3129
|
-
*
|
|
3130
|
-
*
|
|
3285
|
+
* Variant (the app sets it, the page header doesn't guess):
|
|
3286
|
+
* - data-top: the page header is the screen's top bar, at the very top of a phone's screen. It
|
|
3287
|
+
* adds env(safe-area-inset-top) above its height, which pads it below the status bar and the
|
|
3288
|
+
* notch on a phone that draws under them (viewport-fit=cover). Leave it off under an app bar.
|
|
3131
3289
|
*
|
|
3132
|
-
*
|
|
3133
|
-
*
|
|
3134
|
-
*
|
|
3135
|
-
*
|
|
3136
|
-
*
|
|
3137
|
-
*
|
|
3138
|
-
*
|
|
3139
|
-
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
3290
|
+
* Its height is the page-header.height component token (56px). The inline padding clears the
|
|
3291
|
+
* notch on a phone held sideways. It is sticky on the layer.sticky z-index, so it sits above the
|
|
3292
|
+
* page and below sheets and menus (the top layer), and it sticks --plinth-page-header-offset from
|
|
3293
|
+
* the top: 0 by default, or the app bar's height in a shell that has one. Content scrolls under
|
|
3294
|
+
* it, so the app sets scroll-padding-block-start on its scrolling element (the offset plus the
|
|
3295
|
+
* height, plus env(safe-area-inset-top) with data-top) to keep a focused element or an anchor's
|
|
3296
|
+
* target from landing under the page header.
|
|
3140
3297
|
*
|
|
3141
|
-
*
|
|
3142
|
-
*
|
|
3143
|
-
*
|
|
3144
|
-
*
|
|
3298
|
+
* The actions sit var(--plinth-space-2) apart, so on a touch screen the 40px hit areas of 32px
|
|
3299
|
+
* icon buttons don't overlap.
|
|
3300
|
+
*
|
|
3301
|
+
* The app owns the state: the page header has no states of its own. What the actions do is the
|
|
3302
|
+
* app's (a link, a commandfor, wire:click, @click on the icon button).
|
|
3145
3303
|
*/
|
|
3146
3304
|
|
|
3147
3305
|
@layer plinth {
|
|
3148
|
-
.plinth-
|
|
3306
|
+
.plinth-page-header {
|
|
3307
|
+
position: sticky;
|
|
3308
|
+
inset-block-start: var(--plinth-page-header-offset);
|
|
3309
|
+
z-index: var(--plinth-layer-sticky);
|
|
3149
3310
|
box-sizing: border-box;
|
|
3150
3311
|
display: flex;
|
|
3151
3312
|
gap: var(--plinth-space-2);
|
|
3152
3313
|
align-items: center;
|
|
3153
|
-
|
|
3154
|
-
padding-
|
|
3155
|
-
var(--plinth-space-
|
|
3156
|
-
|
|
3157
|
-
);
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3314
|
+
min-block-size: var(--plinth-page-header-height);
|
|
3315
|
+
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
|
|
3316
|
+
max(var(--plinth-space-2), env(safe-area-inset-right));
|
|
3317
|
+
font-family: var(--plinth-font-family-body);
|
|
3318
|
+
color: var(--plinth-color-text);
|
|
3319
|
+
background-color: var(--plinth-color-surface);
|
|
3320
|
+
border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
.plinth-page-header:dir(rtl) {
|
|
3324
|
+
padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
|
|
3325
|
+
max(var(--plinth-space-2), env(safe-area-inset-left));
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
/* The screen's top bar clears the status bar and the notch. */
|
|
3329
|
+
.plinth-page-header[data-top] {
|
|
3330
|
+
min-block-size: calc(var(--plinth-page-header-height) + env(safe-area-inset-top));
|
|
3331
|
+
padding-block-start: env(safe-area-inset-top);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
.plinth-page-header-leading,
|
|
3335
|
+
.plinth-page-header-actions {
|
|
3336
|
+
display: flex;
|
|
3337
|
+
flex: none;
|
|
3338
|
+
gap: var(--plinth-space-2);
|
|
3339
|
+
align-items: center;
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
.plinth-page-header-heading {
|
|
3343
|
+
flex: 1 1 auto;
|
|
3344
|
+
min-inline-size: 0;
|
|
3345
|
+
margin: 0;
|
|
3346
|
+
overflow: hidden;
|
|
3347
|
+
text-overflow: ellipsis;
|
|
3348
|
+
font-size: var(--plinth-font-size-lg);
|
|
3349
|
+
font-weight: var(--plinth-font-weight-semibold);
|
|
3350
|
+
line-height: var(--plinth-font-leading-tight);
|
|
3351
|
+
letter-spacing: var(--plinth-font-tracking-tight);
|
|
3352
|
+
white-space: nowrap;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
/* Without a leading action or trailing actions, the heading keeps the text's distance from the edge. */
|
|
3356
|
+
.plinth-page-header-heading:first-child {
|
|
3357
|
+
margin-inline-start: var(--plinth-space-2);
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
.plinth-page-header-heading:last-child {
|
|
3361
|
+
margin-inline-end: var(--plinth-space-2);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
@media (min-width: 768px) {
|
|
3365
|
+
.plinth-page-header {
|
|
3366
|
+
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
|
|
3367
|
+
max(var(--plinth-space-4), env(safe-area-inset-right));
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
.plinth-page-header:dir(rtl) {
|
|
3371
|
+
padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
|
|
3372
|
+
max(var(--plinth-space-4), env(safe-area-inset-left));
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
/* pill-row.css */
|
|
3378
|
+
/*
|
|
3379
|
+
* Pill row: a row of pills for filters, segments or sub-pages.
|
|
3380
|
+
*
|
|
3381
|
+
* <div class="plinth-pill-row" role="group" aria-label="Status">
|
|
3382
|
+
* <button type="button" class="plinth-pill" aria-pressed="true">
|
|
3383
|
+
* <span class="plinth-pill-label">Open</span>
|
|
3384
|
+
* <span class="plinth-pill-count">12</span>
|
|
3385
|
+
* </button>
|
|
3386
|
+
* <button type="button" class="plinth-pill" aria-pressed="false">
|
|
3387
|
+
* <span class="plinth-pill-label">Closed</span>
|
|
3388
|
+
* </button>
|
|
3389
|
+
* </div>
|
|
3390
|
+
*
|
|
3391
|
+
* The root is a <div role="group">, named by aria-label (label in Blade and Vue) or by the app's
|
|
3392
|
+
* own aria-labelledby. A row of links that navigates is <nav class="plinth-pill-row"> instead,
|
|
3393
|
+
* with no role (nav in Blade and Vue), and its pills are links:
|
|
3394
|
+
*
|
|
3395
|
+
* <a class="plinth-pill" href="…" aria-current="page"><span class="plinth-pill-label">…</span></a>
|
|
3396
|
+
*
|
|
3397
|
+
* Each pill is its own component (<x-plinth::pill>, PlinthPill), so the app's attributes and
|
|
3398
|
+
* listeners land on the pill (ADR 2). The count is optional. plinth-pill-label has no rule of its
|
|
3399
|
+
* own: it is a hook for a project's styles.
|
|
3400
|
+
*
|
|
3401
|
+
* States, set by the app:
|
|
3402
|
+
* - selected: a button pill always prints aria-pressed, "true" or "false"; a link pill prints
|
|
3403
|
+
* aria-current="page" for the page being shown. Selected takes the selection tint
|
|
3404
|
+
* (accent-subtle, accent-text) inside a 2px accent edge (border-width.thick), where the others
|
|
3405
|
+
* have a 1px neutral one, so colour is not its only cue. A pressed button still answers hover
|
|
3406
|
+
* and press with a step on the tint (accent-subtle-hover, accent-subtle-active).
|
|
3407
|
+
* - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
|
|
3408
|
+
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
3409
|
+
*
|
|
3410
|
+
* Layout: on a phone the row scrolls sideways, with no visible scrollbar, and snaps to the pills;
|
|
3411
|
+
* from 768px it wraps. The row's padding keeps the focus ring and the 40px touch target of the
|
|
3412
|
+
* pills inside the scrolling area, whatever --plinth-pill-height is (32px by default). A pill is
|
|
3413
|
+
* at least 40px wide.
|
|
3414
|
+
*/
|
|
3415
|
+
|
|
3416
|
+
@layer plinth {
|
|
3417
|
+
.plinth-pill-row {
|
|
3418
|
+
box-sizing: border-box;
|
|
3419
|
+
display: flex;
|
|
3420
|
+
gap: var(--plinth-space-2);
|
|
3421
|
+
align-items: center;
|
|
3422
|
+
max-inline-size: 100%;
|
|
3423
|
+
padding-block: max(
|
|
3424
|
+
var(--plinth-space-1),
|
|
3425
|
+
(var(--plinth-touch-target) - var(--plinth-pill-height)) / 2
|
|
3426
|
+
);
|
|
3427
|
+
padding-inline: var(--plinth-space-1);
|
|
3428
|
+
margin: 0;
|
|
3429
|
+
overflow-x: auto;
|
|
3430
|
+
overscroll-behavior-inline: contain;
|
|
3431
|
+
scroll-snap-type: inline proximity;
|
|
3432
|
+
scroll-padding-inline: var(--plinth-space-1);
|
|
3164
3433
|
scrollbar-width: none;
|
|
3165
3434
|
}
|
|
3166
3435
|
|
|
@@ -3306,7 +3575,7 @@
|
|
|
3306
3575
|
|
|
3307
3576
|
/* section-menu.css */
|
|
3308
3577
|
/*
|
|
3309
|
-
* Section menu: the row under the header that switches between the sections of a page.
|
|
3578
|
+
* Section menu: the row under the page header that switches between the sections of a page.
|
|
3310
3579
|
*
|
|
3311
3580
|
* <nav class="plinth-section-menu" aria-label="Project">
|
|
3312
3581
|
* <a class="plinth-section-menu-item" href="/overview" aria-current="page">
|
|
@@ -3340,15 +3609,16 @@
|
|
|
3340
3609
|
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
3341
3610
|
*
|
|
3342
3611
|
* Layout: a row --plinth-section-menu-height (56px) tall, on the surface with a divider below,
|
|
3343
|
-
* the same inline padding as the header and clear of the notch on a phone held sideways. Its
|
|
3612
|
+
* the same inline padding as the page header and clear of the notch on a phone held sideways. Its
|
|
3344
3613
|
* items are --plinth-section-menu-item-height (40px) tall, never less than the 40px touch
|
|
3345
3614
|
* target, and at least 40px wide. The row never wraps: when the items don't fit it scrolls
|
|
3346
3615
|
* sideways and snaps to them, with a thin scrollbar for a mouse and none on a touch screen; the
|
|
3347
3616
|
* row's block padding keeps the focus ring inside it. The app scrolls a current item that starts
|
|
3348
3617
|
* out of view into view.
|
|
3349
|
-
* Placement is the app's: put it under the header, or make it sticky below it with
|
|
3350
|
-
* position: sticky, inset-block-start: calc(var(--plinth-header-
|
|
3351
|
-
* env(safe-area-inset-top)
|
|
3618
|
+
* Placement is the app's: put it under the page header, or make it sticky below it with
|
|
3619
|
+
* position: sticky, inset-block-start: calc(var(--plinth-page-header-offset) +
|
|
3620
|
+
* var(--plinth-page-header-height)), plus env(safe-area-inset-top) under the screen's top bar,
|
|
3621
|
+
* and z-index: var(--plinth-layer-sticky).
|
|
3352
3622
|
*/
|
|
3353
3623
|
|
|
3354
3624
|
@layer plinth {
|
|
@@ -3771,3 +4041,406 @@
|
|
|
3771
4041
|
}
|
|
3772
4042
|
}
|
|
3773
4043
|
}
|
|
4044
|
+
|
|
4045
|
+
/* text-field.css */
|
|
4046
|
+
/*
|
|
4047
|
+
* Text field: one line of text, with its label, an optional description and an error.
|
|
4048
|
+
*
|
|
4049
|
+
* <div class="plinth-text-field" data-size="md">
|
|
4050
|
+
* <label class="plinth-text-field-label" for="email">Email</label>
|
|
4051
|
+
* <div class="plinth-text-field-control">
|
|
4052
|
+
* <span class="plinth-icon" data-size="sm" aria-hidden="true"><svg>…</svg></span>
|
|
4053
|
+
* <input type="email" class="plinth-text-field-input" id="email"
|
|
4054
|
+
* aria-describedby="email-description email-error" aria-invalid="true">
|
|
4055
|
+
* <span class="plinth-text-field-trailing">…</span>
|
|
4056
|
+
* </div>
|
|
4057
|
+
* <p class="plinth-text-field-description" id="email-description">For receipts only.</p>
|
|
4058
|
+
* <p class="plinth-text-field-error" id="email-error">Enter an email address.</p>
|
|
4059
|
+
* </div>
|
|
4060
|
+
*
|
|
4061
|
+
* The input needs an id: the label points at it with for, and the description's and error's ids
|
|
4062
|
+
* are the input's id with -description and -error. The icon, the trailing slot, the description
|
|
4063
|
+
* and the error are optional; aria-describedby lists the description, then the error, and is left
|
|
4064
|
+
* out when there is neither.
|
|
4065
|
+
*
|
|
4066
|
+
* Attributes go to the input, not the root (name, value, placeholder, autocomplete, required,
|
|
4067
|
+
* wire:model, x-model, data-*, aria-*), so the app talks to the input; only a project's class is
|
|
4068
|
+
* added to the root's.
|
|
4069
|
+
*
|
|
4070
|
+
* - type (on the input): text by default; email, url, tel, password, number or search. A search
|
|
4071
|
+
* field takes a search icon and, in the trailing slot, a clear button (a small ghost icon
|
|
4072
|
+
* button) that the app shows when there is text and wires up. With a trailing slot, the
|
|
4073
|
+
* browser's own clear button is hidden, so there is only one.
|
|
4074
|
+
* - the leading icon is 16px and decorative (aria-hidden); the label names the field.
|
|
4075
|
+
* - the trailing slot holds one small control or hint, in the room the field keeps at its end
|
|
4076
|
+
* (--plinth-text-field-trailing-size, 32px): a small icon button, or a keyboard hint as <kbd>.
|
|
4077
|
+
* - data-label="hidden" on the root hides the label visually; screen readers still read it. Use it
|
|
4078
|
+
* only where the field's purpose is plain without it, such as a search box with its icon.
|
|
4079
|
+
*
|
|
4080
|
+
* Sizes (data-size): md 32px (default, also when missing) and sm 28px. On a touch screen both are
|
|
4081
|
+
* 40px tall. The text is 16px on a phone, so iOS doesn't zoom into the field, and from 768px
|
|
4082
|
+
* up it is 14px (md) or 13px (sm).
|
|
4083
|
+
*
|
|
4084
|
+
* States:
|
|
4085
|
+
* - error: aria-invalid="true" on the input and the error message under it; the edge turns
|
|
4086
|
+
* danger and thick, so it isn't only a colour, and the message says what to fix.
|
|
4087
|
+
* - disabled: `disabled` on the input; the field is faded and can't be focused.
|
|
4088
|
+
* - read-only: `readonly` on the input; it keeps its text and focus, without the field's fill
|
|
4089
|
+
* and with a dashed edge.
|
|
4090
|
+
* - the focus ring on the input's :focus-visible.
|
|
4091
|
+
*/
|
|
4092
|
+
|
|
4093
|
+
@layer plinth {
|
|
4094
|
+
.plinth-text-field {
|
|
4095
|
+
--plinth-text-field-height: var(--plinth-control-md);
|
|
4096
|
+
--plinth-text-field-text: var(--plinth-font-size-md);
|
|
4097
|
+
|
|
4098
|
+
display: grid;
|
|
4099
|
+
gap: var(--plinth-space-2);
|
|
4100
|
+
min-inline-size: 0;
|
|
4101
|
+
font-family: var(--plinth-font-family-body);
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
.plinth-text-field[data-size='sm'] {
|
|
4105
|
+
--plinth-text-field-height: var(--plinth-control-sm);
|
|
4106
|
+
--plinth-text-field-text: var(--plinth-font-size-sm);
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
.plinth-text-field-label {
|
|
4110
|
+
font-size: var(--plinth-font-size-sm);
|
|
4111
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
4112
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4113
|
+
color: var(--plinth-color-text-muted);
|
|
4114
|
+
overflow-wrap: anywhere;
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
/* Hidden from sight, still read by screen readers. */
|
|
4118
|
+
.plinth-text-field[data-label='hidden'] .plinth-text-field-label {
|
|
4119
|
+
position: absolute;
|
|
4120
|
+
inline-size: 1px;
|
|
4121
|
+
block-size: 1px;
|
|
4122
|
+
overflow: hidden;
|
|
4123
|
+
white-space: nowrap;
|
|
4124
|
+
clip-path: inset(50%);
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
.plinth-text-field-control {
|
|
4128
|
+
position: relative;
|
|
4129
|
+
display: flex;
|
|
4130
|
+
align-items: center;
|
|
4131
|
+
min-inline-size: 0;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
.plinth-text-field-input {
|
|
4135
|
+
box-sizing: border-box;
|
|
4136
|
+
flex: 1;
|
|
4137
|
+
min-inline-size: 0;
|
|
4138
|
+
min-block-size: var(--plinth-text-field-height);
|
|
4139
|
+
padding-block: 0;
|
|
4140
|
+
padding-inline: var(--plinth-space-3);
|
|
4141
|
+
margin: 0;
|
|
4142
|
+
font-family: inherit;
|
|
4143
|
+
font-size: var(--plinth-font-size-field);
|
|
4144
|
+
line-height: var(--plinth-font-leading-tight);
|
|
4145
|
+
color: var(--plinth-color-text);
|
|
4146
|
+
appearance: none;
|
|
4147
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
4148
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
4149
|
+
border-radius: var(--plinth-radius-control);
|
|
4150
|
+
transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
.plinth-text-field-input::placeholder {
|
|
4154
|
+
color: var(--plinth-color-text-subtle);
|
|
4155
|
+
opacity: 1;
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
@media (hover: hover) {
|
|
4159
|
+
.plinth-text-field-input:hover:not(:disabled, :read-only, [aria-invalid='true']) {
|
|
4160
|
+
border-color: var(--plinth-color-text-subtle);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
.plinth-text-field-input:focus-visible {
|
|
4165
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
4166
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
.plinth-text-field-input[aria-invalid='true'] {
|
|
4170
|
+
border-color: var(--plinth-color-danger);
|
|
4171
|
+
border-width: var(--plinth-border-width-thick);
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
.plinth-text-field-input:disabled {
|
|
4175
|
+
cursor: not-allowed;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
/* Without the fill, and with a dashed edge, so it doesn't read as a field to type in. */
|
|
4179
|
+
.plinth-text-field-input:read-only:not(:disabled) {
|
|
4180
|
+
background-color: transparent;
|
|
4181
|
+
border-style: dashed;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
.plinth-text-field-control:has(.plinth-text-field-input:disabled) {
|
|
4185
|
+
opacity: var(--plinth-opacity-disabled);
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
/* The leading icon sits over the input's start, which keeps room for it. */
|
|
4189
|
+
.plinth-text-field-control > .plinth-icon {
|
|
4190
|
+
position: absolute;
|
|
4191
|
+
inset-inline-start: var(--plinth-space-3);
|
|
4192
|
+
color: var(--plinth-color-text-subtle);
|
|
4193
|
+
pointer-events: none;
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
.plinth-text-field-control:has(> .plinth-icon) > .plinth-text-field-input {
|
|
4197
|
+
padding-inline-start: calc(
|
|
4198
|
+
var(--plinth-space-3) + var(--plinth-size-icon-sm) + var(--plinth-space-2)
|
|
4199
|
+
);
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
.plinth-text-field-trailing {
|
|
4203
|
+
position: absolute;
|
|
4204
|
+
inset-inline-end: var(--plinth-space-1);
|
|
4205
|
+
display: flex;
|
|
4206
|
+
gap: var(--plinth-space-1);
|
|
4207
|
+
align-items: center;
|
|
4208
|
+
justify-content: center;
|
|
4209
|
+
min-inline-size: var(--plinth-text-field-trailing-size);
|
|
4210
|
+
color: var(--plinth-color-text-subtle);
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
.plinth-text-field-control:has(> .plinth-text-field-trailing) > .plinth-text-field-input {
|
|
4214
|
+
padding-inline-end: calc(var(--plinth-space-1) + var(--plinth-text-field-trailing-size));
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
/* With its own clear button in the trailing slot, the browser's is one too many. */
|
|
4218
|
+
.plinth-text-field-control:has(> .plinth-text-field-trailing)
|
|
4219
|
+
> .plinth-text-field-input::-webkit-search-cancel-button {
|
|
4220
|
+
display: none;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
.plinth-text-field-trailing kbd {
|
|
4224
|
+
box-sizing: border-box;
|
|
4225
|
+
min-inline-size: var(--plinth-size-icon-md);
|
|
4226
|
+
padding-block: 0;
|
|
4227
|
+
padding-inline: var(--plinth-space-1);
|
|
4228
|
+
font-family: var(--plinth-font-family-mono);
|
|
4229
|
+
font-size: var(--plinth-font-size-xs);
|
|
4230
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4231
|
+
color: var(--plinth-color-text-muted);
|
|
4232
|
+
text-align: center;
|
|
4233
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
4234
|
+
border-radius: var(--plinth-radius-control);
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
.plinth-text-field-description,
|
|
4238
|
+
.plinth-text-field-error {
|
|
4239
|
+
margin: 0;
|
|
4240
|
+
font-size: var(--plinth-font-size-sm);
|
|
4241
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4242
|
+
overflow-wrap: anywhere;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
.plinth-text-field-description {
|
|
4246
|
+
color: var(--plinth-color-text-muted);
|
|
4247
|
+
}
|
|
4248
|
+
|
|
4249
|
+
.plinth-text-field-error {
|
|
4250
|
+
color: var(--plinth-color-danger-text);
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
@media (pointer: coarse) {
|
|
4254
|
+
.plinth-text-field-input {
|
|
4255
|
+
min-block-size: var(--plinth-touch-target);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
@media (min-width: 768px) {
|
|
4260
|
+
.plinth-text-field-input {
|
|
4261
|
+
font-size: var(--plinth-text-field-text);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
@media (forced-colors: active) {
|
|
4266
|
+
.plinth-text-field-control:has(.plinth-text-field-input:disabled) {
|
|
4267
|
+
color: GrayText;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
.plinth-text-field-input:disabled {
|
|
4271
|
+
color: GrayText;
|
|
4272
|
+
border-color: GrayText;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
/* textarea.css */
|
|
4278
|
+
/*
|
|
4279
|
+
* Textarea: several lines of text, with its label, an optional description and an error. With
|
|
4280
|
+
* grow, it grows with its content.
|
|
4281
|
+
*
|
|
4282
|
+
* <div class="plinth-textarea">
|
|
4283
|
+
* <label class="plinth-textarea-label" for="reply">Reply</label>
|
|
4284
|
+
* <textarea class="plinth-textarea-input" id="reply" rows="3"
|
|
4285
|
+
* aria-describedby="reply-description reply-error" aria-invalid="true"></textarea>
|
|
4286
|
+
* <p class="plinth-textarea-description" id="reply-description">The customer sees it.</p>
|
|
4287
|
+
* <p class="plinth-textarea-error" id="reply-error">Write a reply first.</p>
|
|
4288
|
+
* </div>
|
|
4289
|
+
*
|
|
4290
|
+
* The textarea needs an id: the label points at it with for, and the description's and error's
|
|
4291
|
+
* ids are the textarea's id with -description and -error. The description and the error are
|
|
4292
|
+
* optional; aria-describedby lists the description, then the error, and is left out when there is
|
|
4293
|
+
* neither. rows is 3 by default.
|
|
4294
|
+
*
|
|
4295
|
+
* Attributes go to the textarea, not the root (name, placeholder, maxlength, required,
|
|
4296
|
+
* wire:model, x-model, data-*, aria-*), so the app talks to the textarea; only a project's class
|
|
4297
|
+
* is added to the root's.
|
|
4298
|
+
*
|
|
4299
|
+
* - grow: data-grow on the textarea, with its rows and maximum as custom properties:
|
|
4300
|
+
* <textarea class="plinth-textarea-input" id="reply" rows="3" data-grow
|
|
4301
|
+
* style="--plinth-textarea-rows: 3; --plinth-textarea-max-rows: 8"></textarea>
|
|
4302
|
+
* It starts at rows lines and grows with its content (field-sizing: content) up to max-rows
|
|
4303
|
+
* (8 by default), then scrolls. Where the browser has no field-sizing it keeps rows lines and
|
|
4304
|
+
* the person can resize it, as without grow.
|
|
4305
|
+
* - data-label="hidden" on the root hides the label visually; screen readers still read it. Use it
|
|
4306
|
+
* only where the field's purpose is plain without it, such as a reply box under a thread.
|
|
4307
|
+
*
|
|
4308
|
+
* The text is 16px on a phone, so iOS doesn't zoom into the field, and 14px from 768px up.
|
|
4309
|
+
*
|
|
4310
|
+
* States:
|
|
4311
|
+
* - error: aria-invalid="true" on the textarea and the error message under it; the edge turns
|
|
4312
|
+
* danger and thick, so it isn't only a colour, and the message says what to fix.
|
|
4313
|
+
* - disabled: `disabled` on the textarea; the field is faded and can't be focused.
|
|
4314
|
+
* - read-only: `readonly` on the textarea; it keeps its text and focus, without the field's fill
|
|
4315
|
+
* and with a dashed edge.
|
|
4316
|
+
* - the focus ring on the textarea's :focus-visible.
|
|
4317
|
+
*/
|
|
4318
|
+
|
|
4319
|
+
@layer plinth {
|
|
4320
|
+
.plinth-textarea {
|
|
4321
|
+
display: grid;
|
|
4322
|
+
gap: var(--plinth-space-2);
|
|
4323
|
+
min-inline-size: 0;
|
|
4324
|
+
font-family: var(--plinth-font-family-body);
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
.plinth-textarea-label {
|
|
4328
|
+
font-size: var(--plinth-font-size-sm);
|
|
4329
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
4330
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4331
|
+
color: var(--plinth-color-text-muted);
|
|
4332
|
+
overflow-wrap: anywhere;
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
/* Hidden from sight, still read by screen readers. */
|
|
4336
|
+
.plinth-textarea[data-label='hidden'] .plinth-textarea-label {
|
|
4337
|
+
position: absolute;
|
|
4338
|
+
inline-size: 1px;
|
|
4339
|
+
block-size: 1px;
|
|
4340
|
+
overflow: hidden;
|
|
4341
|
+
white-space: nowrap;
|
|
4342
|
+
clip-path: inset(50%);
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
.plinth-textarea-input {
|
|
4346
|
+
box-sizing: border-box;
|
|
4347
|
+
inline-size: 100%;
|
|
4348
|
+
min-block-size: var(--plinth-touch-target);
|
|
4349
|
+
padding-block: var(--plinth-space-2);
|
|
4350
|
+
padding-inline: var(--plinth-space-3);
|
|
4351
|
+
margin: 0;
|
|
4352
|
+
font-family: inherit;
|
|
4353
|
+
font-size: var(--plinth-font-size-field);
|
|
4354
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4355
|
+
color: var(--plinth-color-text);
|
|
4356
|
+
resize: vertical;
|
|
4357
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
4358
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
4359
|
+
border-radius: var(--plinth-radius-control);
|
|
4360
|
+
transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
.plinth-textarea-input::placeholder {
|
|
4364
|
+
color: var(--plinth-color-text-subtle);
|
|
4365
|
+
opacity: 1;
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
@media (hover: hover) {
|
|
4369
|
+
.plinth-textarea-input:hover:not(:disabled, :read-only, [aria-invalid='true']) {
|
|
4370
|
+
border-color: var(--plinth-color-text-subtle);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
.plinth-textarea-input:focus-visible {
|
|
4375
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
4376
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
.plinth-textarea-input[aria-invalid='true'] {
|
|
4380
|
+
border-color: var(--plinth-color-danger);
|
|
4381
|
+
border-width: var(--plinth-border-width-thick);
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
.plinth-textarea-input:disabled {
|
|
4385
|
+
cursor: not-allowed;
|
|
4386
|
+
resize: none;
|
|
4387
|
+
opacity: var(--plinth-opacity-disabled);
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
/* Without the fill, and with a dashed edge, so it doesn't read as a field to type in. */
|
|
4391
|
+
.plinth-textarea-input:read-only:not(:disabled) {
|
|
4392
|
+
background-color: transparent;
|
|
4393
|
+
border-style: dashed;
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
/*
|
|
4397
|
+
* Grow: from rows lines to max-rows lines (in the textarea's own line height, with its padding
|
|
4398
|
+
* and edge), then it scrolls.
|
|
4399
|
+
*/
|
|
4400
|
+
@supports (field-sizing: content) {
|
|
4401
|
+
.plinth-textarea-input[data-grow] {
|
|
4402
|
+
field-sizing: content;
|
|
4403
|
+
min-block-size: max(
|
|
4404
|
+
var(--plinth-touch-target),
|
|
4405
|
+
calc(
|
|
4406
|
+
var(--plinth-textarea-rows, 3) * 1lh + 2 * var(--plinth-space-2) + 2 *
|
|
4407
|
+
var(--plinth-border-width-thin)
|
|
4408
|
+
)
|
|
4409
|
+
);
|
|
4410
|
+
max-block-size: calc(
|
|
4411
|
+
var(--plinth-textarea-max-rows, 8) * 1lh + 2 * var(--plinth-space-2) + 2 *
|
|
4412
|
+
var(--plinth-border-width-thin)
|
|
4413
|
+
);
|
|
4414
|
+
resize: none;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
.plinth-textarea-description,
|
|
4419
|
+
.plinth-textarea-error {
|
|
4420
|
+
margin: 0;
|
|
4421
|
+
font-size: var(--plinth-font-size-sm);
|
|
4422
|
+
line-height: var(--plinth-font-leading-normal);
|
|
4423
|
+
overflow-wrap: anywhere;
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
.plinth-textarea-description {
|
|
4427
|
+
color: var(--plinth-color-text-muted);
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
.plinth-textarea-error {
|
|
4431
|
+
color: var(--plinth-color-danger-text);
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
@media (min-width: 768px) {
|
|
4435
|
+
.plinth-textarea-input {
|
|
4436
|
+
font-size: var(--plinth-font-size-md);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
@media (forced-colors: active) {
|
|
4441
|
+
.plinth-textarea-input:disabled {
|
|
4442
|
+
color: GrayText;
|
|
4443
|
+
border-color: GrayText;
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
}
|