@0001-labs/design-system-1 0.3.0-alpha.17 → 0.3.0-alpha.19
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/DS1/0-face/app-banner.ts +148 -0
- package/DS1/1-root/cjk.css +1 -1
- package/DS1/1-root/context.css +152 -0
- package/DS1/1-root/controls.css +2 -1
- package/DS1/1-root/fonts.css +1 -1
- package/DS1/1-root/notice.css +81 -0
- package/DS1/1-root/one.css +1 -1
- package/DS1/1-root/one.semantic.css +1 -1
- package/DS1/1-root/row.css +74 -10
- package/DS1/1-root/table.css +89 -0
- package/DS1/2-core/ds-image.ts +42 -0
- package/DS1/2-core/ds-message.ts +75 -4
- package/DS1/2-core/styles/ds-image.css +14 -0
- package/DS1/2-core/styles/ds-notice.css +13 -0
- package/DS1/3-unit/ds-calendar-large.ts +5 -1
- package/DS1/3-unit/ds-context.ts +2 -2
- package/DS1/3-unit/ds-menu.ts +7 -5
- package/DS1/3-unit/ds-row.ts +6 -2
- package/DS1/3-unit/ds-select.ts +4 -3
- package/DS1/3-unit/ds-table.ts +35 -1
- package/DS1/3-unit/ds-token-field.ts +2 -2
- package/DS1/3-unit/styles/ds-calendar-large.css +13 -0
- package/DS1/3-unit/styles/ds-context.css +15 -7
- package/DS1/3-unit/styles/ds-row.css +15 -0
- package/DS1/3-unit/styles/ds-select.css +4 -3
- package/DS1/3-unit/styles/ds-tabs.css +1 -1
- package/DS1/index.ts +2 -0
- package/DS1/react/image.ts +10 -0
- package/DS1/react/message.ts +16 -3
- package/DS1/react/page.ts +3 -2
- package/dist/0-face/app-banner.d.ts +60 -0
- package/dist/0-face/app-banner.d.ts.map +1 -0
- package/dist/0-face/app-banner.js +138 -0
- package/dist/2-core/ds-image.d.ts +59 -0
- package/dist/2-core/ds-image.d.ts.map +1 -0
- package/dist/2-core/ds-image.js +42 -0
- package/dist/2-core/ds-message.d.ts +49 -3
- package/dist/2-core/ds-message.d.ts.map +1 -1
- package/dist/2-core/ds-message.js +71 -3
- package/dist/3-unit/ds-calendar-large.d.ts +5 -0
- package/dist/3-unit/ds-calendar-large.d.ts.map +1 -1
- package/dist/3-unit/ds-calendar-large.js +4 -1
- package/dist/3-unit/ds-context.d.ts +2 -3
- package/dist/3-unit/ds-context.d.ts.map +1 -1
- package/dist/3-unit/ds-context.js +2 -2
- package/dist/3-unit/ds-menu.d.ts +5 -3
- package/dist/3-unit/ds-menu.d.ts.map +1 -1
- package/dist/3-unit/ds-menu.js +6 -4
- package/dist/3-unit/ds-row.d.ts +6 -0
- package/dist/3-unit/ds-row.d.ts.map +1 -1
- package/dist/3-unit/ds-row.js +5 -2
- package/dist/3-unit/ds-select.d.ts +0 -1
- package/dist/3-unit/ds-select.d.ts.map +1 -1
- package/dist/3-unit/ds-select.js +4 -3
- package/dist/3-unit/ds-table.d.ts +10 -0
- package/dist/3-unit/ds-table.d.ts.map +1 -1
- package/dist/3-unit/ds-table.js +31 -1
- package/dist/3-unit/ds-token-field.d.ts +1 -1
- package/dist/3-unit/ds-token-field.d.ts.map +1 -1
- package/dist/3-unit/ds-token-field.js +2 -2
- package/dist/design-system-1.bundle.js +288 -28
- package/dist/design-system-1.bundle.js.map +1 -1
- package/dist/design-system-1.bundle.min.js +2177 -1997
- package/dist/design-system-1.bundle.min.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/react/image.d.ts +29 -0
- package/dist/react/image.d.ts.map +1 -0
- package/dist/react/image.js +4 -0
- package/dist/react/message.d.ts +26 -2
- package/dist/react/message.d.ts.map +1 -1
- package/dist/react/message.js +3 -2
- package/dist/react/page.d.ts +4 -3
- package/dist/react/page.d.ts.map +1 -1
- package/dist/react/page.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// app-banner.ts
|
|
2
|
+
// Where a banner leading to a site's iPhone app shows, and what its Open does.
|
|
3
|
+
// `<ds-message form="app">` uses it, and a page without Lit calls
|
|
4
|
+
// `startAppBanners()` for every `.ds-notice--app` block it wrote.
|
|
5
|
+
//
|
|
6
|
+
// On a phone or tablet running iOS, Safari draws Apple's own Smart App Banner
|
|
7
|
+
// from `<meta name="apple-itunes-app">`, so the page draws nothing there; in
|
|
8
|
+
// another iOS browser (Chrome, Firefox, Edge…) the page draws Design system
|
|
9
|
+
// 1's banner. Never on a computer, an Android phone, a page opened from the
|
|
10
|
+
// home screen or an app's own web view, and never again once closed in this
|
|
11
|
+
// browser.
|
|
12
|
+
|
|
13
|
+
import { detectMobileDevice } from "./scaling";
|
|
14
|
+
|
|
15
|
+
export interface AppBannerOptions {
|
|
16
|
+
/** The app's App Store id, the digits after `id` in its store address. */
|
|
17
|
+
appId?: string;
|
|
18
|
+
/** The app's store address; `https://apps.apple.com/app/id<appId>` by default. */
|
|
19
|
+
appUrl?: string;
|
|
20
|
+
/** The address that opens the app at this page: a universal link or the app's own scheme. */
|
|
21
|
+
open?: string;
|
|
22
|
+
/** What remembers the close in this browser; one key per app by default. */
|
|
23
|
+
key?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `safari`: Apple's banner from the meta tag; `drawn`: the page's own; `none`: nothing. */
|
|
27
|
+
export type AppBannerPlacement = "safari" | "drawn" | "none";
|
|
28
|
+
|
|
29
|
+
export interface AppBannerDevice {
|
|
30
|
+
userAgent: string;
|
|
31
|
+
touchPoints: number;
|
|
32
|
+
/** A phone or tablet, as the scale reads it. */
|
|
33
|
+
mobile: boolean;
|
|
34
|
+
/** Opened from the home screen. */
|
|
35
|
+
standalone: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** iOS's other browsers, each with Safari's engine and its own token. */
|
|
39
|
+
const OTHER_BROWSERS = /CriOS|FxiOS|EdgiOS|OPiOS|OPT\/|GSA\/|DuckDuckGo|Ddg\/|YaBrowser|Focus\/|Brave\//;
|
|
40
|
+
|
|
41
|
+
export function appBannerDevice(): AppBannerDevice {
|
|
42
|
+
if (typeof navigator === "undefined" || typeof window === "undefined") return { userAgent: "", touchPoints: 0, mobile: false, standalone: false };
|
|
43
|
+
const standalone = Boolean((navigator as Navigator & { standalone?: boolean }).standalone)
|
|
44
|
+
|| Boolean(window.matchMedia?.("(display-mode: standalone)").matches);
|
|
45
|
+
return { userAgent: navigator.userAgent || "", touchPoints: navigator.maxTouchPoints || 0, mobile: detectMobileDevice(), standalone };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** An iPhone, iPod or iPad, including an iPad that asks for the desktop site. */
|
|
49
|
+
export function isAppleMobile({ userAgent, touchPoints }: Pick<AppBannerDevice, "userAgent" | "touchPoints">): boolean {
|
|
50
|
+
return /iPhone|iPad|iPod/.test(userAgent) || (/Macintosh/.test(userAgent) && touchPoints > 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Safari, another browser, or an app's web view. A web view's agent has no
|
|
55
|
+
* `Safari/` token (Safari's view controller and sign-in sheet keep it).
|
|
56
|
+
*/
|
|
57
|
+
export function appleBrowser(userAgent: string): "safari" | "other" | "webview" {
|
|
58
|
+
if (OTHER_BROWSERS.test(userAgent)) return "other";
|
|
59
|
+
return /Safari\//.test(userAgent) ? "safari" : "webview";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function appBannerKey(options: AppBannerOptions): string {
|
|
63
|
+
return options.key || `ds-app-banner:${options.appId || options.appUrl || options.open || ""}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function appStoreUrl(options: AppBannerOptions): string {
|
|
67
|
+
return options.appUrl || (options.appId ? `https://apps.apple.com/app/id${options.appId}` : "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isAppBannerClosed(options: AppBannerOptions): boolean {
|
|
71
|
+
try { return localStorage.getItem(appBannerKey(options)) === "closed"; } catch { return false; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Remembers the close in this browser, so the banner never comes back here. */
|
|
75
|
+
export function closeAppBanner(options: AppBannerOptions): void {
|
|
76
|
+
try { localStorage.setItem(appBannerKey(options), "closed"); } catch { /* private mode keeps it for the page */ }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Where the banner shows on this device. Safari takes Apple's banner only
|
|
81
|
+
* when there is an App Store id for its meta tag; without one it draws the
|
|
82
|
+
* page's own, like the other browsers.
|
|
83
|
+
*/
|
|
84
|
+
export function appBannerPlacement(options: AppBannerOptions, device: AppBannerDevice = appBannerDevice()): AppBannerPlacement {
|
|
85
|
+
if (!appStoreUrl(options) && !options.open) return "none";
|
|
86
|
+
if (!device.mobile || device.standalone || !isAppleMobile(device)) return "none";
|
|
87
|
+
const browser = appleBrowser(device.userAgent);
|
|
88
|
+
if (browser === "webview") return "none";
|
|
89
|
+
if (browser === "safari" && options.appId) return "safari";
|
|
90
|
+
return isAppBannerClosed(options) ? "none" : "drawn";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Writes Apple's `<meta name="apple-itunes-app">` when the page has none.
|
|
95
|
+
* Safari reads it as the page loads, so a page that knows its app writes the
|
|
96
|
+
* tag in its own head; this is the fallback for one that did not.
|
|
97
|
+
*/
|
|
98
|
+
export function ensureSmartAppBanner(options: AppBannerOptions): void {
|
|
99
|
+
if (typeof document === "undefined" || !options.appId) return;
|
|
100
|
+
if (document.head.querySelector('meta[name="apple-itunes-app"]')) return;
|
|
101
|
+
const meta = document.createElement("meta");
|
|
102
|
+
meta.name = "apple-itunes-app";
|
|
103
|
+
meta.content = [`app-id=${options.appId}`, options.open ? `app-argument=${options.open}` : ""].filter(Boolean).join(", ");
|
|
104
|
+
document.head.append(meta);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Opens the app: the deep link first, then the App Store when the page is
|
|
109
|
+
* still in view a moment later (the app was not there to take it). Without a
|
|
110
|
+
* deep link, the store, which offers Open when the app is installed.
|
|
111
|
+
*/
|
|
112
|
+
export function openApp(options: AppBannerOptions, wait = 1500): void {
|
|
113
|
+
const store = appStoreUrl(options);
|
|
114
|
+
if (!options.open) { if (store) location.href = store; return; }
|
|
115
|
+
location.href = options.open;
|
|
116
|
+
if (!store) return;
|
|
117
|
+
const timer = window.setTimeout(() => { if (document.visibilityState === "visible") location.href = store; }, wait);
|
|
118
|
+
const left = () => { window.clearTimeout(timer); document.removeEventListener("visibilitychange", left); window.removeEventListener("pagehide", left); };
|
|
119
|
+
document.addEventListener("visibilitychange", left);
|
|
120
|
+
window.addEventListener("pagehide", left);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Puts every `.ds-notice--app` block on the page in place: hides it unless
|
|
125
|
+
* this device draws it, writes the meta tag for Safari, and wires its Open
|
|
126
|
+
* link (`.ds-notice__open`) and close button (`.ds-notice__dismiss`). The
|
|
127
|
+
* block names its app in `data-app-id`, `data-app-url`, `data-open` and
|
|
128
|
+
* optionally `data-remember`. Returns a function that unwires them.
|
|
129
|
+
*/
|
|
130
|
+
export function startAppBanners(root: ParentNode = document): () => void {
|
|
131
|
+
const undo: (() => void)[] = [];
|
|
132
|
+
for (const block of root.querySelectorAll<HTMLElement>(".ds-notice--app")) {
|
|
133
|
+
const options: AppBannerOptions = { appId: block.dataset.appId, appUrl: block.dataset.appUrl, open: block.dataset.open, key: block.dataset.remember };
|
|
134
|
+
const placement = appBannerPlacement(options);
|
|
135
|
+
if (placement === "safari") ensureSmartAppBanner(options);
|
|
136
|
+
block.hidden = placement !== "drawn";
|
|
137
|
+
if (block.hidden) continue;
|
|
138
|
+
const link = block.querySelector<HTMLAnchorElement>(".ds-notice__open");
|
|
139
|
+
const close = block.querySelector<HTMLButtonElement>(".ds-notice__dismiss");
|
|
140
|
+
if (link && !link.getAttribute("href")) link.href = appStoreUrl(options);
|
|
141
|
+
const onOpen = (event: MouseEvent) => { if (!options.open) return; event.preventDefault(); openApp(options); };
|
|
142
|
+
const onClose = () => { closeAppBanner(options); block.hidden = true; };
|
|
143
|
+
link?.addEventListener("click", onOpen);
|
|
144
|
+
close?.addEventListener("click", onClose);
|
|
145
|
+
undo.push(() => { link?.removeEventListener("click", onOpen); close?.removeEventListener("click", onClose); });
|
|
146
|
+
}
|
|
147
|
+
return () => undo.forEach(fn => fn());
|
|
148
|
+
}
|
package/DS1/1-root/cjk.css
CHANGED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
@import "./component-metrics.css";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* `.ds-context` is the `ds-context` element's look on a native menu, for a page
|
|
5
|
+
* without Lit or a menu a library draws: Ezo's gap face. The rows carry the
|
|
6
|
+
* colour and the menu only cuts the outer silhouette, so the page shows
|
|
7
|
+
* through a pixel between rows and two between groups. The rows are black
|
|
8
|
+
* with white words in both themes, as Ezo's menus always were (the slate in
|
|
9
|
+
* dark, darker than the page). Every row is 19px with the pixel under it, a square's pitch, its words
|
|
10
|
+
* on the sixteen and an icon at its end. A Select's menu is the light face,
|
|
11
|
+
* `.ds-menu`; a Context's is this dark one.
|
|
12
|
+
*
|
|
13
|
+
* ds-context the menu (`role="menu"`); its rows are its buttons,
|
|
14
|
+
* links and menu items, or `ds-context__item`
|
|
15
|
+
* ds-context__item a row; `--danger` (or `data-danger`) in red
|
|
16
|
+
* ds-context__label a heading row, muted
|
|
17
|
+
* ds-context__shortcut a shortcut at the row's end, muted
|
|
18
|
+
* ds-context__separator a group gap (or an `hr`, or `role="separator"`):
|
|
19
|
+
* never a line; first, last or doubled it is quiet
|
|
20
|
+
* [data-ds-refused] a refused change: the row turns red
|
|
21
|
+
*
|
|
22
|
+
* The page places and opens it (a popover at the pointer, say) and owns its
|
|
23
|
+
* keys; the faces of hover, focus, disabled and the wait of a pending menu
|
|
24
|
+
* (`aria-busy="true"`) are here. Tokens as the element's: --ds-context-fill,
|
|
25
|
+
* --ds-context-ink, --ds-context-hover, --ds-context-danger,
|
|
26
|
+
* --ds-context-refused, --ds-context-radius.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
.ds-context {
|
|
30
|
+
--ds-context-row: calc(var(--ds-row-height) - 1px);
|
|
31
|
+
/* Black in both themes, as Ezo's menus always were: in dark the slate, darker than the page. */
|
|
32
|
+
--ds-context-fill: light-dark(var(--ds-black, #2a2a2a), var(--ds-slate, #1e1e1e));
|
|
33
|
+
--ds-context-ink: var(--ds-white, #ffffff);
|
|
34
|
+
--ds-context-hover: color-mix(in srgb, var(--ds-context-fill) 78%, var(--ds-context-ink));
|
|
35
|
+
--ds-context-danger: var(--ds-tuned-red);
|
|
36
|
+
--ds-context-refused: var(--ds-tuned-red);
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: 1px;
|
|
41
|
+
width: var(--ds-menu-width, max-content);
|
|
42
|
+
min-width: calc(4 * var(--ds-unit));
|
|
43
|
+
max-height: var(--ds-menu-max-height, calc(16 * var(--ds-unit)));
|
|
44
|
+
overflow-y: auto;
|
|
45
|
+
margin: 0;
|
|
46
|
+
padding: 0;
|
|
47
|
+
border: 0;
|
|
48
|
+
background: transparent;
|
|
49
|
+
box-shadow: none;
|
|
50
|
+
clip-path: inset(0 round var(--ds-context-radius, var(--ds-radius, 4px)));
|
|
51
|
+
color: var(--ds-context-ink);
|
|
52
|
+
font-family: var(--ds-typeface-regular);
|
|
53
|
+
font-feature-settings: var(--ds-font-features);
|
|
54
|
+
font-size: var(--ds-type-size-default);
|
|
55
|
+
font-weight: var(--ds-type-weight-default);
|
|
56
|
+
line-height: var(--ds-type-lineheight-default);
|
|
57
|
+
letter-spacing: var(--ds-type-letterspacing-default);
|
|
58
|
+
list-style: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ds-context > [role="group"] {
|
|
62
|
+
display: contents;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* A row: whatever the page or library styles its buttons with, one row on the sixteen. */
|
|
66
|
+
.ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"]):not(.ds-context__label, .ds-context__separator, [role="separator"])) {
|
|
67
|
+
box-sizing: border-box !important;
|
|
68
|
+
display: flex !important;
|
|
69
|
+
flex: 0 0 auto !important;
|
|
70
|
+
align-items: flex-end !important;
|
|
71
|
+
justify-content: space-between !important;
|
|
72
|
+
gap: calc(4px * var(--ds-sf));
|
|
73
|
+
width: 100%;
|
|
74
|
+
height: var(--ds-context-row) !important;
|
|
75
|
+
min-height: var(--ds-context-row) !important;
|
|
76
|
+
max-height: var(--ds-context-row) !important;
|
|
77
|
+
margin: 0 !important;
|
|
78
|
+
padding: 0 calc(4px * var(--ds-sf)) !important;
|
|
79
|
+
border: 0 !important;
|
|
80
|
+
border-radius: 0 !important;
|
|
81
|
+
background: var(--ds-context-fill) !important;
|
|
82
|
+
color: var(--ds-context-ink) !important;
|
|
83
|
+
font: inherit;
|
|
84
|
+
line-height: var(--ds-type-lineheight-default) !important;
|
|
85
|
+
letter-spacing: inherit;
|
|
86
|
+
text-align: start;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
outline: none;
|
|
91
|
+
appearance: none;
|
|
92
|
+
-webkit-tap-highlight-color: transparent;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"])):is(:hover, :focus-visible, [data-highlighted]):not(:disabled, [aria-disabled="true"], [data-disabled]) {
|
|
96
|
+
background: var(--ds-context-hover) !important;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"])):focus-visible {
|
|
100
|
+
box-shadow: inset 0 0 0 2px var(--ds-focus-ring);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"])):is(:disabled, [aria-disabled="true"], [data-disabled]) {
|
|
104
|
+
cursor: default;
|
|
105
|
+
opacity: 0.5;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.ds-context[aria-busy="true"] > * {
|
|
109
|
+
cursor: wait !important;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Danger is red, on either fill. */
|
|
113
|
+
.ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"])):is(.ds-context__item--danger, [data-danger], [data-variant="destructive"]) {
|
|
114
|
+
color: var(--ds-context-danger) !important;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* A refused change: its row turns red a moment. */
|
|
118
|
+
.ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"]))[data-ds-refused] {
|
|
119
|
+
background: var(--ds-context-refused) !important;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Groups are a gap, never a line: the separator takes nothing but the gap after it. */
|
|
123
|
+
.ds-context > :is(hr, .ds-context__separator, [role="separator"]) {
|
|
124
|
+
flex: 0 0 0 !important;
|
|
125
|
+
height: 0 !important;
|
|
126
|
+
margin: 0 !important;
|
|
127
|
+
padding: 0 !important;
|
|
128
|
+
border: 0 !important;
|
|
129
|
+
background: none !important;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* A group left empty leaves no gap. */
|
|
133
|
+
.ds-context > :is(hr, .ds-context__separator, [role="separator"]):is(:first-child, :last-child),
|
|
134
|
+
.ds-context > :is(hr, .ds-context__separator, [role="separator"]) + :is(hr, .ds-context__separator, [role="separator"]) {
|
|
135
|
+
display: none !important;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ds-context :is(.ds-context__label, [data-ds-menu-label]) {
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
height: var(--ds-context-row);
|
|
141
|
+
/* The spare three above the sixteen. */
|
|
142
|
+
padding: calc(var(--ds-context-row) - var(--ds-type-lineheight-default)) calc(4px * var(--ds-sf)) 0;
|
|
143
|
+
background: var(--ds-context-fill);
|
|
144
|
+
color: color-mix(in srgb, var(--ds-context-ink) 60%, var(--ds-context-fill));
|
|
145
|
+
line-height: var(--ds-type-lineheight-default);
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ds-context__shortcut {
|
|
150
|
+
margin-left: auto;
|
|
151
|
+
color: color-mix(in srgb, var(--ds-context-ink) 60%, var(--ds-context-fill));
|
|
152
|
+
}
|
package/DS1/1-root/controls.css
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
@import "./figures.css";
|
|
13
13
|
@import "./progress.css";
|
|
14
14
|
@import "./row.css";
|
|
15
|
+
@import "./context.css";
|
|
15
16
|
|
|
16
17
|
/*
|
|
17
18
|
* Native controls in shared dress.
|
|
@@ -454,7 +455,7 @@ textarea.ds-input {
|
|
|
454
455
|
display: flex;
|
|
455
456
|
flex-direction: row;
|
|
456
457
|
align-items: center;
|
|
457
|
-
gap:
|
|
458
|
+
gap: calc(4px * var(--ds-sf));
|
|
458
459
|
min-width: 0;
|
|
459
460
|
height: var(--ds-row-height);
|
|
460
461
|
}
|
package/DS1/1-root/fonts.css
CHANGED
package/DS1/1-root/notice.css
CHANGED
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
* ds-notice--banner a strip across the top of its positioning
|
|
22
22
|
* context instead: one square high, filled in
|
|
23
23
|
* the tone's colour, everything on one line
|
|
24
|
+
* ds-notice--app a row at the top of the page leading to the
|
|
25
|
+
* site's iPhone app: its picture
|
|
26
|
+
* (ds-notice__picture, two squares), its name
|
|
27
|
+
* and one line about it, Open
|
|
28
|
+
* (a.ds-notice__open) and the close; three
|
|
29
|
+
* squares high with a hairline at its foot.
|
|
30
|
+
* startAppBanners() from 0-face/app-banner.ts
|
|
31
|
+
* shows it only where it belongs
|
|
24
32
|
*
|
|
25
33
|
* Give the block role="status", or role="alert" for danger, so assistive
|
|
26
34
|
* technology reads it when it appears.
|
|
@@ -163,3 +171,76 @@
|
|
|
163
171
|
margin-block: 0;
|
|
164
172
|
margin-inline-start: 0;
|
|
165
173
|
}
|
|
174
|
+
|
|
175
|
+
/* The app banner: a row at the top of the page, in the flow, three squares
|
|
176
|
+
high: half a square above and below two squares of content, a hairline at
|
|
177
|
+
its foot. The app's picture fills two squares with a picture's corners; the
|
|
178
|
+
name and the line about it take a square each, on the sixteen; Open and the
|
|
179
|
+
close stand on the sixteen of the square in the middle of the two. */
|
|
180
|
+
.ds-notice--app {
|
|
181
|
+
grid-template-columns: calc(2 * var(--ds-unit)) minmax(0, 1fr) auto var(--ds-unit);
|
|
182
|
+
grid-template-rows: var(--ds-unit) var(--ds-unit);
|
|
183
|
+
column-gap: calc(var(--ds-unit) / 2);
|
|
184
|
+
align-items: end;
|
|
185
|
+
padding: calc(var(--ds-unit) / 2) var(--ds-unit);
|
|
186
|
+
background: var(--ds-paper);
|
|
187
|
+
box-shadow: inset 0 -1px 0 var(--ds-border-color);
|
|
188
|
+
line-height: var(--ds-type-lineheight-default);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.ds-notice--app[hidden] {
|
|
192
|
+
display: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.ds-notice__picture {
|
|
196
|
+
box-sizing: border-box;
|
|
197
|
+
grid-column: 1;
|
|
198
|
+
grid-row: 1 / 3;
|
|
199
|
+
align-self: stretch;
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
border-radius: var(--ds-radius);
|
|
202
|
+
background: var(--ds-fill);
|
|
203
|
+
outline: 1px solid var(--ds-border-color);
|
|
204
|
+
outline-offset: -1px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.ds-notice__picture img {
|
|
208
|
+
display: block;
|
|
209
|
+
width: 100%;
|
|
210
|
+
height: 100%;
|
|
211
|
+
object-fit: cover;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.ds-notice--app .ds-notice__title,
|
|
215
|
+
.ds-notice--app .ds-notice__text {
|
|
216
|
+
grid-column: 2;
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
text-overflow: ellipsis;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ds-notice--app .ds-notice__title { grid-row: 1; }
|
|
223
|
+
|
|
224
|
+
.ds-notice--app .ds-notice__text {
|
|
225
|
+
grid-row: 2;
|
|
226
|
+
color: var(--ds-text-color-muted);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Open and the close: their square starts half a square into the two, its
|
|
230
|
+
control on the lower sixteen of it. */
|
|
231
|
+
.ds-notice--app .ds-notice__open,
|
|
232
|
+
.ds-notice--app .ds-notice__dismiss {
|
|
233
|
+
grid-row: 1 / 3;
|
|
234
|
+
align-self: start;
|
|
235
|
+
margin: calc(1.5 * var(--ds-unit) - var(--ds-control-height)) 0 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.ds-notice--app .ds-notice__open {
|
|
239
|
+
grid-column: 3;
|
|
240
|
+
text-decoration: none;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.ds-notice--app .ds-notice__dismiss {
|
|
244
|
+
grid-column: 4;
|
|
245
|
+
justify-self: end;
|
|
246
|
+
}
|
package/DS1/1-root/one.css
CHANGED
package/DS1/1-root/row.css
CHANGED
|
@@ -132,18 +132,52 @@ button.ds-row {
|
|
|
132
132
|
z-index: 2;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/*
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
/*
|
|
136
|
+
* The pointer, the press, the current destination and the focus ring are one layer under the
|
|
137
|
+
* row's content (Joachim, 19 October 2026). On a one-square row it is a square high and sits two
|
|
138
|
+
* pixels around what the row holds: from two above the lower sixteen to two below the row's foot,
|
|
139
|
+
* and two past the content at each side, its corners the icon tiles' 4px and those two more, so a
|
|
140
|
+
* 16px icon stands in it with two all round. It is drawn over the row, never moving its words or
|
|
141
|
+
* icons off the grid. An open row (`data-open`) wears the same layer. A taller row (a stacked
|
|
142
|
+
* tile, `auto`) is filled whole.
|
|
143
|
+
* Tokens: `--ds-row-fill-radius` (6px), `--ds-row-fill-overhang` (2px).
|
|
144
|
+
*/
|
|
145
|
+
a.ds-row,
|
|
146
|
+
button.ds-row {
|
|
147
|
+
isolation: isolate;
|
|
148
|
+
}
|
|
149
|
+
a.ds-row::after,
|
|
150
|
+
button.ds-row::after {
|
|
151
|
+
content: "";
|
|
152
|
+
position: absolute;
|
|
153
|
+
z-index: -1;
|
|
154
|
+
pointer-events: none;
|
|
155
|
+
}
|
|
156
|
+
a.ds-row::after,
|
|
157
|
+
button.ds-row::after,
|
|
158
|
+
.ds-row__target::after {
|
|
159
|
+
--ds-row-overhang: var(--ds-row-fill-overhang, calc(2px * var(--ds-sf)));
|
|
160
|
+
inset:
|
|
161
|
+
calc(var(--ds-row-height) - var(--ds-control-height) - var(--ds-row-overhang))
|
|
162
|
+
calc(var(--ds-row-padding, calc(4px * var(--ds-sf))) - var(--ds-row-overhang))
|
|
163
|
+
calc(0px - var(--ds-row-overhang));
|
|
164
|
+
border-radius: var(--ds-row-radius, var(--ds-row-fill-radius, calc(6px * var(--ds-sf))));
|
|
165
|
+
}
|
|
166
|
+
:is(a, button).ds-row:is(.ds-row--stacked, .ds-row--auto)::after,
|
|
167
|
+
:is(.ds-row--stacked, .ds-row--auto) .ds-row__target::after {
|
|
168
|
+
inset: 0;
|
|
169
|
+
}
|
|
170
|
+
a.ds-row[href]:hover::after,
|
|
171
|
+
button.ds-row:hover:not(:disabled)::after,
|
|
138
172
|
.ds-row__target:is(a[href], button:not(:disabled)):hover::after {
|
|
139
173
|
background: var(--ds-row-fill-hover, var(--ds-fill));
|
|
140
174
|
}
|
|
141
|
-
a.ds-row[href]:active,
|
|
142
|
-
button.ds-row:active:not(:disabled),
|
|
175
|
+
a.ds-row[href]:active::after,
|
|
176
|
+
button.ds-row:active:not(:disabled)::after,
|
|
143
177
|
.ds-row__target:is(a[href], button:not(:disabled)):active::after {
|
|
144
178
|
background: var(--ds-row-fill-press, var(--ds-fill-hover));
|
|
145
179
|
}
|
|
146
|
-
.ds-row:is(a, button)[aria-current]:not([aria-current="false"]),
|
|
180
|
+
.ds-row:is(a, button)[aria-current]:not([aria-current="false"])::after,
|
|
147
181
|
.ds-row__target[aria-current]:not([aria-current="false"])::after {
|
|
148
182
|
background: var(--ds-row-fill-current, var(--ds-fill-hover));
|
|
149
183
|
}
|
|
@@ -152,8 +186,8 @@ button.ds-row:active:not(:disabled),
|
|
|
152
186
|
--ds-type-weight-default: var(--ds-row-current-weight, 500);
|
|
153
187
|
font-weight: var(--ds-row-current-weight, 500);
|
|
154
188
|
}
|
|
155
|
-
a.ds-row:focus-visible,
|
|
156
|
-
button.ds-row:focus-visible,
|
|
189
|
+
a.ds-row:focus-visible::after,
|
|
190
|
+
button.ds-row:focus-visible::after,
|
|
157
191
|
.ds-row__target:focus-visible::after {
|
|
158
192
|
outline: 2px solid var(--ds-focus-ring);
|
|
159
193
|
outline-offset: -2px;
|
|
@@ -169,8 +203,8 @@ button.ds-row[aria-disabled="true"],
|
|
|
169
203
|
.ds-row__target:is(:disabled, [aria-disabled="true"], a:not([href])) {
|
|
170
204
|
cursor: default;
|
|
171
205
|
}
|
|
172
|
-
a.ds-row[aria-disabled="true"]:hover,
|
|
173
|
-
button.ds-row[aria-disabled="true"]:hover {
|
|
206
|
+
a.ds-row[aria-disabled="true"]:hover::after,
|
|
207
|
+
button.ds-row[aria-disabled="true"]:hover::after {
|
|
174
208
|
background: none;
|
|
175
209
|
}
|
|
176
210
|
|
|
@@ -298,3 +332,33 @@ button.ds-row[aria-disabled="true"]:hover {
|
|
|
298
332
|
background-color: CanvasText;
|
|
299
333
|
}
|
|
300
334
|
}
|
|
335
|
+
|
|
336
|
+
/* A controlled detail view: the caller marks its open row, and clears it on close.
|
|
337
|
+
Navigation's current state and the text weight remain independent. */
|
|
338
|
+
.ds-row[data-open]:not([data-open="false"]) {
|
|
339
|
+
position: relative;
|
|
340
|
+
isolation: isolate;
|
|
341
|
+
}
|
|
342
|
+
.ds-row[data-open]:not([data-open="false"])::after {
|
|
343
|
+
content: "";
|
|
344
|
+
position: absolute;
|
|
345
|
+
z-index: -1;
|
|
346
|
+
pointer-events: none;
|
|
347
|
+
--ds-row-overhang: var(--ds-row-fill-overhang, calc(2px * var(--ds-sf)));
|
|
348
|
+
inset: calc(var(--ds-row-height) - var(--ds-control-height) - var(--ds-row-overhang))
|
|
349
|
+
calc(var(--ds-row-padding, calc(4px * var(--ds-sf))) - var(--ds-row-overhang))
|
|
350
|
+
calc(0px - var(--ds-row-overhang));
|
|
351
|
+
border-radius: var(--ds-row-radius, var(--ds-row-fill-radius, calc(6px * var(--ds-sf))));
|
|
352
|
+
background: var(--ds-row-fill-current, var(--ds-fill-hover));
|
|
353
|
+
}
|
|
354
|
+
/* A plain row (no press target inside) has no padding of its own: its words and icons stand on
|
|
355
|
+
the row's edges, so the layer reaches two past them, as a flush row's does. */
|
|
356
|
+
.ds-row[data-open]:not([data-open="false"]):not(a, button, .ds-row--stacked, .ds-row--auto, :has(.ds-row__target))::after {
|
|
357
|
+
inset: calc(var(--ds-row-height) - var(--ds-control-height) - var(--ds-row-overhang))
|
|
358
|
+
calc(0px - var(--ds-row-overhang))
|
|
359
|
+
calc(0px - var(--ds-row-overhang));
|
|
360
|
+
}
|
|
361
|
+
.ds-row[data-open]:is(.ds-row--stacked, .ds-row--auto)::after { inset: 0; }
|
|
362
|
+
@media (forced-colors: active) {
|
|
363
|
+
.ds-row[data-open]:not([data-open="false"])::after { outline: 1px solid Highlight; outline-offset: -1px; }
|
|
364
|
+
}
|
package/DS1/1-root/table.css
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* <th colspan="2"> a column group, in a header row
|
|
17
17
|
* <th scope="rowgroup"> a row group, a heading row in grey
|
|
18
18
|
* <td aria-selected="true"> a selected cell
|
|
19
|
+
*
|
|
20
|
+
* `data-sheet` makes it a spreadsheet: every cell four squares by one (80 by
|
|
21
|
+
* 20), its 1px lines at its end and foot so the words have 79 by 19, set 4px
|
|
22
|
+
* in on the sixteen, as an Ezo item's are. Column letters and the row numbers
|
|
23
|
+
* (`.ds-table__letters`, `.ds-table__number`, two squares wide) stay in place
|
|
24
|
+
* while the sheet scrolls both ways inside `.ds-table-scroll`; give the table
|
|
25
|
+
* `--ds-table-columns` (the number of data columns) and the scroll a height
|
|
26
|
+
* with `--ds-table-height`.
|
|
19
27
|
*/
|
|
20
28
|
|
|
21
29
|
.ds-table {
|
|
@@ -109,3 +117,84 @@
|
|
|
109
117
|
.ds-table[data-sticky] [aria-selected="true"]:first-child {
|
|
110
118
|
background: var(--ds-accent-color, Highlight);
|
|
111
119
|
}
|
|
120
|
+
|
|
121
|
+
/* Keep the row whose details are open visible, including its sticky first cell.
|
|
122
|
+
Cell selection remains independent and keeps its accent. */
|
|
123
|
+
.ds-table tbody > tr[data-open]:not([data-open="false"]) > :is(th, td):not([aria-selected="true"]) {
|
|
124
|
+
background: var(--ds-row-fill-current, var(--ds-fill-hover));
|
|
125
|
+
}
|
|
126
|
+
@media (forced-colors: active) {
|
|
127
|
+
.ds-table tbody > tr[data-open]:not([data-open="false"]) > :is(th, td):not([aria-selected="true"]) {
|
|
128
|
+
background: Highlight;
|
|
129
|
+
color: HighlightText;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* A spreadsheet: cells four squares by one, on lines, lettered and numbered. */
|
|
134
|
+
.ds-table[data-sheet] {
|
|
135
|
+
--ds-table-cell: calc(4 * var(--ds-table-row));
|
|
136
|
+
--ds-table-index: calc(2 * var(--ds-table-row));
|
|
137
|
+
--ds-table-text: calc(4px * var(--ds-sf, 1));
|
|
138
|
+
table-layout: fixed;
|
|
139
|
+
width: calc(var(--ds-table-index) + var(--ds-table-columns, 8) * var(--ds-table-cell));
|
|
140
|
+
min-width: 0;
|
|
141
|
+
border-collapse: separate;
|
|
142
|
+
border-spacing: 0;
|
|
143
|
+
}
|
|
144
|
+
.ds-table-scroll > .ds-table[data-sheet] {
|
|
145
|
+
width: calc(var(--ds-table-index) + var(--ds-table-columns, 8) * var(--ds-table-cell));
|
|
146
|
+
min-width: 0;
|
|
147
|
+
}
|
|
148
|
+
.ds-table[data-sheet] :is(th, td) {
|
|
149
|
+
width: var(--ds-table-cell);
|
|
150
|
+
max-width: var(--ds-table-cell);
|
|
151
|
+
height: var(--ds-table-row);
|
|
152
|
+
padding: 0 var(--ds-table-text);
|
|
153
|
+
border-bottom: 1px solid var(--ds-table-rule);
|
|
154
|
+
border-inline-end: 1px solid var(--ds-table-rule);
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
text-overflow: ellipsis;
|
|
157
|
+
}
|
|
158
|
+
.ds-table[data-sheet] :is(th, td):last-child {
|
|
159
|
+
padding-inline-end: var(--ds-table-text);
|
|
160
|
+
}
|
|
161
|
+
.ds-table[data-sheet] .ds-table__number {
|
|
162
|
+
width: var(--ds-table-index);
|
|
163
|
+
max-width: var(--ds-table-index);
|
|
164
|
+
position: sticky;
|
|
165
|
+
inset-inline-start: 0;
|
|
166
|
+
z-index: 1;
|
|
167
|
+
background: var(--ds-table-paper);
|
|
168
|
+
color: var(--ds-title-color, light-dark(#797979, #9a9a9a));
|
|
169
|
+
text-align: center;
|
|
170
|
+
font-variant-numeric: tabular-nums;
|
|
171
|
+
}
|
|
172
|
+
.ds-table[data-sheet] thead :is(th, td) {
|
|
173
|
+
position: sticky;
|
|
174
|
+
top: 0;
|
|
175
|
+
z-index: 2;
|
|
176
|
+
background: var(--ds-table-paper);
|
|
177
|
+
}
|
|
178
|
+
.ds-table[data-sheet] thead tr + tr :is(th, td) {
|
|
179
|
+
top: var(--ds-table-row);
|
|
180
|
+
}
|
|
181
|
+
.ds-table[data-sheet] thead .ds-table__number {
|
|
182
|
+
z-index: 3;
|
|
183
|
+
}
|
|
184
|
+
.ds-table[data-sheet] .ds-table__letters td {
|
|
185
|
+
color: var(--ds-title-color, light-dark(#797979, #9a9a9a));
|
|
186
|
+
text-align: center;
|
|
187
|
+
}
|
|
188
|
+
.ds-table[data-sheet] tbody td:not([data-numeric]) {
|
|
189
|
+
color: inherit;
|
|
190
|
+
}
|
|
191
|
+
.ds-table[data-sheet] [aria-selected="true"] {
|
|
192
|
+
background: transparent;
|
|
193
|
+
color: inherit;
|
|
194
|
+
box-shadow: inset 0 0 0 2px var(--ds-focus-ring, Highlight);
|
|
195
|
+
}
|
|
196
|
+
.ds-table-scroll:has(> .ds-table[data-sheet]) {
|
|
197
|
+
max-height: var(--ds-table-height, none);
|
|
198
|
+
overflow: auto;
|
|
199
|
+
overscroll-behavior: contain;
|
|
200
|
+
}
|