@arqel-dev/theme 0.8.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arqel Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @arqel-dev/theme
2
+
3
+ > Tokens semânticos + dark-mode + ThemeProvider/ThemeToggle para painéis Arqel.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ pnpm add @arqel-dev/theme
9
+ ```
10
+
11
+ ## Uso rápido
12
+
13
+ ```tsx
14
+ import { ThemeProvider, ThemeToggle } from '@arqel-dev/theme';
15
+ import '@arqel-dev/theme/tokens.css';
16
+
17
+ <ThemeProvider>
18
+ <App />
19
+ <ThemeToggle />
20
+ </ThemeProvider>
21
+ ```
22
+
23
+ Ver `SKILL.md` para documentação completa e `apps/docs/guide/theming.md` para o guia.
24
+
25
+ ## Licença
26
+
27
+ MIT.
package/SKILL.md ADDED
@@ -0,0 +1,133 @@
1
+ # SKILL.md — arqel-dev/theme
2
+
3
+ ## Purpose
4
+
5
+ Pacote responsável por **dark-mode**, **tokens de design semânticos** e **theming runtime** para painéis Arqel.
6
+
7
+ Fornece:
8
+
9
+ - `tokens.css` — variáveis CSS canônicas (`--arqel-color-bg`, `--arqel-color-fg`, `--arqel-color-primary`, etc.) em `:root` (light) e `.dark` (dark).
10
+ - `<ThemeProvider>` — Context React que aplica/remove a classe `dark` no `<html>` baseado em preferência do utilizador + `prefers-color-scheme`.
11
+ - `<ThemeToggle>` — botão acessível que cicla `system → light → dark`.
12
+ - `useTheme()` — hook para ler/atualizar tema em qualquer componente.
13
+ - `preventFlashScript()` — snippet inline para evitar FOUC (flash branco antes do React montar).
14
+
15
+ ## Key Contracts
16
+
17
+ ### `Theme` (type)
18
+
19
+ ```ts
20
+ type Theme = 'light' | 'dark' | 'system';
21
+ type ResolvedTheme = 'light' | 'dark';
22
+ ```
23
+
24
+ `system` segue `prefers-color-scheme` do SO. `resolvedTheme` é sempre concreto.
25
+
26
+ ### `<ThemeProvider>`
27
+
28
+ ```tsx
29
+ <ThemeProvider defaultTheme="system" storageKey="arqel-theme" darkClass="dark">
30
+ {children}
31
+ </ThemeProvider>
32
+ ```
33
+
34
+ Props:
35
+
36
+ | Prop | Default | Descrição |
37
+ | --- | --- | --- |
38
+ | `defaultTheme` | `'system'` | Tema usado quando nada está em localStorage |
39
+ | `storageKey` | `'arqel-theme'` | Chave em localStorage |
40
+ | `darkClass` | `'dark'` | Classe aplicada em `<html>` quando dark |
41
+ | `attribute` | `'class'` | `'class'` ou `'data-theme'` |
42
+
43
+ ### `useTheme()`
44
+
45
+ ```ts
46
+ const { theme, resolvedTheme, setTheme } = useTheme();
47
+ ```
48
+
49
+ Lança erro se chamado fora de `<ThemeProvider>` (fail-fast).
50
+
51
+ ### `preventFlashScript(options?)`
52
+
53
+ Retorna string IIFE para inserir antes dos bundles React:
54
+
55
+ ```blade
56
+ <script>{!! \Arqel\Theme\preventFlashScript() !!}</script>
57
+ ```
58
+
59
+ ## Conventions
60
+
61
+ 1. **Tokens semânticos, nunca cores cruas** — componentes Arqel usam `var(--arqel-color-bg)`, nunca `#ffffff`.
62
+ 2. **localStorage opt-out** — falhas de `localStorage.setItem/getItem` são silenciadas (Safari private mode, iframes).
63
+ 3. **SSR-safe** — todas funções verificam `typeof window === 'undefined'`.
64
+ 4. **Sem dependências runtime extras** — apenas `react` (peer).
65
+ 5. **Class-based dark mode** (Tailwind v4 syntax: `@variant dark (&:where(.dark, .dark *))`).
66
+
67
+ ## Examples
68
+
69
+ ### Setup mínimo num app Inertia
70
+
71
+ ```tsx
72
+ // resources/js/app.tsx
73
+ import { ThemeProvider } from '@arqel-dev/theme';
74
+ import '@arqel-dev/theme/tokens.css';
75
+
76
+ createInertiaApp({
77
+ setup({ el, App, props }) {
78
+ createRoot(el).render(
79
+ <ThemeProvider>
80
+ <App {...props} />
81
+ </ThemeProvider>,
82
+ );
83
+ },
84
+ });
85
+ ```
86
+
87
+ ### Toggle no header
88
+
89
+ ```tsx
90
+ import { ThemeToggle } from '@arqel-dev/theme';
91
+
92
+ <header className="flex items-center justify-between">
93
+ <h1>Dashboard</h1>
94
+ <ThemeToggle className="rounded-md p-2 hover:bg-muted" />
95
+ </header>
96
+ ```
97
+
98
+ ### FOUC prevention (Blade)
99
+
100
+ ```blade
101
+ <head>
102
+ {{-- ... --}}
103
+ <script>(function(){try{var k="arqel-theme",t=null;try{t=localStorage.getItem(k)}catch(e){}if(t!=="light"&&t!=="dark"&&t!=="system")t="system";var r=t==="system"?(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):t;if(r==="dark")document.documentElement.classList.add("dark");document.documentElement.style.colorScheme=r;}catch(e){}})();</script>
104
+ @vite([...])
105
+ </head>
106
+ ```
107
+
108
+ ### Override de tema (custom branding)
109
+
110
+ ```css
111
+ :root {
112
+ --arqel-color-primary: #ff6b35;
113
+ --arqel-color-primary-fg: #ffffff;
114
+ }
115
+ .dark {
116
+ --arqel-color-primary: #ffa07a;
117
+ }
118
+ ```
119
+
120
+ ## Anti-patterns
121
+
122
+ - ❌ Usar `<ThemeToggle>` fora de `<ThemeProvider>` — lança erro em runtime.
123
+ - ❌ Hard-coding `bg-white dark:bg-neutral-900` em components quando há token semântico equivalente. Prefira `bg-[var(--arqel-color-bg)]`.
124
+ - ❌ Múltiplos `<ThemeProvider>` aninhados — o mais interno vence, mas confunde devtools.
125
+ - ❌ Esquecer o snippet `preventFlashScript` no Blade — usuários verão flash branco em dark mode.
126
+ - ❌ Usar `window` ou `document` no top-level de componentes — quebra SSR.
127
+
128
+ ## Related
129
+
130
+ - ADR-001 — Inertia-only (theme não usa fetch)
131
+ - ADR-016 — Sem libs de fetch para CRUD
132
+ - `@arqel-dev/ui` — consome tokens via Tailwind v4 `@theme`
133
+ - `apps/docs/guide/theming.md` — guia completo de theming
@@ -0,0 +1,94 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface PreventFlashOptions {
5
+ storageKey?: string;
6
+ darkClass?: string;
7
+ attribute?: 'class' | 'data-theme';
8
+ }
9
+ /**
10
+ * Retorna um snippet JS (string) para inserir inline no `<head>` do HTML
11
+ * **antes** dos bundles React. Lê localStorage + system pref e aplica
12
+ * imediatamente a classe `dark` em `<html>`, evitando flash branco no
13
+ * carregamento (FOUC).
14
+ *
15
+ * Uso (Blade):
16
+ * <script>{!! \Arqel\Theme\preventFlashScript() !!}</script>
17
+ *
18
+ * Uso direto:
19
+ * <script dangerouslySetInnerHTML={{ __html: preventFlashScript() }} />
20
+ *
21
+ * O snippet é IIFE que falha silenciosamente — nenhuma corrupção de página
22
+ * mesmo se localStorage estiver bloqueado.
23
+ */
24
+ declare function preventFlashScript(options?: PreventFlashOptions): string;
25
+
26
+ /**
27
+ * Theme preference armazenada em localStorage / Context.
28
+ *
29
+ * - `light` — força modo claro
30
+ * - `dark` — força modo escuro
31
+ * - `system` — segue `prefers-color-scheme` do SO
32
+ */
33
+ type Theme = 'light' | 'dark' | 'system';
34
+ /**
35
+ * Tema efetivamente aplicado ao DOM (já resolvido a partir de `Theme`).
36
+ */
37
+ type ResolvedTheme = 'light' | 'dark';
38
+ interface ThemeContextValue {
39
+ /** Preferência do utilizador (incluindo `system`). */
40
+ theme: Theme;
41
+ /** Tema concreto aplicado no `<html>` (sempre `light` ou `dark`). */
42
+ resolvedTheme: ResolvedTheme;
43
+ /** Atualiza preferência (persiste em localStorage). */
44
+ setTheme: (theme: Theme) => void;
45
+ }
46
+
47
+ declare const DEFAULT_STORAGE_KEY = "arqel-theme";
48
+ declare function isTheme(value: unknown): value is Theme;
49
+ /**
50
+ * Lê preferência do localStorage. SSR-safe: retorna `null` no servidor.
51
+ */
52
+ declare function readStoredTheme(key?: string): Theme | null;
53
+ /**
54
+ * Persiste preferência em localStorage. Silencioso em erro.
55
+ */
56
+ declare function writeStoredTheme(theme: Theme, key?: string): void;
57
+ /**
58
+ * Detecta system preference. SSR-safe: retorna `light` no servidor.
59
+ */
60
+ declare function getSystemTheme(): 'light' | 'dark';
61
+
62
+ declare const ThemeContext: react.Context<ThemeContextValue | null>;
63
+ interface ThemeProviderProps {
64
+ children: ReactNode;
65
+ /** Tema padrão quando nada está armazenado. Default: `system`. */
66
+ defaultTheme?: Theme;
67
+ /** Chave usada em localStorage. Default: `arqel-theme`. */
68
+ storageKey?: string;
69
+ /** Classe aplicada no `<html>` quando dark. Default: `dark`. */
70
+ darkClass?: string;
71
+ /** Atributo opcional usado em vez de classe (`data-theme`). Útil para integrações. */
72
+ attribute?: 'class' | 'data-theme';
73
+ }
74
+ declare function ThemeProvider({ children, defaultTheme, storageKey, darkClass, attribute, }: ThemeProviderProps): ReactNode;
75
+
76
+ interface ThemeToggleProps {
77
+ className?: string;
78
+ }
79
+ /**
80
+ * Botão que cicla entre `system → light → dark → system`.
81
+ * Mostra ícone correspondente ao **tema atualmente selecionado**
82
+ * (não ao próximo) e usa `aria-label` descritivo para acessibilidade.
83
+ */
84
+ declare function ThemeToggle({ className }?: ThemeToggleProps): ReactNode;
85
+
86
+ /**
87
+ * Hook para acessar o tema atual e atualizá-lo.
88
+ *
89
+ * Lança erro se chamado fora de `<ThemeProvider>` (fail-fast — evita
90
+ * componentes silenciosamente mostrando defaults errados).
91
+ */
92
+ declare function useTheme(): ThemeContextValue;
93
+
94
+ export { DEFAULT_STORAGE_KEY, type PreventFlashOptions, type ResolvedTheme, type Theme, ThemeContext, type ThemeContextValue, ThemeProvider, type ThemeProviderProps, ThemeToggle, type ThemeToggleProps, getSystemTheme, isTheme, preventFlashScript, readStoredTheme, useTheme, writeStoredTheme };
package/dist/index.js ADDED
@@ -0,0 +1,209 @@
1
+ import { createContext, useState, useEffect, useCallback, useMemo, useContext } from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/storage.ts
5
+ var DEFAULT_STORAGE_KEY = "arqel-theme";
6
+ var VALID_THEMES = ["light", "dark", "system"];
7
+ function isTheme(value) {
8
+ return typeof value === "string" && VALID_THEMES.includes(value);
9
+ }
10
+ function readStoredTheme(key = DEFAULT_STORAGE_KEY) {
11
+ if (typeof window === "undefined") return null;
12
+ try {
13
+ const raw = window.localStorage.getItem(key);
14
+ return isTheme(raw) ? raw : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function writeStoredTheme(theme, key = DEFAULT_STORAGE_KEY) {
20
+ if (typeof window === "undefined") return;
21
+ try {
22
+ window.localStorage.setItem(key, theme);
23
+ } catch {
24
+ }
25
+ }
26
+ function getSystemTheme() {
27
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
28
+ return "light";
29
+ }
30
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
31
+ }
32
+
33
+ // src/preventFlash.ts
34
+ function preventFlashScript(options = {}) {
35
+ const { storageKey = DEFAULT_STORAGE_KEY, darkClass = "dark", attribute = "class" } = options;
36
+ const k = JSON.stringify(storageKey);
37
+ const c = JSON.stringify(darkClass);
38
+ const a = JSON.stringify(attribute);
39
+ return [
40
+ "(function(){try{",
41
+ "var k=",
42
+ k,
43
+ ",c=",
44
+ c,
45
+ ",a=",
46
+ a,
47
+ ";",
48
+ "var t=null;try{t=localStorage.getItem(k);}catch(e){}",
49
+ 'if(t!=="light"&&t!=="dark"&&t!=="system")t="system";',
50
+ "var r=t;",
51
+ 'if(t==="system"){',
52
+ 'r=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light";',
53
+ "}",
54
+ "var el=document.documentElement;",
55
+ 'if(a==="class"){if(r==="dark")el.classList.add(c);else el.classList.remove(c);}',
56
+ 'else{el.setAttribute("data-theme",r);}',
57
+ "el.style.colorScheme=r;",
58
+ "}catch(e){}})();"
59
+ ].join("");
60
+ }
61
+ var ThemeContext = createContext(null);
62
+ function resolve(theme) {
63
+ return theme === "system" ? getSystemTheme() : theme;
64
+ }
65
+ function applyToDom(resolved, darkClass, attribute) {
66
+ if (typeof document === "undefined") return;
67
+ const root = document.documentElement;
68
+ if (attribute === "class") {
69
+ if (resolved === "dark") root.classList.add(darkClass);
70
+ else root.classList.remove(darkClass);
71
+ } else {
72
+ root.setAttribute("data-theme", resolved);
73
+ }
74
+ root.style.colorScheme = resolved;
75
+ }
76
+ function ThemeProvider({
77
+ children,
78
+ defaultTheme = "system",
79
+ storageKey = DEFAULT_STORAGE_KEY,
80
+ darkClass = "dark",
81
+ attribute = "class"
82
+ }) {
83
+ const [theme, setThemeState] = useState(() => readStoredTheme(storageKey) ?? defaultTheme);
84
+ const [resolvedTheme, setResolvedTheme] = useState(() => resolve(theme));
85
+ useEffect(() => {
86
+ const next = resolve(theme);
87
+ setResolvedTheme(next);
88
+ applyToDom(next, darkClass, attribute);
89
+ if (theme !== "system" || typeof window === "undefined" || typeof window.matchMedia !== "function") {
90
+ return;
91
+ }
92
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
93
+ const onChange = () => {
94
+ const r = media.matches ? "dark" : "light";
95
+ setResolvedTheme(r);
96
+ applyToDom(r, darkClass, attribute);
97
+ };
98
+ media.addEventListener("change", onChange);
99
+ return () => {
100
+ media.removeEventListener("change", onChange);
101
+ };
102
+ }, [theme, darkClass, attribute]);
103
+ const setTheme = useCallback(
104
+ (t) => {
105
+ setThemeState(t);
106
+ writeStoredTheme(t, storageKey);
107
+ },
108
+ [storageKey]
109
+ );
110
+ const value = useMemo(
111
+ () => ({ theme, resolvedTheme, setTheme }),
112
+ [theme, resolvedTheme, setTheme]
113
+ );
114
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
115
+ }
116
+ function useTheme() {
117
+ const ctx = useContext(ThemeContext);
118
+ if (!ctx) {
119
+ throw new Error("[arqel-dev/theme] useTheme() deve ser usado dentro de <ThemeProvider>.");
120
+ }
121
+ return ctx;
122
+ }
123
+ var CYCLE = ["system", "light", "dark"];
124
+ var LABELS = {
125
+ system: "Tema: sistema (clique para claro)",
126
+ light: "Tema: claro (clique para escuro)",
127
+ dark: "Tema: escuro (clique para sistema)"
128
+ };
129
+ function nextTheme(current) {
130
+ const idx = CYCLE.indexOf(current);
131
+ return CYCLE[(idx + 1) % CYCLE.length] ?? "system";
132
+ }
133
+ function SunIcon() {
134
+ return /* @__PURE__ */ jsxs(
135
+ "svg",
136
+ {
137
+ "aria-hidden": "true",
138
+ width: "16",
139
+ height: "16",
140
+ viewBox: "0 0 24 24",
141
+ fill: "none",
142
+ stroke: "currentColor",
143
+ strokeWidth: "2",
144
+ strokeLinecap: "round",
145
+ strokeLinejoin: "round",
146
+ children: [
147
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4" }),
148
+ /* @__PURE__ */ jsx("path", { d: "M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" })
149
+ ]
150
+ }
151
+ );
152
+ }
153
+ function MoonIcon() {
154
+ return /* @__PURE__ */ jsx(
155
+ "svg",
156
+ {
157
+ "aria-hidden": "true",
158
+ width: "16",
159
+ height: "16",
160
+ viewBox: "0 0 24 24",
161
+ fill: "none",
162
+ stroke: "currentColor",
163
+ strokeWidth: "2",
164
+ strokeLinecap: "round",
165
+ strokeLinejoin: "round",
166
+ children: /* @__PURE__ */ jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
167
+ }
168
+ );
169
+ }
170
+ function MonitorIcon() {
171
+ return /* @__PURE__ */ jsxs(
172
+ "svg",
173
+ {
174
+ "aria-hidden": "true",
175
+ width: "16",
176
+ height: "16",
177
+ viewBox: "0 0 24 24",
178
+ fill: "none",
179
+ stroke: "currentColor",
180
+ strokeWidth: "2",
181
+ strokeLinecap: "round",
182
+ strokeLinejoin: "round",
183
+ children: [
184
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
185
+ /* @__PURE__ */ jsx("path", { d: "M8 21h8M12 17v4" })
186
+ ]
187
+ }
188
+ );
189
+ }
190
+ function ThemeToggle({ className } = {}) {
191
+ const { theme, setTheme } = useTheme();
192
+ const icon = theme === "light" ? /* @__PURE__ */ jsx(SunIcon, {}) : theme === "dark" ? /* @__PURE__ */ jsx(MoonIcon, {}) : /* @__PURE__ */ jsx(MonitorIcon, {});
193
+ return /* @__PURE__ */ jsx(
194
+ "button",
195
+ {
196
+ type: "button",
197
+ onClick: () => setTheme(nextTheme(theme)),
198
+ "aria-label": LABELS[theme],
199
+ title: LABELS[theme],
200
+ "data-theme-current": theme,
201
+ className,
202
+ children: icon
203
+ }
204
+ );
205
+ }
206
+
207
+ export { DEFAULT_STORAGE_KEY, ThemeContext, ThemeProvider, ThemeToggle, getSystemTheme, isTheme, preventFlashScript, readStoredTheme, useTheme, writeStoredTheme };
208
+ //# sourceMappingURL=index.js.map
209
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage.ts","../src/preventFlash.ts","../src/ThemeProvider.tsx","../src/useTheme.ts","../src/ThemeToggle.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import type { Theme } from './types';\n\nexport const DEFAULT_STORAGE_KEY = 'arqel-theme';\n\nconst VALID_THEMES: readonly Theme[] = ['light', 'dark', 'system'];\n\nexport function isTheme(value: unknown): value is Theme {\n return typeof value === 'string' && (VALID_THEMES as readonly string[]).includes(value);\n}\n\n/**\n * Lê preferência do localStorage. SSR-safe: retorna `null` no servidor.\n */\nexport function readStoredTheme(key: string = DEFAULT_STORAGE_KEY): Theme | null {\n if (typeof window === 'undefined') return null;\n try {\n const raw = window.localStorage.getItem(key);\n return isTheme(raw) ? raw : null;\n } catch {\n // localStorage pode estar bloqueado (Safari private, iframes, etc).\n return null;\n }\n}\n\n/**\n * Persiste preferência em localStorage. Silencioso em erro.\n */\nexport function writeStoredTheme(theme: Theme, key: string = DEFAULT_STORAGE_KEY): void {\n if (typeof window === 'undefined') return;\n try {\n window.localStorage.setItem(key, theme);\n } catch {\n // ignore\n }\n}\n\n/**\n * Detecta system preference. SSR-safe: retorna `light` no servidor.\n */\nexport function getSystemTheme(): 'light' | 'dark' {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {\n return 'light';\n }\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n}\n","import { DEFAULT_STORAGE_KEY } from './storage';\n\nexport interface PreventFlashOptions {\n storageKey?: string;\n darkClass?: string;\n attribute?: 'class' | 'data-theme';\n}\n\n/**\n * Retorna um snippet JS (string) para inserir inline no `<head>` do HTML\n * **antes** dos bundles React. Lê localStorage + system pref e aplica\n * imediatamente a classe `dark` em `<html>`, evitando flash branco no\n * carregamento (FOUC).\n *\n * Uso (Blade):\n * <script>{!! \\Arqel\\Theme\\preventFlashScript() !!}</script>\n *\n * Uso direto:\n * <script dangerouslySetInnerHTML={{ __html: preventFlashScript() }} />\n *\n * O snippet é IIFE que falha silenciosamente — nenhuma corrupção de página\n * mesmo se localStorage estiver bloqueado.\n */\nexport function preventFlashScript(options: PreventFlashOptions = {}): string {\n const { storageKey = DEFAULT_STORAGE_KEY, darkClass = 'dark', attribute = 'class' } = options;\n\n // JSON.stringify garante escape correto de aspas e quebras.\n const k = JSON.stringify(storageKey);\n const c = JSON.stringify(darkClass);\n const a = JSON.stringify(attribute);\n\n return [\n '(function(){try{',\n 'var k=',\n k,\n ',c=',\n c,\n ',a=',\n a,\n ';',\n 'var t=null;try{t=localStorage.getItem(k);}catch(e){}',\n 'if(t!==\"light\"&&t!==\"dark\"&&t!==\"system\")t=\"system\";',\n 'var r=t;',\n 'if(t===\"system\"){',\n 'r=(window.matchMedia&&window.matchMedia(\"(prefers-color-scheme: dark)\").matches)?\"dark\":\"light\";',\n '}',\n 'var el=document.documentElement;',\n 'if(a===\"class\"){if(r===\"dark\")el.classList.add(c);else el.classList.remove(c);}',\n 'else{el.setAttribute(\"data-theme\",r);}',\n 'el.style.colorScheme=r;',\n '}catch(e){}})();',\n ].join('');\n}\n","import { createContext, type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { DEFAULT_STORAGE_KEY, getSystemTheme, readStoredTheme, writeStoredTheme } from './storage';\nimport type { ResolvedTheme, Theme, ThemeContextValue } from './types';\n\nexport const ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** Tema padrão quando nada está armazenado. Default: `system`. */\n defaultTheme?: Theme;\n /** Chave usada em localStorage. Default: `arqel-theme`. */\n storageKey?: string;\n /** Classe aplicada no `<html>` quando dark. Default: `dark`. */\n darkClass?: string;\n /** Atributo opcional usado em vez de classe (`data-theme`). Útil para integrações. */\n attribute?: 'class' | 'data-theme';\n}\n\nfunction resolve(theme: Theme): ResolvedTheme {\n return theme === 'system' ? getSystemTheme() : theme;\n}\n\nfunction applyToDom(\n resolved: ResolvedTheme,\n darkClass: string,\n attribute: 'class' | 'data-theme',\n): void {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n if (attribute === 'class') {\n if (resolved === 'dark') root.classList.add(darkClass);\n else root.classList.remove(darkClass);\n } else {\n root.setAttribute('data-theme', resolved);\n }\n // colorScheme ajuda nativos (scrollbars, form controls)\n root.style.colorScheme = resolved;\n}\n\nexport function ThemeProvider({\n children,\n defaultTheme = 'system',\n storageKey = DEFAULT_STORAGE_KEY,\n darkClass = 'dark',\n attribute = 'class',\n}: ThemeProviderProps): ReactNode {\n // Inicialização lazy — lê localStorage 1x.\n const [theme, setThemeState] = useState<Theme>(() => readStoredTheme(storageKey) ?? defaultTheme);\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolve(theme));\n\n // Aplica no DOM e mantém em sync com matchMedia se theme === 'system'.\n useEffect(() => {\n const next = resolve(theme);\n setResolvedTheme(next);\n applyToDom(next, darkClass, attribute);\n\n if (\n theme !== 'system' ||\n typeof window === 'undefined' ||\n typeof window.matchMedia !== 'function'\n ) {\n return;\n }\n\n const media = window.matchMedia('(prefers-color-scheme: dark)');\n const onChange = (): void => {\n const r: ResolvedTheme = media.matches ? 'dark' : 'light';\n setResolvedTheme(r);\n applyToDom(r, darkClass, attribute);\n };\n media.addEventListener('change', onChange);\n return () => {\n media.removeEventListener('change', onChange);\n };\n }, [theme, darkClass, attribute]);\n\n const setTheme = useCallback(\n (t: Theme) => {\n setThemeState(t);\n writeStoredTheme(t, storageKey);\n },\n [storageKey],\n );\n\n const value = useMemo<ThemeContextValue>(\n () => ({ theme, resolvedTheme, setTheme }),\n [theme, resolvedTheme, setTheme],\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n","import { useContext } from 'react';\n\nimport { ThemeContext } from './ThemeProvider';\nimport type { ThemeContextValue } from './types';\n\n/**\n * Hook para acessar o tema atual e atualizá-lo.\n *\n * Lança erro se chamado fora de `<ThemeProvider>` (fail-fast — evita\n * componentes silenciosamente mostrando defaults errados).\n */\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext);\n if (!ctx) {\n throw new Error('[arqel-dev/theme] useTheme() deve ser usado dentro de <ThemeProvider>.');\n }\n return ctx;\n}\n","import type { ReactNode } from 'react';\nimport type { Theme } from './types';\nimport { useTheme } from './useTheme';\n\nconst CYCLE: readonly Theme[] = ['system', 'light', 'dark'];\n\nconst LABELS: Record<Theme, string> = {\n system: 'Tema: sistema (clique para claro)',\n light: 'Tema: claro (clique para escuro)',\n dark: 'Tema: escuro (clique para sistema)',\n};\n\nfunction nextTheme(current: Theme): Theme {\n const idx = CYCLE.indexOf(current);\n return CYCLE[(idx + 1) % CYCLE.length] ?? 'system';\n}\n\nfunction SunIcon(): ReactNode {\n return (\n <svg\n aria-hidden=\"true\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"4\" />\n <path d=\"M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41\" />\n </svg>\n );\n}\n\nfunction MoonIcon(): ReactNode {\n return (\n <svg\n aria-hidden=\"true\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n </svg>\n );\n}\n\nfunction MonitorIcon(): ReactNode {\n return (\n <svg\n aria-hidden=\"true\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"2\" y=\"3\" width=\"20\" height=\"14\" rx=\"2\" />\n <path d=\"M8 21h8M12 17v4\" />\n </svg>\n );\n}\n\nexport interface ThemeToggleProps {\n className?: string;\n}\n\n/**\n * Botão que cicla entre `system → light → dark → system`.\n * Mostra ícone correspondente ao **tema atualmente selecionado**\n * (não ao próximo) e usa `aria-label` descritivo para acessibilidade.\n */\nexport function ThemeToggle({ className }: ThemeToggleProps = {}): ReactNode {\n const { theme, setTheme } = useTheme();\n\n const icon = theme === 'light' ? <SunIcon /> : theme === 'dark' ? <MoonIcon /> : <MonitorIcon />;\n\n return (\n <button\n type=\"button\"\n onClick={() => setTheme(nextTheme(theme))}\n aria-label={LABELS[theme]}\n title={LABELS[theme]}\n data-theme-current={theme}\n className={className}\n >\n {icon}\n </button>\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@arqel-dev/theme",
3
+ "version": "0.8.0",
4
+ "description": "Tokens semânticos + dark-mode + ThemeProvider/ThemeToggle para painéis Arqel.",
5
+ "license": "MIT",
6
+ "homepage": "https://arqel.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/arqel-dev/arqel.git",
10
+ "directory": "packages-js/theme"
11
+ },
12
+ "keywords": [
13
+ "arqel",
14
+ "theme",
15
+ "dark-mode",
16
+ "tailwind",
17
+ "tokens",
18
+ "react"
19
+ ],
20
+ "type": "module",
21
+ "sideEffects": [
22
+ "**/*.css"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ },
29
+ "./tokens.css": "./src/tokens.css"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src/tokens.css",
34
+ "README.md",
35
+ "SKILL.md"
36
+ ],
37
+ "peerDependencies": {
38
+ "react": "^19.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@testing-library/react": "^16.1.0",
42
+ "@types/react": "^19.0.0",
43
+ "@types/react-dom": "^19.0.0",
44
+ "jsdom": "^25.0.1",
45
+ "react": "^19.0.0",
46
+ "react-dom": "^19.0.0",
47
+ "tsup": "^8.5.0",
48
+ "typescript": "^5.6.3",
49
+ "vitest": "^3.0.0"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "engines": {
55
+ "node": ">=20.9.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "dev": "tsup --watch",
60
+ "typecheck": "tsc --noEmit",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest"
63
+ }
64
+ }
package/src/tokens.css ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @arqel/theme — semantic design tokens
3
+ *
4
+ * Estes tokens são as variáveis canônicas usadas pelos componentes Arqel.
5
+ * Eles são "semânticos" (descrevem intenção: bg, fg, primary) e não cores
6
+ * concretas — o que permite trocar paleta inteira sem refatorar componentes.
7
+ *
8
+ * Override:
9
+ * :root { --arqel-color-primary: #ff0080; }
10
+ *
11
+ * Tailwind v4 usage (via @theme):
12
+ * @import 'tailwindcss';
13
+ * @import '@arqel/theme/tokens.css';
14
+ * @theme {
15
+ * --color-primary: var(--arqel-color-primary);
16
+ * }
17
+ */
18
+
19
+ :root {
20
+ /* Surface */
21
+ --arqel-color-bg: #ffffff;
22
+ --arqel-color-bg-muted: #f5f5f5;
23
+ --arqel-color-bg-subtle: #fafafa;
24
+
25
+ /* Foreground */
26
+ --arqel-color-fg: #0a0a0a;
27
+ --arqel-color-fg-muted: #525252;
28
+ --arqel-color-fg-subtle: #737373;
29
+
30
+ /* Borders */
31
+ --arqel-color-border: #e5e5e5;
32
+ --arqel-color-border-strong: #d4d4d4;
33
+
34
+ /* Brand / accent */
35
+ --arqel-color-primary: #6366f1;
36
+ --arqel-color-primary-fg: #ffffff;
37
+ --arqel-color-primary-hover: #4f46e5;
38
+
39
+ /* Semantic */
40
+ --arqel-color-success: #10b981;
41
+ --arqel-color-success-fg: #ffffff;
42
+ --arqel-color-warning: #f59e0b;
43
+ --arqel-color-warning-fg: #0a0a0a;
44
+ --arqel-color-danger: #ef4444;
45
+ --arqel-color-danger-fg: #ffffff;
46
+ --arqel-color-info: #0ea5e9;
47
+ --arqel-color-info-fg: #ffffff;
48
+
49
+ /* Focus ring */
50
+ --arqel-color-ring: #6366f1;
51
+
52
+ /* Radii */
53
+ --arqel-radius-sm: 0.25rem;
54
+ --arqel-radius-md: 0.375rem;
55
+ --arqel-radius-lg: 0.5rem;
56
+ }
57
+
58
+ .dark {
59
+ /* Surface */
60
+ --arqel-color-bg: #0a0a0a;
61
+ --arqel-color-bg-muted: #171717;
62
+ --arqel-color-bg-subtle: #1f1f1f;
63
+
64
+ /* Foreground */
65
+ --arqel-color-fg: #fafafa;
66
+ --arqel-color-fg-muted: #a3a3a3;
67
+ --arqel-color-fg-subtle: #737373;
68
+
69
+ /* Borders */
70
+ --arqel-color-border: #262626;
71
+ --arqel-color-border-strong: #404040;
72
+
73
+ /* Brand / accent */
74
+ --arqel-color-primary: #818cf8;
75
+ --arqel-color-primary-fg: #0a0a0a;
76
+ --arqel-color-primary-hover: #6366f1;
77
+
78
+ /* Semantic */
79
+ --arqel-color-success: #34d399;
80
+ --arqel-color-success-fg: #022c22;
81
+ --arqel-color-warning: #fbbf24;
82
+ --arqel-color-warning-fg: #1c1917;
83
+ --arqel-color-danger: #f87171;
84
+ --arqel-color-danger-fg: #1f1f1f;
85
+ --arqel-color-info: #38bdf8;
86
+ --arqel-color-info-fg: #0c1f2c;
87
+
88
+ /* Focus ring */
89
+ --arqel-color-ring: #818cf8;
90
+ }