@0001-labs/design-system-1 0.3.0-alpha.17 → 0.3.0-alpha.18

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 (53) hide show
  1. package/DS1/1-root/cjk.css +1 -1
  2. package/DS1/1-root/context.css +151 -0
  3. package/DS1/1-root/controls.css +2 -1
  4. package/DS1/1-root/fonts.css +1 -1
  5. package/DS1/1-root/one.css +1 -1
  6. package/DS1/1-root/one.semantic.css +1 -1
  7. package/DS1/1-root/row.css +65 -10
  8. package/DS1/1-root/table.css +12 -0
  9. package/DS1/2-core/ds-image.ts +42 -0
  10. package/DS1/2-core/styles/ds-image.css +14 -0
  11. package/DS1/3-unit/ds-calendar-large.ts +5 -1
  12. package/DS1/3-unit/ds-menu.ts +7 -5
  13. package/DS1/3-unit/ds-row.ts +6 -2
  14. package/DS1/3-unit/ds-select.ts +4 -3
  15. package/DS1/3-unit/ds-token-field.ts +2 -2
  16. package/DS1/3-unit/styles/ds-calendar-large.css +13 -0
  17. package/DS1/3-unit/styles/ds-context.css +3 -1
  18. package/DS1/3-unit/styles/ds-row.css +15 -0
  19. package/DS1/3-unit/styles/ds-select.css +4 -3
  20. package/DS1/3-unit/styles/ds-tabs.css +1 -1
  21. package/DS1/index.ts +2 -0
  22. package/DS1/react/image.ts +10 -0
  23. package/dist/2-core/ds-image.d.ts +59 -0
  24. package/dist/2-core/ds-image.d.ts.map +1 -0
  25. package/dist/2-core/ds-image.js +42 -0
  26. package/dist/3-unit/ds-calendar-large.d.ts +5 -0
  27. package/dist/3-unit/ds-calendar-large.d.ts.map +1 -1
  28. package/dist/3-unit/ds-calendar-large.js +4 -1
  29. package/dist/3-unit/ds-context.d.ts +0 -1
  30. package/dist/3-unit/ds-context.d.ts.map +1 -1
  31. package/dist/3-unit/ds-menu.d.ts +5 -3
  32. package/dist/3-unit/ds-menu.d.ts.map +1 -1
  33. package/dist/3-unit/ds-menu.js +6 -4
  34. package/dist/3-unit/ds-row.d.ts +6 -0
  35. package/dist/3-unit/ds-row.d.ts.map +1 -1
  36. package/dist/3-unit/ds-row.js +5 -2
  37. package/dist/3-unit/ds-select.d.ts +0 -1
  38. package/dist/3-unit/ds-select.d.ts.map +1 -1
  39. package/dist/3-unit/ds-select.js +4 -3
  40. package/dist/3-unit/ds-token-field.d.ts +1 -1
  41. package/dist/3-unit/ds-token-field.d.ts.map +1 -1
  42. package/dist/3-unit/ds-token-field.js +2 -2
  43. package/dist/design-system-1.bundle.js +86 -23
  44. package/dist/design-system-1.bundle.js.map +1 -1
  45. package/dist/design-system-1.bundle.min.js +1213 -1168
  46. package/dist/design-system-1.bundle.min.js.map +1 -1
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  50. package/dist/react/image.d.ts +29 -0
  51. package/dist/react/image.d.ts.map +1 -0
  52. package/dist/react/image.js +4 -0
  53. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.17 */
