@urbicon-ui/blocks 6.1.4 → 6.1.5

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.
@@ -10,7 +10,8 @@
10
10
  toIso,
11
11
  stripTime,
12
12
  addDays,
13
- clampMonth
13
+ clampMonth,
14
+ daysInMonth
14
15
  } from '../../date';
15
16
  import { DateGridController } from '../../internal/date-grid';
16
17
  import type { DateGridSelection, DateGridView } from '../../internal/date-grid';
@@ -43,6 +44,7 @@
43
44
  // Selection
44
45
  selectionMode = 'single',
45
46
  value = $bindable(undefined),
47
+ defaultDate,
46
48
  defaultMonth,
47
49
  defaultYear,
48
50
  // Locale
@@ -130,8 +132,12 @@
130
132
  // --- Reference date: the single anchor the grid is built around ---
131
133
  // Priority:
132
134
  // 1. the first selected date — a populated picker opens on its selection
133
- // 2. `defaultMonth` / `defaultYear` fallback when no value
134
- // 3. today
135
+ // 2. `defaultDate` an explicit day anchor (the only one that anchors a
136
+ // week/day view on a specific week without also selecting that day)
137
+ // 3. `defaultMonth` / `defaultYear` — month/year anchor (resolves to the 1st;
138
+ // fine for month/year views, but a week/day view then opens on the 1st's
139
+ // week, which can fall mostly in the prior month — use `defaultDate` there)
140
+ // 4. today
135
141
  // Read at mount time only; the user navigates freely afterwards. Remount (e.g.
136
142
  // via DatePicker's popover open/close) to re-anchor on the current selection.
137
143
  function firstDateOf(v: CalendarProps['value']): Date | undefined {
@@ -142,16 +148,19 @@
142
148
  }
143
149
  function resolveInitialReference(
144
150
  v: CalendarProps['value'],
151
+ date: Date | undefined,
145
152
  month: number | undefined,
146
153
  year: number | undefined
147
154
  ): Date {
148
- const anchor = firstDateOf(v);
155
+ const anchor = firstDateOf(v) ?? date;
149
156
  if (anchor) return stripTime(anchor);
150
157
  const today = stripTime(new Date());
151
158
  if (month == null && year == null) return today;
152
159
  return new Date(year ?? today.getFullYear(), month ?? today.getMonth(), 1);
153
160
  }
154
- let referenceDate = $state(resolveInitialReference(value, defaultMonth, defaultYear));
161
+ let referenceDate = $state(
162
+ resolveInitialReference(value, defaultDate, defaultMonth, defaultYear)
163
+ );
155
164
 
156
165
  // The month/year derived from the single reference date.
157
166
  const displayedMonth = $derived(referenceDate.getMonth());
@@ -373,7 +382,15 @@
373
382
  const targetMonth = ((total % 12) + 12) % 12;
374
383
  const clamped = clampMonth(targetMonth, targetYear, minDate, maxDate);
375
384
  controller.navDirection = delta > 0 ? 'forward' : 'backward';
376
- referenceDate = new Date(clamped.year, clamped.month, 1);
385
+ // Preserve the day-of-month (clamped) so a later switch to week/day view
386
+ // anchors on a real in-month day, not the 1st's (possibly prior-month) week.
387
+ const day = Math.min(referenceDate.getDate(), daysInMonth(clamped.year, clamped.month));
388
+ let next = new Date(clamped.year, clamped.month, day);
389
+ // clampMonth bounds the month; clamp the day too so it never lands before
390
+ // minDate / after maxDate within that boundary month.
391
+ if (minDate && next < stripTime(minDate)) next = stripTime(minDate);
392
+ if (maxDate && next > stripTime(maxDate)) next = stripTime(maxDate);
393
+ referenceDate = next;
377
394
  onMonthChange?.(clamped.month, clamped.year);
378
395
  }
379
396
 
@@ -391,8 +408,10 @@
391
408
 
392
409
  function navigateYear(delta: number) {
393
410
  controller.navDirection = delta > 0 ? 'forward' : 'backward';
394
- referenceDate = new Date(displayedYear + delta, displayedMonth, 1);
395
- onMonthChange?.(displayedMonth, displayedYear + delta);
411
+ const targetYear = displayedYear + delta;
412
+ const day = Math.min(referenceDate.getDate(), daysInMonth(targetYear, displayedMonth));
413
+ referenceDate = new Date(targetYear, displayedMonth, day);
414
+ onMonthChange?.(displayedMonth, targetYear);
396
415
  }
397
416
 
398
417
  function goToToday() {
@@ -400,7 +419,8 @@
400
419
  }
401
420
 
402
421
  function goToMonth(month: number, year: number) {
403
- referenceDate = new Date(year, month, 1);
422
+ const day = Math.min(referenceDate.getDate(), daysInMonth(year, month));
423
+ referenceDate = new Date(year, month, day);
404
424
  }
405
425
 
