@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.
Files changed (77) hide show
  1. package/DS1/0-face/app-banner.ts +148 -0
  2. package/DS1/1-root/cjk.css +1 -1
  3. package/DS1/1-root/context.css +152 -0
  4. package/DS1/1-root/controls.css +2 -1
  5. package/DS1/1-root/fonts.css +1 -1
  6. package/DS1/1-root/notice.css +81 -0
  7. package/DS1/1-root/one.css +1 -1
  8. package/DS1/1-root/one.semantic.css +1 -1
  9. package/DS1/1-root/row.css +74 -10
  10. package/DS1/1-root/table.css +89 -0
  11. package/DS1/2-core/ds-image.ts +42 -0
  12. package/DS1/2-core/ds-message.ts +75 -4
  13. package/DS1/2-core/styles/ds-image.css +14 -0
  14. package/DS1/2-core/styles/ds-notice.css +13 -0
  15. package/DS1/3-unit/ds-calendar-large.ts +5 -1
  16. package/DS1/3-unit/ds-context.ts +2 -2
  17. package/DS1/3-unit/ds-menu.ts +7 -5
  18. package/DS1/3-unit/ds-row.ts +6 -2
  19. package/DS1/3-unit/ds-select.ts +4 -3
  20. package/DS1/3-unit/ds-table.ts +35 -1
  21. package/DS1/3-unit/ds-token-field.ts +2 -2
  22. package/DS1/3-unit/styles/ds-calendar-large.css +13 -0
  23. package/DS1/3-unit/styles/ds-context.css +15 -7
  24. package/DS1/3-unit/styles/ds-row.css +15 -0
  25. package/DS1/3-unit/styles/ds-select.css +4 -3
  26. package/DS1/3-unit/styles/ds-tabs.css +1 -1
  27. package/DS1/index.ts +2 -0
  28. package/DS1/react/image.ts +10 -0
  29. package/DS1/react/message.ts +16 -3
  30. package/DS1/react/page.ts +3 -2
  31. package/dist/0-face/app-banner.d.ts +60 -0
  32. package/dist/0-face/app-banner.d.ts.map +1 -0
  33. package/dist/0-face/app-banner.js +138 -0
  34. package/dist/2-core/ds-image.d.ts +59 -0
  35. package/dist/2-core/ds-image.d.ts.map +1 -0
  36. package/dist/2-core/ds-image.js +42 -0
  37. package/dist/2-core/ds-message.d.ts +49 -3
  38. package/dist/2-core/ds-message.d.ts.map +1 -1
  39. package/dist/2-core/ds-message.js +71 -3
  40. package/dist/3-unit/ds-calendar-large.d.ts +5 -0
  41. package/dist/3-unit/ds-calendar-large.d.ts.map +1 -1
  42. package/dist/3-unit/ds-calendar-large.js +4 -1
  43. package/dist/3-unit/ds-context.d.ts +2 -3
  44. package/dist/3-unit/ds-context.d.ts.map +1 -1
  45. package/dist/3-unit/ds-context.js +2 -2
  46. package/dist/3-unit/ds-menu.d.ts +5 -3
  47. package/dist/3-unit/ds-menu.d.ts.map +1 -1
  48. package/dist/3-unit/ds-menu.js +6 -4
  49. package/dist/3-unit/ds-row.d.ts +6 -0
  50. package/dist/3-unit/ds-row.d.ts.map +1 -1
  51. package/dist/3-unit/ds-row.js +5 -2
  52. package/dist/3-unit/ds-select.d.ts +0 -1
  53. package/dist/3-unit/ds-select.d.ts.map +1 -1
  54. package/dist/3-unit/ds-select.js +4 -3
  55. package/dist/3-unit/ds-table.d.ts +10 -0
  56. package/dist/3-unit/ds-table.d.ts.map +1 -1
  57. package/dist/3-unit/ds-table.js +31 -1
  58. package/dist/3-unit/ds-token-field.d.ts +1 -1
  59. package/dist/3-unit/ds-token-field.d.ts.map +1 -1
  60. package/dist/3-unit/ds-token-field.js +2 -2
  61. package/dist/design-system-1.bundle.js +288 -28
  62. package/dist/design-system-1.bundle.js.map +1 -1
  63. package/dist/design-system-1.bundle.min.js +2177 -1997
  64. package/dist/design-system-1.bundle.min.js.map +1 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -0
  68. package/dist/react/image.d.ts +29 -0
  69. package/dist/react/image.d.ts.map +1 -0
  70. package/dist/react/image.js +4 -0
  71. package/dist/react/message.d.ts +26 -2
  72. package/dist/react/message.d.ts.map +1 -1
  73. package/dist/react/message.js +3 -2
  74. package/dist/react/page.d.ts +4 -3
  75. package/dist/react/page.d.ts.map +1 -1
  76. package/dist/react/page.js +2 -1
  77. 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
+ }
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.17 */
1
+ /* version 0.3.0-alpha.19 */
2
2
 
3
3
  /* Noto Sans for Japanese and Chinese, for a product that shows them. Opt in:
4
4
  link it beside one.css or fonts.css, or ask Sync 1 for "cjk": true. Without
@@ -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
+ }
@@ -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: var(--ds-tabs-gap, calc(10px * var(--ds-sf)));
458
+ gap: calc(4px * var(--ds-sf));
458
459
  min-width: 0;
459
460
  height: var(--ds-row-height);
460
461
  }
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.17 */
1
+ /* version 0.3.0-alpha.19 */
2
2
 
3
3
  /* Design system 1's faces and the typeface tokens that name them, on their
4
4
  own: a page that does not load one.css links this sheet (one.css imports
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.17 */
1
+ /* version 0.3.0-alpha.19 */
2
2
 
3
3
  @import "./grid.css";
4
4
  @import "./component-tokens.css";
@@ -1,4 +1,4 @@
1
- /* DS1 — version 0.3.0-alpha.17 */
1
+ /* DS1 — version 0.3.0-alpha.19 */
2
2
  /* A classless stylesheet for native HTML. No JavaScript is required. */
3
3
 
4
4
  /* It draws on Design system 1's measures: the 20px square, 14px type on
@@ -132,18 +132,52 @@ button.ds-row {
132
132
  z-index: 2;
133
133
  }
134
134
 
135
- /* The pointer, the press, the current destination and the focus ring. */
136
- a.ds-row[href]:hover,
137
- button.ds-row:hover:not(:disabled),
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
+ }
@@ -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
+ }