1
+ /* version 0.3.0-alpha.18 */
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,151 @@
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 ink is the rows'
8
+ * fill and the paper their words, turned over with the theme as the tooltip
9
+ * is. 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
+ --ds-context-fill: light-dark(var(--ds-black, #1e1e1e), var(--ds-white, #ffffff));
32
+ --ds-context-ink: light-dark(var(--ds-white, #ffffff), var(--ds-black, #1e1e1e));
33
+ --ds-context-hover: color-mix(in srgb, var(--ds-context-fill) 78%, var(--ds-context-ink));
34
+ --ds-context-danger: light-dark(var(--ds-tuned-red), var(--ds-red-for-blood));
35
+ --ds-context-refused: var(--ds-tuned-red);
36
+ box-sizing: border-box;
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 1px;
40
+ width: var(--ds-menu-width, max-content);
41
+ min-width: calc(4 * var(--ds-unit));
42
+ max-height: var(--ds-menu-max-height, calc(16 * var(--ds-unit)));
43
+ overflow-y: auto;
44
+ margin: 0;
45
+ padding: 0;
46
+ border: 0;
47
+ background: transparent;
48
+ box-shadow: none;
49
+ clip-path: inset(0 round var(--ds-context-radius, var(--ds-radius, 4px)));
50
+ color: var(--ds-context-ink);
51
+ font-family: var(--ds-typeface-regular);
52
+ font-feature-settings: var(--ds-font-features);
53
+ font-size: var(--ds-type-size-default);
54
+ font-weight: var(--ds-type-weight-default);
55
+ line-height: var(--ds-type-lineheight-default);
56
+ letter-spacing: var(--ds-type-letterspacing-default);
57
+ list-style: none;
58
+ }
59
+
60
+ .ds-context > [role="group"] {
61
+ display: contents;
62
+ }
63
+
64
+ /* A row: whatever the page or library styles its buttons with, one row on the sixteen. */
65
+ .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"])) {
66
+ box-sizing: border-box !important;
67
+ display: flex !important;
68
+ flex: 0 0 auto !important;
69
+ align-items: flex-end !important;
70
+ justify-content: space-between !important;
71
+ gap: calc(4px * var(--ds-sf));
72
+ width: 100%;
73
+ height: var(--ds-context-row) !important;
74
+ min-height: var(--ds-context-row) !important;
75
+ max-height: var(--ds-context-row) !important;
76
+ margin: 0 !important;
77
+ padding: 0 calc(4px * var(--ds-sf)) !important;
78
+ border: 0 !important;
79
+ border-radius: 0 !important;
80
+ background: var(--ds-context-fill) !important;
81
+ color: var(--ds-context-ink) !important;
82
+ font: inherit;
83
+ line-height: var(--ds-type-lineheight-default) !important;
84
+ letter-spacing: inherit;
85
+ text-align: start;
86
+ text-decoration: none;
87
+ white-space: nowrap;
88
+ cursor: pointer;
89
+ outline: none;
90
+ appearance: none;
91
+ -webkit-tap-highlight-color: transparent;
92
+ }
93
+
94
+ .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]) {
95
+ background: var(--ds-context-hover) !important;
96
+ }
97
+
98
+ .ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"])):focus-visible {
99
+ box-shadow: inset 0 0 0 2px var(--ds-focus-ring);
100
+ }
101
+
102
+ .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]) {
103
+ cursor: default;
104
+ opacity: 0.5;
105
+ }
106
+
107
+ .ds-context[aria-busy="true"] > * {
108
+ cursor: wait !important;
109
+ }
110
+
111
+ /* Danger is red, on either fill. */
112
+ .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"]) {
113
+ color: var(--ds-context-danger) !important;
114
+ }
115
+
116
+ /* A refused change: its row turns red a moment. */
117
+ .ds-context :is(.ds-context__item, :is(.ds-context, .ds-context > [role="group"]) > :is(button, a, li, [role^="menuitem"]))[data-ds-refused] {
118
+ background: var(--ds-context-refused) !important;
119
+ }
120
+
121
+ /* Groups are a gap, never a line: the separator takes nothing but the gap after it. */
122
+ .ds-context > :is(hr, .ds-context__separator, [role="separator"]) {
123
+ flex: 0 0 0 !important;
124
+ height: 0 !important;
125
+ margin: 0 !important;
126
+ padding: 0 !important;
127
+ border: 0 !important;
128
+ background: none !important;
129
+ }
130
+
131
+ /* A group left empty leaves no gap. */
132
+ .ds-context > :is(hr, .ds-context__separator, [role="separator"]):is(:first-child, :last-child),
133
+ .ds-context > :is(hr, .ds-context__separator, [role="separator"]) + :is(hr, .ds-context__separator, [role="separator"]) {
134
+ display: none !important;
135
+ }
136
+
137
+ .ds-context :is(.ds-context__label, [data-ds-menu-label]) {
138
+ box-sizing: border-box;
139
+ height: var(--ds-context-row);
140
+ /* The spare three above the sixteen. */
141
+ padding: calc(var(--ds-context-row) - var(--ds-type-lineheight-default)) calc(4px * var(--ds-sf)) 0;
142
+ background: var(--ds-context-fill);
143
+ color: color-mix(in srgb, var(--ds-context-ink) 60%, var(--ds-context-fill));
144
+ line-height: var(--ds-type-lineheight-default);
145
+ white-space: nowrap;
146
+ }
147
+
148
+ .ds-context__shortcut {
149
+ margin-left: auto;
150
+ color: color-mix(in srgb, var(--ds-context-ink) 60%, var(--ds-context-fill));
151
+ }
@@ -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.18 */
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
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.17 */
1
+ /* version 0.3.0-alpha.18 */
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.18 */
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,50 @@ 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. A taller row (a stacked tile, `auto`) is filled whole.
141
+ * Tokens: `--ds-row-fill-radius` (6px), `--ds-row-fill-overhang` (2px).
142
+ */
143
+ a.ds-row,
144
+ button.ds-row {
145
+ isolation: isolate;
146
+ }
147
+ a.ds-row::after,
148
+ button.ds-row::after {
149
+ content: "";
150
+ position: absolute;
151
+ z-index: -1;
152
+ pointer-events: none;
153
+ }
154
+ a.ds-row::after,
155
+ button.ds-row::after,
156
+ .ds-row__target::after {
157
+ --ds-row-overhang: var(--ds-row-fill-overhang, calc(2px * var(--ds-sf)));
158
+ inset:
159
+ calc(var(--ds-row-height) - var(--ds-control-height) - var(--ds-row-overhang))
160
+ calc(var(--ds-row-padding, calc(4px * var(--ds-sf))) - var(--ds-row-overhang))
161
+ calc(0px - var(--ds-row-overhang));
162
+ border-radius: var(--ds-row-radius, var(--ds-row-fill-radius, calc(6px * var(--ds-sf))));
163
+ }
164
+ :is(a, button).ds-row:is(.ds-row--stacked, .ds-row--auto)::after,
165
+ :is(.ds-row--stacked, .ds-row--auto) .ds-row__target::after {
166
+ inset: 0;
167
+ }
168
+ a.ds-row[href]:hover::after,
169
+ button.ds-row:hover:not(:disabled)::after,
138
170
  .ds-row__target:is(a[href], button:not(:disabled)):hover::after {
139
171
  background: var(--ds-row-fill-hover, var(--ds-fill));
140
172
  }
