@r2digisolutions/components 0.3.2 → 0.3.4

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.
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { createId } from '../../../utils/id.js';
3
+
2
4
  interface CheckboxProps {
3
5
  checked?: boolean;
4
6
  indeterminate?: boolean;
@@ -32,7 +34,12 @@
32
34
  onclick
33
35
  }: CheckboxProps = $props();
34
36
 
35
- const inputId = $derived(id ?? `checkbox-${Math.random().toString(36).slice(2, 9)}`);
37
+ /** Assigned after mount so SSR HTML matches the first client paint. */
38
+ let autoId = $state<string | undefined>(undefined);
39
+ $effect(() => {
40
+ if (id == null) autoId ??= createId('checkbox');
41
+ });
42
+ const inputId = $derived(id ?? autoId);
36
43
 
37
44
  const sizeClasses = {
38
45
  sm: 'h-3.5 w-3.5 rounded',
@@ -2,13 +2,13 @@ import type { StoryObj } from '@storybook/svelte';
2
2
  declare const meta: {
3
3
  title: string;
4
4
  component: import("svelte").Component<{
5
- cols?: (1 | 2 | 9 | 3 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12) | {
6
- base?: 1 | 2 | 9 | 3 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
7
- sm?: 1 | 2 | 9 | 3 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
8
- md?: 1 | 2 | 9 | 3 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
9
- lg?: 1 | 2 | 9 | 3 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
10
- xl?: 1 | 2 | 9 | 3 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
11
- '2xl'?: 1 | 2 | 9 | 3 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
5
+ cols?: (1 | 2 | 3 | 9 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12) | {
6
+ base?: 1 | 2 | 3 | 9 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
7
+ sm?: 1 | 2 | 3 | 9 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
8
+ md?: 1 | 2 | 3 | 9 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
9
+ lg?: 1 | 2 | 3 | 9 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
10
+ xl?: 1 | 2 | 3 | 9 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
11
+ '2xl'?: 1 | 2 | 3 | 9 | 4 | 8 | 5 | 6 | 7 | 10 | 11 | 12;
12
12
  };
13
13
  }, {}, "">;
14
14
  tags: string[];
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { createId } from '../../../utils/id.js';
3
4
 
4
5
  type InputStatus = 'default' | 'error' | 'success' | 'warning';
5
6
  type InputSize = 'sm' | 'md' | 'lg';
@@ -60,8 +61,12 @@
60
61
  onkeydown
61
62
  }: InputProps = $props();
62
63
 
63
- const inputId = $derived(id ?? `input-${Math.random().toString(36).slice(2, 9)}`);
64
- const helperId = $derived(`${inputId}-helper`);
64
+ let autoId = $state<string | undefined>(undefined);
65
+ $effect(() => {
66
+ if (id == null) autoId ??= createId('input');
67
+ });
68
+ const inputId = $derived(id ?? autoId);
69
+ const helperId = $derived(inputId ? `${inputId}-helper` : undefined);
65
70
  const hasClear = $derived(clearable && value.length > 0 && !disabled && !readonly);
66
71
 
67
72
  const wrapperSizeClasses: Record<InputSize, string> = {
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { createId } from '../../../utils/id.js';
3
+
2
4
  interface TextareaProps {
3
5
  id?: string;
4
6
  name?: string;
@@ -45,8 +47,12 @@
45
47
  onkeydown
46
48
  }: TextareaProps = $props();
47
49
 
48
- const textareaId = $derived(id ?? `textarea-${Math.random().toString(36).slice(2, 9)}`);
49
- const helperId = $derived(`${textareaId}-helper`);
50
+ let autoId = $state<string | undefined>(undefined);
51
+ $effect(() => {
52
+ if (id == null) autoId ??= createId('textarea');
53
+ });
54
+ const textareaId = $derived(id ?? autoId);
55
+ const helperId = $derived(textareaId ? `${textareaId}-helper` : undefined);
50
56
  const characterCount = $derived(value.length);
51
57
 
52
58
  const statusRingClasses: Record<string, string> = {
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { createId } from '../../../utils/id.js';
3
+
2
4
  interface ToggleProps {
3
5
  checked?: boolean;
4
6
  disabled?: boolean;
@@ -21,7 +23,11 @@
21
23
  onchange
22
24
  }: ToggleProps = $props();
23
25
 
24
- const inputId = $derived(id ?? `toggle-${Math.random().toString(36).slice(2, 9)}`);
26
+ let autoId = $state<string | undefined>(undefined);
27
+ $effect(() => {
28
+ if (id == null) autoId ??= createId('toggle');
29
+ });
30
+ const inputId = $derived(id ?? autoId);
25
31
 
26
32
  const trackClasses = {
27
33
  sm: 'h-4 w-7',
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { createId } from '../../../utils/id.js';
3
4
 
4
5
  type TooltipSide = 'top' | 'bottom' | 'left' | 'right';
5
6
 
@@ -27,7 +28,10 @@
27
28
  let tipStyle = $state('');
28
29
  let timer: ReturnType<typeof setTimeout> | undefined;
29
30
 
30
- const tipId = `tooltip-${Math.random().toString(36).slice(2, 9)}`;
31
+ let tipId = $state<string | undefined>(undefined);
32
+ $effect(() => {
33
+ tipId ??= createId('tooltip');
34
+ });
31
35
 
32
36
  function position() {
33
37
  if (!triggerEl || !tipEl) return;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { createId } from '../../../utils/id.js';
3
+
2
4
  export type ColorFormat = 'hex' | 'rgb' | 'hsl';
3
5
  type ColorPickerSize = 'sm' | 'md' | 'lg';
4
6
 
@@ -83,7 +85,10 @@
83
85
  let dragging: 'sv' | 'hue' | 'alpha' | null = null;
84
86
  let syncing = false;
85
87
 
86
- const panelId = `color-picker-${Math.random().toString(36).slice(2, 9)}`;
88
+ let panelId = $state<string | undefined>(undefined);
89
+ $effect(() => {
90
+ panelId ??= createId('color-picker');
91
+ });
87
92
 
88
93
  const rgba = $derived(hsvToRgb(hue, sat, val));
89
94
 
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Component, Snippet } from 'svelte';
3
3
  import { on } from 'svelte/events';
4
+ import { createId } from '../../../utils/id.js';
4
5
 
5
6
  export interface DropdownItem {
6
7
  id: string;
@@ -52,7 +53,7 @@
52
53
  }
53
54
 
54
55
  let {
55
- id = `menu-${Math.random().toString(36).slice(2, 9)}`,
56
+ id: idProp,
56
57
  items = [],
57
58
  label = 'Options',
58
59
  align = 'start',
@@ -73,6 +74,13 @@
73
74
  onopenchange
74
75
  }: DropdownMenuProps = $props();
75
76
 
77
+ let autoId = $state<string | undefined>(undefined);
78
+ $effect(() => {
79
+ if (idProp == null) autoId ??= createId('menu');
80
+ });
81
+ const id = $derived(idProp ?? autoId);
82
+ const menuId = $derived(id ? `${id}-list` : undefined);
83
+
76
84
  let triggerEl = $state<HTMLButtonElement | null>(null);
77
85
  let menuEl = $state<HTMLDivElement | null>(null);
78
86
  let listEl = $state<HTMLDivElement | null>(null);
@@ -82,8 +90,6 @@
82
90
  let path = $state<string[]>([]);
83
91
  let lastSelectedId = $state<string | null>(null);
84
92
 
85
- const menuId = $derived(`${id}-list`);
86
-
87
93
  function findPathToItem(list: DropdownItem[], targetId: string, parents: string[] = []): string[] | null {
88
94
  for (const item of list) {
89
95
  if (item.id === targetId) return parents;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { on } from 'svelte/events';
3
+ import { createId } from '../../../utils/id.js';
3
4
 
4
5
  export interface SelectOption {
5
6
  value: string;
@@ -60,9 +61,13 @@
60
61
  /** After keyboard nav, ignore hover until the mouse actually moves */
61
62
  let ignoreHover = $state(false);
62
63
 
63
- const selectId = $derived(id ?? `select-${Math.random().toString(36).slice(2, 9)}`);
64
- const listboxId = $derived(`${selectId}-listbox`);
65
- const helperId = $derived(`${selectId}-helper`);
64
+ let autoId = $state<string | undefined>(undefined);
65
+ $effect(() => {
66
+ if (id == null) autoId ??= createId('select');
67
+ });
68
+ const selectId = $derived(id ?? autoId);
69
+ const listboxId = $derived(selectId ? `${selectId}-listbox` : undefined);
70
+ const helperId = $derived(selectId ? `${selectId}-helper` : undefined);
66
71
 
67
72
  function findPathToValue(
68
73
  list: SelectOption[],
@@ -148,7 +153,7 @@
148
153
  );
149
154
 
150
155
  const activeOptionId = $derived(
151
- highlightedIndex >= 0 ? `${listboxId}-option-${highlightedIndex}` : undefined
156
+ listboxId && highlightedIndex >= 0 ? `${listboxId}-option-${highlightedIndex}` : undefined
152
157
  );
153
158
 
154
159
  const sizeClasses = {
@@ -586,7 +591,7 @@
586
591
  <!-- svelte-ignore a11y_click_events_have_key_events -->
587
592
  <!-- svelte-ignore a11y_no_static_element_interactions -->
588
593
  <div
589
- id={`${listboxId}-option-${index}`}
594
+ id={listboxId ? `${listboxId}-option-${index}` : undefined}
590
595
  role="option"
591
596
  aria-selected={isSelected}
592
597
  aria-disabled={option.disabled || undefined}
@@ -104,6 +104,7 @@
104
104
  import { filterRows, cellText, uniqueColumnValues } from './filterRows.js';
105
105
 
106
106
  function portalToBody(node: HTMLElement) {
107
+ if (typeof document === 'undefined') return {};
107
108
  document.body.appendChild(node);
108
109
  return {
109
110
  destroy() {
@@ -256,9 +257,8 @@
256
257
  let contextTarget = $state<CellRef | null>(null);
257
258
  let contextOpen = $state(false);
258
259
  let contextAnchor = $state<ContextMenuAnchor | null>(null);
259
- let viewportWidth = $state(
260
- typeof window !== 'undefined' ? window.innerWidth : 1280
261
- );
260
+ /** Same on SSR + first client paint to avoid hydration mismatch; real width set in $effect. */
261
+ let viewportWidth = $state(1280);
262
262
  let filterColumnDraft = $state('');
263
263
  let filterValueDraft = $state('');
264
264
 
package/dist/index.d.ts CHANGED
@@ -812,6 +812,7 @@ export { default as VideoEditorTemplate } from './components/templates/VideoEdit
812
812
  export { default as AudioEditorTemplate } from './components/templates/AudioEditorTemplate/AudioEditorTemplate.svelte';
813
813
  export { themeStore } from './utils/theme.svelte.js';
814
814
  export type { Theme } from './utils/theme.svelte.js';
815
+ export { createId } from './utils/id.js';
815
816
  export { pageVisibility } from './utils/pageVisibility.svelte.js';
816
817
  export type { VisibilityState } from './utils/pageVisibility.svelte.js';
817
818
  export { network } from './utils/network.svelte.js';
package/dist/index.js CHANGED
@@ -553,6 +553,7 @@ export { default as VideoEditorTemplate } from './components/templates/VideoEdit
553
553
  export { default as AudioEditorTemplate } from './components/templates/AudioEditorTemplate/AudioEditorTemplate.svelte';
554
554
  // ── Utils ────────────────────────────────────────────────────────────────────
555
555
  export { themeStore } from './utils/theme.svelte.js';
556
+ export { createId } from './utils/id.js';
556
557
  export { pageVisibility } from './utils/pageVisibility.svelte.js';
557
558
  export { network } from './utils/network.svelte.js';
558
559
  export { DEFAULT_COLS, DEFAULT_ROW_HEIGHT, DEFAULT_GAP, GRID_DENSITY, clampItem, rectsOverlap, findCollisions, resolveCollisions, compactLayout, updateItem, addItem, removeItem, serializeLayout, parseLayout, layoutBounds, rescaleLayout, resizeItemByEdge } from './utils/layoutGrid.js';
@@ -0,0 +1,2 @@
1
+ /** Create a unique DOM id. Prefer calling from `$effect` so SSR/hydration stay in sync. */
2
+ export declare function createId(prefix: string): string;
@@ -0,0 +1,7 @@
1
+ /** Monotonic counter for client-assigned DOM ids (not used during SSR). */
2
+ let count = 0;
3
+ /** Create a unique DOM id. Prefer calling from `$effect` so SSR/hydration stay in sync. */
4
+ export function createId(prefix) {
5
+ count += 1;
6
+ return `${prefix}-${count}`;
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",