@iyulab/components 1.26.0 → 1.27.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.27.0] - 2026-08-08
4
+
5
+ ### Added
6
+
7
+ - **`formatNumber`/`formatCurrency`/`formatDate` utilities** (`utilities/format.ts`). Thin
8
+ wrappers around `Intl.NumberFormat`/`Intl.DateTimeFormat` that default to the active
9
+ `Locale.get()` locale. `formatCurrency` has no default currency — callers must pass one
10
+ explicitly.
11
+ - **`u-date-picker`** — a single-date picker form control (`UFormControlElement`), matching
12
+ the value contract of a native `input[type=date]` (ISO `YYYY-MM-DD`). Renders a calendar
13
+ grid in a popover with full keyboard navigation (arrow keys, Home/End, Enter/Space, Escape)
14
+ following the WAI-ARIA Date Picker Dialog pattern. `min`/`max` mark out-of-range days
15
+ `aria-disabled` (focusable but not selectable, so keyboard users can still perceive and
16
+ navigate past them) and drive `rangeUnderflow`/`rangeOverflow` validity. Date ranges and
17
+ `datetime-local` are out of scope for this release.
18
+
3
19
  ## [1.26.0] - 2026-08-07
4
20
 
5
21
  ### Added
package/README.md CHANGED
@@ -70,7 +70,7 @@ npx skills add ./node_modules/@iyulab/components
70
70
 
71
71
  **Buttons & Actions** — `u-button`, `u-button-group`, `u-icon-button`, `u-copy-button`, `u-chip`
72
72
 
73
- **Form Controls** — `u-input`, `u-textarea`, `u-select`, `u-checkbox`, `u-radio`, `u-switch`, `u-slider`, `u-rating`, `u-field`, `u-form`, `u-option`
73
+ **Form Controls** — `u-input`, `u-textarea`, `u-select`, `u-date-picker`, `u-checkbox`, `u-radio`, `u-switch`, `u-slider`, `u-rating`, `u-field`, `u-form`, `u-option`
74
74
 
75
75
  **Overlay & Floating** — `u-dialog`, `u-drawer`, `u-popover`, `u-tooltip`
76
76
 
