@0001-labs/design-system-1 0.3.0-alpha.18 → 0.3.0-alpha.20
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 +7 -6
- 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 +10 -1
- package/DS1/1-root/table.css +77 -0
- package/DS1/2-core/ds-message.ts +75 -4
- package/DS1/2-core/styles/ds-notice.css +13 -0
- package/DS1/3-unit/ds-context.ts +2 -2
- package/DS1/3-unit/ds-table.ts +35 -1
- package/DS1/3-unit/styles/ds-context.css +14 -8
- 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-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-context.d.ts +2 -2
- package/dist/3-unit/ds-context.js +2 -2
- 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/design-system-1.bundle.js +206 -9
- package/dist/design-system-1.bundle.js.map +1 -1
- package/dist/design-system-1.bundle.min.js +2122 -1987
- package/dist/design-system-1.bundle.min.js.map +1 -1
- 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.appendChild(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
package/DS1/1-root/context.css
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* `.ds-context` is the `ds-context` element's look on a native menu, for a page
|
|
5
5
|
* without Lit or a menu a library draws: Ezo's gap face. The rows carry the
|
|
6
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
10
|
* on the sixteen and an icon at its end. A Select's menu is the light face,
|
|
11
11
|
* `.ds-menu`; a Context's is this dark one.
|
|
12
12
|
*
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
.ds-context {
|
|
30
30
|
--ds-context-row: calc(var(--ds-row-height) - 1px);
|
|
31
|
-
|
|
32
|
-
--ds-context-
|
|
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);
|
|
33
34
|
--ds-context-hover: color-mix(in srgb, var(--ds-context-fill) 78%, var(--ds-context-ink));
|
|
34
|
-
--ds-context-danger:
|
|
35
|
+
--ds-context-danger: var(--ds-tuned-red);
|
|
35
36
|
--ds-context-refused: var(--ds-tuned-red);
|
|
36
37
|
box-sizing: border-box;
|
|
37
38
|
display: flex;
|
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
|
@@ -137,7 +137,9 @@ button.ds-row {
|
|
|
137
137
|
* row's content (Joachim, 19 October 2026). On a one-square row it is a square high and sits two
|
|
138
138
|
* pixels around what the row holds: from two above the lower sixteen to two below the row's foot,
|
|
139
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.
|
|
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.
|
|
141
143
|
* Tokens: `--ds-row-fill-radius` (6px), `--ds-row-fill-overhang` (2px).
|
|
142
144
|
*/
|
|
143
145
|
a.ds-row,
|
|
@@ -349,6 +351,13 @@ button.ds-row[aria-disabled="true"]:hover::after {
|
|
|
349
351
|
border-radius: var(--ds-row-radius, var(--ds-row-fill-radius, calc(6px * var(--ds-sf))));
|
|
350
352
|
background: var(--ds-row-fill-current, var(--ds-fill-hover));
|
|
351
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
|
+
}
|
|
352
361
|
.ds-row[data-open]:is(.ds-row--stacked, .ds-row--auto)::after { inset: 0; }
|
|
353
362
|
@media (forced-colors: active) {
|
|
354
363
|
.ds-row[data-open]:not([data-open="false"])::after { outline: 1px solid Highlight; outline-offset: -1px; }
|
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 {
|
|
@@ -121,3 +129,72 @@
|
|
|
121
129
|
color: HighlightText;
|
|
122
130
|
}
|
|
123
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
|
+
}
|
package/DS1/2-core/ds-message.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// ds-message.ts
|
|
2
2
|
// An inline notice in the flow of the page: a tone, a title and/or text,
|
|
3
3
|
// actions and an optional dismiss; with `banner`, a strip across the top of
|
|
4
|
-
// its positioning context instead
|
|
5
|
-
//
|
|
4
|
+
// its positioning context instead; with `form="app"`, a row at the top of the
|
|
5
|
+
// page leading to the site's iPhone app (0-face/app-banner.ts decides where).
|
|
6
|
+
// Drawn by 1-root/notice.css, the same classes a page without scripts writes.
|
|
6
7
|
|
|
7
8
|
import { LitElement, html, nothing, unsafeCSS, type PropertyValues } from "lit";
|
|
8
9
|
import { getText } from "../0-face/i18n";
|
|
@@ -10,9 +11,10 @@ import { define } from "../0-face/define";
|
|
|
10
11
|
import "./ds-icon";
|
|
11
12
|
import "./ds-toast";
|
|
12
13
|
import type { ToastPosition } from "../0-face/toast-queue";
|
|
14
|
+
import { appBannerPlacement, appStoreUrl, closeAppBanner, ensureSmartAppBanner, openApp, type AppBannerOptions, type AppBannerPlacement } from "../0-face/app-banner";
|
|
13
15
|
export { toast as message } from "../0-face/toast-queue";
|
|
14
16
|
export type { ToastOptions as MessageOptions, ToastId as MessageId } from "../0-face/toast-queue";
|
|
15
|
-
export type MessageForm = "inline" | "banner" | "toast" | "assistant";
|
|
17
|
+
export type MessageForm = "inline" | "banner" | "toast" | "assistant" | "app";
|
|
16
18
|
import metrics from "../1-root/component-metrics.css?inline";
|
|
17
19
|
import controls from "../1-root/controls.css?inline";
|
|
18
20
|
import styles from "./styles/ds-notice.css?inline";
|
|
@@ -42,6 +44,19 @@ export interface MessageDismissDetail { tone: MessageTone }
|
|
|
42
44
|
* @prop {boolean} banner - A strip one square high across the top of its
|
|
43
45
|
* positioning context, filled in the tone's colour, its words, actions and
|
|
44
46
|
* close button on one line; it reserves no room in the flow.
|
|
47
|
+
* @prop {string} form - `inline` (the default), `banner`, `toast`,
|
|
48
|
+
* `assistant`, or `app`: a row at the top of the page leading to the site's
|
|
49
|
+
* iPhone app, with its picture, `heading` (the app's name), `text` (one
|
|
50
|
+
* line about it), Open and a close. On iOS Safari it draws nothing and puts
|
|
51
|
+
* Apple's Smart App Banner on instead; in iOS's other browsers it draws
|
|
52
|
+
* itself; never on a computer, Android, from the home screen or in a web
|
|
53
|
+
* view; once closed, never again in this browser.
|
|
54
|
+
* @prop {string} src - (app) The app's icon.
|
|
55
|
+
* @prop {string} app-id - (app) The App Store id, for Apple's banner and the store address.
|
|
56
|
+
* @prop {string} app-url - (app) The store address, when it is not the App Store's own.
|
|
57
|
+
* @prop {string} open - (app) The address that opens the app at this page.
|
|
58
|
+
* @prop {string} remember - (app) The name the close is remembered by; one per app by default.
|
|
59
|
+
* @prop {boolean} preview - (app) Draw it on any device, for documentation.
|
|
45
60
|
*
|
|
46
61
|
* @slot - The text, when it needs more than words: a link, a strong word.
|
|
47
62
|
* @slot heading - The title, as inline content.
|
|
@@ -79,6 +94,12 @@ export class Message extends LitElement {
|
|
|
79
94
|
icon: { type: String },
|
|
80
95
|
dismissible: { type: Boolean, reflect: true },
|
|
81
96
|
banner: { type: Boolean, reflect: true },
|
|
97
|
+
src: { type: String },
|
|
98
|
+
appId: { type: String, attribute: "app-id" },
|
|
99
|
+
appUrl: { type: String, attribute: "app-url" },
|
|
100
|
+
open: { type: String },
|
|
101
|
+
remember: { type: String },
|
|
102
|
+
preview: { type: Boolean },
|
|
82
103
|
};
|
|
83
104
|
|
|
84
105
|
static styles = [unsafeCSS(metrics), unsafeCSS(controls), unsafeCSS(styles)];
|
|
@@ -102,6 +123,14 @@ export class Message extends LitElement {
|
|
|
102
123
|
declare icon: string | undefined;
|
|
103
124
|
declare dismissible: boolean;
|
|
104
125
|
declare banner: boolean;
|
|
126
|
+
declare src: string | undefined;
|
|
127
|
+
declare appId: string | undefined;
|
|
128
|
+
declare appUrl: string | undefined;
|
|
129
|
+
declare open: string | undefined;
|
|
130
|
+
declare remember: string | undefined;
|
|
131
|
+
declare preview: boolean;
|
|
132
|
+
/** (app) Where this device shows it, decided before each render. */
|
|
133
|
+
private placement: AppBannerPlacement = "none";
|
|
105
134
|
|
|
106
135
|
/** The role this element last set, so a role the page wrote is left alone. */
|
|
107
136
|
private ownRole: string | null = null;
|
|
@@ -123,6 +152,7 @@ export class Message extends LitElement {
|
|
|
123
152
|
this.literal = false;
|
|
124
153
|
this.dismissible = false;
|
|
125
154
|
this.banner = false;
|
|
155
|
+
this.preview = false;
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
/** The tone in effect: an unknown value reads as neutral. */
|
|
@@ -146,13 +176,28 @@ export class Message extends LitElement {
|
|
|
146
176
|
protected willUpdate(changed: PropertyValues<this>) {
|
|
147
177
|
super.willUpdate(changed);
|
|
148
178
|
if (changed.has("tone") || changed.has("form")) this.syncRole();
|
|
179
|
+
if (this.form === "app") this.place();
|
|
180
|
+
else if (changed.get("form") === "app") this.hidden = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private get app(): AppBannerOptions {
|
|
184
|
+
return { appId: this.appId, appUrl: this.appUrl, open: this.open, key: this.remember };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Decides where the app banner shows, before it draws, so it never pops in after the page. */
|
|
188
|
+
private place() {
|
|
189
|
+
this.placement = this.preview ? "drawn" : appBannerPlacement(this.app);
|
|
190
|
+
if (this.placement === "safari") ensureSmartAppBanner(this.app);
|
|
191
|
+
this.hidden = this.placement !== "drawn";
|
|
192
|
+
const name = this.words(this.heading) || this.querySelector('[slot="heading"]')?.textContent?.trim();
|
|
193
|
+
if (name && this.getAttribute("aria-label") !== name) this.setAttribute("aria-label", name);
|
|
149
194
|
}
|
|
150
195
|
|
|
151
196
|
private syncRole() {
|
|
152
197
|
if (!this.isConnected) return;
|
|
153
198
|
const current = this.getAttribute("role");
|
|
154
199
|
if (current !== null && current !== this.ownRole) return;
|
|
155
|
-
this.ownRole = this.form === "toast" ? "presentation" : this.resolvedTone === "danger" ? "alert" : "status";
|
|
200
|
+
this.ownRole = this.form === "toast" ? "presentation" : this.form === "app" ? "region" : this.resolvedTone === "danger" ? "alert" : "status";
|
|
156
201
|
if (current !== this.ownRole) this.setAttribute("role", this.ownRole);
|
|
157
202
|
}
|
|
158
203
|
|
|
@@ -178,13 +223,39 @@ export class Message extends LitElement {
|
|
|
178
223
|
const event = new CustomEvent<MessageDismissDetail>(this.dismissEvent, {
|
|
179
224
|
bubbles: true, composed: true, cancelable: true, detail: { tone: this.resolvedTone },
|
|
180
225
|
});
|
|
226
|
+
// An app banner closed stays closed in this browser, whoever removes it.
|
|
227
|
+
if (this.form === "app" && !this.preview) closeAppBanner(this.app);
|
|
181
228
|
if (!this.dispatchEvent(event)) return false;
|
|
182
229
|
this.hidden = true;
|
|
183
230
|
return true;
|
|
184
231
|
}
|
|
185
232
|
|
|
233
|
+
private openApp(event: MouseEvent) {
|
|
234
|
+
if (!this.open || this.preview) return;
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
openApp(this.app);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private renderApp() {
|
|
240
|
+
const heading = this.words(this.heading);
|
|
241
|
+
const text = this.words(this.text);
|
|
242
|
+
const changed = () => this.requestUpdate();
|
|
243
|
+
return html`<div class="ds-notice ds-notice--app" part="message notice">
|
|
244
|
+
<span class="ds-notice__picture" part="picture" aria-hidden="true">
|
|
245
|
+
<slot name="icon" @slotchange=${changed}>${this.src ? html`<img src=${this.src} alt="">` : nothing}</slot>
|
|
246
|
+
</span>
|
|
247
|
+
<div class="ds-notice__title" part="title"><slot name="heading" @slotchange=${changed}>${heading}</slot></div>
|
|
248
|
+
<div class="ds-notice__text" part="text"><slot @slotchange=${changed}></slot>${this.slotted("") ? nothing : text}</div>
|
|
249
|
+
<a class="ds-button ds-button--primary ds-notice__open" part="open" href=${appStoreUrl(this.app) || this.open || nothing} @click=${(event: MouseEvent) => this.openApp(event)}>${getText("Open") || "Open"}</a>
|
|
250
|
+
<button class="ds-button ds-button--icon ds-notice__dismiss" part="dismiss" type="button" aria-label=${getText("Close") || "Close"} @click=${() => this.dismiss()}>
|
|
251
|
+
<ds-icon type="close" aria-hidden="true"></ds-icon>
|
|
252
|
+
</button>
|
|
253
|
+
</div>`;
|
|
254
|
+
}
|
|
255
|
+
|
|
186
256
|
render() {
|
|
187
257
|
if (this.form === "toast") return html`<ds-toast .expand=${this.expand} .visibleToasts=${this.visibleToasts} .richColors=${this.richColors} .contained=${this.contained} .label=${this.label} .closeLabel=${this.closeLabel} .position=${this.position} .duration=${this.duration} .closeButton=${this.closeButton}></ds-toast>`;
|
|
258
|
+
if (this.form === "app") return this.placement === "drawn" ? this.renderApp() : nothing;
|
|
188
259
|
const tone = this.resolvedTone;
|
|
189
260
|
const heading = this.words(this.heading);
|
|
190
261
|
const text = this.words(this.text);
|
|
@@ -25,8 +25,21 @@
|
|
|
25
25
|
position: static;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/* The app banner stands in the flow, at the top of the page. */
|
|
29
|
+
:host([form="app"]) {
|
|
30
|
+
display: block;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
/* Slotted words take the notice's lines rather than their own margins. */
|
|
29
34
|
::slotted([slot="heading"]),
|
|
30
35
|
::slotted(:not([slot])) {
|
|
31
36
|
margin: 0;
|
|
32
37
|
}
|
|
38
|
+
|
|
39
|
+
/* The app's own picture in the icon slot fills its two squares. */
|
|
40
|
+
::slotted(img[slot="icon"]) {
|
|
41
|
+
display: block;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
object-fit: cover;
|
|
45
|
+
}
|
package/DS1/3-unit/ds-context.ts
CHANGED
|
@@ -15,8 +15,8 @@ const REFUSED = 1400;
|
|
|
15
15
|
* from the keyboard. `context-selector` delegates one menu to many rows of a
|
|
16
16
|
* long list, and focus returns to the row that opened it.
|
|
17
17
|
*
|
|
18
|
-
* It wears Ezo's gap face: each row
|
|
19
|
-
*
|
|
18
|
+
* It wears Ezo's gap face: each row is black with white words in both themes
|
|
19
|
+
* (the slate in dark, darker than the page), rows a pixel apart and groups two (an
|
|
20
20
|
* `hr data-ds-menu-separator` between them, never a line), the page showing
|
|
21
21
|
* through the gaps, the outer corners rounded as one silhouette, and an icon
|
|
22
22
|
* at a row's end. A group left empty leaves no gap. A row hovered or focused
|