svelte-comp 1.2.2 → 1.2.7

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/README.md CHANGED
@@ -53,7 +53,7 @@ npm i prismjs @types/prismjs
53
53
 
54
54
  ## 📁 Components included
55
55
 
56
- Accordion • Button • Card • Carousel • CheckBox • CodeView • ColorPicker •
56
+ Accordion • Button • Calendar • Card • Carousel • CheckBox • CodeView • ColorPicker •
57
57
  DatePicker • Dialog • Field • FilePicker • Form • Hamburger • Menu •
58
58
  PaginatedCard • Pagination • PrimaryColorSelect • ProgressBar • ProgressCircle •
59
59
  Radio • SearchInput • Select • Slider • Splitter • Switch • Tabs • Table • ThemeToggle •
package/dist/lang.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export declare const TEXTS: {
2
2
  readonly en: {
3
3
  readonly app: {
4
- readonly version: "v1.2.1";
4
+ readonly version: "v1.2.6";
5
5
  readonly title: "Svelte 5 UI Components";
6
6
  readonly footer: "© 2025 MaestroFusion360";
7
7
  readonly authorUrl: "https://github.com/MaestroFusion360/svelte-comp";
@@ -360,7 +360,7 @@ export declare const TEXTS: {
360
360
  };
361
361
  readonly ru: {
362
362
  readonly app: {
363
- readonly version: "v1.2.1";
363
+ readonly version: "v1.2.6";
364
364
  readonly title: "Svelte 5 UI Components";
365
365
  readonly footer: "© 2025 MaestroFusion360";
366
366
  readonly authorUrl: "https://github.com/MaestroFusion360/svelte-comp";
@@ -720,7 +720,7 @@ export declare const TEXTS: {
720
720
  };
721
721
  readonly es: {
722
722
  readonly app: {
723
- readonly version: "v1.2.1";
723
+ readonly version: "v1.2.6";
724
724
  readonly title: "Svelte 5 UI Components";
725
725
  readonly footer: "© 2025 MaestroFusion360";
726
726
  readonly authorUrl: "https://github.com/MaestroFusion360/svelte-comp";
package/dist/lang.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var enTexts = {
2
2
  app: {
3
- version: "v1.2.1",
3
+ version: "v1.2.6",
4
4
  title: "Svelte 5 UI Components",
5
5
  footer: "© 2025 MaestroFusion360",
6
6
  authorUrl: "https://github.com/MaestroFusion360/svelte-comp",
@@ -361,7 +361,7 @@ var enTexts = {
361
361
  };
362
362
  var ruTexts = {
363
363
  app: {
364
- version: "v1.2.1",
364
+ version: "v1.2.6",
365
365
  title: "Svelte 5 UI Components",
366
366
  footer: "© 2025 MaestroFusion360",
367
367
  authorUrl: "https://github.com/MaestroFusion360/svelte-comp",
@@ -723,7 +723,7 @@ var ruTexts = {
723
723
  };
724
724
  var esTexts = {
725
725
  app: {
726
- version: "v1.2.1",
726
+ version: "v1.2.6",
727
727
  title: "Svelte 5 UI Components",
728
728
  footer: "© 2025 MaestroFusion360",
729
729
  authorUrl: "https://github.com/MaestroFusion360/svelte-comp",
@@ -0,0 +1,377 @@
1
+ <!-- src/lib/Calendar.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component Calendar
5
+ * @description Monthly calendar grid with navigation and date selection.
6
+ *
7
+ * @prop value {string | null} - Selected date in ISO `YYYY-MM-DD` (bindable)
8
+ * @default null
9
+ *
10
+ * @prop min {string} - Minimum selectable date (ISO `YYYY-MM-DD`)
11
+ *
12
+ * @prop max {string} - Maximum selectable date (ISO `YYYY-MM-DD`)
13
+ *
14
+ * @prop locale {string} - Locale used for month/day labels
15
+ * @default "en-US"
16
+ *
17
+ * @prop weekStartsOn {0|1|2|3|4|5|6} - First day of week (0=Sun ... 6=Sat)
18
+ * @default 1
19
+ *
20
+ * @prop showOutsideDays {boolean} - Render days from adjacent months
21
+ * @default true
22
+ *
23
+ * @prop disabled {boolean} - Disables selection and navigation
24
+ * @default false
25
+ *
26
+ * @prop onChange {(value: string | null) => void} - Fired on date selection
27
+ *
28
+ * @prop class {string} - Additional classes for the root wrapper
29
+ * @default ""
30
+ */
31
+ import type { HTMLAttributes } from "svelte/elements";
32
+ import { cx } from "../utils";
33
+ import { TEXT } from "./types";
34
+
35
+ type Props = HTMLAttributes<HTMLDivElement> & {
36
+ value?: string | null;
37
+ min?: string;
38
+ max?: string;
39
+ locale?: string;
40
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
41
+ showOutsideDays?: boolean;
42
+ disabled?: boolean;
43
+ onChange?: (value: string | null) => void;
44
+ class?: string;
45
+ };
46
+
47
+ let {
48
+ value = $bindable<string | null>(null),
49
+ min = "1926-01-01",
50
+ max,
51
+ locale = "en-US",
52
+ weekStartsOn = 1,
53
+ showOutsideDays = true,
54
+ disabled = false,
55
+ onChange,
56
+ class: externalClass = "",
57
+ ...rest
58
+ }: Props = $props();
59
+
60
+ type ViewMode = "days" | "months" | "years";
61
+
62
+ function startOfDay(date: Date) {
63
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
64
+ }
65
+
66
+ function parseIso(value: string | null | undefined): Date | null {
67
+ if (!value) return null;
68
+ const [y, m, d] = value.split("-").map((part) => Number(part));
69
+ if (!y || !m || !d) return null;
70
+ const date = new Date(y, m - 1, d);
71
+ return Number.isNaN(date.getTime()) ? null : date;
72
+ }
73
+
74
+ function toIso(date: Date) {
75
+ const y = date.getFullYear();
76
+ const m = String(date.getMonth() + 1).padStart(2, "0");
77
+ const d = String(date.getDate()).padStart(2, "0");
78
+ return `${y}-${m}-${d}`;
79
+ }
80
+
81
+ function isSameDay(a: Date, b: Date) {
82
+ return (
83
+ a.getFullYear() === b.getFullYear() &&
84
+ a.getMonth() === b.getMonth() &&
85
+ a.getDate() === b.getDate()
86
+ );
87
+ }
88
+
89
+ let viewDate = $state(startOfDay(parseIso(value) ?? new Date()));
90
+ let viewMode = $state<ViewMode>("days");
91
+
92
+ const selectedDate = $derived(parseIso(value));
93
+ const minDate = $derived(parseIso(min));
94
+ const maxDate = $derived(parseIso(max));
95
+ const today = $derived(startOfDay(new Date()));
96
+
97
+ let lastValue = $state<string | null>(value ?? null);
98
+
99
+ $effect(() => {
100
+ if (value === lastValue) return;
101
+ lastValue = value;
102
+ const selected = parseIso(value);
103
+ if (!selected) return;
104
+ viewDate = new Date(selected.getFullYear(), selected.getMonth(), 1);
105
+ });
106
+
107
+ const monthLabel = $derived(
108
+ new Intl.DateTimeFormat(locale, { month: "long" }).format(viewDate)
109
+ );
110
+
111
+ const yearLabel = $derived(String(viewDate.getFullYear()));
112
+
113
+ const weekdayLabels = $derived.by(() => {
114
+ const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
115
+ const labels: string[] = [];
116
+ const base = new Date(2023, 0, 1);
117
+ for (let i = 0; i < 7; i += 1) {
118
+ const offset = (weekStartsOn + i) % 7;
119
+ const date = new Date(base);
120
+ date.setDate(base.getDate() + offset);
121
+ labels.push(formatter.format(date));
122
+ }
123
+ return labels;
124
+ });
125
+
126
+ const dayLabelFormatter = $derived.by(
127
+ () =>
128
+ new Intl.DateTimeFormat(locale, {
129
+ weekday: "long",
130
+ year: "numeric",
131
+ month: "long",
132
+ day: "numeric",
133
+ })
134
+ );
135
+
136
+ const monthOptions = $derived.by(() => {
137
+ const formatter = new Intl.DateTimeFormat(locale, { month: "short" });
138
+ return Array.from({ length: 12 }, (_, idx) => ({
139
+ index: idx,
140
+ label: formatter.format(new Date(2024, idx, 1)),
141
+ }));
142
+ });
143
+
144
+ const yearRangeStart = $derived(() => {
145
+ const year = viewDate.getFullYear();
146
+ return year - (year % 12);
147
+ });
148
+
149
+ const yearOptions = $derived.by(() =>
150
+ Array.from({ length: 12 }, (_, idx) => yearRangeStart() + idx)
151
+ );
152
+
153
+ type DayCell = {
154
+ date: Date;
155
+ iso: string;
156
+ inMonth: boolean;
157
+ isToday: boolean;
158
+ isSelected: boolean;
159
+ isDisabled: boolean;
160
+ };
161
+
162
+ const days = $derived.by(() => {
163
+ const year = viewDate.getFullYear();
164
+ const month = viewDate.getMonth();
165
+ const first = new Date(year, month, 1);
166
+ const startOffset = (first.getDay() - weekStartsOn + 7) % 7;
167
+ const start = new Date(year, month, 1 - startOffset);
168
+ const cells: DayCell[] = [];
169
+ for (let i = 0; i < 42; i += 1) {
170
+ const date = new Date(start);
171
+ date.setDate(start.getDate() + i);
172
+ const day = startOfDay(date);
173
+ const inMonth = day.getMonth() === month;
174
+ const isToday = isSameDay(day, today);
175
+ const isSelected = selectedDate ? isSameDay(day, selectedDate) : false;
176
+ const beforeMin = minDate ? day < startOfDay(minDate) : false;
177
+ const afterMax = maxDate ? day > startOfDay(maxDate) : false;
178
+ const isDisabled =
179
+ disabled || beforeMin || afterMax || (!showOutsideDays && !inMonth);
180
+ cells.push({
181
+ date: day,
182
+ iso: toIso(day),
183
+ inMonth,
184
+ isToday,
185
+ isSelected,
186
+ isDisabled,
187
+ });
188
+ }
189
+ return cells;
190
+ });
191
+
192
+ function shift(delta: number) {
193
+ if (disabled) return;
194
+ if (viewMode === "days") {
195
+ viewDate = new Date(
196
+ viewDate.getFullYear(),
197
+ viewDate.getMonth() + delta,
198
+ 1
199
+ );
200
+ } else if (viewMode === "months") {
201
+ viewDate = new Date(viewDate.getFullYear() + delta, viewDate.getMonth(), 1);
202
+ } else {
203
+ viewDate = new Date(
204
+ viewDate.getFullYear() + delta * 12,
205
+ viewDate.getMonth(),
206
+ 1
207
+ );
208
+ }
209
+ }
210
+
211
+ function selectDay(cell: DayCell) {
212
+ if (cell.isDisabled) return;
213
+ value = cell.iso;
214
+ onChange?.(cell.iso);
215
+ }
216
+
217
+ function selectMonth(index: number) {
218
+ if (disabled) return;
219
+ viewDate = new Date(viewDate.getFullYear(), index, 1);
220
+ viewMode = "days";
221
+ }
222
+
223
+ function selectYear(year: number) {
224
+ if (disabled) return;
225
+ viewDate = new Date(year, viewDate.getMonth(), 1);
226
+ viewMode = "months";
227
+ }
228
+
229
+ const wrapperClass = $derived(cx("w-full", externalClass));
230
+
231
+ const headerButtonBase =
232
+ "px-1 py-0.5 rounded-[var(--radius-sm)] transition-colors text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed text-[var(--text-xs)]";
233
+
234
+ const arrowButtonBase =
235
+ "inline-flex items-center justify-center h-3 w-3 rounded-[var(--radius-sm)] text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed";
236
+
237
+ const dayButtonBase =
238
+ "rounded-full flex items-center justify-center transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] text-[var(--text-xs)]";
239
+ </script>
240
+
241
+ <div class={wrapperClass} style="width: 240px; max-width: 100%;" {...rest}>
242
+ <div class="flex items-center justify-between mb-[var(--spacing-sm)]">
243
+ <div class="flex items-center gap-1">
244
+ <button
245
+ type="button"
246
+ class={cx(headerButtonBase, TEXT.sm)}
247
+ onclick={() => (viewMode = viewMode === "months" ? "days" : "months")}
248
+ disabled={disabled}
249
+ >
250
+ {monthLabel}
251
+ </button>
252
+ <button
253
+ type="button"
254
+ class={cx(headerButtonBase, TEXT.sm)}
255
+ onclick={() => (viewMode = viewMode === "years" ? "days" : "years")}
256
+ disabled={disabled}
257
+ >
258
+ {yearLabel}
259
+ </button>
260
+ </div>
261
+
262
+ <div class="flex items-center gap-1">
263
+ <button
264
+ type="button"
265
+ class={arrowButtonBase}
266
+ aria-label="Previous"
267
+ onclick={() => shift(-1)}
268
+ disabled={disabled}
269
+ >
270
+
271
+ </button>
272
+ <button
273
+ type="button"
274
+ class={arrowButtonBase}
275
+ aria-label="Next"
276
+ onclick={() => shift(1)}
277
+ disabled={disabled}
278
+ >
279
+
280
+ </button>
281
+ </div>
282
+ </div>
283
+
284
+ {#if viewMode === "days"}
285
+ <div
286
+ class={cx("text-center", TEXT.xs)}
287
+ style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--spacing-xs);"
288
+ >
289
+ {#each weekdayLabels as label, i (i)}
290
+ <div class="py-[var(--spacing-xs)] [color:var(--color-text-muted)]">
291
+ {label}
292
+ </div>
293
+ {/each}
294
+ </div>
295
+
296
+ <div
297
+ style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--spacing-xs); margin-top: var(--spacing-xs);"
298
+ >
299
+ {#each days as cell (cell.iso)}
300
+ <button
301
+ type="button"
302
+ class={cx(
303
+ dayButtonBase,
304
+ cell.isSelected &&
305
+ "bg-[var(--color-bg-primary)] text-white hover:brightness-110",
306
+ !cell.isSelected && "text-[var(--color-text-default)]",
307
+ cell.isToday &&
308
+ !cell.isSelected &&
309
+ "border border-[var(--border-color-primary)]",
310
+ !cell.inMonth && "text-[var(--color-text-muted)]",
311
+ cell.isDisabled &&
312
+ "opacity-[var(--opacity-disabled)] cursor-not-allowed",
313
+ !cell.isDisabled && "hover:bg-[var(--color-bg-hover)]"
314
+ )}
315
+ style="width: 22px; height: 22px; justify-self: center;"
316
+ aria-pressed={cell.isSelected}
317
+ aria-current={cell.isToday ? "date" : undefined}
318
+ aria-label={dayLabelFormatter.format(cell.date)}
319
+ disabled={cell.isDisabled}
320
+ onclick={() => selectDay(cell)}
321
+ >
322
+ {#if cell.inMonth || showOutsideDays}
323
+ {cell.date.getDate()}
324
+ {/if}
325
+ </button>
326
+ {/each}
327
+ </div>
328
+ {:else if viewMode === "months"}
329
+ <div
330
+ style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--spacing-sm);"
331
+ >
332
+ {#each monthOptions as month (month.index)}
333
+ <button
334
+ type="button"
335
+ class={cx(
336
+ "rounded-[var(--radius-md)] text-center transition-colors",
337
+ TEXT.xs,
338
+ month.index === viewDate.getMonth() &&
339
+ "bg-[var(--color-bg-primary)] text-white",
340
+ month.index !== viewDate.getMonth() &&
341
+ "text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)]",
342
+ disabled && "opacity-[var(--opacity-disabled)] cursor-not-allowed"
343
+ )}
344
+ style="height: 22px; width: 22px;"
345
+ onclick={() => selectMonth(month.index)}
346
+ disabled={disabled}
347
+ >
348
+ {month.label}
349
+ </button>
350
+ {/each}
351
+ </div>
352
+ {:else}
353
+ <div
354
+ style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--spacing-sm);"
355
+ >
356
+ {#each yearOptions as year (year)}
357
+ <button
358
+ type="button"
359
+ class={cx(
360
+ "rounded-[var(--radius-md)] text-center transition-colors",
361
+ TEXT.xs,
362
+ year === viewDate.getFullYear() &&
363
+ "bg-[var(--color-bg-primary)] text-white",
364
+ year !== viewDate.getFullYear() &&
365
+ "text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)]",
366
+ disabled && "opacity-[var(--opacity-disabled)] cursor-not-allowed"
367
+ )}
368
+ style="height: 22px; width: 22px;"
369
+ onclick={() => selectYear(year)}
370
+ disabled={disabled}
371
+ >
372
+ {year}
373
+ </button>
374
+ {/each}
375
+ </div>
376
+ {/if}
377
+ </div>
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @component Calendar
3
+ * @description Monthly calendar grid with navigation and date selection.
4
+ *
5
+ * @prop value {string | null} - Selected date in ISO `YYYY-MM-DD` (bindable)
6
+ * @default null
7
+ *
8
+ * @prop min {string} - Minimum selectable date (ISO `YYYY-MM-DD`)
9
+ *
10
+ * @prop max {string} - Maximum selectable date (ISO `YYYY-MM-DD`)
11
+ *
12
+ * @prop locale {string} - Locale used for month/day labels
13
+ * @default "en-US"
14
+ *
15
+ * @prop weekStartsOn {0|1|2|3|4|5|6} - First day of week (0=Sun ... 6=Sat)
16
+ * @default 1
17
+ *
18
+ * @prop showOutsideDays {boolean} - Render days from adjacent months
19
+ * @default true
20
+ *
21
+ * @prop disabled {boolean} - Disables selection and navigation
22
+ * @default false
23
+ *
24
+ * @prop onChange {(value: string | null) => void} - Fired on date selection
25
+ *
26
+ * @prop class {string} - Additional classes for the root wrapper
27
+ * @default ""
28
+ */
29
+ import type { HTMLAttributes } from "svelte/elements";
30
+ type Props = HTMLAttributes<HTMLDivElement> & {
31
+ value?: string | null;
32
+ min?: string;
33
+ max?: string;
34
+ locale?: string;
35
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
36
+ showOutsideDays?: boolean;
37
+ disabled?: boolean;
38
+ onChange?: (value: string | null) => void;
39
+ class?: string;
40
+ };
41
+ declare const Calendar: import("svelte").Component<Props, {}, "value">;
42
+ type Calendar = ReturnType<typeof Calendar>;
43
+ export default Calendar;
@@ -146,12 +146,12 @@
146
146
  </script>
147
147
 
148
148
  <div
149
- class={cx(
150
- "cv-root w-full h-full min-h-0 flex flex-col border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]",
151
- "text-[var(--color-text-default)]",
152
- externalClass
153
- )}
154
- >
149
+ class={cx(
150
+ "cv-root w-full h-full min-h-0 flex flex-col border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]",
151
+ "text-[var(--color-text-default)]",
152
+ externalClass
153
+ )}
154
+ >
155
155
  {#if title}
156
156
  <div
157
157
  class={cx(
@@ -176,29 +176,29 @@
176
176
  </div>
177
177
  {/if}
178
178
 
179
- <div
180
- class={cx(
181
- "cv-body flex flex-1 min-h-0 font-mono",
182
- TEXT[sz],
183
- LINE_HEIGHT[sz]
184
- )}
185
- >
186
- {#if showLineNumbers}
187
- <div
188
- bind:this={gutterEl}
189
- class={cx(
190
- "select-none px-3 py-[12px] border-r border-[var(--border-color-default)]",
191
- "text-[var(--color-text-muted)] text-right overflow-hidden",
192
- "cv-gutter bg-[var(--color-bg-surface)] tabular-nums h-full min-h-0"
193
- )}
194
- >
195
- {#each lines as _, i (i)}
196
- <div class={LINE_HEIGHT[sz]}>{i + 1}</div>
197
- {/each}
198
- </div>
199
- {/if}
200
-
201
- <div class="cv-editor relative flex-1 min-h-0">
179
+ <div
180
+ class={cx(
181
+ "cv-body flex flex-1 min-h-0 font-mono",
182
+ TEXT[sz],
183
+ LINE_HEIGHT[sz]
184
+ )}
185
+ >
186
+ {#if showLineNumbers}
187
+ <div
188
+ bind:this={gutterEl}
189
+ class={cx(
190
+ "select-none px-3 py-[12px] border-r border-[var(--border-color-default)]",
191
+ "text-[var(--color-text-muted)] text-right overflow-hidden",
192
+ "cv-gutter bg-[var(--color-bg-surface)] tabular-nums h-full min-h-0"
193
+ )}
194
+ >
195
+ {#each lines as _, i (i)}
196
+ <div class={LINE_HEIGHT[sz]}>{i + 1}</div>
197
+ {/each}
198
+ </div>
199
+ {/if}
200
+
201
+ <div class="cv-editor relative flex-1 min-h-0">
202
202
  <div
203
203
  bind:this={highlightEl}
204
204
  class={cx("cv-highlight cv-layer", TEXT[sz], LINE_HEIGHT[sz])}
@@ -28,10 +28,9 @@
28
28
  * @note `clearable=false` hides the clear button; when `disabled`, pointer/keyboard handlers are skipped.
29
29
  */
30
30
  import type { HTMLAttributes } from "svelte/elements";
31
- import { getContext } from "svelte";
32
- import Button from "./Button.svelte";
33
- import { cx } from "../utils";
34
- import { TEXTS } from "./lang";
31
+ import Button from "./Button.svelte";
32
+ import { cx } from "../utils";
33
+ import { getComponentText, getLangContext, getLangKey } from "./lang-context";
35
34
 
36
35
  type Props = HTMLAttributes<HTMLDivElement> & {
37
36
  value?: string | null;
@@ -54,10 +53,9 @@
54
53
  ...rest
55
54
  }: Props = $props();
56
55
 
57
- const langCtx =
58
- getContext<{ value: keyof typeof TEXTS } | undefined>("lang") ?? null;
59
- const langKey = $derived(langCtx?.value ?? "en");
60
- const L = $derived(TEXTS[langKey].components.colorPicker);
56
+ const langCtx = getLangContext();
57
+ const langKey = $derived(getLangKey(langCtx));
58
+ const L = $derived(getComponentText("colorPicker", langKey));
61
59
 
62
60
  const labelFinal = $derived(label ?? L.text);
63
61
  const placeholderFinal = $derived(placeholder ?? L.placeholder);