@@ -0,0 +1,4 @@
1
+ //#region \0glob-assets_raw/calendar.svg.02c3abd8
2
+ var calendar_svg_default = "<svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/>\n <path d=\"M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z\" />\n <path d=\"M16 3v4\" />\n <path d=\"M8 3v4\" />\n <path d=\"M4 11h16\" />\n <path d=\"M11 15h1\" />\n <path d=\"M12 15v3\" />\n</svg>\n";
3
+ //#endregion
4
+ export { calendar_svg_default as default };
@@ -0,0 +1,70 @@
1
+ import { PropertyValues } from 'lit';
2
+ import { UFormControlElement } from '../UFormControlElement.js';
3
+ import { UPopover } from '../popover/UPopover.js';
4
+ /**
5
+ * A single-date-selection form control. The value follows the same convention as the
6
+ * native `input[type=date]`: an ISO `YYYY-MM-DD` string.
7
+ *
8
+ * The calendar week always starts on Sunday, regardless of locale — harmless for the
9
+ * locales this library currently ships (en/ko), but not correct for locales where Monday
10
+ * (most of Europe) or Saturday is conventional. Fix when a consumer needs it: derive the
11
+ * first day of week from `Intl.Locale(locale).weekInfo?.firstDay`, falling back to Sunday
12
+ * where unsupported.
13
+ *
14
+ * @csspart field - the u-field element
15
+ * @csspart container - the element wrapping the trigger area
16
+ * @csspart popover - the popover element showing the calendar
17
+ * @csspart calendar - the calendar container
18
+ * @csspart calendar-header - the month navigation header
19
+ * @csspart calendar-title - the "Month Year" title
20
+ * @csspart calendar-weekdays - the weekday header row
21
+ * @csspart calendar-grid - the date grid
22
+ * @csspart day - a date cell button
23
+ *
24
+ * @cssprop --date-picker-popover-width - width of the calendar popover (default: 296px, independent of trigger width — a fixed-width calendar reads more naturally)
25
+ *
26
+ * @event change - fires when the user clicks a date cell, confirms via keyboard, or clicks the clear button.
27
+ * Programmatic value assignment does not fire it (same contract as native form controls).
28
+ */
29
+ export declare class UDatePicker extends UFormControlElement<string> {
30
+ static styles: import('lit').CSSResultGroup[];
31
+ /** Minimum value (ISO YYYY-MM-DD) — dates before this cannot be selected. */
32
+ min?: string;
33
+ /** Maximum value (ISO YYYY-MM-DD) — dates after this cannot be selected. */
34
+ max?: string;
35
+ /** Whether to show the clear button */
36
+ clearable: boolean;
37
+ /** Placeholder text (shown on the trigger when there is no value) */
38
+ placeholder?: string;
39
+ containerEl?: HTMLElement;
40
+ popoverEl?: UPopover;
41
+ /** Unique id wiring the combobox's `aria-controls` to the calendar dialog — mirrors USelect's `listboxId`. */
42
+ private readonly calendarId;
43
+ private open;
44
+ private viewDate;
45
+ private focusedDate;
46
+ private grabFocusOnUpdate;
47
+ protected updated(changed: PropertyValues): void;
48
+ render(): import('lit-html').TemplateResult<1>;
49
+ private renderCalendar;
50
+ private renderDay;
51
+ private isOutOfRange;
52
+ private focusDayButton;
53
+ private selectDay;
54
+ private handlePrevMonth;
55
+ private handleNextMonth;
56
+ private navigateMonth;
57
+ private handleDayKeydown;
58
+ private moveFocus;
59
+ private handlePopoverShow;
60
+ private handlePopoverHide;
61
+ private handleClearClick;
62
+ private emitChange;
63
+ protected setValidity(): void;
64
+ reset(): void;
65
+ }
66
+ declare global {
67
+ interface HTMLElementTagNameMap {
68
+ 'u-date-picker': UDatePicker;
69
+ }
70
+ }
@@ -0,0 +1,306 @@
1
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
3
+ import { UFormControlElement } from "../UFormControlElement.js";
4
+ import "../icon/UIcon.js";
5
+ import "../icon-button/UIconButton.js";
6
+ import { Locale } from "../../utilities/Locale.js";
7
+ import "../field/UField.js";
8
+ import { UPopover } from "../popover/UPopover.js";
9
+ import { formatDate } from "../../utilities/format.js";
10
+ import { styles } from "./UDatePicker.styles.js";
11
+ import { html } from "lit";
12
+ import { customElement, property, query, state } from "lit/decorators.js";
13
+ import { ifDefined } from "lit/directives/if-defined.js";
14
+ //#region src/components/date-picker/UDatePicker.ts
15
+ function parseISODate(iso) {
16
+ const [y, m, d] = iso.split("-").map(Number);
17
+ return new Date(y, m - 1, d);
18
+ }
19
+ function toISODate(date) {
20
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
21
+ }
22
+ function isSameDay(a, b) {
23
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
24
+ }
25
+ function startOfMonth(date) {
26
+ return new Date(date.getFullYear(), date.getMonth(), 1);
27
+ }
28
+ function addMonths(date, delta) {
29
+ return new Date(date.getFullYear(), date.getMonth() + delta, 1);
30
+ }
31
+ function addDays(date, delta) {
32
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta);
33
+ }
34
+ function daysInMonth(date) {
35
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
36
+ }
37
+ /** Cells to render for the calendar grid — leading `null`s pad the previous month's weekday offset. */
38
+ function buildMonthGrid(viewDate) {
39
+ const startOffset = startOfMonth(viewDate).getDay();
40
+ const total = daysInMonth(viewDate);
41
+ const cells = [];
42
+ for (let i = 0; i < startOffset; i++) cells.push(null);
43
+ for (let d = 1; d <= total; d++) cells.push(new Date(viewDate.getFullYear(), viewDate.getMonth(), d));
44
+ return cells;
45
+ }
46
+ /** Splits the flat cell list into 7-day weeks — the `role="row"` grouping the APG grid pattern expects. */
47
+ function chunkWeeks(cells) {
48
+ const weeks = [];
49
+ for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
50
+ return weeks;
51
+ }
52
+ /** 2023-01-01 was a Sunday — a fixed reference date that always yields Sun..Sat order regardless of today. */
53
+ function getWeekdayLabels(locale) {
54
+ const formatter = new Intl.DateTimeFormat(locale ?? Locale.get(), { weekday: "narrow" });
55
+ const sunday = new Date(2023, 0, 1);
56
+ return Array.from({ length: 7 }, (_, i) => formatter.format(addDays(sunday, i)));
57
+ }
58
+ var UDatePicker = class UDatePicker extends UFormControlElement {
59
+ constructor(..._args) {
60
+ super(..._args);
61
+ this.clearable = false;
62
+ this.calendarId = `u-date-picker-calendar-${Math.random().toString(36).slice(2, 8)}`;
63
+ this.open = false;
64
+ this.viewDate = startOfMonth(/* @__PURE__ */ new Date());
65
+ this.focusedDate = /* @__PURE__ */ new Date();
66
+ this.grabFocusOnUpdate = false;
67
+ this.handlePrevMonth = () => this.navigateMonth(-1);
68
+ this.handleNextMonth = () => this.navigateMonth(1);
69
+ this.handleDayKeydown = (e, date) => {
70
+ switch (e.key) {
71
+ case "ArrowRight":
72
+ e.preventDefault();
73
+ this.moveFocus(date, 1);
74
+ break;
75
+ case "ArrowLeft":
76
+ e.preventDefault();
77
+ this.moveFocus(date, -1);
78
+ break;
79
+ case "ArrowDown":
80
+ e.preventDefault();
81
+ this.moveFocus(date, 7);
82
+ break;
83
+ case "ArrowUp":
84
+ e.preventDefault();
85
+ this.moveFocus(date, -7);
86
+ break;
87
+ case "Home":
88
+ e.preventDefault();
89
+ this.moveFocus(date, -date.getDay());
90
+ break;
91
+ case "End":
92
+ e.preventDefault();
93
+ this.moveFocus(date, 6 - date.getDay());
94
+ break;
95
+ case "Enter":
96
+ case " ":
97
+ e.preventDefault();
98
+ this.selectDay(date);
99
+ break;
100
+ case "Escape":
101
+ e.preventDefault();
102
+ this.popoverEl?.hide();
103
+ this.containerEl?.focus();
104
+ }
105
+ };
106
+ this.handlePopoverShow = () => {
107
+ this.open = true;
108
+ this.grabFocusOnUpdate = true;
109
+ const base = this.value ? parseISODate(this.value) : /* @__PURE__ */ new Date();
110
+ this.viewDate = startOfMonth(base);
111
+ this.focusedDate = base;
112
+ };
113
+ this.handlePopoverHide = () => {
114
+ this.open = false;
115
+ };
116
+ this.handleClearClick = (e) => {
117
+ e.preventDefault();
118
+ e.stopPropagation();
119
+ const hadValue = !!this.value;
120
+ this.value = void 0;
121
+ if (hadValue) this.emitChange();
122
+ this.containerEl?.focus();
123
+ };
124
+ }
125
+ static {
126
+ this.styles = [super.styles, styles];
127
+ }
128
+ updated(changed) {
129
+ super.updated(changed);
130
+ if (changed.has("value")) this.internals?.setFormValue(this.value ?? "");
131
+ if (changed.has("open") && this.open || changed.has("focusedDate") && this.open && this.grabFocusOnUpdate) {
132
+ this.grabFocusOnUpdate = false;
133
+ this.popoverEl?.updateComplete.then(() => this.focusDayButton(this.focusedDate));
134
+ }
135
+ }
136
+ render() {
137
+ const displayText = this.value ? formatDate(this.value) : "";
138
+ return html`
139
+ <u-field part="field"
140
+ ?required=${this.required}
141
+ ?disabled=${this.disabled}
142
+ ?invalid=${this.invalid}
143
+ .label=${this.label}
144
+ .description=${this.description}
145
+ .validationMessage=${this.validationMessage}
146
+ >
147
+ <div class="container" part="container"
148
+ tabindex=${this.disabled ? "-1" : "0"}
149
+ role="combobox"
150
+ aria-haspopup="dialog"
151
+ aria-expanded=${this.open}
152
+ aria-label=${ifDefined(this.label)}
153
+ aria-description=${ifDefined(this.description)}
154
+ aria-controls=${this.calendarId}
155
+ >
156
+ <span class="text-content ${!displayText ? "placeholder" : ""}">${displayText || this.placeholder || ""}</span>
157
+ <u-icon class="suffix-item"
158
+ ?hidden=${!this.clearable || !this.value || this.disabled || this.readonly}
159
+ lib="internal"
160
+ name="x"
161
+ @click=${this.handleClearClick}
162
+ ></u-icon>
163
+ <u-icon class="suffix-item"
164
+ lib="internal"
165
+ name="calendar"
166
+ ></u-icon>
167
+ </div>
168
+ </u-field>
169
+
170
+ <u-popover part="popover"
171
+ id=${this.calendarId}
172
+ role="dialog"
173
+ aria-label="Choose date"
174
+ for=".container"
175
+ trigger="click"
176
+ strategy="fixed"
177
+ placement="bottom-start"
178
+ offset="4"
179
+ @show=${this.handlePopoverShow}
180
+ @hide=${this.handlePopoverHide}
181
+ >
182
+ ${this.open ? this.renderCalendar() : ""}
183
+ </u-popover>
184
+ `;
185
+ }
186
+ renderCalendar() {
187
+ const cells = buildMonthGrid(this.viewDate);
188
+ const monthLabel = formatDate(this.viewDate, {
189
+ year: "numeric",
190
+ month: "long"
191
+ });
192
+ const weekdayLabels = getWeekdayLabels();
193
+ return html`
194
+ <div class="calendar" part="calendar">
195
+ <div class="calendar-header" part="calendar-header">
196
+ <u-icon-button lib="internal" name="chevron-left" aria-label="Previous month" @click=${this.handlePrevMonth}></u-icon-button>
197
+ <span class="calendar-title" part="calendar-title">${monthLabel}</span>
198
+ <u-icon-button lib="internal" name="chevron-right" aria-label="Next month" @click=${this.handleNextMonth}></u-icon-button>
199
+ </div>
200
+ <div class="calendar-weekdays" part="calendar-weekdays" role="row">
201
+ ${weekdayLabels.map((w) => html`<span class="weekday" role="columnheader">${w}</span>`)}
202
+ </div>
203
+ <div class="calendar-grid" part="calendar-grid" role="grid">
204
+ ${chunkWeeks(cells).map((week) => html`
205
+ <div class="calendar-week" part="calendar-week" role="row">
206
+ ${week.map((date) => date ? this.renderDay(date) : html`<span class="day-empty" role="gridcell" aria-hidden="true"></span>`)}
207
+ </div>
208
+ `)}
209
+ </div>
210
+ </div>
211
+ `;
212
+ }
213
+ renderDay(date) {
214
+ const selected = this.value ? isSameDay(date, parseISODate(this.value)) : false;
215
+ const focused = isSameDay(date, this.focusedDate);
216
+ const today = isSameDay(date, /* @__PURE__ */ new Date());
217
+ const outOfRange = this.isOutOfRange(date);
218
+ return html`
219
+ <button type="button" class="day" part="day"
220
+ role="gridcell"
221
+ data-iso=${toISODate(date)}
222
+ tabindex=${focused ? 0 : -1}
223
+ aria-selected=${selected}
224
+ aria-disabled=${outOfRange}
225
+ ?data-today=${today}
226
+ @click=${() => this.selectDay(date)}
227
+ @keydown=${(e) => this.handleDayKeydown(e, date)}
228
+ @focus=${() => {
229
+ this.focusedDate = date;
230
+ }}
231
+ >${date.getDate()}</button>
232
+ `;
233
+ }
234
+ isOutOfRange(date) {
235
+ if (this.min && date.getTime() < parseISODate(this.min).getTime()) return true;
236
+ if (this.max && date.getTime() > parseISODate(this.max).getTime()) return true;
237
+ return false;
238
+ }
239
+ focusDayButton(date) {
240
+ const iso = toISODate(date);
241
+ this.renderRoot.querySelector(`button.day[data-iso="${iso}"]`)?.focus();
242
+ }
243
+ selectDay(date) {
244
+ if (this.isOutOfRange(date)) return;
245
+ const iso = toISODate(date);
246
+ const changed = iso !== this.value;
247
+ this.value = iso;
248
+ if (changed) this.emitChange();
249
+ this.popoverEl?.hide();
250
+ this.containerEl?.focus();
251
+ }
252
+ navigateMonth(delta) {
253
+ this.grabFocusOnUpdate = false;
254
+ const next = addMonths(this.viewDate, delta);
255
+ this.viewDate = next;
256
+ const clampedDay = Math.min(this.focusedDate.getDate(), daysInMonth(next));
257
+ this.focusedDate = new Date(next.getFullYear(), next.getMonth(), clampedDay);
258
+ }
259
+ moveFocus(from, deltaDays) {
260
+ this.grabFocusOnUpdate = true;
261
+ const next = addDays(from, deltaDays);
262
+ if (next.getMonth() !== this.viewDate.getMonth() || next.getFullYear() !== this.viewDate.getFullYear()) this.viewDate = startOfMonth(next);
263
+ this.focusedDate = next;
264
+ }
265
+ emitChange() {
266
+ if (!this.novalidate) this.validate();
267
+ this.dispatchEvent(new Event("change", {
268
+ bubbles: true,
269
+ composed: true
270
+ }));
271
+ }
272
+ setValidity() {
273
+ let flags = {};
274
+ let message = "";
275
+ if (this.required && !this.value) {
276
+ flags = { valueMissing: true };
277
+ message = Locale.getValue("valueMissing");
278
+ } else if (this.value && this.min && parseISODate(this.value).getTime() < parseISODate(this.min).getTime()) {
279
+ flags = { rangeUnderflow: true };
280
+ message = Locale.getValue("rangeUnderflow", { min: this.min });
281
+ } else if (this.value && this.max && parseISODate(this.value).getTime() > parseISODate(this.max).getTime()) {
282
+ flags = { rangeOverflow: true };
283
+ message = Locale.getValue("rangeOverflow", { max: this.max });
284
+ }
285
+ this.commit(flags, message, this.containerEl ?? void 0);
286
+ }
287
+ reset() {
288
+ this.value = void 0;
289
+ this.invalid = false;
290
+ }
291
+ };
292
+ __decorate([property({ type: String }), __decorateMetadata("design:type", String)], UDatePicker.prototype, "min", void 0);
293
+ __decorate([property({ type: String }), __decorateMetadata("design:type", String)], UDatePicker.prototype, "max", void 0);
294
+ __decorate([property({
295
+ type: Boolean,
296
+ reflect: true
297
+ }), __decorateMetadata("design:type", Boolean)], UDatePicker.prototype, "clearable", void 0);
298
+ __decorate([property({ type: String }), __decorateMetadata("design:type", String)], UDatePicker.prototype, "placeholder", void 0);
299
+ __decorate([query(".container", true), __decorateMetadata("design:type", typeof HTMLElement === "undefined" ? Object : HTMLElement)], UDatePicker.prototype, "containerEl", void 0);
300
+ __decorate([query("u-popover", true), __decorateMetadata("design:type", typeof UPopover === "undefined" ? Object : UPopover)], UDatePicker.prototype, "popoverEl", void 0);
301
+ __decorate([state(), __decorateMetadata("design:type", Boolean)], UDatePicker.prototype, "open", void 0);
302
+ __decorate([state(), __decorateMetadata("design:type", typeof Date === "undefined" ? Object : Date)], UDatePicker.prototype, "viewDate", void 0);
303
+ __decorate([state(), __decorateMetadata("design:type", typeof Date === "undefined" ? Object : Date)], UDatePicker.prototype, "focusedDate", void 0);
304
+ UDatePicker = __decorate([customElement("u-date-picker")], UDatePicker);
305
+ //#endregion
306
+ export { UDatePicker };
@@ -0,0 +1 @@
1
+ export declare const styles: import('lit').CSSResult;
@@ -0,0 +1,147 @@
1
+ import { css } from "lit";
2
+ //#region src/components/date-picker/UDatePicker.styles.ts
3
+ var styles = css`
4
+ :host {
5
+ --date-picker-popover-width: 296px;
6
+ }
7
+
8
+ :host {
9
+ position: relative;
10
+ display: var(--u-date-picker-display, inline-block);
11
+ width: var(--u-date-picker-width, auto);
12
+ color: var(--u-txt-color, #212121);
13
+ font-size: inherit;
14
+ font-family: var(--u-font-base);
15
+ }
16
+
17
+ .container {
18
+ display: flex;
19
+ flex-direction: row;
20
+ align-items: center;
21
+ gap: 0.4em;
22
+ padding: 0.3em 0.6em;
23
+ border: 1px solid var(--u-input-border-color, #E0E0E0);
24
+ border-radius: 0.25em;
25
+ background-color: var(--u-input-bg-color, #FFFFFF);
26
+ cursor: pointer;
27
+ transition: border-color var(--u-duration-normal, 220ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1)),
28
+ box-shadow var(--u-duration-normal, 220ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1));
29
+ }
30
+ :host([readonly]) .container,
31
+ :host([disabled]) .container {
32
+ cursor: not-allowed;
33
+ border-color: var(--u-border-color-weak, #EEEEEE);
34
+ background-color: var(--u-bg-color-disabled, #FAFAFA);
35
+ }
36
+ :host(:not([readonly]):not([disabled])) .container:hover {
37
+ box-shadow: 0 0 0 1px var(--u-input-border-color-hover, #BDBDBD);
38
+ }
39
+ :host(:not([readonly]):not([disabled])) .container:focus-within {
40
+ box-shadow: 0 0 0 1px var(--u-input-border-color-focus, #1565C0);
41
+ }
42
+ :host([invalid]:not([readonly]):not([disabled])) .container {
43
+ box-shadow: 0 0 0 1px var(--u-input-border-color-invalid, #C62828);
44
+ }
45
+
46
+ .text-content {
47
+ flex: 1 0 auto;
48
+ min-width: 0;
49
+ font-size: 1em;
50
+ line-height: 1.5;
51
+ overflow: hidden;
52
+ text-overflow: ellipsis;
53
+ white-space: nowrap;
54
+ }
55
+ .text-content.placeholder {
56
+ color: var(--u-txt-color-weak, #757575);
57
+ }
58
+
59
+ .suffix-item {
60
+ color: var(--u-icon-color, #616161);
61
+ font-size: 1em;
62
+ transition: color var(--u-duration-normal, 220ms) var(--u-ease-standard, cubic-bezier(0.2, 0, 0, 1));
63
+ }
64
+ .suffix-item:hover {
65
+ color: var(--u-icon-color-hover, #1565C0);
66
+ }
67
+
68
+ u-popover {
69
+ width: var(--date-picker-popover-width);
70
+ padding: 8px;
71
+ border: 1px solid var(--u-border-color, #E0E0E0);
72
+ border-radius: var(--u-radius-lg, 6px);
73
+ background-color: var(--u-panel-bg-color, #FFFFFF);
74
+ box-shadow: var(--u-shadow-lg, 0 4px 12px rgba(0, 0, 0, 0.16), 0 2px 4px rgba(0, 0, 0, 0.06));
75
+ }
76
+
77
+ .calendar-header {
78
+ display: flex;
79
+ flex-direction: row;
80
+ align-items: center;
81
+ justify-content: space-between;
82
+ padding: 0 4px 8px;
83
+ }
84
+ .calendar-title {
85
+ font-size: var(--u-text-label-size, 13px);
86
+ font-weight: var(--u-text-label-weight, 600);
87
+ }
88
+
89
+ .calendar-weekdays,
90
+ .calendar-grid {
91
+ display: grid;
92
+ grid-template-columns: repeat(7, 1fr);
93
+ }
94
+ .calendar-weekdays {
95
+ padding-bottom: 4px;
96
+ }
97
+ /* role="row" wrapper for APG grid semantics — display:contents keeps it out of the
98
+ CSS grid track so its day cells still lay out as direct grid items. */
99
+ .calendar-week {
100
+ display: contents;
101
+ }
102
+ .weekday {
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ font-size: var(--u-text-caption-size, 12px);
107
+ color: var(--u-txt-color-weak, #757575);
108
+ }
109
+
110
+ .day,
111
+ .day-empty {
112
+ aspect-ratio: 1;
113
+ }
114
+ .day {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ border: none;
119
+ border-radius: var(--u-radius-circle, 50%);
120
+ background: transparent;
121
+ color: var(--u-txt-color, #212121);
122
+ font-size: 1em;
123
+ font-family: inherit;
124
+ cursor: pointer;
125
+ }
126
+ .day:hover:not([aria-disabled="true"]) {
127
+ background-color: var(--u-bg-color-hover, #F5F5F5);
128
+ }
129
+ .day:focus-visible {
130
+ outline: none;
131
+ box-shadow: 0 0 0 2px var(--u-input-border-color-focus, #1565C0);
132
+ }
133
+ .day[data-today] {
134
+ font-weight: 700;
135
+ color: var(--u-primary-color, #1976D2);
136
+ }
137
+ .day[aria-selected="true"] {
138
+ background-color: var(--u-primary-color, #1976D2);
139
+ color: var(--u-primary-txt-color, #FFFFFF);
140
+ }
141
+ .day[aria-disabled="true"] {
142
+ color: var(--u-txt-color-disabled, #BDBDBD);
143
+ cursor: not-allowed;
144
+ }
145
+ `;
146
+ //#endregion
147
+ export { styles };
@@ -5,8 +5,8 @@ import "../spinner/USpinner.js";
5
5
  import "../icon/UIcon.js";