406
426
  function setView(v: CalendarViewMode) {
@@ -5,10 +5,13 @@ import type { CalendarEvent, CalendarEventCategory, CalendarSelection, CalendarV
5
5
  import type { CalendarVariants } from './calendar.variants';
6
6
  export type CalendarSlotName = 'base' | 'header' | 'title' | 'nav' | 'navButton' | 'grid' | 'weekdayHeader' | 'weekday' | 'weekRow' | 'weekNumber' | 'day' | 'dayNumber' | 'dotContainer' | 'dot' | 'list' | 'dateHeader' | 'empty' | 'item' | 'colorBar' | 'eventTitle' | 'eventDescription' | 'eventHelper' | 'legend' | 'legendItem' | 'legendDot' | 'legendLabel' | 'yearGrid' | 'yearMonth' | 'yearMonthTitle' | 'yearMiniDay' | 'yearMiniDot' | 'weekGrid' | 'weekColumn' | 'weekColumnHeader' | 'weekColumnDayName' | 'weekColumnDayNumber' | 'weekEventList' | 'weekAllDayEvent' | 'multiDayBar' | 'multiDayBarContainer' | 'agendaView' | 'agendaDayGroup' | 'agendaDayHeader' | 'agendaEventList' | 'dayView' | 'dayViewHeader' | 'dayEventList' | 'timeGrid' | 'timeLabel' | 'timeSlotRow' | 'timeDayColumn' | 'timeEvent' | 'allDayArea' | 'currentTimeLine' | 'weekTimeLayout' | 'eventPopover' | 'eventPopoverItem' | 'miniCalendar' | 'miniCalendarHeader' | 'miniCalendarTitle' | 'miniCalendarNavButton' | 'miniCalendarWeekday' | 'miniCalendarDay';
7
7
  /**
8
- * @description Flexible calendar component with month, year, week, and day views,
9
- * event display, date selection, and configurable layout.
8
+ * @description Flexible calendar component with month, year, week, and day views.
9
+ * Renders timed appointments, multi-day spans and recurrence on a time grid, with
10
+ * event display, date selection and configurable layout. For a headless grid that
11
+ * buckets your own domain content (meals, shifts, bookings) per day, use `Planner`.
10
12
  *
11
13
  * @tag display
14
+ * @related Planner
12
15
  * @related DatePicker
13
16
  * @related DateRangePicker
14
17
  *
@@ -53,8 +56,17 @@ export interface CalendarProps extends Omit<CalendarVariants, 'dayState' | 'hasE
53
56
  /** Currently selected date(s). Supports bind:value. */
54
57
  value?: CalendarSelection;
55
58
  /**
56
- * Initial displayed month (0–11). Used only when `value` is unset;
57
- * when `value` is provided, the calendar opens on the value's month.
59
+ * Initial reference day the grid is anchored on, without selecting it. Use
60
+ * this to open a **week** or **day** view on a specific week — `defaultMonth`/
61
+ * `defaultYear` resolve to the 1st, whose week can fall mostly in the previous
62
+ * month. Ignored when `value` is set (the selection anchors instead); takes
63
+ * priority over `defaultMonth`/`defaultYear`. Read at mount only.
64
+ */
65
+ defaultDate?: Date;
66
+ /**
67
+ * Initial displayed month (0–11). Used only when `value` and `defaultDate`
68
+ * are unset; when `value` is provided, the calendar opens on the value's
69
+ * month. Best for month/year views — for week/day views prefer `defaultDate`.
58
70
  * Defaults to current month.
59
71
  */
60
72
  defaultMonth?: number;
@@ -12,6 +12,16 @@ import type { ChartMargin, ChartPlot, ChartSlotClasses } from '../../internal/ch
12
12
  * @related BarChart
13
13
  * @related Sankey
14
14
  * @stability beta
15
+ *
16
+ * @example
17
+ * ```svelte
18
+ * <ChartFrame height={200} ariaLabel="Weekly revenue">
19
+ * {#snippet children({ innerWidth, innerHeight })}
20
+ * <!-- map your data onto innerWidth / innerHeight, then draw SVG marks -->
21
+ * <polyline points={pts} fill="none" class="stroke-primary" stroke-width="2" />
22
+ * {/snippet}
23
+ * </ChartFrame>
24
+ * ```
15
25
  */
16
26
  export interface ChartFrameProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
17
27
  /** Fixed SVG height in px. @default 240 */
@@ -37,6 +37,11 @@ export declare function getYearMonths(year: number): {
37
37
  month: number;
38
38
  year: number;
39
39
  }[];
40
+ /**
41
+ * Number of days in a month, leap years included. `month` is 0-based
42
+ * (0 = January). Day 0 of the next month is the last day of this one.
43
+ */
44
+ export declare function daysInMonth(year: number, month: number): number;
40
45
  /**
41
46
  * Clamp a month/year to optional min/max date boundaries.
42
47
  * Returns the (possibly adjusted) month/year plus flags for whether navigating
@@ -77,6 +77,13 @@ export function getWeekNumber(date) {
77
77
  export function getYearMonths(year) {
78
78
  return Array.from({ length: 12 }, (_, i) => ({ month: i, year }));
79
79
  }
80
+ /**
81
+ * Number of days in a month, leap years included. `month` is 0-based
82
+ * (0 = January). Day 0 of the next month is the last day of this one.
83
+ */
84
+ export function daysInMonth(year, month) {
85
+ return new Date(year, month + 1, 0).getDate();
86
+ }
80
87
  /**
81
88
  * Clamp a month/year to optional min/max date boundaries.
82
89
  * Returns the (possibly adjusted) month/year plus flags for whether navigating
@@ -14,5 +14,5 @@
14
14
  */
15
15
  export { daysBetween, isInMonth, isInRange, isSameDay, isWeekend, stripTime } from './compare';
16
16
  export { formatDate, formatDateFull, formatDateRange, formatDayTitle, formatMonthShort, formatMonthYear, formatWeekRange, formatWeekTitle, getWeekdayNames } from './format';
17
- export { clampMonth, getMonthGrid, getWeekDates, getWeekNumber, getYearMonths } from './geometry';
17
+ export { clampMonth, daysInMonth, getMonthGrid, getWeekDates, getWeekNumber, getYearMonths } from './geometry';
18
18
  export { addDays, eachDayOfRange, endOfWeek, isoToDate, startOfWeek, toIso } from './range';
@@ -14,5 +14,5 @@
14
14
  */
15
15
  export { daysBetween, isInMonth, isInRange, isSameDay, isWeekend, stripTime } from './compare';
16
16
  export { formatDate, formatDateFull, formatDateRange, formatDayTitle, formatMonthShort, formatMonthYear, formatWeekRange, formatWeekTitle, getWeekdayNames } from './format';
17
- export { clampMonth, getMonthGrid, getWeekDates, getWeekNumber, getYearMonths } from './geometry';
17
+ export { clampMonth, daysInMonth, getMonthGrid, getWeekDates, getWeekNumber, getYearMonths } from './geometry';
18
18
  export { addDays, eachDayOfRange, endOfWeek, isoToDate, startOfWeek, toIso } from './range';
@@ -13,7 +13,7 @@
13
13
  * the source of truth (`bind:value`). Only `focusedDate`, `hoveredDate` and
14
14
  * `navDirection` are controller-owned `$state`.
15
15
  */
16
- import { addDays, clampMonth, daysBetween, endOfWeek, formatDateRange, formatDayTitle, formatMonthYear, formatWeekTitle, getMonthGrid, getWeekDates, getWeekdayNames, getWeekNumber, isInMonth, isInRange, isSameDay, isWeekend, startOfWeek, stripTime, toIso } from '../../date';
16
+ import { addDays, clampMonth, daysBetween, daysInMonth, endOfWeek, formatDateRange, formatDayTitle, formatMonthYear, formatWeekTitle, getMonthGrid, getWeekDates, getWeekdayNames, getWeekNumber, isInMonth, isInRange, isSameDay, isWeekend, startOfWeek, stripTime, toIso } from '../../date';
17
17
  export class DateGridController {
18
18
  #opts;
19
19
  /** The roving keyboard focus target (local midnight). Controller-owned. */
@@ -228,7 +228,21 @@ export class DateGridController {
228
228
  const targetYear = Math.floor(total / 12);
229
229
  const targetMonth = ((total % 12) + 12) % 12;
230
230
  const clamped = clampMonth(targetMonth, targetYear, minDate, maxDate);
231
- next = new Date(clamped.year, clamped.month, 1);
231
+ // Preserve the day-of-month (clamped to the target month's length, e.g.
232
+ // 31 Jan → 28 Feb) rather than snapping to the 1st. Month view ignores the
233
+ // day, but a week/day view sharing this reference then anchors on a real
234
+ // in-month day — anchoring on the 1st can land its week mostly in the prior
235
+ // month (1 Mar 2026 is a Sunday → its Monday-week is 23 Feb–1 Mar). Matches
236
+ // the keyboard PageUp/PageDown month step, which already clamps the day.
237
+ const day = Math.min(referenceDate.getDate(), daysInMonth(clamped.year, clamped.month));
238
+ next = new Date(clamped.year, clamped.month, day);
239
+ // clampMonth bounds the month; clamp the preserved day too so it never lands
240
+ // before minDate / after maxDate within that boundary month (a week view
241
+ // would otherwise open on an all-disabled week just outside the range).
242
+ if (minDate && next < stripTime(minDate))
243
+ next = stripTime(minDate);
244
+ if (maxDate && next > stripTime(maxDate))
245
+ next = stripTime(maxDate);
232
246
  break;
233
247
  }
234
248
  case 'week':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/blocks",
3
- "version": "6.1.4",
3
+ "version": "6.1.5",
4
4
  "description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -90,8 +90,8 @@
90
90
  "@sveltejs/package": "^2.5.8",
91
91
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
92
92
  "@tailwindcss/vite": "^4.3.1",
93
- "@urbicon-ui/i18n": "6.1.4",
94
- "@urbicon-ui/shared-types": "6.1.4",
93
+ "@urbicon-ui/i18n": "6.1.5",
94
+ "@urbicon-ui/shared-types": "6.1.5",
95
95
  "prettier": "^3.8.4",
96
96
  "prettier-plugin-svelte": "^4.1.1",
97
97
  "prettier-plugin-tailwindcss": "^0.8.0",