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