6
6
  import { Locale } from "../../utilities/Locale.js";
7
7
  import "../field/UField.js";
8
- import { UOption } from "../option/UOption.js";
9
8
  import { UPopover } from "../popover/UPopover.js";
9
+ import { UOption } from "../option/UOption.js";
10
10
  import { styles } from "./UInput.styles.js";
11
11
  import { html } from "lit";
12
12
  import { customElement, property, query, state } from "lit/decorators.js";
@@ -2,8 +2,8 @@ import { UElement } from "../UElement.js";
2
2
  import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
3
3
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
4
4
  import "../icon/UIcon.js";
5
- import { UDivider } from "../divider/UDivider.js";
6
5
  import "../popover/UPopover.js";
6
+ import { UDivider } from "../divider/UDivider.js";
7
7
  import { styles } from "./UMenuItem.styles.js";
8
8
  import { html } from "lit";
9
9
  import { customElement, property, state } from "lit/decorators.js";
@@ -5,8 +5,8 @@ import "../spinner/USpinner.js";
5
5
  import "../icon/UIcon.js";
6
6
  import { Locale } from "../../utilities/Locale.js";
7
7
  import "../field/UField.js";
8
- import { UOption } from "../option/UOption.js";
9
8
  import { UPopover } from "../popover/UPopover.js";
