@r2digisolutions/components 0.14.0 → 0.14.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.
package/README.md CHANGED
@@ -97,24 +97,27 @@ Hay dos ejes independientes en `<html>`:
97
97
  - **slate** — gris frío (hue 265, el look actual).
98
98
  - **neutral** — croma 0, sin castaño azul. Misma familia que Tailwind `neutral` (`#171717`, `#fafafa`).
99
99
 
100
- Para evitar FOUC, fíjalo en `app.html` antes de pintar:
100
+ Para evitar FOUC, fíjalo en `app.html` con **cookie** (no `localStorage`; en SSR no existe `setItem`):
101
101
 
102
102
  ```html
103
103
  <html lang="es" data-theme="neutral">
104
104
  <head>
105
105
  <script>
106
106
  (function () {
107
- var palette = localStorage.getItem('r2-theme-palette') || 'neutral';
108
- document.documentElement.setAttribute('data-theme', palette);
109
- if (
110
- localStorage.theme === 'dark' ||
111
- localStorage.getItem('r2-theme') === 'dark' ||
112
- (!('theme' in localStorage) &&
113
- !localStorage.getItem('r2-theme') &&
114
- window.matchMedia('(prefers-color-scheme: dark)').matches)
115
- ) {
116
- document.documentElement.classList.add('dark');
107
+ function getCookie(name) {
108
+ var m = document.cookie.match(
109
+ new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)')
110
+ );
111
+ return m ? decodeURIComponent(m[1]) : null;
117
112
  }
113
+ var palette = getCookie('r2-theme-palette') || 'neutral';
114
+ document.documentElement.setAttribute('data-theme', palette);
115
+ var theme = getCookie('r2-theme');
116
+ var dark =
117
+ theme === 'dark' ||
118
+ ((!theme || theme === 'system') &&
119
+ window.matchMedia('(prefers-color-scheme: dark)').matches);
120
+ document.documentElement.classList.toggle('dark', dark);
118
121
  })();
119
122
  </script>
120
123
  </head>
@@ -136,23 +136,24 @@
136
136
  ]}
137
137
  >
138
138
  {#snippet contextualHeaderDefault()}
139
- {#if resolvedContextual}
139
+ {@const ctx = resolvedContextual}
140
+ {#if ctx}
140
141
  <div class="min-w-0 space-y-1">
141
- {#if resolvedContextual.parentHref && resolvedContextual.parentLabel}
142
+ {#if ctx?.parentHref && ctx?.parentLabel}
142
143
  <a
143
- href={resolvedContextual.parentHref}
144
+ href={ctx?.parentHref}
144
145
  class="text-muted hover:text-primary block truncate text-[11px]"
145
146
  >
146
- {resolvedContextual.parentLabel}
147
+ {ctx?.parentLabel}
147
148
  </a>
148
149
  {/if}
149
- <p class="text-sm font-semibold text-primary truncate">{resolvedContextual.brand}</p>
150
+ <p class="text-sm font-semibold text-primary truncate">{ctx?.brand}</p>
150
151
  <div class="gap-1.5 flex flex-wrap items-center">
151
- {#if resolvedContextual.status}
152
- <Badge variant="secondary" size="sm">{resolvedContextual.status}</Badge>
152
+ {#if ctx?.status}
153
+ <Badge variant="secondary" size="sm">{ctx?.status}</Badge>
153
154
  {/if}
154
- {#if resolvedContextual.description}
155
- <span class="text-muted truncate text-[11px]">{resolvedContextual.description}</span>
155
+ {#if ctx?.description}
156
+ <span class="text-muted truncate text-[11px]">{ctx?.description}</span>
156
157
  {/if}
157
158
  </div>
158
159
  </div>
@@ -216,11 +217,11 @@
216
217
  ]}
217
218
  >
218
219
  <Sidebar
219
- brand={resolvedContextual.brand ?? brand}
220
- groups={resolvedContextual.groups}
221
- value={resolvedContextual.value}
220
+ brand={resolvedContextual?.brand ?? brand}
221
+ groups={resolvedContextual?.groups ?? []}
222
+ value={resolvedContextual?.value ?? ''}
222
223
  collapsible={false}
223
- header={resolvedContextual.header ?? contextualHeaderDefault}
224
+ header={resolvedContextual?.header ?? contextualHeaderDefault}
224
225
  onchange={onContextualNav}
225
226
  />
226
227
  </div>
@@ -4,9 +4,7 @@
4
4
  * Mode: `class="dark"` on `<html>`
5
5
  * Palette: `data-theme="slate" | "neutral"` on `<html>`
6
6
  *
7
- * - Persists preference to localStorage
8
- * - Detects system preference via prefers-color-scheme
9
- * - Reactive via Svelte 5 runes ($state)
7
+ * Persists to cookies (readable by the FOUC script in `app.html` and by SSR).
10
8
  */
11
9
  export type Theme = 'light' | 'dark' | 'system';
12
10
  export type ThemePalette = 'slate' | 'neutral';
@@ -23,7 +21,7 @@ declare class ThemeStore {
23
21
  * `slate` = cool (hue 265). `neutral` = chroma 0, no blue cast.
24
22
  *
25
23
  * Pass `{ persist: false }` for a page-level override that does not
26
- * stick in localStorage.
24
+ * write the cookie.
27
25
  */
28
26
  setPalette(palette: ThemePalette, opts?: {
29
27
  persist?: boolean;
@@ -4,32 +4,67 @@
4
4
  * Mode: `class="dark"` on `<html>`
5
5
  * Palette: `data-theme="slate" | "neutral"` on `<html>`
6
6
  *
7
- * - Persists preference to localStorage
8
- * - Detects system preference via prefers-color-scheme
9
- * - Reactive via Svelte 5 runes ($state)
7
+ * Persists to cookies (readable by the FOUC script in `app.html` and by SSR).
10
8
  */
11
- const STORAGE_KEY = 'r2-theme';
12
- const PALETTE_KEY = 'r2-theme-palette';
9
+ const THEME_COOKIE = 'r2-theme';
10
+ const PALETTE_COOKIE = 'r2-theme-palette';
11
+ const LEGACY_THEME_KEY = 'r2-theme';
12
+ const LEGACY_PALETTE_KEY = 'r2-theme-palette';
13
13
  const DEFAULT_PALETTE = 'slate';
14
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
14
15
  function getSystemTheme() {
15
16
  if (typeof window === 'undefined')
16
17
  return 'light';
17
18
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
18
19
  }
19
- function getStoredTheme() {
20
- if (typeof localStorage === 'undefined')
21
- return 'system';
22
- return localStorage.getItem(STORAGE_KEY) ?? 'system';
20
+ function isTheme(value) {
21
+ return value === 'light' || value === 'dark' || value === 'system';
23
22
  }
24
23
  function isPalette(value) {
25
24
  return value === 'slate' || value === 'neutral';
26
25
  }
26
+ function readCookie(name) {
27
+ if (typeof document === 'undefined')
28
+ return null;
29
+ const escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1');
30
+ const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
31
+ return match ? decodeURIComponent(match[1]) : null;
32
+ }
33
+ function writeCookie(name, value) {
34
+ if (typeof document === 'undefined')
35
+ return;
36
+ document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${COOKIE_MAX_AGE};SameSite=Lax`;
37
+ }
38
+ function readLegacyStorage(key) {
39
+ if (typeof localStorage?.getItem !== 'function')
40
+ return null;
41
+ try {
42
+ return localStorage.getItem(key);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function getStoredTheme() {
49
+ const fromCookie = readCookie(THEME_COOKIE);
50
+ if (isTheme(fromCookie))
51
+ return fromCookie;
52
+ const legacy = readLegacyStorage(LEGACY_THEME_KEY);
53
+ if (isTheme(legacy)) {
54
+ writeCookie(THEME_COOKIE, legacy);
55
+ return legacy;
56
+ }
57
+ return 'system';
58
+ }
27
59
  function getStoredPalette() {
28
- if (typeof localStorage === 'undefined')
29
- return DEFAULT_PALETTE;
30
- const stored = localStorage.getItem(PALETTE_KEY);
31
- if (isPalette(stored))
32
- return stored;
60
+ const fromCookie = readCookie(PALETTE_COOKIE);
61
+ if (isPalette(fromCookie))
62
+ return fromCookie;
63
+ const legacy = readLegacyStorage(LEGACY_PALETTE_KEY);
64
+ if (isPalette(legacy)) {
65
+ writeCookie(PALETTE_COOKIE, legacy);
66
+ return legacy;
67
+ }
33
68
  if (typeof document !== 'undefined') {
34
69
  const attr = document.documentElement.getAttribute('data-theme');
35
70
  if (isPalette(attr))
@@ -53,17 +88,16 @@ class ThemeStore {
53
88
  #palette = $state(DEFAULT_PALETTE);
54
89
  #resolved = $derived(this.#theme === 'system' ? getSystemTheme() : this.#theme);
55
90
  constructor() {
56
- if (typeof window !== 'undefined') {
57
- this.#theme = getStoredTheme();
58
- this.#palette = getStoredPalette();
59
- applyMode(this.#theme);
60
- applyPalette(this.#palette);
61
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
62
- if (this.#theme === 'system') {
63
- applyMode('system');
64
- }
65
- });
66
- }
91
+ if (typeof window === 'undefined')
92
+ return;
93
+ this.#theme = getStoredTheme();
94
+ this.#palette = getStoredPalette();
95
+ applyMode(this.#theme);
96
+ applyPalette(this.#palette);
97
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
98
+ if (this.#theme === 'system')
99
+ applyMode('system');
100
+ });
67
101
  }
68
102
  get theme() {
69
103
  return this.#theme;
@@ -79,9 +113,7 @@ class ThemeStore {
79
113
  }
80
114
  set(theme) {
81
115
  this.#theme = theme;
82
- if (typeof localStorage !== 'undefined') {
83
- localStorage.setItem(STORAGE_KEY, theme);
84
- }
116
+ writeCookie(THEME_COOKIE, theme);
85
117
  applyMode(theme);
86
118
  }
87
119
  /**
@@ -89,13 +121,12 @@ class ThemeStore {
89
121
  * `slate` = cool (hue 265). `neutral` = chroma 0, no blue cast.
90
122
  *
91
123
  * Pass `{ persist: false }` for a page-level override that does not
92
- * stick in localStorage.
124
+ * write the cookie.
93
125
  */
94
126
  setPalette(palette, opts) {
95
127
  this.#palette = palette;
96
- if (opts?.persist !== false && typeof localStorage !== 'undefined') {
97
- localStorage.setItem(PALETTE_KEY, palette);
98
- }
128
+ if (opts?.persist !== false)
129
+ writeCookie(PALETTE_COOKIE, palette);
99
130
  applyPalette(palette);
100
131
  }
101
132
  toggle() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",