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

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 (43) hide show
  1. package/DS1/0-face/bento.ts +84 -0
  2. package/DS1/0-face/calendar.ts +2 -2
  3. package/DS1/1-root/bento.css +97 -0
  4. package/DS1/1-root/card.css +6 -1
  5. package/DS1/1-root/cjk.css +1 -1
  6. package/DS1/1-root/controls.css +1 -0
  7. package/DS1/1-root/fonts.css +1 -1
  8. package/DS1/1-root/one.css +1 -1
  9. package/DS1/1-root/one.semantic.css +1 -1
  10. package/DS1/3-unit/calendar-base.ts +31 -22
  11. package/DS1/3-unit/ds-bento.ts +67 -0
  12. package/DS1/3-unit/ds-calendar.ts +477 -22
  13. package/DS1/3-unit/styles/ds-bento.css +66 -0
  14. package/DS1/3-unit/styles/ds-calendar.css +119 -1
  15. package/DS1/index.ts +1 -1
  16. package/dist/0-face/bento.d.ts +41 -0
  17. package/dist/0-face/bento.d.ts.map +1 -0
  18. package/dist/0-face/bento.js +77 -0
  19. package/dist/0-face/calendar.js +2 -2
  20. package/dist/3-unit/calendar-base.d.ts +17 -12
  21. package/dist/3-unit/calendar-base.d.ts.map +1 -1
  22. package/dist/3-unit/calendar-base.js +21 -16
  23. package/dist/3-unit/ds-bento.d.ts +31 -0
  24. package/dist/3-unit/ds-bento.d.ts.map +1 -0
  25. package/dist/3-unit/ds-bento.js +47 -0
  26. package/dist/3-unit/ds-calendar-large.d.ts +3 -4
  27. package/dist/3-unit/ds-calendar-large.d.ts.map +1 -1
  28. package/dist/3-unit/ds-calendar.d.ts +139 -12
  29. package/dist/3-unit/ds-calendar.d.ts.map +1 -1
  30. package/dist/3-unit/ds-calendar.js +464 -23
  31. package/dist/design-system-1.bundle.js +1124 -1094
  32. package/dist/design-system-1.bundle.js.map +1 -1
  33. package/dist/design-system-1.bundle.min.js +2483 -2456
  34. package/dist/design-system-1.bundle.min.js.map +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/package.json +1 -1
  39. package/DS1/3-unit/ds-date-picker.ts +0 -540
  40. package/DS1/3-unit/styles/ds-date-picker.css +0 -124
  41. package/dist/3-unit/ds-date-picker.d.ts +0 -203
  42. package/dist/3-unit/ds-date-picker.d.ts.map +0 -1
  43. package/dist/3-unit/ds-date-picker.js +0 -499