9
+ import { UOption } from "../option/UOption.js";
10
10
  import { styles } from "./USelect.styles.js";
11
11
  import { html } from "lit";
12
12
  import { customElement, property, query, state } from "lit/decorators.js";
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from './components/carousel/UCarousel.js';
14
14
  export * from './components/checkbox/UCheckbox.js';
15
15
  export * from './components/chip/UChip.js';
16
16
  export * from './components/copy-button/UCopyButton.js';
17
+ export * from './components/date-picker/UDatePicker.js';
17
18
  export * from './components/dialog/UDialog.js';
18
19
  export * from './components/divider/UDivider.js';
19
20
  export * from './components/drawer/UDrawer.js';
@@ -59,6 +60,7 @@ export * from './utilities/BrowserStorage.js';
59
60
  export * from './utilities/converters.js';
60
61
  export * from './utilities/Dialog.js';
61
62
  export * from './utilities/elements.js';
63
+ export * from './utilities/format.js';
62
64
  export * from './utilities/icons.js';
63
65
  export * from './utilities/Locale.js';
64
66
  export * from './utilities/OverlayManager.js';
package/dist/index.js CHANGED
@@ -24,14 +24,16 @@ import { UCheckbox } from "./components/checkbox/UCheckbox.js";
24
24
  import { UTag } from "./components/tag/UTag.js";