141
- a.ds-row[href]:active,
142
- button.ds-row:active:not(:disabled),
173
+ a.ds-row[href]:active::after,
174
+ button.ds-row:active:not(:disabled)::after,
143
175
  .ds-row__target:is(a[href], button:not(:disabled)):active::after {
144
176
  background: var(--ds-row-fill-press, var(--ds-fill-hover));
145
177
  }
146
- .ds-row:is(a, button)[aria-current]:not([aria-current="false"]),
178
+ .ds-row:is(a, button)[aria-current]:not([aria-current="false"])::after,
147
179
  .ds-row__target[aria-current]:not([aria-current="false"])::after {
148
180
  background: var(--ds-row-fill-current, var(--ds-fill-hover));
149
181
  }
@@ -152,8 +184,8 @@ button.ds-row:active:not(:disabled),
152
184
  --ds-type-weight-default: var(--ds-row-current-weight, 500);
153
185
  font-weight: var(--ds-row-current-weight, 500);
154
186
  }
155
- a.ds-row:focus-visible,
156
- button.ds-row:focus-visible,
187
+ a.ds-row:focus-visible::after,
188
+ button.ds-row:focus-visible::after,
157
189
  .ds-row__target:focus-visible::after {
158
190
  outline: 2px solid var(--ds-focus-ring);
159
191
  outline-offset: -2px;
@@ -169,8 +201,8 @@ button.ds-row[aria-disabled="true"],
169
201
  .ds-row__target:is(:disabled, [aria-disabled="true"], a:not([href])) {
170
202
  cursor: default;
171
203
  }
172
- a.ds-row[aria-disabled="true"]:hover,
173
- button.ds-row[aria-disabled="true"]:hover {
204
+ a.ds-row[aria-disabled="true"]:hover::after,
205
+ button.ds-row[aria-disabled="true"]:hover::after {
174
206
  background: none;
175
207
  }
176
208
 
@@ -298,3 +330,26 @@ button.ds-row[aria-disabled="true"]:hover {
298
330
  background-color: CanvasText;
299
331
  }
300
332
  }
333
+
334
+ /* A controlled detail view: the caller marks its open row, and clears it on close.
335
+ Navigation's current state and the text weight remain independent. */
336
+ .ds-row[data-open]:not([data-open="false"]) {
337
+ position: relative;
338
+ isolation: isolate;
339
+ }
340
+ .ds-row[data-open]:not([data-open="false"])::after {
341
+ content: "";
342
+ position: absolute;
343
+ z-index: -1;
344
+ pointer-events: none;
345
+ --ds-row-overhang: var(--ds-row-fill-overhang, calc(2px * var(--ds-sf)));
346
+ inset: calc(var(--ds-row-height) - var(--ds-control-height) - var(--ds-row-overhang))
347
+ calc(var(--ds-row-padding, calc(4px * var(--ds-sf))) - var(--ds-row-overhang))
348
+ calc(0px - var(--ds-row-overhang));
349
+ border-radius: var(--ds-row-radius, var(--ds-row-fill-radius, calc(6px * var(--ds-sf))));
350
+ background: var(--ds-row-fill-current, var(--ds-fill-hover));
351
+ }
352
+ .ds-row[data-open]:is(.ds-row--stacked, .ds-row--auto)::after { inset: 0; }
353
+ @media (forced-colors: active) {
354
+ .ds-row[data-open]:not([data-open="false"])::after { outline: 1px solid Highlight; outline-offset: -1px; }
355
+ }
@@ -109,3 +109,15 @@
109
109
  .ds-table[data-sticky] [aria-selected="true"]:first-child {
110
110
  background: var(--ds-accent-color, Highlight);
111
111
  }
112
+
113
+ /* Keep the row whose details are open visible, including its sticky first cell.
114
+ Cell selection remains independent and keeps its accent. */
115
+ .ds-table tbody > tr[data-open]:not([data-open="false"]) > :is(th, td):not([aria-selected="true"]) {
116
+ background: var(--ds-row-fill-current, var(--ds-fill-hover));
117
+ }
118
+ @media (forced-colors: active) {
119
+ .ds-table tbody > tr[data-open]:not([data-open="false"]) > :is(th, td):not([aria-selected="true"]) {
120
+ background: Highlight;
121
+ color: HighlightText;
122
+ }
123
+ }
@@ -0,0 +1,42 @@
1
+ import { LitElement, html, nothing, unsafeCSS } from "lit";
2
+ import { ifDefined } from "lit/directives/if-defined.js";
3
+ import { styleMap } from "lit/directives/style-map.js";
4
+ import { define } from "../0-face/define";
5
+ import styles from "./styles/ds-image.css?inline";
6
+
7
+ /** Responsive image with native alternative text, optional crop and caption. */
8
+ export class Image extends LitElement {
9
+ static properties = {
10
+ src: { type: String }, alt: { type: String }, caption: { type: String },
11
+ srcset: { type: String }, sizes: { type: String },
12
+ width: { type: Number }, height: { type: Number },
13
+ ratio: { type: String }, fit: { type: String }, position: { type: String },
14
+ loading: { type: String },
15
+ };
16
+ static styles = unsafeCSS(styles);
17
+ declare src: string; declare alt: string; declare caption: string;
18
+ declare srcset: string; declare sizes: string;
19
+ declare width?: number; declare height?: number;
20
+ declare ratio: string; declare fit: string; declare position: string;
21
+ declare loading: string;
22
+ constructor() {
23
+ super(); this.src = ""; this.alt = ""; this.caption = "";
24
+ this.srcset = ""; this.sizes = ""; this.ratio = "";
25
+ this.fit = "cover"; this.position = "center"; this.loading = "lazy";
26
+ }
27
+ render() {
28
+ const dimension = (value?: number) => value && Number.isFinite(value) && value > 0 ? value : undefined;
29
+ return html`<figure>
30
+ <div class="media" part="media" style=${styleMap({ aspectRatio: this.ratio || undefined })}>
31
+ <img part="image" src=${ifDefined(this.src || undefined)} alt=${this.alt}
32
+ srcset=${ifDefined(this.srcset || undefined)} sizes=${ifDefined(this.sizes || undefined)}
33
+ width=${ifDefined(dimension(this.width))} height=${ifDefined(dimension(this.height))}
34
+ loading=${this.loading === "eager" ? "eager" : "lazy"} decoding="async"
35
+ style=${styleMap({ height: this.ratio ? "100%" : "auto", objectFit: this.fit === "contain" ? "contain" : "cover", objectPosition: this.position })} />
36
+ </div>
37
+ ${this.caption ? html`<figcaption part="caption">${this.caption}</figcaption>` : nothing}
38
+ </figure>`;
39
+ }
40
+ }
41
+ define("ds-image", Image);
42
+ declare global { interface HTMLElementTagNameMap { "ds-image": Image } }
@@ -0,0 +1,14 @@
1
+ @import "../../1-root/component-metrics.css";
2
+ :host { display: block; width: 100%; max-width: 100%; min-width: 0; }
3
+ :host([hidden]) { display: none !important; }
4
+ figure { margin: 0; }
5
+ .media { overflow: hidden; border-radius: var(--ds-radius); background: var(--ds-image-background, var(--ds-fill)); }
6
+ img { display: block; width: 100%; max-width: 100%; }
7
+ figcaption {
8
+ margin-top: calc(var(--ds-unit) / 2);
9
+ color: var(--ds-text-color-muted);
10
+ font-family: var(--ds-typeface-regular);
11
+ font-feature-settings: var(--ds-font-features);
12
+ font-size: var(--ds-type-size-default);
13
+ line-height: var(--ds-row-height);
14
+ }
@@ -66,6 +66,7 @@ export class CalendarLarge extends CalendarBase {
66
66
  start: { type: String, reflect: true },
67
67
  months: { type: Number },
68
68
  scale: { type: Number },
69
+ activity: { type: Boolean, reflect: true },
69
70
  noHeader: { type: Boolean, attribute: "no-header", reflect: true },
70
71
  visible: { state: true },
71
72
  };
@@ -75,6 +76,7 @@ export class CalendarLarge extends CalendarBase {
75
76
  declare start: string | undefined;
76
77
  declare months: number | undefined;
77
78
  declare scale: number | undefined;
79
+ declare activity: boolean;
78
80
  declare noHeader: boolean;
79
81
  /** The month at the start of the view, named in the title. */
80
82
  declare visible: Day | undefined;
@@ -88,6 +90,7 @@ export class CalendarLarge extends CalendarBase {
88
90
  constructor() {
89
91
  super();
90
92
  this.noHeader = false;
93
+ this.activity = false;
91
94
  }
92
95
 
93
96
  /** The first of the first month drawn. */
@@ -120,7 +123,7 @@ export class CalendarLarge extends CalendarBase {
120
123
 
121
124
  protected willUpdate(changed: PropertyValues<this>) {
122
125
  super.willUpdate(changed);
123
- const scale = typeof this.scale === "number" && Number.isFinite(this.scale) && this.scale > 0 ? this.scale : 1.5;
126
+ const scale = typeof this.scale === "number" && Number.isFinite(this.scale) && this.scale > 0 ? this.scale : this.activity ? 1 : 1.5;
124
127
  this.style.setProperty("--ds-calendar-scale", String(scale));
125
128
  }
126
129
 
@@ -314,6 +317,7 @@ export class CalendarLarge extends CalendarBase {
314
317
  data-day=${day}
315
318
  tabindex=${day === tab ? 0 : -1}
316
319
  aria-label=${this.cellLabel(day, events)}
320
+ title=${this.activity ? this.cellLabel(day, events) : nothing}
317
321
  aria-selected=${this.selectable ? String(selected || inside) : nothing}
318
322
  aria-current=${isToday ? "date" : nothing}
319
323
  aria-disabled=${this.allowed(day) ? nothing : "true"}
@@ -30,7 +30,7 @@ export class Menu extends LitElement {
30
30
  anchor: { attribute: false },
31
31
  placement: { type: String },
32
32
  trigger: { attribute: false },
33
- for: { type: String, reflect: true },
33
+ for: { type: String },
34
34
  triggerMode: { type: String, attribute: "trigger-mode", reflect: true },
35
35
  contextSelector: { type: String, attribute: "context-selector" },
36
36
  hover: { type: Boolean, reflect: true },
@@ -64,7 +64,8 @@ export class Menu extends LitElement {
64
64
  private search = "";
65
65
  private searchAt = 0;
66
66
  private gridColumn = 0;
67
- private focusOnOpen = true;
67
+ /** Move focus into the menu when it opens (a person's opening always does); off to show a menu open without taking the reader's focus, as an example beside the page does. */
68
+ focusOnOpen = true;
68
69
  private hoverTimer?: ReturnType<typeof setTimeout>;
69
70
  private shown = false;
70
71
  private observer?: ResizeObserver;
@@ -181,7 +182,7 @@ export class Menu extends LitElement {
181
182
  if (this.dispatchEvent(event)) this.open = open;
182
183
  }
183
184
  /** Open a context menu from an actual viewport point or the trigger's keyboard position. */
184
- openAt(point: { x: number; y: number }, invokingControl = this.trigger) {
185
+ openAt(point: { x: number; y: number }, invokingControl = this.trigger, { focus = true }: { focus?: boolean } = {}) {
185
186
  if (this.disabled) return;
186
187
  if (this.trigger && !this.enabled(this.trigger)) return;
187
188
  if (invokingControl && (!invokingControl.isConnected || !this.enabled(invokingControl))) return;
@@ -191,7 +192,7 @@ export class Menu extends LitElement {
191
192
  : invokingControl?.contains(focused) && focused instanceof HTMLElement ? focused
192
193
  : invokingControl?.querySelector<HTMLElement>(FOCUSABLE) ?? invokingControl;
193
194
  if (this.shown) this.opener = this.invokingControl;
194
- this.focusOnOpen = true; this.openingEdge = "first";
195
+ this.focusOnOpen = focus; this.openingEdge = "first";
195
196
  this.toggleFromTrigger(true, { trigger: invokingControl, point });
196
197
  if (this.open) this.place();
197
198
  }
@@ -401,7 +402,8 @@ export class Menu extends LitElement {
401
402
  if (this.hasAttribute("data-ds-hug") && !view.getComputedStyle(this).getPropertyValue("--ds-menu-width").trim()) {
402
403
  this.style.width = "max-content";
403
404
  const natural = this.getBoundingClientRect().width;
404
- const least = this.triggerMode === "context" || this.triggerMode === "manual" ? 4 * unit : anchorWidth;
405
+ // Opened at a point it needs only four squares; by an element, at least that element's width.
406
+ const least = (this.triggerMode === "context" || this.triggerMode === "manual") && this.contextPoint ? 4 * unit : anchorWidth;
405
407
  this.style.width = `${Math.min(room, Math.max(least, squares(natural, Math.ceil)))}px`;
406
408
  }
407
409
  if (this.placement === "right" || this.placement === "left" || this.triggerMode === "submenu") {
@@ -25,6 +25,7 @@ const CURRENT = new Set(["page", "step", "location", "date", "time", "true"]);
25
25
  * @attr interactive - Make the row a button.
26
26
  * @attr disabled - Keep the row from being pressed.
27
27
  * @attr current - The destination being shown: `aria-current="page"`, or the value given (`step`, `location`, …).
28
+ * @attr open - Whether this row’s details are open; controlled by the caller, with aria-expanded on its target.
28
29
  * @attr nav - Draw a navigation row: corners of two pixels, as the sidebar's rows.
29
30
  * @attr label - The accessible name, when the row's words do not say it.
30
31
  * @attr target - Where a link opens (`_blank`, …).
@@ -45,6 +46,7 @@ export class Row extends LitElement {
45
46
  interactive: { type: Boolean, reflect: true },
46
47
  disabled: { type: Boolean, reflect: true },
47
48
  current: { type: String, reflect: true },
49
+ open: { type: Boolean, reflect: true },
48
50
  nav: { type: Boolean, reflect: true },
49
51
  label: { type: String },
50
52
  target: { type: String },
@@ -60,6 +62,7 @@ export class Row extends LitElement {
60
62
  declare interactive: boolean;
61
63
  declare disabled: boolean;
62
64
  declare current: string | null | undefined;
65
+ declare open: boolean | undefined;
63
66
  declare nav: boolean;
64
67
  declare label: string | undefined;
65
68
  declare target: string | undefined;
@@ -76,6 +79,7 @@ export class Row extends LitElement {
76
79
  this.interactive = false;
77
80
  this.disabled = false;
78
81
  this.current = undefined;
82
+ this.open = undefined;
79
83
  this.nav = false;
80
84
  this.label = undefined;
81
85
  this.target = undefined;
@@ -115,9 +119,9 @@ export class Row extends LitElement {
115
119
  ? this.disabled
116
120
  ? html`<a class="target" part="target" role="link" aria-disabled="true" aria-label=${ifDefined(label)}>${content}</a>`
117
121
  : html`<a class="target" part="target" href=${this.href} target=${ifDefined(this.target || undefined)} rel=${ifDefined(this.rel || undefined)}
118
- aria-current=${ifDefined(current)} aria-label=${ifDefined(label)} @click=${this.press}>${content}</a>`
122
+ aria-expanded=${ifDefined(this.open === undefined ? undefined : String(this.open))} aria-current=${ifDefined(current)} aria-label=${ifDefined(label)} @click=${this.press}>${content}</a>`
119
123
  : html`<button class="target" part="target" type="button" ?disabled=${this.disabled}
120
- aria-current=${ifDefined(current)} aria-label=${ifDefined(label)} @click=${this.press}>${content}</button>`;
124
+ aria-expanded=${ifDefined(this.open === undefined ? undefined : String(this.open))} aria-current=${ifDefined(current)} aria-label=${ifDefined(label)} @click=${this.press}>${content}</button>`;
121
125
  return html`${target}<slot name="end"></slot>`;
122
126
  }
123
127
  }
@@ -83,9 +83,10 @@ export class Select extends Menu {
83
83
  protected willUpdate(changed: PropertyValues) {
84
84
  // An action menu with an opener floats; one without stays in place.
85
85
  if (!this.field && !this.hasAttribute("floating") && (this.trigger || this.for)) this.floating = true;
86
- this.closesOnChoice = !this.field;
86
+ // A field's suggestions (a controller steering it) close when the field says so.
87
+ this.closesOnChoice = !this.field && !this.controller;
87
88
  // A menu from a button fits its words, on whole squares.
88
- this.toggleAttribute("data-ds-hug", !this.field && this.triggerMode !== "submenu");
89
+ this.toggleAttribute("data-ds-hug", !this.field && this.triggerMode !== "submenu" && !this.controller);
89
90
  super.willUpdate(changed);
90
91
  }
91
92
  focus(options?: FocusOptions) {
@@ -203,7 +204,7 @@ export class Select extends Menu {
203
204
  const select = this.control();
204
205
  const label = this.fieldName(select);
205
206
  const disabled = this.fieldDisabled ? "disabled" : "";
206
- return html`<slot @slotchange=${this.observeControl}></slot><span aria-hidden="true" ?hidden=${this.listbox || Boolean(this.swatch)} class="arrow ${disabled}"></span>${this.swatch && !this.listbox ? html`<ds-swatch aria-hidden="true" .value=${this.swatch} class=${disabled}></ds-swatch>` : null}${this.listed ? html`<ds-menu class="list" id=${this.listId} floating role="listbox" trigger-mode="manual" aria-label=${label || nothing} .anchor=${this} ?open=${this.listOpen} @ds-menu-close=${this.onListClose}>${this.listOpen ? this.renderChoices() : nothing}</ds-menu>` : nothing}`;
207
+ return html`<slot @slotchange=${this.observeControl}></slot><span aria-hidden="true" ?hidden=${this.listbox || Boolean(this.swatch)} class="arrow ${disabled}"></span>${this.swatch && !this.listbox ? html`<ds-swatch aria-hidden="true" .value=${this.swatch} class=${disabled}></ds-swatch>` : null}${this.listed ? html`<ds-menu class="list" id=${this.listId} floating data-ds-hug role="listbox" trigger-mode="manual" aria-label=${label || nothing} .anchor=${this} ?open=${this.listOpen} @ds-menu-close=${this.onListClose}>${this.listOpen ? this.renderChoices() : nothing}</ds-menu>` : nothing}`;
207
208
  }
208
209
  }
209
210
 
@@ -7,7 +7,7 @@ import styles from "./styles/ds-token-field.css?inline";
7
7
  let nextId = 0;
8
8
  export type TokenCommit = { text: string; reason: "enter" | "separator" | "input" | "blur" };
9
9
  /** Native field and ARIA targets stay together in light DOM. This component
10
- * owns chip layout, editing gestures and suggestion navigation through Menu. */
10
+ * owns chip layout, editing gestures and suggestion navigation through its Select (`ds-select slot="suggestions"`). */
11
11
  export class TokenField extends LitElement {
12
12
  static formAssociated = true;
13
13
  static properties = {
@@ -70,7 +70,7 @@ export class TokenField extends LitElement {
70
70
  menu.floating = true; menu.anchor = this; menu.controller = input;
71
71
  menu.trigger = this.shadowRoot?.querySelector<HTMLButtonElement>("button") ?? null;
72
72
  menu.open = this.expanded;
73
- const choices = Array.from(menu.querySelectorAll<HTMLElement>('[role="option"]')).filter(option => option.closest("ds-menu") === menu);
73
+ const choices = Array.from(menu.querySelectorAll<HTMLElement>('[role="option"]')).filter(option => option.closest("ds-menu, ds-select") === menu);
74
74
  const options = choices.filter(option => !option.matches(':disabled, [disabled], [aria-disabled="true"]') && !option.closest("[hidden], [inert]"));
75
75
  for (const option of options) { if (!option.id) option.id = `${this.idPrefix}-option-${++this.optionId}`; option.tabIndex = -1; }
76
76
  if (!options.some(option => option.id === this.activeId)) this.activeId = options[0]?.id ?? "";
@@ -163,3 +163,16 @@
163
163
  .rail .weekday:not(:empty) { font-size: 0; }
164
164
  .rail .weekday:not(:empty)::after { content: attr(data-narrow); font-size: var(--ds-type-size-default); }
165
165
  }
166
+
167
+ /* Activity keeps the day grid, with quiet separated squares and event colours. */
168
+ :host([activity]) .frame { border: 0; border-radius: 0; }
169
+ :host([activity]) .cell { position: relative; border: 0; background: none; }
170
+ :host([activity]) .cell::before {
171
+ content: "";
172
+ position: absolute;
173
+ inset: calc(2px * var(--ds-sf, 1));
174
+ border-radius: calc(2px * var(--ds-sf, 1));
175
+ background: var(--ds-calendar-cell-fill, var(--ds-fill));
176
+ }
177
+ :host([activity]) .date { display: none; }
178
+ :host([activity]) .cell[data-selected]::before { outline: 1px solid var(--ds-calendar-ink); }
@@ -37,7 +37,8 @@
37
37
  ::slotted(ds-button) {
38
38
  --ds-button-height: var(--ds-menu-row);
39
39
  --ds-button-padding: 0 calc(4px * var(--ds-sf));
40
- --ds-button-align: flex-start;
40
+ /* The words at the row's start, an icon at its end. */
41
+ --ds-button-align: space-between;
41
42
  --ds-button-background: var(--ds-context-fill);
42
43
  --ds-button-color: var(--ds-context-ink);
43
44
  --ds-radius: 0px;
@@ -50,6 +51,7 @@
50
51
  box-sizing: border-box !important;
51
52
  display: flex !important;
52
53
  align-items: flex-end !important;
54
+ justify-content: space-between !important;
53
55
  gap: calc(4px * var(--ds-sf));
54
56
  width: 100%;
55
57
  padding: 0 calc(4px * var(--ds-sf)) !important;
@@ -94,6 +94,19 @@
94
94
  inset: 0;
95
95
  border-radius: var(--ds-row-radius, 0);
96
96
  }
97
+ /* The state layer two pixels around what a one-square row holds (1-root/row.css says why). */
98
+ .target::after {
99
+ --ds-row-overhang: var(--ds-row-fill-overhang, calc(2px * var(--ds-sf)));
100
+ inset:
101
+ calc(var(--ds-row-height) - var(--ds-control-height) - var(--ds-row-overhang))
102
+ calc(var(--ds-row-padding, calc(4px * var(--ds-sf))) - var(--ds-row-overhang))
103
+ calc(0px - var(--ds-row-overhang));
104
+ border-radius: var(--ds-row-radius, var(--ds-row-fill-radius, calc(6px * var(--ds-sf))));
105
+ }
106
+ :host([stacked]) .target::after,
107
+ :host([auto]) .target::after {
108
+ inset: 0;
109
+ }
97
110
  .target::before { z-index: 1; }
98
111
  .target::after { z-index: -1; }
99
112
  ::slotted([slot="end"]) { position: relative; z-index: 2; }
@@ -105,6 +118,7 @@
105
118
  font-weight: var(--ds-row-current-weight, 500);
106
119
  }
107
120
  .target[aria-current]::after { background: var(--ds-row-fill-current, var(--ds-fill-hover)); }
121
+ :host([open]) .target::after { background: var(--ds-row-fill-current, var(--ds-fill-hover)); }
108
122
  .target:focus-visible::after { outline: 2px solid var(--ds-focus-ring); outline-offset: -2px; }
109
123
 
110
124
  :host([disabled]) .target { cursor: default; opacity: 0.5; }
@@ -114,6 +128,7 @@
114
128
  :host([nav]) { --ds-row-radius: calc(2px * var(--ds-sf)); }
115
129
 
116
130
  @media (forced-colors: active) {
131
+ :host([open]) .target::after,
117
132
  .target[aria-current]::after { outline: 1px solid Highlight; outline-offset: -1px; }
118
133
  .target:focus-visible::after { outline-color: Highlight; }
119
134
  }
@@ -104,14 +104,15 @@ ds-swatch.disabled { opacity: .5; }
104
104
  * The field's list, in place of the platform's picker (`data-listed`). The
105
105
  * select takes no pointer, so a press on the field opens the list and no
106
106
  * platform picker; the keyboard still reaches the select, and its opening keys
107
- * open the list. The list is the menu engine's, as wide as the field rounded
108
- * up to whole squares (`--ds-select-list-width` widens it), each choice one
107
+ * open the list. The list is the menu engine's, as wide as its choices in whole
108
+ * squares and at least the field (`--ds-select-list-width` sets one), each choice one
109
109
  * square on the sixteen: an icon on the first square, the name, muted details,
110
110
  * a shortcut at the end and the check on the last square for the chosen one.
111
111
  */
112
112
  :host([data-listed]) { cursor: pointer; }
113
113
  :host([data-listed]) ::slotted(select) { pointer-events: none !important; }
114
- .list { --ds-menu-width: var(--ds-select-list-width, var(--ds-menu-anchor-width)); }
114
+ /* The list fits its choices on whole squares, at least as wide as the field. */
115
+ .list { --ds-menu-width: var(--ds-select-list-width); }
115
116
  .choice,
116
117
  .group-label {
117
118
  box-sizing: border-box;
@@ -7,7 +7,7 @@
7
7
  display: flex;
8
8
  flex-direction: row;
9
9
  align-items: center;
10
- gap: var(--ds-tabs-gap, calc(10px * var(--ds-sf)));
10
+ gap: calc(4px * var(--ds-sf));
11
11
  min-width: 0;
12
12
  height: var(--ds-row-height);
13
13
  }