@@ -1,499 +0,0 @@
1
- // ds-date-picker.ts
2
- // A field that opens a panel below it: the first and last day written out
3
- // where they can be typed, a small month one square a day, a switch for a
4
- // time, and Clear. It always collects a range: the first day pressed starts
5
- // it and the second ends it; leaving after the first keeps that one day. A
6
- // form takes the value as ISO 8601: a day, a day and a time, or two of them
7
- // joined by a slash.
8
- import { LitElement, html, nothing, unsafeCSS } from "lit";
9
- import { define } from "../0-face/define";
10
- import { getText } from "../0-face/i18n";
11
- import { dateFormatter } from "../0-face/format";
12
- import { dayOf, isDay, today as todayDay } from "../0-face/calendar";
13
- import "../2-core/ds-icon";
14
- import "./ds-calendar";
15
- import metrics from "../1-root/component-metrics.css?inline";
16
- import controls from "../1-root/controls.css?inline";
17
- import styles from "./styles/ds-date-picker.css?inline";
18
- const TIME = /^([01]\d|2[0-3]):([0-5]\d)$/;
19
- /** Reads a value: `2026-10-21`, `2026-10-21T14:30`, or two of them joined by `/`. */
20
- export function readDateValue(value) {
21
- const read = (part) => {
22
- const [day = "", time = ""] = (part ?? "").trim().split("T");
23
- if (!isDay(day))
24
- return undefined;
25
- const clock = time.slice(0, 5);
26
- return { day, time: TIME.test(clock) ? clock : "" };
27
- };
28
- const [first, second] = (value ?? "").split("/");
29
- let start = read(first), end = read(second);
30
- if (start && end && `${end.day}T${end.time}` < `${start.day}T${start.time}`)
31
- [start, end] = [end, start];
32
- return { start, end };
33
- }
34
- /**
35
- * Writes a value from its ends, the times only with `time`. An end that is
36
- * the same moment as the start leaves the value one day.
37
- */
38
- export function writeDateValue(start, end, options) {
39
- if (!start)
40
- return "";
41
- const part = (side) => (options.time && side.time ? `${side.day}T${side.time}` : side.day);
42
- return end && part(end) !== part(start) ? `${part(start)}/${part(end)}` : part(start);
43
- }
44
- /** Reads what a person typed: a day written `YYYY-MM-DD`, or a date the browser can read ("Oct 21, 2026"). */
45
- export function parseTypedDay(text) {
46
- const trimmed = text.trim();
47
- if (isDay(trimmed))
48
- return trimmed;
49
- const moment = Date.parse(trimmed);
50
- return Number.isFinite(moment) ? dayOf(new Date(moment)) : null;
51
- }
52
- const openPickers = new Set();
53
- /**
54
- * @element ds-date-picker
55
- * @prop {string} value - `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM`, or two joined by
56
- * `/` for a range; empty for none.
57
- * @prop {string} name - The form field's name.
58
- * @prop {string} label - The field's accessible name: a translation key, or
59
- * literal words.
60
- * @prop {boolean} literal - Show `label` and `placeholder` as written.
61
- * @prop {string} placeholder - The words before a day is chosen; the shared
62
- * "Choose a date" by default.
63
- * @prop {string} length - How a day is written: `short`, `medium` (the
64
- * default) or `long`.
65
- * @prop {boolean} time - Choose a time with each day (the Include time switch).
66
- * @prop {boolean} noOptions - Leave the Include time switch out (`no-options`);
67
- * `time` still applies.
68
- * @prop {string} min - The first day a person may choose.
69
- * @prop {string} max - The last day a person may choose.
70
- * @prop {string} weekStart - The week's first day (`week-start`); the
71
- * language's own by default.
72
- * @prop {string} today - The day drawn as today; the reader's own by default.
73
- * @prop {boolean} required - A form needs a value; Clear is left out.
74
- * @prop {boolean} disabled - The field cannot be opened.
75
- * @prop {boolean} open - The panel is open.
76
- *
77
- * @fires change - A person changed the value; `{ value, previous }`.
78
- * @fires input - The same moment, for code listening as to a field.
79
- * @fires ds-date-picker-toggle - The panel opened or closed; `{ open }`.
80
- *
81
- * @example
82
- * <ds-date-picker label="When" literal name="when" value="2026-10-21/2026-10-24"></ds-date-picker>
83
- */
84
- export class DatePicker extends LitElement {
85
- constructor() {
86
- super();
87
- this.internals = typeof this.attachInternals === "function" ? this.attachInternals() : null;
88
- /** The value the field started with, for a form's reset. */
89
- this.initial = "";
90
- this.shown = false;
91
- /** Whether opening should move focus into the month (a key opened it). */
92
- this.focusMonth = false;
93
- this.relabel = () => this.requestUpdate();
94
- /** Below the field, half a square from it, or above when there is more room there; held inside the screen. */
95
- this.place = () => {
96
- const panel = this.panel, trigger = this.trigger;
97
- if (!this.shown || !panel || !trigger)
98
- return;
99
- const rect = trigger.getBoundingClientRect();
100
- const view = this.ownerDocument.defaultView;
101
- const viewport = view.visualViewport;
102
- const left = viewport?.offsetLeft ?? 0, top = viewport?.offsetTop ?? 0;
103
- const width = viewport?.width ?? view.innerWidth, height = viewport?.height ?? view.innerHeight;
104
- const box = panel.getBoundingClientRect();
105
- const gap = (parseFloat(getComputedStyle(panel).paddingTop) || 9) + 1;
106
- // The face is one control high at the foot of its square: measure from that square.
107
- const square = Math.max(rect.height, gap * 2);
108
- const foot = rect.bottom, head = rect.bottom - square;
109
- const below = top + height - foot - 2 * gap, above = head - top - 2 * gap;
110
- const up = below < box.height && above > below;
111
- const x = getComputedStyle(this).direction === "rtl" ? rect.right - box.width : rect.left;
112
- panel.style.left = `${Math.max(left + gap, Math.min(x, left + width - gap - box.width))}px`;
113
- panel.style.top = `${Math.max(top + gap, up ? head - gap - box.height : foot + gap)}px`;
114
- };
115
- this.onOutside = (event) => {
116
- if (!event.composedPath().includes(this))
117
- this.open = false;
118
- };
119
- this.toggle = () => {
120
- if (this.disabled)
121
- return;
122
- this.focusMonth = false;
123
- this.open = !this.open;
124
- };
125
- this.onTriggerKey = (event) => {
126
- if (this.disabled)
127
- return;
128
- if (event.key === "ArrowDown" || event.key === "ArrowUp" || ((event.key === "Enter" || event.key === " ") && !this.open)) {
129
- event.preventDefault();
130
- this.focusMonth = true;
131
- if (this.open)
132
- void this.focusDay();
133
- else
134
- this.open = true;
135
- }
136
- };
137
- this.onPanelKey = (event) => {
138
- if (event.key === "Escape" && !event.defaultPrevented) {
139
- event.preventDefault();
140
- event.stopPropagation();
141
- this.open = false;
142
- }
143
- };
144
- this.onFocusOut = (event) => {
145
- const next = event.relatedTarget;
146
- if (this.open && next instanceof Node && !this.contains(next) && !this.renderRoot.contains(next))
147
- this.open = false;
148
- };
149
- /**
150
- * A day pressed in the month. The first press of an opening starts the range
151
- * (the value is that day until another is pressed); the second ends it and,
152
- * without times to set, closes the panel.
153
- */
154
- this.picked = (event) => {
155
- event.stopPropagation();
156
- const { start, end } = this.ends;
157
- const first = { day: event.detail.start, time: start?.time || (this.time ? "09:00" : "") };
158
- if (!event.detail.end) {
159
- this.awaitingEnd = true;
160
- this.write(first, undefined);
161
- return;
162
- }
163
- this.awaitingEnd = false;
164
- const last = { day: event.detail.end, time: end?.time || (this.time ? "10:00" : "") };
165
- this.write(first, last);
166
- if (!this.time)
167
- this.open = false;
168
- };
169
- this.setTime = () => {
170
- this.time = !this.time;
171
- const { start, end } = this.ends;
172
- const timed = (side, clock) => (side ? { ...side, time: this.time ? side.time || clock : "" } : undefined);
173
- this.write(timed(start, "09:00"), timed(end ?? (this.time ? start : undefined), "10:00"));
174
- };
175
- /** Pressing the switch's row presses the switch, as a label would. */
176
- this.passPress = (event) => {
177
- const row = event.currentTarget;
178
- const control = row.querySelector(".ds-switch");
179
- if (control && !event.composedPath().includes(control))
180
- control.click();
181
- };
182
- this.clear = () => {
183
- this.commit("");
184
- this.open = false;
185
- };
186
- this.value = "";
187
- this.literal = false;
188
- this.time = false;
189
- this.noOptions = false;
190
- this.required = false;
191
- this.disabled = false;
192
- this.open = false;
193
- this.awaitingEnd = false;
194
- }
195
- connectedCallback() {
196
- super.connectedCallback();
197
- window.addEventListener("language-changed", this.relabel);
198
- window.addEventListener("translations-loaded", this.relabel);
199
- }
200
- disconnectedCallback() {
201
- window.removeEventListener("language-changed", this.relabel);
202
- window.removeEventListener("translations-loaded", this.relabel);
203
- this.hide(false);
204
- super.disconnectedCallback();
205
- }
206
- firstUpdated() {
207
- this.initial = this.value;
208
- }
209
- /** The form was reset: back to the value it started with. */
210
- formResetCallback() {
211
- this.value = this.initial;
212
- }
213
- formDisabledCallback(disabled) {
214
- this.disabled = disabled;
215
- }
216
- words(value, fallback) {
217
- if (value && this.literal)
218
- return value;
219
- return getText(value || fallback) || value || fallback;
220
- }
221
- get trigger() {
222
- return this.renderRoot.querySelector(".trigger");
223
- }
224
- get panel() {
225
- return this.renderRoot.querySelector(".popover");
226
- }
227
- get calendar() {
228
- return this.renderRoot.querySelector("ds-calendar");
229
- }
230
- get ends() {
231
- return readDateValue(this.value);
232
- }
233
- willUpdate(changed) {
234
- super.willUpdate(changed);
235
- if (changed.has("value") && this.value) {
236
- const { start, end } = this.ends;
237
- // A value with a time turns the switch on.
238
- if (!start)
239
- this.value = "";
240
- else if (start.time || end?.time)
241
- this.time = true;
242
- }
243
- if (changed.has("disabled") && this.disabled)
244
- this.open = false;
245
- }
246
- updated(changed) {
247
- super.updated(changed);
248
- if (changed.has("value") || changed.has("required")) {
249
- this.internals?.setFormValue(this.value || null);
250
- if (this.required && !this.value)
251
- this.internals?.setValidity({ valueMissing: true }, this.words(undefined, "Choose a date"), this.trigger ?? undefined);
252
- else
253
- this.internals?.setValidity({});
254
- }
255
- if (changed.has("open")) {
256
- if (this.open)
257
- this.show();
258
- else
259
- this.hide(changed.get("open") === true);
260
- }
261
- }
262
- /** Opens the panel. */
263
- show() {
264
- if (this.shown || this.disabled || !this.isConnected)
265
- return;
266
- const panel = this.panel;
267
- if (!panel)
268
- return;
269
- for (const other of openPickers)
270
- if (other !== this)
271
- other.open = false;
272
- openPickers.add(this);
273
- this.shown = true;
274
- this.open = true;
275
- this.awaitingEnd = false;
276
- const calendar = this.calendar;
277
- if (calendar)
278
- calendar.month = (this.ends.start?.day || this.today || todayDay()).slice(0, 7);
279
- panel.showPopover?.();
280
- this.place();
281
- if (calendar)
282
- void calendar.updateComplete.then(this.place);
283
- this.ownerDocument.addEventListener("pointerdown", this.onOutside, true);
284
- this.ownerDocument.addEventListener("scroll", this.place, true);
285
- window.addEventListener("resize", this.place);
286
- this.dispatchEvent(new CustomEvent("ds-date-picker-toggle", { bubbles: true, composed: true, detail: { open: true } }));
287
- if (this.focusMonth)
288
- void this.focusDay();
289
- }
290
- /** Closes the panel, giving focus back to the field when it was inside. */
291
- hide(restore = true) {
292
- if (!this.shown)
293
- return;
294
- this.shown = false;
295
- openPickers.delete(this);
296
- this.awaitingEnd = false;
297
- const panel = this.panel;
298
- const within = panel?.matches(":focus-within");
299
- if (panel?.matches(":popover-open"))
300
- panel.hidePopover?.();
301
- this.ownerDocument.removeEventListener("pointerdown", this.onOutside, true);
302
- this.ownerDocument.removeEventListener("scroll", this.place, true);
303
- window.removeEventListener("resize", this.place);
304
- this.open = false;
305
- if (restore && within)
306
- this.trigger?.focus({ preventScroll: true });
307
- this.dispatchEvent(new CustomEvent("ds-date-picker-toggle", { bubbles: true, composed: true, detail: { open: false } }));
308
- }
309
- async focusDay() {
310
- const calendar = this.calendar;
311
- if (!calendar)
312
- return;
313
- await calendar.updateComplete;
314
- calendar.renderRoot.querySelector('[data-day][tabindex="0"]')?.focus({ preventScroll: true });
315
- }
316
- /** Takes a new value and says so. */
317
- commit(value) {
318
- const previous = this.value || "";
319
- if (value === previous)
320
- return;
321
- this.value = value;
322
- const detail = { value, previous };
323
- this.dispatchEvent(new CustomEvent("input", { bubbles: true, composed: true, detail }));
324
- this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true, detail }));
325
- }
326
- write(start, end) {
327
- this.commit(writeDateValue(start, end, { time: this.time }));
328
- }
329
- /** A day typed in a field, read when the person leaves it or presses Enter. */
330
- typed(side, input) {
331
- const { start, end } = this.ends;
332
- const text = input.value.trim();
333
- if (!text) {
334
- if (side === "start")
335
- this.commit("");
336
- else
337
- this.write(start, undefined);
338
- return;
339
- }
340
- const day = parseTypedDay(text);
341
- if (!day) {
342
- input.value = this.fieldText(side === "start" ? start : end);
343
- return;
344
- }
345
- const current = side === "start" ? start : end;
346
- const next = { day, time: current?.time || (this.time ? (side === "start" ? "09:00" : "10:00") : "") };
347
- if (side === "start")
348
- this.write(next, end);
349
- else
350
- this.write(start ?? next, next);
351
- this.awaitingEnd = false;
352
- const calendar = this.calendar;
353
- if (calendar)
354
- calendar.month = day.slice(0, 7);
355
- }
356
- timed(side, input) {
357
- const { start, end } = this.ends;
358
- const clock = TIME.test(input.value) ? input.value : "";
359
- if (side === "start" && start)
360
- this.write({ ...start, time: clock }, end);
361
- if (side === "end" && start)
362
- this.write(start, { day: (end ?? start).day, time: clock });
363
- }
364
- moment(side) {
365
- const [year, month, date] = side.day.split("-").map(Number);
366
- const [hours, minutes] = (side.time || "12:00").split(":").map(Number);
367
- return Date.UTC(year, month - 1, date, hours, minutes);
368
- }
369
- formatter(withTime) {
370
- return dateFormatter({ dateStyle: this.length ?? "medium", ...(withTime ? { timeStyle: "short" } : {}), timeZone: "UTC" });
371
- }
372
- fieldText(side) {
373
- return side ? this.formatter(false).format(this.moment(side)) : "";
374
- }
375
- /** The face's words: one day, or a range written as the language writes one ("Oct 21 – 24, 2026"). */
376
- faceText() {
377
- const { start, end } = this.ends;
378
- if (!start)
379
- return this.words(this.placeholder, "Choose a date");
380
- const formatter = this.formatter(Boolean(start.time || end?.time));
381
- if (!end)
382
- return formatter.format(this.moment(start));
383
- return formatter.formatRange ? formatter.formatRange(this.moment(start), this.moment(end)) : `${formatter.format(this.moment(start))} – ${formatter.format(this.moment(end))}`;
384
- }
385
- renderEnd(side, value, name) {
386
- const label = side === "start" ? name : `${name}, ${this.words(undefined, "End date")}`;
387
- return html `<div class="field-row" part="field">
388
- <input
389
- class="ds-input field"
390
- type="text"
391
- autocomplete="off"
392
- aria-label=${label}
393
- placeholder=${side === "start" ? this.words(this.placeholder, "Choose a date") : this.words(undefined, "End date")}
394
- .value=${this.fieldText(value)}
395
- @change=${(event) => this.typed(side, event.target)}
396
- @keydown=${(event) => { if (event.key === "Enter") {
397
- event.preventDefault();
398
- this.typed(side, event.target);
399
- } }}
400
- />
401
- </div>
402
- ${this.time
403
- ? html `<div class="field-row" part="field">
404
- <input
405
- class="ds-input clock"
406
- type="time"
407
- aria-label=${`${label}, ${this.words(undefined, "Time")}`}
408
- .value=${value?.time ?? ""}
409
- ?disabled=${!this.ends.start}
410
- @change=${(event) => this.timed(side, event.target)}
411
- />
412
- </div>`
413
- : nothing}`;
414
- }
415
- render() {
416
- const { start, end } = this.ends;
417
- const text = this.faceText();
418
- const name = this.label ? this.words(this.label, this.label) : this.words(undefined, "Date");
419
- // While the end is still to come the month holds the start alone; otherwise the range is whole (one day is a range of one).
420
- const days = start ? (this.awaitingEnd ? start.day : `${start.day}/${(end ?? start).day}`) : "";
421
- return html `<button
422
- class="ds-select trigger"
423
- part="trigger"
424
- type="button"
425
- aria-haspopup="dialog"
426
- aria-expanded=${String(this.open)}
427
- aria-label=${start ? `${name}, ${text}` : name}
428
- ?disabled=${this.disabled}
429
- ?data-empty=${!start}
430
- @click=${this.toggle}
431
- @keydown=${this.onTriggerKey}
432
- >
433
- <ds-icon class="icon" type="calendar" aria-hidden="true"></ds-icon>
434
- <span class="text">${text}</span>
435
- <ds-icon class="arrow" type="down" aria-hidden="true"></ds-icon>
436
- </button>
437
- <div
438
- class="popover"
439
- part="popover"
440
- popover="manual"
441
- role="dialog"
442
- aria-label=${name}
443
- @keydown=${this.onPanelKey}
444
- @focusout=${this.onFocusOut}
445
- >
446
- <div class="fields">
447
- ${this.renderEnd("start", start, name)}
448
- ${this.renderEnd("end", start ? end ?? start : undefined, name)}
449
- </div>
450
- <ds-calendar
451
- compact
452
- selectable
453
- range
454
- fixed
455
- outside-days
456
- .value=${days}
457
- .min=${this.min}
458
- .max=${this.max}
459
- .weekStart=${this.weekStart}
460
- .today=${this.today}
461
- @change=${this.picked}
462
- ></ds-calendar>
463
- <div class="options" part="options">
464
- ${this.noOptions
465
- ? nothing
466
- : html `<div class="separator" aria-hidden="true"></div>
467
- <div class="option" @click=${this.passPress}>
468
- <span>${this.words(undefined, "Include time")}</span>
469
- <button class="ds-switch" type="button" role="switch" aria-label=${this.words(undefined, "Include time")} aria-checked=${String(this.time)} @click=${this.setTime}></button>
470
- </div>`}
471
- ${this.required
472
- ? nothing
473
- : html `<div class="separator" aria-hidden="true"></div>
474
- <button class="option clear" part="clear" type="button" ?disabled=${!start} @click=${this.clear}>${this.words(undefined, "Clear")}</button>`}
475
- </div>
476
- </div>`;
477
- }
478
- }
479
- DatePicker.formAssociated = true;
480
- DatePicker.properties = {
481
- value: { type: String, reflect: true },
482
- name: { type: String, reflect: true },
483
- label: { type: String },
484
- literal: { type: Boolean, reflect: true },
485
- placeholder: { type: String },
486
- length: { type: String },
487
- time: { type: Boolean, reflect: true },
488
- noOptions: { type: Boolean, attribute: "no-options", reflect: true },
489
- min: { type: String },
490
- max: { type: String },
491
- weekStart: { type: String, attribute: "week-start" },
492
- today: { type: String },
493
- required: { type: Boolean, reflect: true },
494
- disabled: { type: Boolean, reflect: true },
495
- open: { type: Boolean, reflect: true },
496
- awaitingEnd: { state: true },
497
- };
498
- DatePicker.styles = [unsafeCSS(metrics), unsafeCSS(controls), unsafeCSS(styles)];
499
- define("ds-date-picker", DatePicker);