25
25
  import { UChip } from "./components/chip/UChip.js";
26
26
  import { UCopyButton } from "./components/copy-button/UCopyButton.js";
27
+ import { UField } from "./components/field/UField.js";
28
+ import { UPopover } from "./components/popover/UPopover.js";
29
+ import { formatCurrency, formatDate, formatNumber } from "./utilities/format.js";
30
+ import { UDatePicker } from "./components/date-picker/UDatePicker.js";
27
31
  import { UDialog } from "./components/dialog/UDialog.js";
28
32
  import { UDivider } from "./components/divider/UDivider.js";
29
33
  import { UDrawer } from "./components/drawer/UDrawer.js";
30
34
  import { UExpander } from "./components/expander/UExpander.js";
31
- import { UField } from "./components/field/UField.js";
32
35
  import { UForm } from "./components/form/UForm.js";
33
36
  import { UOption } from "./components/option/UOption.js";
34
- import { UPopover } from "./components/popover/UPopover.js";
35
37
  import { UInput } from "./components/input/UInput.js";
36
38
  import { UMenuItem } from "./components/menu-item/UMenuItem.js";
37
39
  import { UMenu } from "./components/menu/UMenu.js";
@@ -55,4 +57,4 @@ import { BrowserStorage } from "./utilities/BrowserStorage.js";
55
57
  import { Dialog } from "./utilities/Dialog.js";
