svelte-comp 1.0.9 → 1.1.2

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.
@@ -48,6 +48,7 @@
48
48
  import "prismjs/components/prism-json";
49
49
  import "prismjs/components/prism-python";
50
50
  import "prismjs/themes/prism.css";
51
+ import { cx } from "../utils";
51
52
 
52
53
  type Props = {
53
54
  code?: string;
@@ -140,20 +141,27 @@
140
141
  </script>
141
142
 
142
143
  <div
143
- class="w-full border border-[var(--border-color-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-default)]"
144
+ class={cx(
145
+ "w-full border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]",
146
+ "text-[var(--color-text-default)]"
147
+ )}
144
148
  >
145
149
  {#if title}
146
150
  <div
147
- class="px-3 py-1 bg-[var(--color-bg-muted)] font-semibold uppercase flex items-center justify-between {TEXT[
148
- sz
149
- ]}"
151
+ class={cx(
152
+ "px-3 py-1 bg-[var(--color-bg-muted)] font-semibold uppercase flex items-center justify-between",
153
+ TEXT[sz]
154
+ )}
150
155
  >
151
156
  <div>{title}</div>
152
157
 
153
158
  {#if showCopyButton}
154
159
  <button
155
160
  onclick={copyToClipboard}
156
- class="px-3 py-0.5 text-xs rounded bg-[var(--color-primary)] text-white hover:opacity-[var(--opacity-hover)] transition"
161
+ class={cx(
162
+ "px-3 py-0.5 text-xs rounded bg-[var(--color-primary)] text-white hover:opacity-[var(--opacity-hover)]",
163
+ "transition focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
164
+ )}
157
165
  class:!bg-green-600={copied}
158
166
  >
159
167
  {copied ? "Copied" : "Copy"}
@@ -166,9 +174,11 @@
166
174
  {#if showLineNumbers}
167
175
  <div
168
176
  bind:this={gutterEl}
169
- class="select-none px-3 py-[12px] border-r border-[var(--border-color-default)]
170
- text-[var(--color-text-muted)] text-right overflow-hidden
171
- bg-[var(--color-bg-surface)] tabular-nums min-h-[180px] max-h-[480px]"
177
+ class={cx(
178
+ "select-none px-3 py-[12px] border-r border-[var(--border-color-default)]",
179
+ "text-[var(--color-text-muted)] text-right overflow-hidden",
180
+ "bg-[var(--color-bg-surface)] tabular-nums min-h-[180px] max-h-[480px]"
181
+ )}
172
182
  >
173
183
  {#each lines as _, i (i)}
174
184
  <div class={LINE_HEIGHT[sz]}>{i + 1}</div>
@@ -252,6 +262,14 @@
252
262
  box-sizing: border-box;
253
263
  }
254
264
 
265
+ .cv-input:focus {
266
+ outline: none;
267
+ }
268
+
269
+ .cv-input:focus-visible {
270
+ outline: none !important;
271
+ }
272
+
255
273
  /* Prism */
256
274
  .token.comment {
257
275
  color: oklch(0.937 0.019 256 / 0.45);
@@ -226,7 +226,7 @@
226
226
  {L.fileCount.replace("{n}", String(internalValue.length))}
227
227
 
228
228
  {#if multiple && internalValue.length > 1}
229
- • {L.totalSize}: {(
229
+ {L.totalSize}: {(
230
230
  Array.from(internalValue).reduce(
231
231
  (acc, file) => acc + file.size,
232
232
  0
@@ -38,13 +38,13 @@
38
38
  * @prop compact {boolean} - Enables denser sizing across controls
39
39
  * @default false
40
40
  *
41
- * @note Initial value for each field: `value[name]` в†’ `schema.default` в†’ `''` (or `false` for checkboxes).
41
+ * @note Initial value for each field: `value[name]` `schema.default` `''` (or `false` for checkboxes).
42
42
  * @note `validateOn='input'|'blur'|'submit'` controls when validators run; built-in checks: `required`, `number`, and `email` regex.
43
43
  * @note `when(form)` hides a field dynamically; hidden fields are skipped during validation.
44
44
  * @note `Select` options are coerced to strings for the underlying control; provide string values if you rely on strict equality.
45
45
  * @note Errors are rendered with stable `id`s and wired via `aria-describedby`; `invalid` flags are passed to inputs.
46
46
  * @note `expose` provides `{ reset, submit, validate, getData }`; `validate` returns `Promise<boolean>`.
47
- * @note `compact` reduces control sizes (`xs→xs`, `sm→xs`, `md→sm`, `lg→md`, `xl→lg`) and centers labels where applicable.
47
+ * @note `compact` reduces control sizes (`xsxs`, `smxs`, `mdsm`, `lgmd`, `xllg`) and centers labels where applicable.
48
48
  */
49
49
  import Field from "./Field.svelte";
50
50
  import Select from "./Select.svelte";
@@ -1,4 +1,4 @@
1
- <!-- src/lib/Hamburger.svelte -->
1
+ <!-- src/lib/Hamburger.svelte -->
2
2
  <script lang="ts">
3
3
  /**
4
4
  * @component Hamburger
@@ -32,7 +32,7 @@
32
32
  * @note Clicking outside the panel or pressing `Escape` closes the drawer.
33
33
  * @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
34
34
  * @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
35
- * @note When `menuItems` is empty, a “No items” placeholder is shown.
35
+ * @note When `menuItems` is empty, a "No items" placeholder is shown.
36
36
  * @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
37
37
  */
38
38
  import type { Snippet } from "svelte";
@@ -30,7 +30,7 @@
30
30
  * @note Clicking outside the panel or pressing `Escape` closes the drawer.
31
31
  * @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
32
32
  * @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
33
- * @note When `menuItems` is empty, a “No items” placeholder is shown.
33
+ * @note When `menuItems` is empty, a "No items" placeholder is shown.
34
34
  * @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
35
35
  */
36
36
  import type { Snippet } from "svelte";
@@ -1,4 +1,4 @@
1
- <!-- src/lib/Menu.svelte -->
1
+ <!-- src/lib/Menu.svelte -->
2
2
  <script lang="ts">
3
3
  /**
4
4
  * @component Menu
@@ -48,6 +48,7 @@
48
48
 
49
49
  // Refs for focus control
50
50
  let triggerRefs = $state<Record<string, HTMLButtonElement>>({});
51
+ let menuRefs = $state<Record<string, HTMLDivElement>>({});
51
52
  let itemRefs = $state<Record<string, HTMLButtonElement>>({});
52
53
  let subItemRefs = $state<Record<string, HTMLButtonElement>>({});
53
54
 
@@ -55,6 +56,7 @@
55
56
  let menuTop = $state(0);
56
57
  let menuLeft = $state(0);
57
58
 
59
+ let subMenuRefs = $state<Record<string, HTMLDivElement>>({});
58
60
  let subMenuTop = $state(0);
59
61
  let subMenuLeft = $state(0);
60
62
 
@@ -69,8 +71,10 @@
69
71
  const navBase =
70
72
  "flex items-stretch pl-2 gap-1 border-b relative z-10 bg-[var(--color-bg-surface)] text-[var(--color-text-default)] border-[var(--border-color-default)]";
71
73
 
74
+ const subMenuGutter = 8;
75
+
72
76
  const topButtonBase =
73
- "px-4 rounded-xs leading-none transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--border-color-focus)] focus-visible:outline-offset-2";
77
+ "px-4 rounded-xs leading-none transition-colors outline-none focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]";
74
78
 
75
79
  const topButtonActive =
76
80
  "bg-[var(--color-bg-muted)] text-[var(--color-text-default)]";
@@ -78,10 +82,10 @@
78
82
  "hover:bg-[var(--color-bg-muted)] text-[var(--color-text-default)]";
79
83
 
80
84
  const menuStyle = $derived(
81
- `position:fixed; top:${menuTop}px; left:${menuLeft}px; width:max-content;`
85
+ `position:fixed; top:${menuTop}px; left:${menuLeft}px; width:max-content; max-width:calc(100vw - 16px);`
82
86
  );
83
87
  const subMenuStyle = $derived(
84
- `position:fixed; top:${subMenuTop}px; left:${subMenuLeft}px; width:max-content;`
88
+ `position:fixed; top:${subMenuTop}px; left:${subMenuLeft}px; width:max-content; max-width:calc(100vw - 16px);`
85
89
  );
86
90
 
87
91
  const textCls = $derived(TEXT[sz]);
@@ -136,16 +140,51 @@
136
140
  }
137
141
 
138
142
  // Positioning dropdown
139
- function updateMenuPosition(triggerEl: HTMLElement) {
143
+ function updateMenuPosition(
144
+ triggerEl: HTMLElement,
145
+ menuEl?: HTMLElement | null
146
+ ) {
140
147
  const rect = triggerEl.getBoundingClientRect();
148
+ const menuWidth = Math.min(
149
+ menuEl?.getBoundingClientRect().width ?? rect.width,
150
+ window.innerWidth - 16
151
+ );
152
+ const spaceRight = window.innerWidth - rect.left;
153
+ const spaceLeft = rect.right;
154
+ const alignRight = spaceRight < menuWidth && spaceLeft > spaceRight;
155
+ const viewportLeft = window.scrollX;
156
+ const viewportRight = window.scrollX + window.innerWidth;
157
+
141
158
  menuTop = rect.bottom + window.scrollY;
142
- menuLeft = rect.left + window.scrollX;
159
+ const targetLeft = alignRight
160
+ ? rect.right + window.scrollX - menuWidth
161
+ : rect.left + window.scrollX;
162
+ const maxLeft = viewportRight - menuWidth;
163
+ menuLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
143
164
  }
144
165
 
145
- function updateSubMenuPosition(parentItemEl: HTMLElement) {
166
+ function updateSubMenuPosition(
167
+ parentItemEl: HTMLElement,
168
+ subMenuEl?: HTMLElement | null
169
+ ) {
146
170
  const rect = parentItemEl.getBoundingClientRect();
171
+ const subRect = subMenuEl?.getBoundingClientRect();
172
+ const subWidth = Math.min(
173
+ subRect?.width ?? rect.width,
174
+ window.innerWidth - 16
175
+ );
176
+ const spaceRight = window.innerWidth - rect.right;
177
+ const spaceLeft = rect.left;
178
+ const shouldFlipLeft = spaceRight < subWidth && spaceLeft > spaceRight;
179
+
147
180
  subMenuTop = rect.top + window.scrollY;
148
- subMenuLeft = rect.right + window.scrollX;
181
+ const viewportLeft = window.scrollX;
182
+ const viewportRight = window.scrollX + window.innerWidth;
183
+ const targetLeft = shouldFlipLeft
184
+ ? rect.left + window.scrollX - subWidth - subMenuGutter
185
+ : rect.right + window.scrollX + subMenuGutter;
186
+ const maxLeft = viewportRight - subWidth - subMenuGutter;
187
+ subMenuLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
149
188
  }
150
189
 
151
190
  function firstActionIndex(actions: MenuAction[]) {
@@ -209,7 +248,7 @@
209
248
  activeIndex = firstIndex;
210
249
  const triggerEl = triggerRefs[menuItem.name];
211
250
  if (triggerEl) {
212
- updateMenuPosition(triggerEl);
251
+ updateMenuPosition(triggerEl, menuRefs[menuItem.name]);
213
252
  }
214
253
  if (focusFirst && firstIndex !== -1) {
215
254
  focusMenuAction(menuItem, firstIndex);
@@ -221,7 +260,7 @@
221
260
  openSub = actionId(parentAction);
222
261
  const parentEl = itemRefs[actionId(parentAction)];
223
262
  if (parentEl) {
224
- updateSubMenuPosition(parentEl);
263
+ updateSubMenuPosition(parentEl, subMenuRefs[actionId(parentAction)]);
225
264
  }
226
265
  const firstIndex = focusFirst ? firstActionIndex(parentAction.submenu) : -1;
227
266
  activeSubIndex = firstIndex;
@@ -352,10 +391,10 @@
352
391
  if (open) {
353
392
  const triggerEl = triggerRefs[open];
354
393
  if (triggerEl) {
355
- updateMenuPosition(triggerEl);
394
+ updateMenuPosition(triggerEl, menuRefs[open]);
356
395
 
357
396
  const handleScrollResize = () => {
358
- updateMenuPosition(triggerEl);
397
+ updateMenuPosition(triggerEl, menuRefs[open]);
359
398
  };
360
399
 
361
400
  window.addEventListener("scroll", handleScrollResize, true);
@@ -372,11 +411,12 @@
372
411
  $effect(() => {
373
412
  if (openSub) {
374
413
  const itemEl = itemRefs[openSub];
414
+ const subEl = subMenuRefs[openSub];
375
415
  if (itemEl) {
376
- updateSubMenuPosition(itemEl);
416
+ updateSubMenuPosition(itemEl, subEl);
377
417
 
378
418
  const handleScrollResize = () => {
379
- updateSubMenuPosition(itemEl);
419
+ updateSubMenuPosition(itemEl, subMenuRefs[openSub]);
380
420
  };
381
421
 
382
422
  window.addEventListener("scroll", handleScrollResize, true);
@@ -438,6 +478,7 @@
438
478
 
439
479
  <!-- Main Menu -->
440
480
  <div
481
+ bind:this={menuRefs[menuItem.name]}
441
482
  class={cx(
442
483
  "fixed z-50 min-w-44 p-2 rounded-xs shadow-[0_2px_4px_var(--shadow-color)] ",
443
484
  "border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]"
@@ -462,6 +503,7 @@
462
503
  class={cx(
463
504
  "relative text-left rounded-xs transition-colors outline-none px-1.5 py-0.5 my-1 mr-1 min-w-full flex items-center",
464
505
  "gap-3 hover:bg-[var(--color-bg-muted)] focus-visible:bg-[var(--color-bg-muted)]",
506
+ "focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]",
465
507
  textCls
466
508
  )}
467
509
  onmousedown={(e) => e.preventDefault()}
@@ -518,9 +560,10 @@
518
560
  <!-- Sub Menu -->
519
561
  {#if hasSubmenu(action) && openSub === actionId(action)}
520
562
  <div
563
+ bind:this={subMenuRefs[actionId(action)]}
521
564
  class={cx(
522
565
  "fixed z-50 min-w-44 p-2 rounded-xs shadow-[0_2px_4px_var(--shadow-color)]",
523
- "border border-[var(--border-color-default)] bg-[var(--color-bg-surface)] ml-2"
566
+ "border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]"
524
567
  )}
525
568
  style={subMenuStyle}
526
569
  role="menu"
@@ -543,8 +586,7 @@
543
586
  "relative text-left rounded-xs transition-colors outline-none px-1.5 py-0.5",
544
587
  "my-1 mr-1 w-full flex items-center justify-between gap-3",
545
588
  "hover:bg-[var(--color-bg-muted)] focus-visible:bg-[var(--color-bg-muted)]",
546
- "focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)]",
547
- "focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg-surface)]",
589
+ "focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]",
548
590
  "decoration-[var(--color-text-default)]",
549
591
  textCls
550
592
  )}
@@ -1,4 +1,4 @@
1
- <!-- src/lib/PaginatedCard.svelte -->
1
+ <!-- src/lib/PaginatedCard.svelte -->
2
2
  <script lang="ts">
3
3
  /**
4
4
  * @component PaginatedCard
@@ -13,7 +13,7 @@
13
13
  * @prop class {string} - Custom classes applied to the pagination wrapper
14
14
  * @default ""
15
15
  *
16
- * @note Displays “Page X of Y” and numbered page buttons.
16
+ * @note Displays Page X of Y and numbered page buttons.
17
17
  * @note Prev/next buttons are disabled at the edges.
18
18
  * @note Shows up to 3 numbered buttons centered around the current page.
19
19
  * @note Uses `aria-current=\"page\"` on the active page for accessibility.
@@ -0,0 +1,99 @@
1
+ <!-- src/lib/PrimaryColorSelect.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component PrimaryColorSelect
5
+ * @description Theme primary-color selector built on top of Select. Provides a fixed palette,
6
+ * handles persistence, and updates the global <html> attribute.
7
+ *
8
+ * @prop sz {SizeKey} - Sizing preset passed directly to Select
9
+ * @options xs|sm|md|lg|xl
10
+ * @default sm
11
+ *
12
+ * @prop label {string} - Custom label text. Falls back to localized copy when omitted.
13
+ *
14
+ * @prop class {string} - Extra classes forwarded to the underlying Select component
15
+ * @default ""
16
+ *
17
+ * @note The palette is predefined internally (`{ value, label, swatch }`).
18
+ * @note Selected value is stored in localStorage under "primary".
19
+ * @note The `html` element receives `data-primary="{value}"` for theme styling.
20
+ * @note Uses the same onChange contract as Select and forwards palette options as-is.
21
+ */
22
+ import { getContext } from "svelte";
23
+ import Select from "./Select.svelte";
24
+ import type { PrimaryKey, PaletteOption, SizeKey } from "./types";
25
+ import { TEXTS } from "./lang";
26
+
27
+ type Props = {
28
+ sz?: SizeKey;
29
+ label?: string;
30
+ class?: string;
31
+ };
32
+
33
+ let { sz = "sm", label, class: externalClass = "" }: Props = $props();
34
+
35
+ const langCtx =
36
+ getContext<{ value: keyof typeof TEXTS } | undefined>("lang") ?? null;
37
+ const langKey = $derived(langCtx?.value ?? "en");
38
+ const L = $derived(TEXTS[langKey].components.primaryColorSelect);
39
+
40
+ const labelFinal = $derived(label ?? L.text);
41
+
42
+ const palette: PaletteOption[] = [
43
+ {
44
+ value: "default",
45
+ label: "Indigo",
46
+ swatch: "oklch(62.3% 0.214 259.8deg)",
47
+ },
48
+ { value: "cyan", label: "Cyan", swatch: "oklch(71.5% 0.143 215.221)" },
49
+ { value: "red", label: "Red", swatch: "oklch(58% 0.24 30deg)" },
50
+ { value: "green", label: "Green", swatch: "oklch(65% 0.22 140deg)" },
51
+ { value: "yellow", label: "Yellow", swatch: "oklch(75% 0.18 90deg)" },
52
+ { value: "pink", label: "Pink", swatch: "oklch(70% 0.25 350deg)" },
53
+ { value: "orange", label: "Orange", swatch: "oklch(72% 0.22 60deg)" },
54
+ { value: "purple", label: "Purple", swatch: "oklch(55% 0.22 290deg)" },
55
+ ];
56
+
57
+ const paletteMap = palette.reduce<Record<PrimaryKey, PaletteOption>>(
58
+ (acc, option) => {
59
+ acc[option.value] = option;
60
+ return acc;
61
+ },
62
+ {} as Record<PrimaryKey, PaletteOption>
63
+ );
64
+
65
+ let selected = $state<PrimaryKey>("default");
66
+ let mounted = $state(false);
67
+
68
+ function isPrimaryKey(value: unknown): value is PrimaryKey {
69
+ return typeof value === "string" && value in paletteMap;
70
+ }
71
+
72
+ $effect(() => {
73
+ if (!mounted) {
74
+ const saved = localStorage.getItem("primary");
75
+ if (isPrimaryKey(saved)) {
76
+ selected = saved;
77
+ }
78
+ mounted = true;
79
+ }
80
+ });
81
+
82
+ $effect(() => {
83
+ if (mounted) {
84
+ document.documentElement.setAttribute("data-primary", selected);
85
+ localStorage.setItem("primary", selected);
86
+ }
87
+ });
88
+ </script>
89
+
90
+ <Select
91
+ {sz}
92
+ label={labelFinal}
93
+ options={palette}
94
+ value={selected}
95
+ onChange={(v) => {
96
+ if (isPrimaryKey(v)) selected = v;
97
+ }}
98
+ class={externalClass}
99
+ />
@@ -0,0 +1,9 @@
1
+ import type { SizeKey } from "./types";
2
+ type Props = {
3
+ sz?: SizeKey;
4
+ label?: string;
5
+ class?: string;
6
+ };
7
+ declare const PrimaryColorSelect: import("svelte").Component<Props, {}, "">;
8
+ type PrimaryColorSelect = ReturnType<typeof PrimaryColorSelect>;
9
+ export default PrimaryColorSelect;
@@ -188,4 +188,4 @@
188
188
  }
189
189
  }
190
190
  }
191
- </style>
191
+ </style>
@@ -1,4 +1,4 @@
1
- <!-- src/lib/Select.svelte -->
1
+ <!-- src/lib/Select.svelte -->
2
2
  <script lang="ts">
3
3
  /**
4
4
  * @component Select
@@ -167,6 +167,8 @@
167
167
  `position:fixed;top:${menuTop}px;left:${menuLeft}px;min-width:${menuWidth}px;max-height:${menuMaxHeight}px;`
168
168
  );
169
169
 
170
+ const selectedOption = $derived(options.find((o) => o.value === value));
171
+
170
172
  $effect(() => {
171
173
  const currentTriggerEl = triggerEl;
172
174
  const currentListEl = listEl;
@@ -409,14 +411,25 @@
409
411
  onkeydown={onTriggerKeydown}
410
412
  >
411
413
  <span class="min-w-0 grow truncate">
412
- {#if value}
413
- {options.find((o) => o.value === value)?.label}
414
+ {#if selectedOption}
415
+ <span class="inline-flex items-center gap-2 min-w-0">
416
+ {#if selectedOption.swatch}
417
+ <span
418
+ aria-hidden="true"
419
+ class="block w-3 h-3 rounded-[var(--radius-xs)] border border-[var(--border-color-default)] shadow-sm shrink-0"
420
+ style={`background:${selectedOption.swatch}`}
421
+ ></span>
422
+ {/if}
423
+ <span class="truncate">{selectedOption.label}</span>
424
+ </span>
414
425
  {:else}
415
426
  <span class="[color:var(--color-text-muted)]">{placeholder}</span>
416
427
  {/if}
417
428
  </span>
418
429
  <span
419
- class={cx("pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 [color:var(--color-text-default)]")}
430
+ class={cx(
431
+ "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 [color:var(--color-text-default)]"
432
+ )}
420
433
  >
421
434
  <svg
422
435
  class={iconsSizes[sz]}
@@ -470,13 +483,22 @@
470
483
  <button
471
484
  type="button"
472
485
  tabindex="0"
473
- class={cx("w-full text-left focus:outline-[3px] focus:outline-offset-3 focus:outline-[var(--border-color-focus)] rounded")}
486
+ class={cx(
487
+ "w-full text-left focus:outline-[3px] focus:outline-offset-3 focus:outline-[var(--border-color-focus)] rounded flex items-center gap-2"
488
+ )}
474
489
  disabled={opt.disabled}
475
490
  onclick={() => choose(i)}
476
491
  onfocus={() => (highlighted = i)}
477
492
  onmouseenter={() => (highlighted = i)}
478
493
  >
479
- {opt.label}
494
+ {#if opt.swatch}
495
+ <span
496
+ aria-hidden="true"
497
+ class="block w-3 h-3 rounded-[var(--radius-xs)] border border-[var(--border-color-default)] shadow-sm shrink-0"
498
+ style={`background:${opt.swatch}`}
499
+ ></span>
500
+ {/if}
501
+ <span class="truncate">{opt.label}</span>
480
502
  </button>
481
503
  </li>
482
504
  {/each}
@@ -1,4 +1,4 @@
1
- <!-- src/lib/Splitter.svelte -->
1
+ <!-- src/lib/Splitter.svelte -->
2
2
  <script lang="ts">
3
3
  /**
4
4
  * @component Splitter
@@ -203,7 +203,7 @@
203
203
  String(col.key ?? idx);
204
204
  </script>
205
205
 
206
- <div class={wrapperClass}>
206
+ <div class={wrapperClass} tabindex="-1">
207
207
  <table class={tableClass}>
208
208
  <thead class={variantStyles.header}>
209
209
  {#if currentVariant !== "noTitle" && currentVariant !== "list"}
@@ -67,6 +67,14 @@
67
67
  activeTab = tabs[0].id;
68
68
  }
69
69
  });
70
+
71
+ function focusActiveButton() {
72
+ if (!activeTab) return;
73
+ const btn = document.getElementById(
74
+ `tab-${activeTab}`
75
+ ) as HTMLButtonElement | null;
76
+ btn?.focus();
77
+ }
70
78
 
71
79
  function handleTabClick(tab: TabItem) {
72
80
  if (tab.disabled) return;
@@ -95,12 +103,7 @@
95
103
  const nextId = enabled[next].id;
96
104
  activeTab = nextId;
97
105
  onChange?.(nextId);
98
- queueMicrotask(() => {
99
- const btn = document.getElementById(
100
- `tab-${nextId}`
101
- ) as HTMLButtonElement | null;
102
- btn?.focus();
103
- });
106
+ queueMicrotask(focusActiveButton);
104
107
  }
105
108
 
106
109
  const sizes: Record<SizeKey, string> = {
@@ -112,7 +115,7 @@
112
115
  };
113
116
 
114
117
  const base =
115
- "inline-flex items-center justify-center font-medium transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--border-color-focus)] focus-visible:outline-offset-2 disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed";
118
+ "inline-flex items-center justify-center font-medium transition-colors duration-150 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus-visible:ring-offset-0 focus:outline-none disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed";
116
119
 
117
120
  const variants = $derived({
118
121
  default: {
@@ -160,6 +163,7 @@
160
163
  aria-orientation="horizontal"
161
164
  class={tablistClass}
162
165
  onkeydown={handleKeyDown}
166
+ onfocus={focusActiveButton}
163
167
  >
164
168
  {#each tabs as tab (tab.id)}
165
169
  <button
@@ -200,7 +204,7 @@
200
204
  <div
201
205
  id={`panel-${activeTab}`}
202
206
  role="tabpanel"
203
- tabindex="0"
207
+ tabindex="-1"
204
208
  aria-labelledby={`tab-${activeTab}`}
205
209
  class="w-full min-w-0 relative z-0 border-t border-[var(--border-color-default)] bg-[var(--color-bg-surface)] p-[var(--spacing-md)] rounded-b-[var(--radius-sm)] shadow-[0_1px_2px_0_var(--shadow-color)]"
206
210
  >
@@ -14,6 +14,7 @@ export { default as Hamburger } from "./Hamburger.svelte";
14
14
  export { default as Menu } from "./Menu.svelte";
15
15
  export { default as PaginatedCard } from "./PaginatedCard.svelte";
16
16
  export { default as Pagination } from "./Pagination.svelte";
17
+ export { default as PrimaryColorSelect } from "./PrimaryColorSelect.svelte";
17
18
  export { default as ProgressBar } from "./ProgressBar.svelte";
18
19
  export { default as ProgressCircle } from "./ProgressCircle.svelte";
19
20
  export { default as Radio } from "./Radio.svelte";
package/dist/lib/index.js CHANGED
@@ -15,6 +15,7 @@ export { default as Hamburger } from "./Hamburger.svelte";
15
15
  export { default as Menu } from "./Menu.svelte";
16
16
  export { default as PaginatedCard } from "./PaginatedCard.svelte";
17
17
  export { default as Pagination } from "./Pagination.svelte";
18
+ export { default as PrimaryColorSelect } from "./PrimaryColorSelect.svelte";
18
19
  export { default as ProgressBar } from "./ProgressBar.svelte";
19
20
  export { default as ProgressCircle } from "./ProgressCircle.svelte";
20
21
  export { default as Radio } from "./Radio.svelte";
@@ -32,6 +32,9 @@ export declare const TEXTS: {
32
32
  readonly menu: {
33
33
  readonly subtitle: "Menu with size options";
34
34
  };
35
+ readonly primaryColorSelect: {
36
+ readonly text: "Primary color";
37
+ };
35
38
  readonly timePicker: {
36
39
  readonly text: "Choose time";
37
40
  readonly placeholder: "No time selected";
@@ -78,6 +81,9 @@ export declare const TEXTS: {
78
81
  readonly menu: {
79
82
  readonly subtitle: "Меню с опциями размеров";
80
83
  };
84
+ readonly primaryColorSelect: {
85
+ readonly text: "Основной цвет";
86
+ };
81
87
  readonly timePicker: {
82
88
  readonly text: "Выбрать время";
83
89
  readonly placeholder: "Время не выбрано";
@@ -124,6 +130,9 @@ export declare const TEXTS: {
124
130
  readonly menu: {
125
131
  readonly subtitle: "Menú con opciones de tamaño";
126
132
  };
133
+ readonly primaryColorSelect: {
134
+ readonly text: "Color primario";
135
+ };
127
136
  readonly timePicker: {
128
137
  readonly text: "Elegir hora";
129
138
  readonly placeholder: "Ninguna hora seleccionada";
package/dist/lib/lang.js CHANGED
@@ -30,6 +30,7 @@ var enTexts = {
30
30
  totalSize: "Total size",
31
31
  },
32
32
  menu: { subtitle: "Menu with size options" },
33
+ primaryColorSelect: { text: "Primary color" },
33
34
  timePicker: {
34
35
  text: "Choose time",
35
36
  placeholder: "No time selected",
@@ -74,6 +75,7 @@ var ruTexts = {
74
75
  totalSize: "Общий размер",
75
76
  },
76
77
  menu: { subtitle: "Меню с опциями размеров" },
78
+ primaryColorSelect: { text: "Основной цвет" },
77
79
  timePicker: {
78
80
  text: "Выбрать время",
79
81
  placeholder: "Время не выбрано",
@@ -118,6 +120,7 @@ var esTexts = {
118
120
  totalSize: "Tamaño total",
119
121
  },
120
122
  menu: { subtitle: "Menú con opciones de tamaño" },
123
+ primaryColorSelect: { text: "Color primario" },
121
124
  timePicker: {
122
125
  text: "Elegir hora",
123
126
  placeholder: "Ninguna hora seleccionada",