@apliteni/apliteni-ui 0.71.0 → 0.73.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/guidelines/accessibility-floor.md +28 -0
- package/package.json +1 -1
- package/react/README.md +27 -1
- package/react/dist/index.css +83 -0
- package/react/dist/index.d.ts +6 -2
- package/react/dist/index.js +24 -14
- package/src/components/index.js +91 -18
- package/src/components/topbar.js +17 -2
- package/src/index.css +3 -0
- package/src/index.d.ts +6 -0
- package/src/inline.js +4 -0
- package/src/styles/base.css +1 -0
- package/src/styles/code.css +28 -5
- package/src/styles/tap-zone.css +167 -0
|
@@ -14,6 +14,34 @@
|
|
|
14
14
|
|
|
15
15
|
**Except:** WCAG 2.5.8 permits spacing, equivalent controls, inline text, user-agent sizes and essential presentation. Overlays must not reach neighbours.
|
|
16
16
|
|
|
17
|
+
## Tap target on a phone
|
|
18
|
+
|
|
19
|
+
<!-- rule: tap-zone -->
|
|
20
|
+
|
|
21
|
+
**Rule:** Below the phone step, reach 44x44 with a transparent layer outside the control, never by growing the control.
|
|
22
|
+
|
|
23
|
+
**Why:** A finger covers more than a cursor, and a control that grows on a phone moves the page a reader already knows.
|
|
24
|
+
|
|
25
|
+
**Do:** Centre a transparent layer on the control and let it stop at half the gap to the nearest neighbour.
|
|
26
|
+
|
|
27
|
+
**Don't:** Grow the drawn control to 44 and push out every row that holds one.
|
|
28
|
+
|
|
29
|
+
**Except:** Only a coarse pointer gets the layer, or a cursor lights a control it is not over. Rows that share an edge — a menu, a dense table, a settings list — have nothing to grow into and stay as they are drawn. Fields generate no layer, so a field reaches the floor by its own height or not at all.
|
|
30
|
+
|
|
31
|
+
## Space between small targets
|
|
32
|
+
|
|
33
|
+
<!-- rule: tap-spacing -->
|
|
34
|
+
|
|
35
|
+
**Rule:** Below the phone step, open a row of small controls to 44 minus their drawn size, so both tap zones fit between the drawn edges.
|
|
36
|
+
|
|
37
|
+
**Why:** A zone reaches half the gap before it crosses its neighbour's edge, so the gap is what decides whether either control reaches the floor.
|
|
38
|
+
|
|
39
|
+
**Do:** Open a row of 24px marks to 20px on a phone, where both zones fit and neither mark changes size.
|
|
40
|
+
|
|
41
|
+
**Don't:** Keep the desktop 8px and let the control written later in the markup win the overlap.
|
|
42
|
+
|
|
43
|
+
**Except:** A row that is already full opens downward only — widening it across makes the controls narrower, which is the one thing the floor must not do. Rows that share an edge — a menu, a dense table, a settings list — have nothing to open and keep their drawn targets. A row that opens nothing grows no zone at all.
|
|
44
|
+
|
|
17
45
|
## Focus ring contrast
|
|
18
46
|
|
|
19
47
|
<!-- rule: ring-contrast -->
|
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Both forward a span ref and native span attributes, including `className`.
|
|
|
43
43
|
<span><StatusDot live /> API online</span>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`Snippet` accepts `label`, `code`, `reveal`,
|
|
46
|
+
`Snippet` accepts `label`, `code`, `reveal`, `copy`, `copyLabel`, and `children` props. It treats `code`
|
|
47
47
|
as plain text and copies it exactly as provided. After a successful clipboard write,
|
|
48
48
|
it shows “Copied” for 1.4 seconds. If copying fails, it shows “Copy failed” so readers
|
|
49
49
|
can try again or select the text manually.
|
|
@@ -52,6 +52,32 @@ can try again or select the text manually.
|
|
|
52
52
|
<Snippet label="Terminal" code="npm install @apliteni/apliteni-ui" />
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
Set `copy={false}` to omit the copy button. Supply React children for highlighted
|
|
56
|
+
text using the existing `.k` (command), `.f` (flag), `.s` (string), `.u` (URL), and
|
|
57
|
+
`.c` (comment) classes. Keep `code` as the original source: copying always uses it,
|
|
58
|
+
regardless of the displayed children. Neither strings nor children are parsed as HTML.
|
|
59
|
+
|
|
60
|
+
Build the children from `code` with `codeTokens`, the same tokenizer the vanilla
|
|
61
|
+
`hlShell` and `hlCode` use, so the text on screen cannot drift from the text the
|
|
62
|
+
button copies. Writing the spans out by hand means keeping two copies of the string
|
|
63
|
+
in step. The second argument is the language — `shell` (the default), `json` or
|
|
64
|
+
`ts`; `codeLanguages` lists them.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { codeTokens } from '@apliteni/apliteni-ui';
|
|
68
|
+
|
|
69
|
+
const code = '{ "url": "https://example.com/mcp" }';
|
|
70
|
+
|
|
71
|
+
<Snippet label="mcp.json" code={code} copyLabel="Copy configuration">
|
|
72
|
+
{codeTokens(code, 'json').map(({ cls, text }, at) =>
|
|
73
|
+
cls ? <span key={at} className={cls}>{text}</span> : text)}
|
|
74
|
+
</Snippet>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The copy button is icon-only: `copyLabel` is its accessible name and its tooltip
|
|
78
|
+
rather than visible text, so name what is being copied — “Copy command”, “Copy
|
|
79
|
+
configuration”. It still shows words while it confirms.
|
|
80
|
+
|
|
55
81
|
Before a reveal snippet, explain that the secret is stored hashed and will not be
|
|
56
82
|
shown again, and ask the reader to copy it now. The page decides when to remove it.
|
|
57
83
|
|
package/react/dist/index.css
CHANGED
|
@@ -26,6 +26,89 @@
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/* ../src/styles/tap-zone.css */
|
|
30
|
+
:root {
|
|
31
|
+
--tap-min: 44px;
|
|
32
|
+
--tap-aa: 24px;
|
|
33
|
+
--tap-gap: var(--space-5, 20px);
|
|
34
|
+
--tap-clear-x: 0px;
|
|
35
|
+
--tap-clear-y: 0px;
|
|
36
|
+
}
|
|
37
|
+
@media (max-width: 560px) and (pointer: coarse) {
|
|
38
|
+
:where(.ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social) {
|
|
39
|
+
position: relative;
|
|
40
|
+
}
|
|
41
|
+
:where(.ui-btn, .ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social, .ui-switch)::after {
|
|
42
|
+
content: "";
|
|
43
|
+
position: absolute;
|
|
44
|
+
left: 50%;
|
|
45
|
+
top: 50%;
|
|
46
|
+
transform: translate(-50%, -50%);
|
|
47
|
+
width: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x)));
|
|
48
|
+
height: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y)));
|
|
49
|
+
}
|
|
50
|
+
.ui-check input::before,
|
|
51
|
+
.ui-toast__close::before {
|
|
52
|
+
width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
|
|
53
|
+
height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
|
|
54
|
+
}
|
|
55
|
+
.ui-seg {
|
|
56
|
+
row-gap: var(--tap-gap) !important;
|
|
57
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
58
|
+
--tap-clear-y: var(--tap-gap);
|
|
59
|
+
}
|
|
60
|
+
.ui-tabs__list {
|
|
61
|
+
--tap-clear-x: 2px;
|
|
62
|
+
--tap-clear-y: var(--tap-gap);
|
|
63
|
+
}
|
|
64
|
+
.ui-pager__steps {
|
|
65
|
+
row-gap: var(--tap-gap) !important;
|
|
66
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
67
|
+
--tap-clear-y: var(--tap-gap);
|
|
68
|
+
}
|
|
69
|
+
.ui-pager {
|
|
70
|
+
row-gap: var(--tap-gap) !important;
|
|
71
|
+
--tap-clear-y: var(--tap-gap);
|
|
72
|
+
}
|
|
73
|
+
.ui-filter-bar {
|
|
74
|
+
gap: var(--tap-gap) !important;
|
|
75
|
+
--tap-clear-x: var(--tap-gap);
|
|
76
|
+
--tap-clear-y: var(--tap-gap);
|
|
77
|
+
}
|
|
78
|
+
.ui-filter-bar__chip {
|
|
79
|
+
--tap-clear-x: 0px;
|
|
80
|
+
--tap-clear-y: 0px;
|
|
81
|
+
}
|
|
82
|
+
.ui-table__act .ui-btn + .ui-btn {
|
|
83
|
+
margin-inline-start: var(--tap-gap) !important;
|
|
84
|
+
}
|
|
85
|
+
.ui-table__act {
|
|
86
|
+
--tap-clear-x: var(--tap-gap);
|
|
87
|
+
--tap-clear-y: 0px;
|
|
88
|
+
}
|
|
89
|
+
.ui-toast {
|
|
90
|
+
row-gap: var(--tap-gap) !important;
|
|
91
|
+
--tap-clear-y: var(--tap-gap);
|
|
92
|
+
}
|
|
93
|
+
.ui-confirm__acts,
|
|
94
|
+
.ui-toolbar,
|
|
95
|
+
.ui-empty__actions,
|
|
96
|
+
.ui-sx__actions,
|
|
97
|
+
.ui-fbc__foot,
|
|
98
|
+
.ui-drawer__footer,
|
|
99
|
+
.ui-snippet__bar,
|
|
100
|
+
.ui-app__main .ui-card__row {
|
|
101
|
+
row-gap: var(--tap-gap) !important;
|
|
102
|
+
--tap-clear-y: var(--tap-gap);
|
|
103
|
+
}
|
|
104
|
+
.ui-footer__socials,
|
|
105
|
+
.ui-drawer__header {
|
|
106
|
+
gap: var(--tap-gap) !important;
|
|
107
|
+
--tap-clear-x: var(--tap-gap);
|
|
108
|
+
--tap-clear-y: var(--tap-gap);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
29
112
|
/* ../src/styles/input.css */
|
|
30
113
|
.ui-field {
|
|
31
114
|
display: block;
|
package/react/dist/index.d.ts
CHANGED
|
@@ -519,12 +519,16 @@ declare function DrawerSection({ title, headingLevel, className, children, ...re
|
|
|
519
519
|
|
|
520
520
|
type SnippetProps = {
|
|
521
521
|
label?: string;
|
|
522
|
-
/**
|
|
522
|
+
/** Original text to copy; displayed when children are absent. */
|
|
523
523
|
code?: string;
|
|
524
|
+
/** Token markup for display only. Strings are never parsed as HTML. */
|
|
525
|
+
children?: ReactNode;
|
|
526
|
+
copy?: boolean;
|
|
524
527
|
reveal?: boolean;
|
|
528
|
+
/** Accessible name and tooltip for the icon-only copy button; name what it copies. */
|
|
525
529
|
copyLabel?: string;
|
|
526
530
|
};
|
|
527
|
-
declare function Snippet({ label, code, reveal, copyLabel }: SnippetProps): react.JSX.Element;
|
|
531
|
+
declare function Snippet({ label, code, children, copy, reveal, copyLabel }: SnippetProps): react.JSX.Element;
|
|
528
532
|
|
|
529
533
|
type TabItem = {
|
|
530
534
|
value: string;
|
package/react/dist/index.js
CHANGED
|
@@ -2048,8 +2048,8 @@ function DrawerSection({ title, headingLevel = 3, className, children, ...rest }
|
|
|
2048
2048
|
|
|
2049
2049
|
// src/Snippet.tsx
|
|
2050
2050
|
import { useEffect as useEffect7, useRef as useRef12, useState as useState8 } from "react";
|
|
2051
|
-
import { jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2052
|
-
function Snippet({ label = "shell", code = "", reveal = false, copyLabel = "Copy" }) {
|
|
2051
|
+
import { Fragment as Fragment6, jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2052
|
+
function Snippet({ label = "shell", code = "", children, copy = true, reveal = false, copyLabel = "Copy code" }) {
|
|
2053
2053
|
const [status, setStatus] = useState8("idle");
|
|
2054
2054
|
const request = useRef12(0);
|
|
2055
2055
|
const timer = useRef12(void 0);
|
|
@@ -2059,8 +2059,8 @@ function Snippet({ label = "shell", code = "", reveal = false, copyLabel = "Copy
|
|
|
2059
2059
|
request.current++;
|
|
2060
2060
|
clearTimeout(timer.current);
|
|
2061
2061
|
};
|
|
2062
|
-
}, [code]);
|
|
2063
|
-
async function
|
|
2062
|
+
}, [code, copy]);
|
|
2063
|
+
async function handleCopy() {
|
|
2064
2064
|
const current = ++request.current;
|
|
2065
2065
|
clearTimeout(timer.current);
|
|
2066
2066
|
setStatus("idle");
|
|
@@ -2076,19 +2076,29 @@ function Snippet({ label = "shell", code = "", reveal = false, copyLabel = "Copy
|
|
|
2076
2076
|
return /* @__PURE__ */ jsxs20("div", { className: reveal ? "ui-snippet ui-snippet--reveal" : "ui-snippet", children: [
|
|
2077
2077
|
/* @__PURE__ */ jsxs20("div", { className: "ui-snippet__bar", children: [
|
|
2078
2078
|
/* @__PURE__ */ jsx25("span", { children: label }),
|
|
2079
|
-
/* @__PURE__ */ jsxs20(
|
|
2080
|
-
/* @__PURE__ */ jsx25(
|
|
2081
|
-
|
|
2079
|
+
copy && /* @__PURE__ */ jsxs20(Fragment6, { children: [
|
|
2080
|
+
/* @__PURE__ */ jsx25(
|
|
2081
|
+
"button",
|
|
2082
|
+
{
|
|
2083
|
+
type: "button",
|
|
2084
|
+
className: "ui-snippet__copy",
|
|
2085
|
+
"aria-label": copyLabel,
|
|
2086
|
+
title: copyLabel,
|
|
2087
|
+
onClick: handleCopy,
|
|
2088
|
+
children: /* @__PURE__ */ jsx25(Icon, { name: status === "idle" ? "copy" : "check" })
|
|
2089
|
+
}
|
|
2090
|
+
),
|
|
2091
|
+
/* @__PURE__ */ jsx25("span", { className: "ui-sr ui-snippet__status", role: "status", "aria-live": "polite", children: status === "copied" ? "Copied" : status === "failed" ? "Copy failed" : "" })
|
|
2082
2092
|
] })
|
|
2083
2093
|
] }),
|
|
2084
|
-
/* @__PURE__ */ jsx25("pre", { children: code })
|
|
2094
|
+
/* @__PURE__ */ jsx25("pre", { children: children ?? code })
|
|
2085
2095
|
] });
|
|
2086
2096
|
}
|
|
2087
2097
|
|
|
2088
2098
|
// src/Tabs.tsx
|
|
2089
2099
|
import { useEffect as useEffect8, useId as useId9, useRef as useRef13 } from "react";
|
|
2090
2100
|
import { playEntrance } from "@apliteni/apliteni-ui/motion";
|
|
2091
|
-
import { Fragment as
|
|
2101
|
+
import { Fragment as Fragment7, jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
2092
2102
|
function Tabs({ items, value, onChange, label }) {
|
|
2093
2103
|
const id = useId9();
|
|
2094
2104
|
const previous = useRef13(value);
|
|
@@ -2137,7 +2147,7 @@ function Tabs({ items, value, onChange, label }) {
|
|
|
2137
2147
|
},
|
|
2138
2148
|
children: [
|
|
2139
2149
|
item.label,
|
|
2140
|
-
item.count != null && /* @__PURE__ */ jsxs21(
|
|
2150
|
+
item.count != null && /* @__PURE__ */ jsxs21(Fragment7, { children: [
|
|
2141
2151
|
" ",
|
|
2142
2152
|
item.count
|
|
2143
2153
|
] })
|
|
@@ -2524,7 +2534,7 @@ import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName }
|
|
|
2524
2534
|
// src/SidebarNav.tsx
|
|
2525
2535
|
import { forwardRef as forwardRef8, useEffect as useEffect12, useId as useId11, useState as useState11 } from "react";
|
|
2526
2536
|
import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
|
|
2527
|
-
import { Fragment as
|
|
2537
|
+
import { Fragment as Fragment8, jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
2528
2538
|
var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
|
|
2529
2539
|
function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
|
|
2530
2540
|
const uid = useId11();
|
|
@@ -2550,7 +2560,7 @@ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
|
|
|
2550
2560
|
item.disabled && "is-disabled",
|
|
2551
2561
|
item.danger && "is-danger"
|
|
2552
2562
|
].filter(Boolean).join(" ");
|
|
2553
|
-
const children = /* @__PURE__ */ jsxs26(
|
|
2563
|
+
const children = /* @__PURE__ */ jsxs26(Fragment8, { children: [
|
|
2554
2564
|
(item.leading ?? item.icon) && /* @__PURE__ */ jsx31("span", { className: "ui-nav__ic", children: item.leading ?? /* @__PURE__ */ jsx31(Icon, { name: item.icon }) }),
|
|
2555
2565
|
/* @__PURE__ */ jsx31("span", { className: "ui-nav__label", children: item.label }),
|
|
2556
2566
|
count != null && count !== "" && /* @__PURE__ */ jsx31("span", { className: `ui-nav__badge is-${typeof item.badge === "object" ? item.badge.tone ?? "neutral" : "neutral"}`, children: count }),
|
|
@@ -2619,7 +2629,7 @@ var SidebarNav = forwardRef8(function SidebarNav2({
|
|
|
2619
2629
|
});
|
|
2620
2630
|
|
|
2621
2631
|
// src/AppShell.tsx
|
|
2622
|
-
import { Fragment as
|
|
2632
|
+
import { Fragment as Fragment9, jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
2623
2633
|
var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
|
|
2624
2634
|
function AppShell({
|
|
2625
2635
|
sections,
|
|
@@ -2692,7 +2702,7 @@ function AppShell({
|
|
|
2692
2702
|
onClick: (event) => {
|
|
2693
2703
|
if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
|
|
2694
2704
|
},
|
|
2695
|
-
children: /* @__PURE__ */ jsxs27(
|
|
2705
|
+
children: /* @__PURE__ */ jsxs27(Fragment9, { children: [
|
|
2696
2706
|
/* @__PURE__ */ jsx32("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx32(Icon, { name: section.icon }) }),
|
|
2697
2707
|
/* @__PURE__ */ jsx32("span", { className: "ui-nav__label", children: section.label }),
|
|
2698
2708
|
section.count != null && /* @__PURE__ */ jsx32("span", { className: "ui-nav__badge is-neutral", children: section.count })
|
package/src/components/index.js
CHANGED
|
@@ -289,30 +289,103 @@ export function emptyState({ art, icon: ic, title, sub, actions } = {}) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// ---- Snippet -------------------------------------------------------------
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
292
|
+
// The copy button is icon-only: `copy` is on the closed list in
|
|
293
|
+
// src/assets/icons.js, the bar is narrow, and the word said nothing the glyph
|
|
294
|
+
// does not. `copyLabel` is therefore the accessible name and the tooltip rather
|
|
295
|
+
// than visible text, and it should name what is being copied — "Copy command",
|
|
296
|
+
// "Copy configuration" — the way every other icon-only control in the kit is
|
|
297
|
+
// named (aria-label plus title, as button() and the theme toggle write it).
|
|
298
|
+
// The confirmation is a glyph swap, not a word: the button holds its 24px box
|
|
299
|
+
// through it, and the word goes to the live region beside it. A permanent
|
|
300
|
+
// aria-label outranks an element's contents, so swapping text into the button
|
|
301
|
+
// would have changed the pixels and left the announced name frozen — the region
|
|
302
|
+
// is what carries "Copied" to a reader who cannot see the check. It ships empty
|
|
303
|
+
// and is filled later, because a role="status" inserted together with its text is
|
|
304
|
+
// silent on several screen readers. The button's own name stays the action, which
|
|
305
|
+
// is still available; wireTopbar() fills the region. `data-orig` records the
|
|
306
|
+
// resting label for a caller that wants it.
|
|
295
307
|
// `type="button"`: without it a snippet dropped inside a <form> submits the form.
|
|
296
|
-
export function snippet({ label = 'shell', code = '', reveal = false, copy = true, copyLabel = 'Copy' } = {}) {
|
|
308
|
+
export function snippet({ label = 'shell', code = '', reveal = false, copy = true, copyLabel = 'Copy code' } = {}) {
|
|
297
309
|
const copyBtn = copy
|
|
298
|
-
? `<button type="button" class="ui-snippet__copy" data-orig="${esc(copyLabel)}">${icon('copy')}
|
|
310
|
+
? `<button type="button" class="ui-snippet__copy" aria-label="${esc(copyLabel)}" title="${esc(copyLabel)}" data-orig="${esc(copyLabel)}">${icon('copy')}</button>`
|
|
311
|
+
+ '<span class="ui-sr ui-snippet__status" role="status" aria-live="polite"></span>'
|
|
299
312
|
: '';
|
|
300
313
|
return `<div class="${cx('ui-snippet', reveal && 'ui-snippet--reveal')}"><div class="ui-snippet__bar"><span>${esc(label)}</span>${copyBtn}</div><pre>${code}</pre></div>`;
|
|
301
314
|
}
|
|
302
315
|
|
|
303
|
-
// Tiny
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
// Tiny highlighters. Each escapes first, then wraps the tokens its language
|
|
317
|
+
// shows, using the class names in styles/code.css (.k .f .s .u .c). Shell is
|
|
318
|
+
// ported from viz/account.mjs; JSON and TypeScript were added for the snippets
|
|
319
|
+
// the kit's own docs show.
|
|
320
|
+
//
|
|
321
|
+
// Every pattern runs over ESCAPED text, so a quoted string ends at " and a
|
|
322
|
+
// shell URL stops at any entity. The group order is the class order beside it.
|
|
323
|
+
//
|
|
324
|
+
// They are small on purpose — they colour short documentation snippets, not
|
|
325
|
+
// arbitrary programs. Known limits: no nested template expressions, no regex
|
|
326
|
+
// literals, and in TypeScript a `//` inside a string reads as a comment unless a
|
|
327
|
+
// `:` precedes it, which is what keeps a URL in a string whole.
|
|
328
|
+
const LANGUAGES = {
|
|
329
|
+
shell: {
|
|
330
|
+
pattern: /(#[^\n]*)|("(?:[^&]|&(?!quot;))*"|'[^']*')|(https?:\/\/[^\s"'&]+)|(\B--?[A-Za-z][\w-]*)|(^[a-z][\w.-]*)/gm,
|
|
331
|
+
classes: ['c', 's', 'u', 'f', 'k'],
|
|
332
|
+
},
|
|
333
|
+
// A key is a string followed by a colon, matched by lookahead so the colon
|
|
334
|
+
// stays unpainted. Keys, string values and scalars take three different
|
|
335
|
+
// classes, because a number that looks exactly like a key is not highlighting.
|
|
336
|
+
json: {
|
|
337
|
+
pattern: /("(?:[^&]|&(?!quot;))*")(?=\s*:)|("(?:[^&]|&(?!quot;))*")|(\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
|
|
338
|
+
classes: ['k', 's', 'f'],
|
|
339
|
+
},
|
|
340
|
+
ts: {
|
|
341
|
+
pattern: /(\/\*[\s\S]*?\*\/|(?<![:/])\/\/[^\n]*)|("(?:[^&]|&(?!quot;))*"|'[^']*'|`[^`]*`)|(\b(?:import|export|from|const|let|var|function|return|async|await|class|extends|implements|interface|type|enum|new|if|else|for|while|of|in|as|default|readonly|void)\b)|(\b(?:true|false|null|undefined)\b|\b\d+(?:\.\d+)?\b)/g,
|
|
342
|
+
classes: ['c', 's', 'k', 'f'],
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** The languages codeTokens and hlCode understand, so a gate can discover them. */
|
|
347
|
+
export const codeLanguages = Object.keys(LANGUAGES);
|
|
348
|
+
|
|
349
|
+
const ENTITY = { amp: '&', lt: '<', gt: '>', quot: '"' };
|
|
350
|
+
// The exact inverse of esc: every & in escaped text starts one of these four.
|
|
351
|
+
const unesc = (text) => text.replace(/&(amp|lt|gt|quot);/g, (_, name) => ENTITY[name]);
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* A language's tokens, as `[{ cls, text }]` with `cls` null between tokens and
|
|
355
|
+
* `text` the original unescaped source. Concatenating `text` returns `raw`, so a
|
|
356
|
+
* React caller renders the spans from the same string it copies instead of
|
|
357
|
+
* keeping a second hand-written copy in step. hlCode joins the same tokens into
|
|
358
|
+
* HTML; neither can drift from the other. An unrecognised `lang` is read as
|
|
359
|
+
* shell, the way the kit reads every other unknown option name.
|
|
360
|
+
*
|
|
361
|
+
* No pattern ever matches inside an entity — `#`, `h`, `-`, `/`, `*`, a digit and
|
|
362
|
+
* `^[a-z]` do not occur in `&`, `<`, `>` or `"`, no keyword is one
|
|
363
|
+
* of `amp`, `lt`, `gt` or `quot`, and every alternative either stops before `&` or
|
|
364
|
+
* consumes a whole `"` — so each slice is entity-complete and
|
|
365
|
+
* `esc(unesc(slice))` returns the slice unchanged. hlCode's output is therefore
|
|
366
|
+
* byte-identical to wrapping the matches in place, which
|
|
367
|
+
* src/components/snippet-tokens.test.js holds for every language, against a copy
|
|
368
|
+
* of the one-pass implementation hlShell had before #474.
|
|
369
|
+
*/
|
|
370
|
+
export function codeTokens(raw, lang = 'shell') {
|
|
371
|
+
const { pattern, classes } = LANGUAGES[lang] || LANGUAGES.shell;
|
|
372
|
+
const escaped = esc(raw);
|
|
373
|
+
const out = [];
|
|
374
|
+
let at = 0;
|
|
375
|
+
pattern.lastIndex = 0;
|
|
376
|
+
for (const match of escaped.matchAll(pattern)) {
|
|
377
|
+
if (match.index > at) out.push({ cls: null, text: unesc(escaped.slice(at, match.index)) });
|
|
378
|
+
out.push({ cls: classes[match.slice(1).findIndex(Boolean)], text: unesc(match[0]) });
|
|
379
|
+
at = match.index + match[0].length;
|
|
380
|
+
}
|
|
381
|
+
if (at < escaped.length) out.push({ cls: null, text: unesc(escaped.slice(at)) });
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const hlCode = (raw, lang = 'shell') =>
|
|
386
|
+
codeTokens(raw, lang).map(({ cls, text }) => (cls ? `<span class="${cls}">${esc(text)}</span>` : esc(text))).join('');
|
|
387
|
+
|
|
388
|
+
export const hlShell = (raw) => hlCode(raw, 'shell');
|
|
316
389
|
|
|
317
390
|
export { icon };
|
|
318
391
|
export { illo, illoNames } from '../assets/illustrations.js';
|
package/src/components/topbar.js
CHANGED
|
@@ -242,11 +242,26 @@ export function wireTopbar(root = document) {
|
|
|
242
242
|
});
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
|
-
// Copy buttons
|
|
245
|
+
// Copy buttons. The confirmation is a glyph swap plus a word in the live region
|
|
246
|
+
// beside the button, so the control holds its 24px box and a reader who cannot
|
|
247
|
+
// see the check still hears it. Writing the word into the button would do
|
|
248
|
+
// neither: the box jumps, and the permanent aria-label outranks the contents, so
|
|
249
|
+
// the announced name would never move. The restore reads the markup the button
|
|
250
|
+
// started with — putting `data-orig` back wrote the label as words and dropped
|
|
251
|
+
// the glyph, which only looked harmless while a word sat beside it. #474
|
|
246
252
|
root.querySelectorAll('.ui-snippet__copy').forEach((btn) => {
|
|
253
|
+
const resting = btn.innerHTML;
|
|
254
|
+
const status = btn.parentElement?.querySelector('.ui-snippet__status');
|
|
247
255
|
btn.addEventListener('click', () => {
|
|
248
256
|
const pre = btn.closest('.ui-snippet')?.querySelector('pre');
|
|
249
|
-
if (pre)
|
|
257
|
+
if (!pre) return;
|
|
258
|
+
navigator.clipboard?.writeText(pre.innerText);
|
|
259
|
+
btn.innerHTML = icon('check');
|
|
260
|
+
if (status) status.textContent = 'Copied';
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
btn.innerHTML = resting;
|
|
263
|
+
if (status) status.textContent = '';
|
|
264
|
+
}, 1400);
|
|
250
265
|
});
|
|
251
266
|
});
|
|
252
267
|
}
|
package/src/index.css
CHANGED
|
@@ -44,3 +44,6 @@
|
|
|
44
44
|
@import "./styles/feedback.css";
|
|
45
45
|
@import "./styles/success.css";
|
|
46
46
|
@import "./styles/loading.css";
|
|
47
|
+
/* Last, and that is the point: the tap zone opens a gap a component sheet set,
|
|
48
|
+
so it has to be read after every one of them. why: specification, the tap floor */
|
|
49
|
+
@import "./styles/tap-zone.css";
|
package/src/index.d.ts
CHANGED
|
@@ -83,6 +83,12 @@ export declare function topbar(opts?: Record<string, unknown>): string;
|
|
|
83
83
|
export declare function commandPaletteList(groups?: readonly Record<string, unknown>[], opts?: { uid?: string; from?: number }): string;
|
|
84
84
|
export declare function deckTextSwitch(active?: string): string;
|
|
85
85
|
export declare function hlShell(raw: string): string;
|
|
86
|
+
/** Token class from styles/code.css, or null for the text between tokens. */
|
|
87
|
+
export type CodeTokenClass = 'c' | 'f' | 'k' | 's' | 'u';
|
|
88
|
+
export type CodeLanguage = 'shell' | 'json' | 'ts';
|
|
89
|
+
export declare const codeLanguages: readonly CodeLanguage[];
|
|
90
|
+
export declare function codeTokens(raw: string, lang?: CodeLanguage): { cls: CodeTokenClass | null; text: string }[];
|
|
91
|
+
export declare function hlCode(raw: string, lang?: CodeLanguage): string;
|
|
86
92
|
export declare function pill(label: string, variant?: string): string;
|
|
87
93
|
export declare function statusDot(live?: boolean): string;
|
|
88
94
|
export declare function successCheck(variant?: 'line' | 'circled'): string;
|
package/src/inline.js
CHANGED
|
@@ -30,6 +30,7 @@ export const styles = {
|
|
|
30
30
|
base: baseCss,
|
|
31
31
|
reducedMotion: read('styles/reduced-motion.css'),
|
|
32
32
|
fieldZoom: read('styles/field-zoom.css'),
|
|
33
|
+
tapZone: read('styles/tap-zone.css'),
|
|
33
34
|
motion: read('styles/motion.css'),
|
|
34
35
|
button: read('styles/button.css'),
|
|
35
36
|
card: read('styles/card.css'),
|
|
@@ -92,4 +93,7 @@ export const cssText = [
|
|
|
92
93
|
styles.feedback,
|
|
93
94
|
styles.success,
|
|
94
95
|
styles.loading,
|
|
96
|
+
// Last in the list for the reason src/index.css imports it last: it opens a
|
|
97
|
+
// gap a component sheet set, so it is read after every one of them.
|
|
98
|
+
styles.tapZone,
|
|
95
99
|
].join('\n');
|
package/src/styles/base.css
CHANGED
|
@@ -138,6 +138,7 @@ svg:where(:not([width]):not([height])) {
|
|
|
138
138
|
|
|
139
139
|
/* Focus-visible ring, shared by all interactive controls */
|
|
140
140
|
.ui-focusable:focus-visible,
|
|
141
|
+
.ui-snippet :focus-visible,
|
|
141
142
|
.ui-btn:focus-visible,
|
|
142
143
|
.ui-input:focus-visible,
|
|
143
144
|
.ui-seg button:focus-visible,
|
package/src/styles/code.css
CHANGED
|
@@ -30,6 +30,27 @@
|
|
|
30
30
|
white-space: pre;
|
|
31
31
|
font-family: var(--font-mono);
|
|
32
32
|
}
|
|
33
|
+
/* Scrollable code is a keyboard target, but a <pre> flush with the card and
|
|
34
|
+
without its radius draws a square ring that cuts across the card and squares
|
|
35
|
+
its corners. The card carries the ring instead, so it follows --radius-md and
|
|
36
|
+
survives `overflow: hidden`, which clips descendants and not the card's own
|
|
37
|
+
shadow. --ring-gap comes from the page here, matching the container rule in
|
|
38
|
+
tokens.css: a ring outside its own surface cannot use that surface as its gap.
|
|
39
|
+
The card also keeps a transparent outline, which forced colors repaints as the
|
|
40
|
+
focus signal once box-shadow is gone; the block below is why only one box has
|
|
41
|
+
one there. See stories/snippet-focus.test.js. */
|
|
42
|
+
.ui-snippet pre:focus-visible { box-shadow: none; }
|
|
43
|
+
.ui-snippet:has(pre:focus-visible) { outline: 2px solid transparent; --ring-gap: inherit; box-shadow: var(--ring); }
|
|
44
|
+
|
|
45
|
+
/* Forced colors drops box-shadow, so both boxes fall back to their outlines and
|
|
46
|
+
the system repaints the pre's transparent one as the square this rule set out
|
|
47
|
+
to remove — with the card's rounded one 2px further out, two indicators for one
|
|
48
|
+
focus. The card's outline is the one that survives, so the pre keeps none.
|
|
49
|
+
Emulated in stories/snippet-focus.test.js and captured in Chromium. #474 */
|
|
50
|
+
@media (forced-colors: active) {
|
|
51
|
+
.ui-snippet pre:focus-visible { outline: none; }
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
.ui-snippet__copy {
|
|
34
55
|
background: none;
|
|
35
56
|
border: 0;
|
|
@@ -39,14 +60,16 @@
|
|
|
39
60
|
font-size: calc(var(--text-xs) + 1px);
|
|
40
61
|
display: inline-flex;
|
|
41
62
|
align-items: center;
|
|
42
|
-
gap: 6px;
|
|
43
63
|
padding: 2px 4px;
|
|
44
64
|
/* WCAG 2.5.8's 24 CSS px target floor, taken as a real box rather than as an
|
|
45
|
-
overlay: 2 + a 12px label at line-height 1.62
|
|
46
|
-
drawn button
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
overlay. The height came first (#219): 2 + a 12px label at line-height 1.62
|
|
66
|
+
+ 2 came to 23.44, so the drawn button grew by 0.56px and the bar with it.
|
|
67
|
+
The width is the same floor on the other axis, and it is load-bearing now
|
|
68
|
+
that the label is gone: 4 + a 13px glyph + 4 is 21px. Both are measured in
|
|
69
|
+
stories/guidelines/accessibility-floor.test.js. #474 */
|
|
49
70
|
min-height: 24px;
|
|
71
|
+
min-width: 24px;
|
|
72
|
+
justify-content: center;
|
|
50
73
|
transition: color var(--dur-fast) var(--ease);
|
|
51
74
|
}
|
|
52
75
|
.ui-snippet__copy:hover { color: var(--accent); }
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* apliteni-ui — the tap zone. Below the phone step, to a coarse pointer, a
|
|
2
|
+
* control that draws smaller than --tap-min carries a transparent layer that
|
|
3
|
+
* reaches the floor, and no control is drawn any bigger. Zones alone do not
|
|
4
|
+
* finish it: a layer takes HALF the clear space its container declares, so
|
|
5
|
+
* where the kit packs small controls closer than that, the GAP opens to
|
|
6
|
+
* --tap-gap. The controls keep their size; the space between them grows.
|
|
7
|
+
*
|
|
8
|
+
* Read LAST, after every component sheet — see src/index.css — because each
|
|
9
|
+
* opening overrides a gap one of them already set.
|
|
10
|
+
*
|
|
11
|
+
* why: docs/specification.md#a-tap-reaches-the-floor-below-the-phone-step
|
|
12
|
+
* Held by stories/tap-zone.test.js, measured in a browser at 390.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* WCAG 2.5.5, and where iOS and Android land. Not on the spacing scale,
|
|
17
|
+
which runs 40 then 48, because a fingertip is not spacing. Declared here
|
|
18
|
+
rather than in tokens.css for the reason field-zoom.css carries its own 16:
|
|
19
|
+
react/src/index.ts imports this sheet without the token file, and a var()
|
|
20
|
+
resolving to nothing there would size the layer to `auto` in silence. */
|
|
21
|
+
--tap-min: 44px;
|
|
22
|
+
|
|
23
|
+
/* WCAG 2.5.8 (AA), the target the kit holds at every width: the floor this
|
|
24
|
+
sheet raises FROM and may never drop below. */
|
|
25
|
+
--tap-aa: 24px;
|
|
26
|
+
|
|
27
|
+
/* 44 − 24: two zones fit between two of the smallest marks the kit draws. */
|
|
28
|
+
--tap-gap: var(--space-5, 20px);
|
|
29
|
+
|
|
30
|
+
/* Clear space to the nearest neighbour, declared by the container that knows
|
|
31
|
+
its own gap; a zone takes half on each side.
|
|
32
|
+
|
|
33
|
+
ZERO by default, so a zone grows nowhere until a container has opened the
|
|
34
|
+
room — assuming --tap-gap everywhere was measured handing `Keep the
|
|
35
|
+
workspace` a pixel of its own box that answered for `Delete it
|
|
36
|
+
permanently`. */
|
|
37
|
+
--tap-clear-x: 0px;
|
|
38
|
+
--tap-clear-y: 0px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (max-width: 560px) and (pointer: coarse) {
|
|
42
|
+
/* The containing block for every layer below. `.ui-btn` already has it; the
|
|
43
|
+
rest take it only inside this query, because `position: relative` lifts a
|
|
44
|
+
control over the in-flow one beside it and that changes who wins a shared
|
|
45
|
+
pixel. */
|
|
46
|
+
:where(
|
|
47
|
+
.ui-seg button,
|
|
48
|
+
.ui-tabs__tab,
|
|
49
|
+
.ui-dropdown__trigger,
|
|
50
|
+
.ui-toast__action,
|
|
51
|
+
.ui-drawer__close,
|
|
52
|
+
.ui-snippet__copy,
|
|
53
|
+
.ui-footer__social
|
|
54
|
+
) { position: relative; }
|
|
55
|
+
|
|
56
|
+
/* The layer: centred, transparent, sized to the floor, clamped to half the
|
|
57
|
+
clearance. */
|
|
58
|
+
:where(
|
|
59
|
+
.ui-btn,
|
|
60
|
+
.ui-seg button,
|
|
61
|
+
.ui-tabs__tab,
|
|
62
|
+
.ui-dropdown__trigger,
|
|
63
|
+
.ui-toast__action,
|
|
64
|
+
.ui-drawer__close,
|
|
65
|
+
.ui-snippet__copy,
|
|
66
|
+
.ui-footer__social,
|
|
67
|
+
.ui-switch
|
|
68
|
+
)::after {
|
|
69
|
+
content: "";
|
|
70
|
+
position: absolute;
|
|
71
|
+
left: 50%;
|
|
72
|
+
top: 50%;
|
|
73
|
+
transform: translate(-50%, -50%);
|
|
74
|
+
width: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x)));
|
|
75
|
+
height: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ::after on these is spoken for — the tick and the dot, the toast close's
|
|
79
|
+
glyph plate — so the existing ::before grows, floored at --tap-aa. The
|
|
80
|
+
floor is load-bearing: `100%` is the PADDING box, 16 on a 19px checkbox
|
|
81
|
+
with a 1.5px border, so without it the clamp takes this layer DOWN to 22
|
|
82
|
+
and under the kit's own minimum. */
|
|
83
|
+
.ui-check input::before,
|
|
84
|
+
.ui-toast__close::before {
|
|
85
|
+
width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
|
|
86
|
+
height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* -- Where the kit opens a gap, and where it cannot --------------------
|
|
90
|
+
* `!important` for the reason field-zoom.css takes it: each opening
|
|
91
|
+
* overrides a `gap` shorthand a component sheet set, and the React bundle
|
|
92
|
+
* ships this sheet without those sheets, where order is the consumer's. */
|
|
93
|
+
|
|
94
|
+
/* A strip keeps its pills packed across — that tight track IS the control —
|
|
95
|
+
and opens down, where the room is free until it wraps. */
|
|
96
|
+
.ui-seg {
|
|
97
|
+
row-gap: var(--tap-gap) !important;
|
|
98
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
99
|
+
--tap-clear-y: var(--tap-gap);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Tabs are wide enough across; down, the panel under them has the room. */
|
|
103
|
+
.ui-tabs__list { --tap-clear-x: 2px; --tap-clear-y: var(--tap-gap); }
|
|
104
|
+
|
|
105
|
+
/* Equal cells read as one strip, so they stay packed across and open down,
|
|
106
|
+
which is where this strip wraps at this width. */
|
|
107
|
+
.ui-pager__steps {
|
|
108
|
+
row-gap: var(--tap-gap) !important;
|
|
109
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
110
|
+
--tap-clear-y: var(--tap-gap);
|
|
111
|
+
}
|
|
112
|
+
.ui-pager { row-gap: var(--tap-gap) !important; --tap-clear-y: var(--tap-gap); }
|
|
113
|
+
|
|
114
|
+
/* Chips open both ways: the row wraps, and the button at the end of it is a
|
|
115
|
+
target like the chips are. */
|
|
116
|
+
.ui-filter-bar { gap: var(--tap-gap) !important; --tap-clear-x: var(--tap-gap); --tap-clear-y: var(--tap-gap); }
|
|
117
|
+
|
|
118
|
+
/* Inside a chip there is nothing to open: parting the value from its remove
|
|
119
|
+
mark would draw two controls where the reader sees one. Named on the
|
|
120
|
+
Accessibility minimums page. */
|
|
121
|
+
.ui-filter-bar__chip { --tap-clear-x: 0px; --tap-clear-y: 0px; }
|
|
122
|
+
|
|
123
|
+
/* Row actions open across — the pair #488 names — and cannot open down:
|
|
124
|
+
a dense table's rows share an edge, and parting them is the row height the
|
|
125
|
+
guideline protects. Down, they get the row's own padding, twice. */
|
|
126
|
+
/* Row actions — the pair #488 names — part ACROSS with a margin: the cell is
|
|
127
|
+
a `table-cell`, where `gap` does nothing, and declaring it anyway left the
|
|
128
|
+
mark's zone 9px inside `Revoke`. Nothing down, because a dense table's
|
|
129
|
+
rows share an edge and its padding is not clearance to spend. */
|
|
130
|
+
.ui-table__act .ui-btn + .ui-btn { margin-inline-start: var(--tap-gap) !important; }
|
|
131
|
+
.ui-table__act { --tap-clear-x: var(--tap-gap); --tap-clear-y: 0px; }
|
|
132
|
+
|
|
133
|
+
/* Down only, like the nine rows below: a toast is a full row at this width,
|
|
134
|
+
and opening it across took 24px out of the message for no reach at all —
|
|
135
|
+
its action already clears the floor on the vertical half. The box does not
|
|
136
|
+
open either: a toast clips to its own rounded corners for the timer bar,
|
|
137
|
+
and widening its padding grows the surface rather than the space between
|
|
138
|
+
controls, measured at four marks for 8px on every toast. */
|
|
139
|
+
.ui-toast { row-gap: var(--tap-gap) !important; --tap-clear-y: var(--tap-gap); }
|
|
140
|
+
|
|
141
|
+
/* The kit's remaining control rows open DOWN only. A row of labelled buttons
|
|
142
|
+
at 390 is often already full, so widening its column gap does not move the
|
|
143
|
+
buttons apart — it makes them narrower, and that is the half #488 rules
|
|
144
|
+
out. Measured on a toolbar of three buttons and a dropdown. These rows
|
|
145
|
+
wrap, which is also where the review measured them colliding. */
|
|
146
|
+
.ui-confirm__acts,
|
|
147
|
+
.ui-toolbar,
|
|
148
|
+
.ui-empty__actions,
|
|
149
|
+
.ui-sx__actions,
|
|
150
|
+
.ui-fbc__foot,
|
|
151
|
+
.ui-drawer__footer,
|
|
152
|
+
.ui-snippet__bar,
|
|
153
|
+
.ui-app__main .ui-card__row {
|
|
154
|
+
row-gap: var(--tap-gap) !important;
|
|
155
|
+
--tap-clear-y: var(--tap-gap);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Both ways: fixed squares cannot overflow the way labels can, and a drawer
|
|
159
|
+
header spends its column gap on the title. Not the accent picker — its
|
|
160
|
+
swatches are on no carrier list, so opening it bought no reach. */
|
|
161
|
+
.ui-footer__socials,
|
|
162
|
+
.ui-drawer__header {
|
|
163
|
+
gap: var(--tap-gap) !important;
|
|
164
|
+
--tap-clear-x: var(--tap-gap);
|
|
165
|
+
--tap-clear-y: var(--tap-gap);
|
|
166
|
+
}
|
|
167
|
+
}
|