56
58
  import { Theme } from "./utilities/Theme.js";
57
59
  import { Toast } from "./utilities/Toast.js";
58
- export { BrowserStorage, Dialog, IconCache, IconRegistry, Locale, OverlayManager, Theme, Toast, UAlert, UAvatar, UBadge, UBreadcrumb, UBreadcrumbItem, UButton, UButtonGroup, UCard, UCarousel, UCheckbox, UChip, UCopyButton, UDialog, UDivider, UDrawer, UElement, UExpander, UField, UFloatingElement, UForm, UFormControlElement, UIcon, UIconButton, UInput, UMenu, UMenuItem, UOption, UOverlayElement, UPanel, UPopover, UProgressBar, UProgressRing, URadio, URating, USelect, USkeleton, USlider, USpinner, USplitPanel, USwitch, UTab, UTabPanel, UTag, UText, UTextarea, UTooltip, UTree, UTreeItem, arrayAttrConverter, booleanAttrConverter, dateAttrConverter, getDefaultBaseUrl, getParentElement, jsonAttrConverter, querySelectorAllWithin, querySelectorWithin, setDefaultBaseUrl, urlAttrConverter };
60
+ export { BrowserStorage, Dialog, IconCache, IconRegistry, Locale, OverlayManager, Theme, Toast, UAlert, UAvatar, UBadge, UBreadcrumb, UBreadcrumbItem, UButton, UButtonGroup, UCard, UCarousel, UCheckbox, UChip, UCopyButton, UDatePicker, UDialog, UDivider, UDrawer, UElement, UExpander, UField, UFloatingElement, UForm, UFormControlElement, UIcon, UIconButton, UInput, UMenu, UMenuItem, UOption, UOverlayElement, UPanel, UPopover, UProgressBar, UProgressRing, URadio, URating, USelect, USkeleton, USlider, USpinner, USplitPanel, USwitch, UTab, UTabPanel, UTag, UText, UTextarea, UTooltip, UTree, UTreeItem, arrayAttrConverter, booleanAttrConverter, dateAttrConverter, formatCurrency, formatDate, formatNumber, getDefaultBaseUrl, getParentElement, jsonAttrConverter, querySelectorAllWithin, querySelectorWithin, setDefaultBaseUrl, urlAttrConverter };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { UDatePicker as UDatePickerElement } from '../components/date-picker/UDatePicker';
3
+
4
+ export declare const UDatePicker: React.ForwardRefExoticComponent<
5
+ Omit<Partial<UDatePickerElement>, keyof React.HTMLAttributes<UDatePickerElement>>
6
+ & Omit<React.HTMLAttributes<UDatePickerElement>, 'onChange'>
7
+ & React.RefAttributes<UDatePickerElement>
8
+ & {
9
+ onChange?: (event: CustomEvent) => void;
10
+ }
11
+ >;
12
+
13
+ export type UDatePickerProps = React.ComponentProps<typeof UDatePicker>;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { createComponent } from '@lit/react';
3
+ import { UDatePicker as UDatePickerElement } from '../components/date-picker/UDatePicker.js';
4
+
5
+ export const UDatePicker = createComponent({
6
+ react: React,
7
+ tagName: 'u-date-picker',
8
+ elementClass: UDatePickerElement,
9
+ events: {
10
+ onChange: 'change',
11
+ },
12
+ });
@@ -30,6 +30,7 @@ export { UExpander, UExpanderProps } from './UExpander';
30
30
  export { UDrawer, UDrawerProps } from './UDrawer';
