@0001-labs/design-system-1 0.3.0-alpha.21 → 0.3.0-alpha.23

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 (36) hide show
  1. package/DS1/0-face/bento.ts +13 -4
  2. package/DS1/0-face/calendar.ts +23 -0
  3. package/DS1/1-root/bento.css +25 -12
  4. package/DS1/1-root/cjk.css +1 -1
  5. package/DS1/1-root/controls.css +22 -32
  6. package/DS1/1-root/fonts.css +1 -1
  7. package/DS1/1-root/one.css +1 -1
  8. package/DS1/1-root/one.semantic.css +1 -1
  9. package/DS1/2-core/ds-switch.ts +2 -2
  10. package/DS1/2-core/styles/ds-checkbox.css +11 -7
  11. package/DS1/2-core/styles/ds-radio.css +5 -2
  12. package/DS1/2-core/styles/ds-switch.css +7 -23
  13. package/DS1/3-unit/ds-bento.ts +8 -1
  14. package/DS1/3-unit/ds-calendar.ts +21 -4
  15. package/DS1/3-unit/styles/ds-bento.css +16 -10
  16. package/DS1/3-unit/styles/ds-calendar.css +30 -8
  17. package/dist/0-face/bento.d.ts +6 -2
  18. package/dist/0-face/bento.d.ts.map +1 -1
  19. package/dist/0-face/bento.js +12 -4
  20. package/dist/0-face/calendar.d.ts +11 -0
  21. package/dist/0-face/calendar.d.ts.map +1 -1
  22. package/dist/0-face/calendar.js +19 -0
  23. package/dist/2-core/ds-switch.d.ts +2 -2
  24. package/dist/2-core/ds-switch.d.ts.map +1 -1
  25. package/dist/2-core/ds-switch.js +2 -2
  26. package/dist/3-unit/ds-bento.d.ts +12 -0
  27. package/dist/3-unit/ds-bento.d.ts.map +1 -1
  28. package/dist/3-unit/ds-bento.js +4 -1
  29. package/dist/3-unit/ds-calendar.d.ts +17 -0
  30. package/dist/3-unit/ds-calendar.d.ts.map +1 -1
  31. package/dist/3-unit/ds-calendar.js +18 -5
  32. package/dist/design-system-1.bundle.js +66 -15
  33. package/dist/design-system-1.bundle.js.map +1 -1
  34. package/dist/design-system-1.bundle.min.js +903 -872
  35. package/dist/design-system-1.bundle.min.js.map +1 -1
  36. package/package.json +1 -1
@@ -19,7 +19,9 @@
19
19
  * A tile's `data-place` is its square across, its square down, its width and
20
20
  * its height, in whole squares counted from 0. Tiles are placed a square
21
21
  * apart; a tile a square away from another, with the two overlapping along
22
- * that side, reaches towards it.
22
+ * that side, reaches towards it. A tile can have a place per layout,
23
+ * `data-place-phone="0 0 12 7"`, taken while the bento says
24
+ * `data-layout="phone"`; the page sets the layout and the columns for it.
23
25
  */
24
26
 
25
27
  /** A tile's place in squares: across, down, wide, high. */
@@ -51,10 +53,17 @@ export function bentoReach(places: readonly BentoPlace[]): BentoReach[] {
51
53
  });
52
54
  }
53
55
 
