@nomideusz/svelte-calendar 0.11.0 → 0.13.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.
@@ -24,6 +24,7 @@ const hideDays = $derived(ctx.hideDays);
24
24
  const isMobile = $derived(ctx.isMobile);
25
25
  const autoHeight = $derived(ctx.autoHeight);
26
26
  const compact = $derived(ctx.compact);
27
+ const cols = $derived(ctx.columns && !isMobile);
27
28
  const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
28
29
  const oneventhover = $derived(ctx.oneventhover);
29
30
  const disabledSet = $derived(ctx.disabledSet);
@@ -199,7 +200,6 @@ const weekDays = $derived.by(() => {
199
200
  onpointerenter={() => oneventhover?.(ev)}
200
201
  >
201
202
  <EventContent event={ev}>
202
- <span class="ag-compact-dot"></span>
203
203
  <span class="ag-compact-time">{fmt(ev.start)}</span>
204
204
  <div class="ag-compact-main">
205
205
  <span class="ag-compact-title">{ev.title}</span>
@@ -240,13 +240,14 @@ const weekDays = $derived.by(() => {
240
240
  class="ag ag--week"
241
241
  class:ag--mobile={isMobile}
242
242
  class:ag--auto={autoHeight}
243
+ class:ag--cols={cols}
243
244
  style={style || undefined}
244
245
  style:height={height ? `${height}px` : undefined}
245
246
  onpointerdown={onPointerDown}
246
247
  onpointerup={onPointerUp}
247
248
  onpointercancel={onPointerCancel}
248
249
  >
249
- <div class="ag-body" role="list" aria-label={L.weekAhead}>
250
+ <div class="ag-body" role="list" aria-label={L.weekAhead} style:--ag-cols={weekDays.length}>
250
251
  {#each weekDays as day (day.ms)}
251
252
  {@const expanded = day.tier === 'today' || day.tier === 'tomorrow'}
252
253
  {#if day.tier === 'past'}
@@ -329,15 +330,19 @@ const weekDays = $derived.by(() => {
329
330
 
330
331
  {#if day.events.length === 0}
331
332
  <div class="ag-wday-empty">{L.noEvents}</div>
332
- {:else if compact}
333
- <!-- Compact: minimal dot + time + title rows for all days -->
333
+ {:else if compact && !cols}
334
+ <!-- Compact: minimal dot + time + title rows for all days.
335
+ Ignored in columns mode — single-line rows truncate to
336
+ nothing at column width; cards wrap instead. -->
334
337
  <div class="ag-wday-compact">
335
338
  {#each day.timedEvents as ev (ev.id)}
336
339
  {@render compactRow(ev, false, false)}
337
340
  {/each}
338
341
  </div>
339
- {:else if equalDays}
340
- <!-- Equal days: card layout for all days, no time-relative badges -->
342
+ {:else if equalDays || (cols && !expanded)}
343
+ <!-- Equal days (or timetable columns): card layout, no time-relative
344
+ badges. In columns, today/tomorrow still fall through to the
345
+ expanded branch below for now/ETA treatment. -->
341
346
  <div class="ag-wday-expanded">
342
347
  {#each groupIntoSlots(day.timedEvents) as slot (slot.startMs)}
343
348
  <div class="ag-wslot">
@@ -814,8 +819,9 @@ const weekDays = $derived.by(() => {
814
819
  text-decoration: line-through;
815
820
  text-decoration-color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
816
821
  }
817
- .ag-compact--done .ag-compact-dot {
818
- opacity: 0.5;
822
+ .ag-compact--done .ag-compact-time {
823
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
824
+ font-weight: 400;
819
825
  }
820
826
 
821
827
  /* Compact day events */
@@ -834,13 +840,10 @@ const weekDays = $derived.by(() => {
834
840
  .ag-compact--selected {
835
841
  background: color-mix(in srgb, var(--ev-color) 10%, transparent);
836
842
  border-radius: 4px;
837
- /* Highlight gutter comes from negative margins so the row's content
838
- stays aligned with its unselected siblings (no tap-shift). */
839
- padding-left: 6px;
840
- padding-right: 6px;
841
- margin-left: -6px;
842
- margin-right: -6px;
843
- width: calc(100% + 12px);
843
+ /* The highlight bleeds into the gutter via a spread shadow — zero
844
+ layout impact, so nothing shifts or clips even when the host
845
+ reduces the gutters below the bleed width. */
846
+ box-shadow: 0 0 0 6px color-mix(in srgb, var(--ev-color) 10%, transparent);
844
847
  }
845
848
  .ag-compact:hover .ag-compact-title,
846
849
  .ag-compact:active .ag-compact-title {
@@ -855,18 +858,14 @@ const weekDays = $derived.by(() => {
855
858
  box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
856
859
  border-radius: 4px;
857
860
  }
858
- .ag-compact-dot {
859
- width: 5px;
860
- height: 5px;
861
- border-radius: 50%;
862
- background: var(--ev-color, var(--dt-accent));
863
- flex-shrink: 0;
864
- align-self: center;
865
- }
861
+ /* The time label doubles as the class-color signal (replaces the old
862
+ dot): the event color mixed toward the text color, so it stays
863
+ legible on any palette and costs zero horizontal space. */
866
864
  .ag-compact-time {
867
865
  font-size: 11px;
868
866
  font-family: var(--dt-mono, monospace);
869
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
867
+ font-weight: 500;
868
+ color: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 60%, var(--dt-text, rgba(0, 0, 0, 0.87)));
870
869
  min-width: 40px;
871
870
  flex-shrink: 0;
872
871
  white-space: nowrap;
@@ -897,12 +896,6 @@ const weekDays = $derived.by(() => {
897
896
  .ag--mobile .ag-compact-sub {
898
897
  max-width: 100%;
899
898
  }
900
- /* Wrapped rows are two/three lines tall — center-aligning the dot floats
901
- it between lines; pin it optically to the first (title) line instead. */
902
- .ag--mobile .ag-compact-dot {
903
- align-self: flex-start;
904
- margin-top: 8px;
905
- }
906
899
  .ag-compact-title {
907
900
  font-size: 12px;
908
901
  font-weight: 500;
@@ -991,6 +984,51 @@ const weekDays = $derived.by(() => {
991
984
  border-radius: 4px;
992
985
  }
993
986
 
987
+ /* ═══ Timetable columns (desktop) ═══ */
988
+ .ag--cols .ag-body {
989
+ display: grid;
990
+ grid-template-columns: repeat(var(--ag-cols, 7), minmax(0, 1fr));
991
+ }
992
+ /* Columns stretch to the tallest day, so the separator runs full height */
993
+ .ag--cols .ag-wday {
994
+ border-bottom: none;
995
+ border-inline-start: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
996
+ min-width: 0;
997
+ }
998
+ .ag--cols .ag-wday:first-child {
999
+ border-inline-start: none;
1000
+ }
1001
+ /* Uniform head padding — the :first-child top bump would misalign columns.
1002
+ Also overrides the past-day head variant (extra class = higher specificity). */
1003
+ .ag--cols .ag-wday .ag-wday-head {
1004
+ padding: 12px 10px 8px;
1005
+ }
1006
+ /* Badge + name + date won't fit one line in a ~160px column */
1007
+ .ag--cols .ag-wday-head-left {
1008
+ flex-wrap: wrap;
1009
+ row-gap: 2px;
1010
+ }
1011
+ .ag--cols .ag-wday-expanded,
1012
+ .ag--cols .ag-wday-compact,
1013
+ .ag--cols .ag-wday-empty,
1014
+ .ag--cols .ag-wday-past-line--summary {
1015
+ padding-left: 10px;
1016
+ padding-right: 10px;
1017
+ }
1018
+ .ag--cols .ag-allday {
1019
+ padding-left: 10px;
1020
+ padding-right: 10px;
1021
+ }
1022
+ /* Narrow cards: long titles get a third line, meta wraps instead of clipping */
1023
+ .ag--cols .ag-card-title {
1024
+ -webkit-line-clamp: 3;
1025
+ line-clamp: 3;
1026
+ }
1027
+ .ag--cols .ag-card-meta {
1028
+ flex-wrap: wrap;
1029
+ row-gap: 3px;
1030
+ }
1031
+
994
1032
  /* ═══ Mobile adaptations ═══ */
995
1033
  .ag--mobile .ag-wday-head {
996
1034
  padding: 12px 16px;
@@ -1,10 +1,6 @@
1
- <script lang="ts">import PlannerDay from "./PlannerDay.svelte";
2
- import PlannerWeek from "./PlannerWeek.svelte";
1
+ <script lang="ts">import PlannerWeek from "./PlannerWeek.svelte";
3
2
  let { mode = "week", ...rest } = $props();
4
3
  </script>
5
4
 
6
- {#if mode === 'day'}
7
- <PlannerDay {...rest} />
8
- {:else}
9
- <PlannerWeek {...rest} />
10
- {/if}
5
+ <!-- Both planner modes are the same vertical time grid; day is one column. -->
6
+ <PlannerWeek {mode} {...rest} />
@@ -1,5 +1,7 @@
1
1
  <!--
2
- Planner week mode — vertical time grid (Google-Calendar-style).
2
+ Planner time grid — vertical, Google-Calendar-style. Renders both planner
3
+ modes: week (N columns from viewState.dayCount) and day (a single column),
4
+ so the two share one geometry, one drag mapping, and one overlap engine.
3
5
 
4
6
  • Left time gutter with localized hour labels; N day columns (viewState.dayCount).
5
7
  • Sticky day-header row + sticky all-day strip; the grid scrolls vertically.
@@ -17,8 +19,9 @@ import EventContent from "../shared/EventContent.svelte";
17
19
  import { createClock } from "../../core/clock.svelte.js";
18
20
  import { DAY_MS, HOUR_MS, sod, addDaysMs } from "../../core/time.js";
19
21
  import { startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
20
- import { fmtH, fmtTime, fmtDuration, weekdayShort } from "../../core/locale.js";
22
+ import { fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong } from "../../core/locale.js";
21
23
  let {
24
+ mode = "week",
22
25
  mondayStart = true,
23
26
  locale,
24
27
  height = 520,
@@ -59,7 +62,9 @@ const gridHeight = $derived(hourCount * HOUR_H);
59
62
  let scrollEl;
60
63
  let colsEl;
61
64
  const todayMs = $derived(clock.today);
62
- const customDays = $derived(viewState?.dayCount ?? 7);
65
+ const singleDay = $derived(mode === "day" || viewState?.mode === "day");
66
+ const customDays = $derived(singleDay ? 1 : viewState?.dayCount ?? 7);
67
+ const hideDayHead = $derived(singleDay && showDates && !!viewState && !dayHeaderSnippet);
63
68
  const weekStartMs = $derived.by(() => {
64
69
  const r = viewState?.range;
65
70
  if (r) return sod(r.start.getTime());
@@ -259,8 +264,8 @@ function statusText(ev) {
259
264
  }
260
265
  function ghostForDay(dayMs) {
261
266
  if (!drag?.active || !drag.payload) return null;
262
- const mode = drag.mode;
263
- if (mode !== "move" && mode !== "create") return null;
267
+ const mode2 = drag.mode;
268
+ if (mode2 !== "move" && mode2 !== "create") return null;
264
269
  const s = drag.payload.start.getTime();
265
270
  const e = drag.payload.end.getTime();
266
271
  const bandS = dayMs + startHour * HOUR_MS;
@@ -273,7 +278,7 @@ function ghostForDay(dayMs) {
273
278
  height: Math.max(12, (ce - cs) / HOUR_MS * HOUR_H),
274
279
  start: drag.payload.start,
275
280
  end: drag.payload.end,
276
- create: mode === "create",
281
+ create: mode2 === "create",
277
282
  // Only the segment containing the start shows the readout
278
283
  showTime: cs === Math.max(s, dayMs)
279
284
  };
@@ -570,7 +575,9 @@ function onWindowKeydown(e) {
570
575
  <div class="tw-scroll" bind:this={scrollEl}>
571
576
  <div class="tw-inner" style:min-width="{innerMinWidth}px">
572
577
  <!-- Sticky top: day headers + all-day strip -->
578
+ {#if !hideDayHead || hasAllDayRow}
573
579
  <div class="tw-top">
580
+ {#if !hideDayHead}
574
581
  <div class="tw-head">
575
582
  <div class="tw-corner" style:width="{GUTTER_W}px" aria-hidden="true"></div>
576
583
  {#each dayCols as day (day.ms)}
@@ -579,7 +586,7 @@ function onWindowKeydown(e) {
579
586
  class:tw-hd--today={day.isToday}
580
587
  aria-current={day.isToday ? 'date' : undefined}
581
588
  >
582
- <span class="tw-hd-wd">{weekdayShort(day.ms, locale)}</span>
589
+ <span class="tw-hd-wd">{singleDay ? weekdayLong(day.ms, locale) : weekdayShort(day.ms, locale)}</span>
583
590
  {#if showDates}
584
591
  <span class="tw-hd-num" class:tw-hd-num--today={day.isToday}>{day.dayNum}</span>
585
592
  {/if}
@@ -591,6 +598,7 @@ function onWindowKeydown(e) {
591
598
  </div>
592
599
  {/each}
593
600
  </div>
601
+ {/if}
594
602
 
595
603
  {#if hasAllDayRow}
596
604
  <div class="tw-allday">
@@ -618,6 +626,7 @@ function onWindowKeydown(e) {
618
626
  </div>
619
627
  {/if}
620
628
  </div>
629
+ {/if}
621
630
 
622
631
  <!-- Grid body -->
623
632
  <div class="tw-body" style:height="{gridHeight}px">
@@ -1,5 +1,7 @@
1
1
  import type { TimelineEvent } from '../../core/types.js';
2
2
  interface Props {
3
+ /** 'day' renders a single-day column; 'week' the multi-day grid. */
4
+ mode?: 'day' | 'week';
3
5
  mondayStart?: boolean;
4
6
  locale?: string;
5
7
  height?: number | null;
@@ -14,6 +14,7 @@ export interface CalendarContext {
14
14
  readonly isMobile: boolean;
15
15
  readonly autoHeight: boolean;
16
16
  readonly compact: boolean;
17
+ readonly columns: boolean;
17
18
  readonly readOnly: boolean;
18
19
  readonly blockedSlots: BlockedSlot[] | undefined;
19
20
  readonly dayHeaderSnippet: Snippet<[{
@@ -34,6 +34,7 @@ export function useCalendarContext() {
34
34
  get isMobile() { return raw?.mobile ?? false; },
35
35
  get autoHeight() { return raw?.autoHeight ?? false; },
36
36
  get compact() { return raw?.compact ?? false; },
37
+ get columns() { return raw?.columns ?? false; },
37
38
  get readOnly() { return raw?.readOnly ?? false; },
38
39
  get blockedSlots() { return raw?.blockedSlots; },
39
40
  get dayHeaderSnippet() { return raw?.dayHeaderSnippet; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "A themeable Svelte 5 calendar with Day and Week views — Planner and Agenda.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,16 +35,9 @@
35
35
  "test:watch": "vitest"
36
36
  },
37
37
  "peerDependencies": {
38
- "@chenglou/pretext": "^0.0.5",
39
38
  "svelte": "^5.0.0"
40
39
  },
41
- "peerDependenciesMeta": {
42
- "@chenglou/pretext": {
43
- "optional": true
44
- }
45
- },
46
40
  "devDependencies": {
47
- "@chenglou/pretext": "^0.0.5",
48
41
  "@sveltejs/adapter-auto": "^7.0.1",
49
42
  "@sveltejs/kit": "^2.69.1",
50
43
  "@sveltejs/package": "^2.5.8",