31
31
  export { UDivider, UDividerProps } from './UDivider';
32
32
  export { UDialog, UDialogProps } from './UDialog';
33
+ export { UDatePicker, UDatePickerProps } from './UDatePicker';
33
34
  export { UCopyButton, UCopyButtonProps } from './UCopyButton';
34
35
  export { UChip, UChipProps } from './UChip';
35
36
  export { UCheckbox, UCheckboxProps } from './UCheckbox';
@@ -30,6 +30,7 @@ export { UExpander } from './UExpander.js';
30
30
  export { UDrawer } from './UDrawer.js';
31
31
  export { UDivider } from './UDivider.js';
32
32
  export { UDialog } from './UDialog.js';
33
+ export { UDatePicker } from './UDatePicker.js';
33
34
  export { UCopyButton } from './UCopyButton.js';
34
35
  export { UChip } from './UChip.js';
35
36
  export { UCheckbox } from './UCheckbox.js';
@@ -0,0 +1,25 @@
1
+ import { LocaleTag } from './Locale.js';
2
+ /**
3
+ * Wraps `Intl.NumberFormat` with the active locale (`Locale.get()` if omitted).
4
+ */
5
+ export declare function formatNumber(value: number, options?: Intl.NumberFormatOptions, locale?: LocaleTag): string;
6
+ /**
7
+ * Formats a number as currency. The `currency` code is **required and has no default** —
8
+ * the caller must always specify it (e.g. `'KRW'`, `'USD'`). Currency selection is domain knowledge,
9
+ * and this utility does not assume a default.
10
+ *
11
+ * @note If `options` contains `currency` or `style`, they will override the explicit `currency` argument.
12
+ */
13
+ export declare function formatCurrency(value: number, currency: string, options?: Intl.NumberFormatOptions, locale?: LocaleTag): string;
14
+ /**
15
+ * Wraps `Intl.DateTimeFormat` with the active locale. Accepts a Date object or
16
+ * an ISO `YYYY-MM-DD` date string (parsed as local time, not UTC).
17
+ *
18
+ * A value that can't be resolved to a real date (malformed string, or an
19
+ * already-Invalid `Date`) degrades to `String(value)` rather than throwing —
20
+ * `Intl.DateTimeFormat.format()` throws `RangeError` on an Invalid Date, and this
21
+ * utility is called from render paths where an uncaught throw blanks the whole
22
+ * component. Same degrade-instead-of-throw contract as `formatCurrency`'s
23
+ * missing-`currency` fallback.
24
+ */
25
+ export declare function formatDate(value: Date | string, options?: Intl.DateTimeFormatOptions, locale?: LocaleTag): string;
@@ -0,0 +1,61 @@
1
+ import { Locale } from "./Locale.js";
2
+ //#region src/utilities/format.ts
3
+ /**
4
+ * Parses "YYYY-MM-DD" as midnight in the **local** timezone.
5
+ * Using `Date.parse("YYYY-MM-DD")` (UTC interpretation) shifts the date
6
+ * back a day in negative-UTC-offset regions — split y/m/d and construct
7
+ * `new Date(y, m-1, d)` directly instead.
8
+ *
9
+ * A full ISO datetime (e.g. `2026-02-24T09:00:00Z`) is unambiguous — it carries
10
+ * its own timezone — so the local-time-safe split isn't needed there; when the
11
+ * split doesn't yield three numbers, fall back to native parsing instead of
12
+ * producing an Invalid Date.
13
+ */
14
+ function parseISODate(iso) {
15
+ const [y, m, d] = iso.split("-").map(Number);
16
+ return Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d) ? new Date(iso) : new Date(y, m - 1, d);
17
+ }
18
+ /**
19
+ * Resolves a Date or ISO date string to a Date object.
20
+ */
21
+ function resolve(value) {
22
+ return typeof value === "string" ? parseISODate(value) : value;
23
+ }
24
+ /**
25
+ * Wraps `Intl.NumberFormat` with the active locale (`Locale.get()` if omitted).
26
+ */
27
+ function formatNumber(value, options, locale) {
28
+ return new Intl.NumberFormat(locale ?? Locale.get(), options).format(value);
29
+ }
30
+ /**
31
+ * Formats a number as currency. The `currency` code is **required and has no default** —
32
+ * the caller must always specify it (e.g. `'KRW'`, `'USD'`). Currency selection is domain knowledge,
33
+ * and this utility does not assume a default.
34
+ *
35
+ * @note If `options` contains `currency` or `style`, they will override the explicit `currency` argument.
36
+ */
37
+ function formatCurrency(value, currency, options, locale) {
38
+ return new Intl.NumberFormat(locale ?? Locale.get(), {
39
+ style: "currency",
40
+ currency,
41
+ ...options
42
+ }).format(value);
43
+ }
44
+ /**
45
+ * Wraps `Intl.DateTimeFormat` with the active locale. Accepts a Date object or
46
+ * an ISO `YYYY-MM-DD` date string (parsed as local time, not UTC).
47
+ *
48
+ * A value that can't be resolved to a real date (malformed string, or an
49
+ * already-Invalid `Date`) degrades to `String(value)` rather than throwing —
50
+ * `Intl.DateTimeFormat.format()` throws `RangeError` on an Invalid Date, and this
51
+ * utility is called from render paths where an uncaught throw blanks the whole
52
+ * component. Same degrade-instead-of-throw contract as `formatCurrency`'s
53
+ * missing-`currency` fallback.
54
+ */
55
+ function formatDate(value, options, locale) {
56
+ const date = resolve(value);
57
+ if (Number.isNaN(date.getTime())) return String(value);
58
+ return new Intl.DateTimeFormat(locale ?? Locale.get(), options).format(date);
59
+ }
60
+ //#endregion
61
+ export { formatCurrency, formatDate, formatNumber };
@@ -1,6 +1,7 @@
1
1
  import alert_circle_fill_svg_default from "../_virtual/_glob-assets_raw/alert-circle-fill.svg.1e438750.js";