54
- /** Places one squares bento's tiles: their squares and their reach, as custom properties. */
56
+ /** A tile's place in the bento's layout: `data-place-<layout>` where the bento names one and the tile has it, else `data-place`. */
57
+ export function tilePlace(tile: HTMLElement, layout = ""): BentoPlace | undefined {
58
+ const own = layout ? tile.getAttribute(`data-place-${layout}`) : null;
59
+ return bentoPlace(own ?? tile.dataset.place);
60
+ }
61
+
62
+ /** Places one squares bento's tiles: their squares and their reach, as custom properties. The bento's `data-layout` (or `layout`) picks each tile's place for that layout. */
55
63
  export function placeBento(bento: HTMLElement): void {
64
+ const layout = bento.dataset.layout ?? bento.getAttribute("layout") ?? "";
56
65
  const tiles = [...bento.children].filter((child): child is HTMLElement => child instanceof HTMLElement && !child.hidden);
57
- const placed = tiles.map(tile => ({ tile, place: bentoPlace(tile.dataset.place) })).filter((entry): entry is { tile: HTMLElement; place: BentoPlace } => !!entry.place);
66
+ const placed = tiles.map(tile => ({ tile, place: tilePlace(tile, layout) })).filter((entry): entry is { tile: HTMLElement; place: BentoPlace } => !!entry.place);
58
67
  const reach = bentoReach(placed.map(entry => entry.place));
59
68
  placed.forEach(({ tile, place: [x, y, w, h] }, index) => {
60
69
  const sides = reach[index]!;
@@ -77,7 +86,7 @@ export function placeBentos(root: ParentNode = document): () => void {
77
86
  });
78
87
  for (const bento of bentos) {
79
88
  placeBento(bento);
80
- observer.observe(bento, { childList: true, subtree: false, attributes: true, attributeFilter: ["data-place", "hidden"] });
89
+ observer.observe(bento, { childList: true, subtree: false, attributes: true, attributeFilter: ["data-place", "data-layout", "hidden"] });
81
90
  for (const tile of bento.children) observer.observe(tile, { attributes: true, attributeFilter: ["data-place", "hidden"] });
82
91
  }
83
92
  return () => observer.disconnect();
@@ -173,8 +173,31 @@ export type CalendarEvent = {
173
173
  time?: string;
174
174
  /** A colour token or value for the event's fill (`var(--ds-zenith-blue)`). */
175
175
  color?: string;
176
+ /** How much it counts for in a calendar's heat (commits, purchases, hours); 1 when left out. */
177
+ count?: number;
176
178
  };
177
179
 
180
+ /** A day's heat: 1 (a little) to 4 (the most). */
181
+ export type HeatLevel = 1 | 2 | 3 | 4;
182
+
183
+ /**
184
+ * How strongly each day is lit, as a contribution graph lights it: the
185
+ * amounts of its events added up, in four steps of the busiest day (or of
186
+ * `max`, for a scale that stays put from month to month). A day with nothing
187
+ * has no level.
188
+ */
189
+ export function heatLevels(byDay: ReadonlyMap<Day, readonly CalendarEvent[]>, max?: number): Map<Day, HeatLevel> {
190
+ const amounts = new Map<Day, number>();
191
+ for (const [day, events] of byDay) {
192
+ const amount = events.reduce((sum, event) => sum + (typeof event.count === "number" && Number.isFinite(event.count) ? Math.max(0, event.count) : 1), 0);
193
+ if (amount > 0) amounts.set(day, amount);
194
+ }
195
+ const top = typeof max === "number" && Number.isFinite(max) && max > 0 ? max : Math.max(0, ...amounts.values());
196
+ const levels = new Map<Day, HeatLevel>();
197
+ for (const [day, amount] of amounts) levels.set(day, Math.min(4, Math.max(1, Math.ceil((amount / top) * 4))) as HeatLevel);
198
+ return levels;
199
+ }
200
+
178
201
  /** The events that touch each day between `from` and `to`, in order: longest first, then by time. */
179
202
  export function eventsByDay(events: readonly CalendarEvent[], from: Day, to: Day): Map<Day, CalendarEvent[]> {
180
203
  const days = new Map<Day, CalendarEvent[]>();
@@ -24,16 +24,24 @@
24
24
  * data-rows="2" a tile two rows high (to 6)
25
25
  * data-place="0 0 15 7" a tile's square, width and height in squares
26
26
  *
27
- * Tokens: `--ds-bento-gap` (half a square), `--ds-bento-columns`,
28
- * `--ds-bento-row`, and on a tile `--ds-bento-span`, `--ds-bento-rows`.
27
+ * `data-fit` lets a squares bento's columns share a measure other than its
28
+ * squares (each tile still reaches a quarter square, so the gap is a column
29
+ * less half a square); `data-layout` (say `phone`) makes `placeBento()` take each tile's
30
+ * `data-place-phone` where it has one, a place per layout.
31
+ *
32
+ * Tokens: `--ds-bento-unit` (the square, `--ds-unit` by default),
33
+ * `--ds-bento-gap` (half of it), `--ds-bento-columns`, `--ds-bento-row`,
34
+ * `--ds-bento-padding` (a squares tile that is not a card, one square), and
35
+ * on a tile `--ds-bento-span`, `--ds-bento-rows`.
29
36
  */
30
37
 
31
38
  .ds-bento {
32
- --ds-bento-gap: calc(var(--ds-unit) / 2);
39
+ --ds-bento-unit: var(--ds-unit);
40
+ --ds-bento-gap: calc(var(--ds-bento-unit) / 2);
33
41
  box-sizing: border-box;
34
42
  display: grid;
35
43
  grid-template-columns: repeat(var(--ds-bento-columns, 3), minmax(0, 1fr));
36
- grid-auto-rows: minmax(var(--ds-bento-row, calc(10 * var(--ds-unit))), auto);
44
+ grid-auto-rows: minmax(var(--ds-bento-row, calc(10 * var(--ds-bento-unit))), auto);
37
45
  grid-auto-flow: row dense;
38
46
  gap: var(--ds-bento-gap);
39
47
  margin: 0;
@@ -69,15 +77,20 @@
69
77
 
70
78
  /* Squares: every tile on whole squares, a square apart, its surface a quarter square nearer each neighbour. */
71
79
  .ds-bento[data-squares] {
72
- --ds-bento-reach: calc(var(--ds-unit) / 4);
80
+ --ds-bento-reach: calc(var(--ds-bento-unit) / 4);
73
81
  /* Exactly its squares wide: each column one square. */
74
- width: calc(var(--ds-bento-columns, 48) * var(--ds-unit));
82
+ width: calc(var(--ds-bento-columns, 48) * var(--ds-bento-unit));
75
83
  max-width: none;
76
- grid-template-columns: repeat(var(--ds-bento-columns, 48), var(--ds-unit));
77
- grid-auto-rows: var(--ds-unit);
84
+ grid-template-columns: repeat(var(--ds-bento-columns, 48), var(--ds-bento-unit));
85
+ grid-auto-rows: var(--ds-bento-unit);
78
86
  grid-auto-flow: row;
79
87
  gap: 0;
80
88
  }
89
+ /* Fit: the columns share a measure narrower (or wider) than the squares, the rows still one square. */
90
+ .ds-bento[data-squares][data-fit] {
91
+ width: auto;
92
+ grid-template-columns: repeat(var(--ds-bento-columns, 48), minmax(0, 1fr));
93
+ }
81
94
  .ds-bento[data-squares] > * {
82
95
  --ds-bento-inset-top: calc(var(--ds-bento-reach) * var(--ds-bento-top, 0));
83
96
  --ds-bento-inset-end: calc(var(--ds-bento-reach) * var(--ds-bento-end, 0));
@@ -90,8 +103,8 @@
90
103
  /* A card grows its own padding by the reach (card.css); any other tile is padded here, a square inside. */
91
104
  .ds-bento[data-squares] > :not(.ds-card, ds-card) {
92
105
  padding:
93
- calc(var(--ds-unit) + var(--ds-bento-inset-top))
94
- calc(var(--ds-unit) + var(--ds-bento-inset-end))
95
- calc(var(--ds-unit) + var(--ds-bento-inset-bottom))
96
- calc(var(--ds-unit) + var(--ds-bento-inset-start));
106
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-top))
107
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-end))
108
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-bottom))
109
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-start));
97
110
  }
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.21 */
1
+ /* version 0.3.0-alpha.23 */
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
@@ -226,8 +226,10 @@ textarea.ds-input {
226
226
 
227
227
  /*
228
228
  * `.ds-checkbox` and `.ds-radio` are the `ds-checkbox` and `ds-radio` elements'
229
- * faces on a native `<input>`: one square on the field's ground with a 1px
230
- * line. The checkbox has the corners of a control and fills with the accent,
229
+ * faces on a native `<input>`: a box one control (16px) square on the field's
230
+ * ground with a 1px line, standing as an icon does, on the lower sixteen at the
231
+ * start of its square, so it takes one square and the words begin on the next
232
+ * (Joachim, 19 October 2026). The checkbox has the corners of a control and fills with the accent,
231
233
  * the check (`x-icon/check.svg`) in its ink; the radio is a circle with a dot
232
234
  * half a square across in the text colour.
233
235
  */
@@ -238,10 +240,12 @@ textarea.ds-input {
238
240
  display: inline-grid;
239
241
  flex: 0 0 auto;
240
242
  place-items: center;
241
- vertical-align: middle;
242
- width: var(--ds-row-height);
243
- height: var(--ds-row-height);
243
+ vertical-align: top;
244
+ width: var(--ds-control-height);
245
+ height: var(--ds-control-height);
244
246
  margin: 0;
247
+ margin-block-start: calc(var(--ds-row-height) - var(--ds-control-height));
248
+ margin-inline-end: calc(var(--ds-row-height) - var(--ds-control-height));
245
249
  padding: 0;
246
250
  border: 1px solid var(--ds-border-color);
247
251
  background: var(--ds-input-background);
@@ -267,13 +271,13 @@ textarea.ds-input {
267
271
  }
268
272
 
269
273
  .ds-checkbox::before {
270
- width: calc(0.8 * var(--ds-row-height));
271
- height: calc(0.8 * var(--ds-row-height));
274
+ width: var(--ds-control-height);
275
+ height: var(--ds-control-height);
272
276
  mask: var(--ds-checkbox-mark) center / contain no-repeat;
273
277
  }
274
278
 
275
279
  .ds-checkbox:indeterminate::before {
276
- width: calc(var(--ds-row-height) / 2);
280
+ width: calc(var(--ds-control-height) / 2);
277
281
  height: calc(2px * var(--ds-sf));
278
282
  mask: none;
279
283
  }
@@ -284,8 +288,8 @@ textarea.ds-input {
284
288
  }
285
289
 
286
290
  .ds-radio::before {
287
- width: calc(var(--ds-row-height) / 2);
288
- height: calc(var(--ds-row-height) / 2);
291
+ width: calc(var(--ds-control-height) / 2);
292
+ height: calc(var(--ds-control-height) / 2);
289
293
  border-radius: 50%;
290
294
  }
291
295
 
@@ -297,8 +301,9 @@ textarea.ds-input {
297
301
  /*
298
302
  * `.ds-switch` is the `ds-switch` element's look on a native
299
303
  * `<button role="switch" aria-checked>`: two control heights wide, its thumb
300
- * one control high less a 2px inset, travelling one control height.
301
- * `.ds-switch--rounded` is the round variant with a white thumb.
304
+ * one control high less a 2px inset, travelling one control height. A pill with a white
305
+ * round thumb, grey when off and the darker green when on, whatever the page's accent: the
306
+ * one switch (Joachim, 19 October 2026).
302
307
  */
303
308
 
304
309
  .ds-switch {
@@ -314,9 +319,9 @@ textarea.ds-input {
314
319
  margin: 0;
315
320
  padding: calc(2px * var(--ds-sf));
316
321
  border: 0;
317
- border-radius: var(--ds-radius);
322
+ border-radius: var(--ds-choice-unit);
318
323
  background: var(--ds-fill);
319
- color: var(--ds-text-color-muted);
324
+ color: var(--ds-white);
320
325
  cursor: pointer;
321
326
  touch-action: manipulation;
322
327
  }
@@ -326,35 +331,20 @@ textarea.ds-input {
326
331
  display: block;
327
332
  width: calc(var(--ds-choice-unit) - 4px * var(--ds-sf));
328
333
  height: 100%;
329
- border-radius: var(--ds-radius);
330
- background: currentColor;
334
+ border-radius: 50%;
335
+ background: var(--ds-white);
331
336
  transform: translateX(0);
332
337
  transition: transform 140ms ease;
333
338
  }
334
339
 
335
340
  .ds-switch[aria-checked="true"] {
336
- background: var(--ds-accent-color);
337
- color: var(--ds-button-text-color-on-accent);
341
+ background: var(--ds-switch-track-on);
338
342
  }
339
343
 
340
344
  .ds-switch[aria-checked="true"]::after {
341
345
  transform: translateX(var(--ds-choice-unit));
342
346
  }
343
347
 
344
- .ds-switch--rounded {
345
- border-radius: var(--ds-choice-unit);
346
- color: var(--ds-white);
347
- }
348
-
349
- .ds-switch--rounded::after {
350
- border-radius: 50%;
351
- background: var(--ds-white);
352
- }
353
-
354
- .ds-switch--rounded[aria-checked="true"] {
355
- background: var(--ds-switch-track-on);
356
- }
357
-
358
348
  @media (prefers-reduced-motion: reduce) {
359
349
  .ds-switch::after {
360
350
  transition: none;
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.21 */
1
+ /* version 0.3.0-alpha.23 */
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.21 */
1
+ /* version 0.3.0-alpha.23 */
2
2
 
3
3
  @import "./grid.css";
4
4
  @import "./component-tokens.css";
@@ -1,4 +1,4 @@
1
- /* DS1 — version 0.3.0-alpha.21 */
1
+ /* DS1 — version 0.3.0-alpha.23 */
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
@@ -2,8 +2,8 @@ import { LitElement, html, unsafeCSS } from "lit";
2
2
  import { define } from "../0-face/define";
3
3
  import styles from "./styles/ds-switch.css?inline";
4
4
 
5
- /** The shared switch face. The slotted button retains native activation and refs. */
6
- /** rounded selects a pill track, white circular thumb and darker-green on state. */
5
+ /** The switch: a pill with a white round thumb, grey when off and the darker green when on,
6
+ whatever the page's accent. The slotted button retains native activation and refs. */
7
7
  export class Switch extends LitElement {
8
8
  static styles = unsafeCSS(styles);
9
9
  focus(options?: FocusOptions) { this.querySelector<HTMLButtonElement>(":scope > button")?.focus(options); }
@@ -1,10 +1,14 @@
1
1
  @import "../../1-root/component-metrics.css";
2
2
 
3
+ /* One square, the box one control (16px) square on its lower sixteen at its start, as an icon
4
+ stands, so the words begin on the next square (Joachim, 19 October 2026). */
3
5
  :host {
4
6
  --ds-choice-unit: calc(var(--ds-1) * var(--ds-sf));
5
- display: inline-flex;
7
+ display: inline-grid;
8
+ align-items: end;
9
+ justify-items: start;
6
10
  flex: 0 0 auto;
7
- vertical-align: middle;
11
+ vertical-align: top;
8
12
  width: var(--ds-choice-unit);
9
13
  height: var(--ds-choice-unit);
10
14
  }
@@ -14,9 +18,9 @@
14
18
  box-sizing: border-box !important;
15
19
  display: grid !important;
16
20
  place-items: center !important;
17
- width: 100% !important;
21
+ width: var(--ds-control-height) !important;
18
22
  min-width: 0 !important;
19
- height: 100% !important;
23
+ height: var(--ds-control-height) !important;
20
24
  min-height: 0 !important;
21
25
  margin: 0 !important;
22
26
  padding: 0 !important;
@@ -31,15 +35,15 @@
31
35
  ::slotted(input:checked), ::slotted(input:indeterminate) { background: var(--ds-accent-color) !important; }
32
36
  ::slotted(input)::before {
33
37
  content: "";
34
- width: calc(.8 * var(--ds-choice-unit));
35
- height: calc(.8 * var(--ds-choice-unit));
38
+ width: var(--ds-control-height);
39
+ height: var(--ds-control-height);
36
40
  background: currentColor;
37
41
  mask: var(--ds-checkbox-mark) center / contain no-repeat;
38
42
  opacity: 0;
39
43
  }
40
44
  ::slotted(input:checked)::before { opacity: 1; }
41
45
  ::slotted(input:indeterminate)::before {
42
- width: calc(var(--ds-choice-unit) / 2);
46
+ width: calc(var(--ds-control-height) / 2);
43
47
  height: calc(2px * var(--ds-sf));
44
48
  mask: none;
45
49
  opacity: 1;
@@ -9,6 +9,9 @@
9
9
  height: var(--ds-unit);
10
10
  }
11
11
  :host([hidden]) { display: none !important; }
12
+ /* A plain radio as the checkbox: a 16px circle on its square's lower sixteen, at its start. */
13
+ :host(:not([choice])) { display: inline-grid; align-items: end; justify-items: start; vertical-align: top; }
14
+ :host(:not([choice])) ::slotted(input[type="radio"]) { width: var(--ds-control-height) !important; height: var(--ds-control-height) !important; }
12
15
  ::slotted(input[type="radio"]) {
13
16
  appearance: none !important;
14
17
  box-sizing: border-box !important;
@@ -30,8 +33,8 @@
30
33
  ::slotted(input[hidden]) { display: none !important; }
31
34
  ::slotted(input)::before {
32
35
  content: "";
33
- width: calc(var(--ds-unit) / 2);
34
- height: calc(var(--ds-unit) / 2);
36
+ width: calc(var(--ds-control-height) / 2);
37
+ height: calc(var(--ds-control-height) / 2);
35
38
  border-radius: 50%;
36
39
  background: currentColor;
37
40
  opacity: 0;
@@ -21,10 +21,10 @@
21
21
  padding: calc(2px * var(--ds-sf)) !important;
22
22
  margin: 0 !important;
23
23
  border: 0 !important;
24
- border-radius: var(--ds-radius) !important;
25
- corner-shape: var(--ds-corner-shape, round) !important;
24
+ border-radius: var(--ds-choice-unit) !important;
25
+ corner-shape: round !important;
26
26
  background: var(--ds-fill) !important;
27
- color: var(--ds-text-color-muted) !important;
27
+ color: var(--ds-white) !important;
28
28
  box-shadow: none !important;
29
29
  cursor: pointer !important;
30
30
  touch-action: manipulation;
@@ -34,32 +34,16 @@
34
34
  display: block;
35
35
  width: calc(var(--ds-choice-unit) - 4px * var(--ds-sf));
36
36
  height: 100%;
37
- border-radius: var(--ds-radius);
38
- corner-shape: var(--ds-corner-shape, round);
39
- background: currentColor;
37
+ border-radius: 50%;
38
+ corner-shape: round;
39
+ background: var(--ds-white);
40
40
  transform: translateX(0);
41
41
  transition: transform 140ms ease;
42
42
  }
43
43
  ::slotted(button[aria-checked="true"]) {
44
- background: var(--ds-accent-color) !important;
45
- color: var(--ds-button-text-color-on-accent) !important;
44
+ background: var(--ds-switch-track-on) !important;
46
45
  }
47
46
  ::slotted(button[aria-checked="true"])::after { transform: translateX(var(--ds-choice-unit)); }
48
47
  ::slotted(button:focus-visible) { outline: 2px solid var(--ds-focus-ring) !important; outline-offset: 1px !important; }
49
48
  ::slotted(button:disabled) { cursor: default !important; opacity: .5 !important; }
50
49
  @media (prefers-reduced-motion: reduce) { ::slotted(button)::after { transition: none; } }
51
-
52
- /* Rounded switches keep their shape and white thumb independently of page accents. */
53
- :host([rounded]) ::slotted(button) {
54
- border-radius: var(--ds-choice-unit) !important;
55
- corner-shape: round !important;
56
- color: var(--ds-white) !important;
57
- }
58
- :host([rounded]) ::slotted(button)::after {
59
- border-radius: 50%;
60
- corner-shape: round;
61
- background: var(--ds-white);
62
- }
63
- :host([rounded]) ::slotted(button[aria-checked="true"]) {
64
- background: var(--ds-switch-track-on) !important;
65
- }
@@ -16,12 +16,18 @@ export class Bento extends LitElement {
16
16
  static properties = {
17
17
  columns: { type: Number, reflect: true },
18
18
  squares: { type: Boolean, reflect: true },
19
+ fit: { type: Boolean, reflect: true },
20
+ layout: { type: String, reflect: true },
19
21
  };
20
22
 
21
23
  /** The number of columns (flow, three by default), or the squares across (`squares`, 48 by default). */
22
24
  declare columns: number | undefined;
23
25
  /** Places every tile on whole squares by its `data-place`. */
24
26
  declare squares: boolean;
27
+ /** With `squares`, lets the columns share a measure other than the squares. */
28
+ declare fit: boolean;
29
+ /** Takes each tile's `data-place-<layout>` where it has one (say `phone`). */
30
+ declare layout: string | undefined;
25
31
 
26
32
  static styles = [unsafeCSS(metrics), unsafeCSS(styles)];
27
33
 
@@ -30,12 +36,13 @@ export class Bento extends LitElement {
30
36
  constructor() {
31
37
  super();
32
38
  this.squares = false;
39
+ this.fit = false;
33
40
  }
34
41
 
35
42
  connectedCallback() {
36
43
  super.connectedCallback();
37
44
  this.observer = new MutationObserver(() => this.place());
38
- this.observer.observe(this, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-place", "hidden"] });
45
+ this.observer.observe(this, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-place", "hidden"], attributeOldValue: false });
39
46
  }
40
47
 
41
48
  disconnectedCallback() {
@@ -21,6 +21,7 @@ import {
21
21
  dateNumber,
22
22
  dayOf,
23
23
  eventsByDay,
24
+ heatLevels,
24
25
  isDay,
25
26
  isoWeek,
26
27
  monthTitle,
@@ -30,6 +31,7 @@ import {
30
31
  weekdayNames,
31
32
  type CalendarEvent,
32
33
  type Day,
34
+ type HeatLevel,
33
35
  } from "../0-face/calendar";
34
36
  import { CalendarBase, readMonth, readRange, type CalendarChangeDetail } from "./calendar-base";
35
37
  import "../2-core/ds-icon";
@@ -105,6 +107,12 @@ const openPickers = new Set<Calendar>();
105
107
  * @prop {boolean} fixed - Always draw six weeks, so the month keeps its height.
106
108
  * @prop {boolean} outsideDays - Draw the days of the months before and after
107
109
  * in the title grey (`outside-days`); pressing one moves there.
110
+ * @prop {boolean} heat - Light each day by how much happens on it, as a
111
+ * contribution graph does: the events' `count`s added up (one each by
112
+ * default), in four steps of the busiest day shown.
113
+ * @prop {number} heatMax - The amount that lights a day fully (`heat-max`),
114
+ * for a scale that stays put from month to month; the busiest day shown
115
+ * by default.
108
116
  * @prop {boolean} noHeader - Leave out the title and arrows (`no-header`).
109
117
  * @prop {boolean} noToday - Leave out the Today button (`no-today`).
110
118
  * @prop {string} min - The first day a person may choose.
@@ -153,6 +161,8 @@ export class Calendar extends CalendarBase {
153
161
  outsideDays: { type: Boolean, attribute: "outside-days", reflect: true },
154
162
  noHeader: { type: Boolean, attribute: "no-header", reflect: true },
155
163
  noToday: { type: Boolean, attribute: "no-today", reflect: true },
164
+ heat: { type: Boolean, reflect: true },
165
+ heatMax: { type: Number, attribute: "heat-max" },
156
166
  picker: { type: Boolean, reflect: true },
157
167
  name: { type: String, reflect: true },
158
168
  placeholder: { type: String },
@@ -175,6 +185,8 @@ export class Calendar extends CalendarBase {
175
185
  declare outsideDays: boolean;
176
186
  declare noHeader: boolean;
177
187
  declare noToday: boolean;
188
+ declare heat: boolean;
189
+ declare heatMax: number | undefined;
178
190
  declare picker: boolean;
179
191
  declare name: string | undefined;
180
192
  declare placeholder: string | undefined;
@@ -189,6 +201,7 @@ export class Calendar extends CalendarBase {
189
201
  protected readonly weeksRun = "rows" as const;
190
202
 
191
203
  private byDay = new Map<Day, CalendarEvent[]>();
204
+ private heatByDay = new Map<Day, HeatLevel>();
192
205
  private readonly internals = typeof this.attachInternals === "function" ? this.attachInternals() : null;
193
206
  /** The value the field started with, for a form's reset. */
194
207
  private initial = "";
@@ -204,6 +217,7 @@ export class Calendar extends CalendarBase {
204
217
  this.outsideDays = false;
205
218
  this.noHeader = false;
206
219
  this.noToday = false;
220
+ this.heat = false;
207
221
  this.picker = false;
208
222
  this.time = false;
209
223
  this.required = false;
@@ -400,7 +414,8 @@ export class Calendar extends CalendarBase {
400
414
  const unfolded = this.unfolded === day;
401
415
  const shown = unfolded || events.length <= lines ? events : events.slice(0, Math.max(0, lines - 1));
402
416
  const hidden = events.length - shown.length;
403
- const parts = ["day", isToday ? "today" : "", selected ? "selected" : ""].filter(Boolean).join(" ");
417
+ const heat = this.heatByDay.get(day);
418
+ const parts = ["day", isToday ? "today" : "", selected ? "selected" : "", heat ? "heat" : ""].filter(Boolean).join(" ");
404
419
  return html`<div
405
420
  role="gridcell"
406
421
  class="day"
@@ -418,6 +433,7 @@ export class Calendar extends CalendarBase {
418
433
  ?data-last=${last}
419
434
  ?data-outside=${outside}
420
435
  ?data-busy=${events.length > 0}
436
+ data-heat=${heat ?? nothing}
421
437
  ?data-open=${unfolded}
422
438
  >
423
439
  <span class="date-line" aria-hidden="true">
@@ -444,6 +460,8 @@ export class Calendar extends CalendarBase {
444
460
  const first = this.firstDay;
445
461
  const weeks = monthWeeks(month, first, this.fixed);
446
462
  this.byDay = eventsByDay(this.events ?? [], weeks[0][0], weeks.at(-1)![6]);
463
+ // Heat is measured over the days of the month shown, so the neighbours' days never set its scale.
464
+ this.heatByDay = this.heat ? heatLevels(new Map([...this.byDay].filter(([day]) => sameMonth(day, month))), this.heatMax) : new Map();
447
465
  // A compact month names its days in a letter (one square) or two (wider); a full month in short.
448
466
  const short = this.compact
449
467
  ? this.squaresPerDay < 2
@@ -735,7 +753,7 @@ export class Calendar extends CalendarBase {
735
753
  const name = this.label ? this.pickerWords(this.label, this.label) : this.pickerWords(undefined, "Date");
736
754
  const days = start ? (end ? `${start.day}/${end.day}` : start.day) : "";
737
755
  return html`<button
738
- class="ds-select trigger"
756
+ class="ds-input trigger"
739
757
  part="trigger"
740
758
  type="button"
741
759
  aria-haspopup="dialog"
@@ -746,9 +764,8 @@ export class Calendar extends CalendarBase {
746
764
  @click=${this.toggle}
747
765
  @keydown=${this.onTriggerKey}
748
766
  >
749
- <ds-icon class="icon" type="calendar" aria-hidden="true"></ds-icon>
750
767
  <span class="text">${text}</span>
751
- <ds-icon class="arrow" type="down" aria-hidden="true"></ds-icon>
768
+ <ds-icon class="arrow" type="chevron-down" size="large" aria-hidden="true"></ds-icon>
752
769
  </button>
753
770
  <div
754
771
  class="popover"
@@ -2,11 +2,12 @@
2
2
  /* The element's bento: the same as 1-root/bento.css's .ds-bento, on the host and its slotted tiles. */
3
3
 
4
4
  :host {
5
- --ds-bento-gap: calc(var(--ds-unit) / 2);
5
+ --ds-bento-unit: var(--ds-unit);
6
+ --ds-bento-gap: calc(var(--ds-bento-unit) / 2);
6
7
  box-sizing: border-box;
7
8
  display: grid;
8
9
  grid-template-columns: repeat(var(--ds-bento-columns, 3), minmax(0, 1fr));
9
- grid-auto-rows: minmax(var(--ds-bento-row, calc(10 * var(--ds-unit))), auto);
10
+ grid-auto-rows: minmax(var(--ds-bento-row, calc(10 * var(--ds-bento-unit))), auto);
10
11
  grid-auto-flow: row dense;
11
12
  gap: var(--ds-bento-gap);
12
13
  }
@@ -38,15 +39,20 @@
38
39
  }
39
40
 
40
41
  :host([squares]) {
41
- --ds-bento-reach: calc(var(--ds-unit) / 4);
42
+ --ds-bento-reach: calc(var(--ds-bento-unit) / 4);
42
43
  /* Exactly its squares wide: each column one square. */
43
- width: calc(var(--ds-bento-columns, 48) * var(--ds-unit));
44
+ width: calc(var(--ds-bento-columns, 48) * var(--ds-bento-unit));
44
45
  max-width: none;
45
- grid-template-columns: repeat(var(--ds-bento-columns, 48), var(--ds-unit));
46
- grid-auto-rows: var(--ds-unit);
46
+ grid-template-columns: repeat(var(--ds-bento-columns, 48), var(--ds-bento-unit));
47
+ grid-auto-rows: var(--ds-bento-unit);
47
48
  grid-auto-flow: row;
48
49
  gap: 0;
49
50
  }
51
+ /* Fit: the columns share a measure narrower (or wider) than the squares, the rows still one square. */
52
+ :host([squares][fit]) {
53
+ width: auto;
54
+ grid-template-columns: repeat(var(--ds-bento-columns, 48), minmax(0, 1fr));
55
+ }
50
56
  :host([squares]) ::slotted(*) {
51
57
  --ds-bento-inset-top: calc(var(--ds-bento-reach) * var(--ds-bento-top, 0));
52
58
  --ds-bento-inset-end: calc(var(--ds-bento-reach) * var(--ds-bento-end, 0));
@@ -59,8 +65,8 @@
59
65
  /* A card grows its own padding by the reach (card.css); any other tile is padded here, a square inside. */
60
66
  :host([squares]) ::slotted(:not(.ds-card, ds-card)) {
61
67
  padding:
62
- calc(var(--ds-unit) + var(--ds-bento-inset-top))
63
- calc(var(--ds-unit) + var(--ds-bento-inset-end))
64
- calc(var(--ds-unit) + var(--ds-bento-inset-bottom))
65
- calc(var(--ds-unit) + var(--ds-bento-inset-start));
68
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-top))
69
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-end))
70
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-bottom))
71
+ calc(var(--ds-bento-padding, var(--ds-bento-unit)) + var(--ds-bento-inset-start));
66
72
  }