2
2
  import alert_triangle_fill_svg_default from "../_virtual/_glob-assets_raw/alert-triangle-fill.svg.1a8797c4.js";
3
3
  import bell_fill_svg_default from "../_virtual/_glob-assets_raw/bell-fill.svg.d37c41d9.js";
4
+ import calendar_svg_default from "../_virtual/_glob-assets_raw/calendar.svg.02c3abd8.js";
4
5
  import check_svg_default from "../_virtual/_glob-assets_raw/check.svg.4690b01e.js";
5
6
  import chevron_down_svg_default from "../_virtual/_glob-assets_raw/chevron-down.svg.0ccd06ea.js";
6
7
  import chevron_left_svg_default from "../_virtual/_glob-assets_raw/chevron-left.svg.b4601050.js";
@@ -41,6 +42,7 @@ var InternalIconBundle = new Map(Object.entries(/* #__PURE__ */ Object.assign({
41
42
  "../assets/icons/alert-circle-fill.svg": alert_circle_fill_svg_default,
42
43
  "../assets/icons/alert-triangle-fill.svg": alert_triangle_fill_svg_default,
43
44
  "../assets/icons/bell-fill.svg": bell_fill_svg_default,
45
+ "../assets/icons/calendar.svg": calendar_svg_default,
44
46
  "../assets/icons/check.svg": check_svg_default,
45
47
  "../assets/icons/chevron-down.svg": chevron_down_svg_default,
46
48
  "../assets/icons/chevron-left.svg": chevron_left_svg_default,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/components",
3
3
  "description": "web-components library based on lit-element made by iyulab",
4
- "version": "1.26.0",
4
+ "version": "1.27.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "components",
@@ -0,0 +1,71 @@
1
+ # u-date-picker
2
+
3
+ ```ts
4
+ import '@iyulab/components/dist/components/date-picker/UDatePicker.js';
5
+ ```
6
+
7
+ **Tag:** `u-date-picker`
8
+
9
+ Single-date selection with a popover calendar. The value follows the same convention as the native `input[type=date]`: an ISO `YYYY-MM-DD` string. Form-associated.
10
+
11
+ > The calendar week always starts on Sunday, regardless of locale.
12
+
13
+ ```html
14
+ <u-date-picker name="start-date" label="Start date"></u-date-picker>
15
+
16
+ <!-- Clearable with a bounded range -->
17
+ <u-date-picker name="due-date" label="Due date" clearable min="2026-01-01" max="2026-12-31"></u-date-picker>
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Properties
23
+
24
+ | Property | Type | Default | Reflect | Description |
25
+ |----------|------|---------|---------|-------------|
26
+ | `value` | `string` | — | — | Selected date (ISO `YYYY-MM-DD`) |
27
+ | `min` | `string` | — | — | Minimum selectable date (ISO `YYYY-MM-DD`) |
28
+ | `max` | `string` | — | — | Maximum selectable date (ISO `YYYY-MM-DD`) |
29
+ | `clearable` | `boolean` | `false` | ✓ | Show clear button |
30
+ | `placeholder` | `string` | — | — | Placeholder text |
31
+ | `disabled` | `boolean` | `false` | ✓ | Disable |
32
+ | `readonly` | `boolean` | `false` | ✓ | Read-only |
33
+ | `required` | `boolean` | `false` | ✓ | Required |
34
+ | `invalid` | `boolean` | `false` | ✓ | Validation failed |
35
+ | `name` | `string` | — | — | Form field name |
36
+ | `label` | `string` | — | — | Field label |
37
+ | `description` | `string` | — | — | Helper text |
38
+ | `validationMessage` | `string` | — | — | Custom validation message |
39
+
40
+ ## Events
41
+
42
+ | Event | Description |
43
+ |-------|-------------|
44
+ | `change` | Fires when the user clicks a date cell, confirms via keyboard, or clicks the clear button. Programmatic value assignment does not fire it. |
45
+
46
+ ## Methods
47
+
48
+ | Method | Description |
49
+ |--------|--------------|
50
+ | `validate()` | Validate; sets `invalid` |
51
+ | `reset()` | Reset value |
52
+
53
+ ## CSS Parts
54
+
55
+ | Part | Description |
56
+ |------|-------------|
57
+ | `field` | The `u-field` element |
58
+ | `container` | The element wrapping the trigger area |
59
+ | `popover` | The popover element showing the calendar |
60
+ | `calendar` | The calendar container |
61
+ | `calendar-header` | The month navigation header |
62
+ | `calendar-title` | The "Month Year" title |
63
+ | `calendar-weekdays` | The weekday header row |
64
+ | `calendar-grid` | The date grid |
65
+ | `day` | A date cell button |
66
+
67
+ ## CSS Custom Properties
68
+
69
+ | Property | Description |
70
+ |----------|-------------|
71
+ | `--date-picker-popover-width` | Width of the calendar popover (default: 296px, independent of trigger width — a fixed-width calendar reads more naturally) |