@tokiforge/remix 2.3.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,17 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 TokiForge Community
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @tokiforge/remix
2
+
3
+ Remix integration for TokiForge theming with session-based persistence.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @tokiforge/remix @tokiforge/core @remix-run/react @remix-run/node
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Setup (app/root.tsx)
14
+
15
+ ```tsx
16
+ import { json, type LoaderFunctionArgs } from "@remix-run/node";
17
+ import { useLoaderData } from "@remix-run/react";
18
+ import { ThemeProvider } from "@tokiforge/remix";
19
+ import { createThemeSessionStorage } from "@tokiforge/remix/server";
20
+
21
+ const themeStorage = createThemeSessionStorage(process.env.SESSION_SECRET!);
22
+
23
+ export async function loader({ request }: LoaderFunctionArgs) {
24
+ const theme = await themeStorage.getTheme(request);
25
+ return json({ theme });
26
+ }
27
+
28
+ export default function App() {
29
+ const { theme } = useLoaderData<typeof loader>();
30
+
31
+ return (
32
+ <html>
33
+ <body>
34
+ <ThemeProvider config={config} initialTheme={theme || undefined}>
35
+ <Outlet />
36
+ </ThemeProvider>
37
+ </body>
38
+ </html>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Theme Switching Action
44
+
45
+ ```tsx
46
+ import { redirect, type ActionFunctionArgs } from "@remix-run/node";
47
+
48
+ export async function action({ request }: ActionFunctionArgs) {
49
+ const formData = await request.formData();
50
+ const theme = formData.get("theme") as string;
51
+
52
+ const setCookie = await themeStorage.setTheme(request, theme);
53
+
54
+ return redirect(request.headers.get("Referer") || "/", {
55
+ headers: { "Set-Cookie": setCookie },
56
+ });
57
+ }
58
+ ```
59
+
60
+ ### Using Theme in Components
61
+
62
+ ```tsx
63
+ import { useTheme } from "@tokiforge/remix";
64
+ import { Form } from "@remix-run/react";
65
+
66
+ export function ThemeSwitcher() {
67
+ const { theme, availableThemes } = useTheme();
68
+
69
+ return (
70
+ <Form method="post">
71
+ <select name="theme" defaultValue={theme}>
72
+ {availableThemes.map((t) => (
73
+ <option key={t} value={t}>
74
+ {t}
75
+ </option>
76
+ ))}
77
+ </select>
78
+ <button type="submit">Switch Theme</button>
79
+ </Form>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## API
85
+
86
+ ### Client (`@tokiforge/remix`)
87
+
88
+ #### `ThemeProvider`
89
+
90
+ Provider component for theme context.
91
+
92
+ #### `useTheme()`
93
+
94
+ Hook to access theme context.
95
+
96
+ ### Server (`@tokiforge/remix/server`)
97
+
98
+ #### `createThemeSessionStorage(secret)`
99
+
100
+ Creates session storage for theme persistence.
101
+
102
+ ## Features
103
+
104
+ - Remix 2.0+ support
105
+ - Session-based theme persistence
106
+ - Form action integration
107
+ - Flash-free hydration
108
+ - TypeScript support
109
+
110
+ ## License
111
+
112
+ AGPL-3.0
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var react=require('react'),core=require('@tokiforge/core'),node=require('@remix-run/node'),jsxRuntime=require('react/jsx-runtime');var l="tokiforge-theme";function y(t){let n=node.createCookieSessionStorage({cookie:{name:"__tokiforge_theme",secrets:[t],sameSite:"lax",path:"/",httpOnly:true,secure:process.env.NODE_ENV==="production"}});return {getTheme:async o=>(await n.getSession(o.headers.get("Cookie"))).get(l)||null,setTheme:async(o,r)=>{let i=await n.getSession(o.headers.get("Cookie"));return i.set(l,r),await n.commitSession(i)}}}var x=react.createContext(null),_="tokiforge-theme";function K({config:t,initialTheme:n,selector:o=":root",prefix:r="hf",storageKey:i=_,persist:f=true,onThemeChange:a,children:d}){let c=react.useRef(a);c.current=a;let m=react.useRef(null);m.current??(m.current=new core.ThemeController(t,{selector:o,prefix:r,defaultTheme:n,storageKey:i,persist:f,onThemeChange:s=>c.current?.(s)}));let e=m.current,h=react.useSyncExternalStore(react.useCallback(s=>e.subscribe(s),[e]),()=>e.getSnapshot(),()=>e.getSnapshot());react.useEffect(()=>(e.init(),()=>{e.destroy();}),[e]);let u=react.useCallback(async s=>{e.setTheme(s);},[e]),g=react.useCallback(async()=>{e.nextTheme();},[e]),C=react.useMemo(()=>({theme:h.theme,tokens:h.tokens,setTheme:u,nextTheme:g,availableThemes:e.getAvailableThemes(),runtime:e.runtime}),[h,u,g,e]);return jsxRuntime.jsx(x.Provider,{value:C,children:d})}function q(){let t=react.useContext(x);if(!t)throw new Error("useTheme must be used within a ThemeProvider");return t}exports.ThemeProvider=K;exports.createThemeSessionStorage=y;exports.useTheme=q;//# sourceMappingURL=index.cjs.map
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/index.tsx"],"names":["THEME_SESSION_KEY","createThemeSessionStorage","secret","sessionStorage","createCookieSessionStorage","request","theme","session","ThemeContext","createContext","DEFAULT_STORAGE_KEY","ThemeProvider","config","initialTheme","selector","prefix","storageKey","persist","onThemeChange","children","onThemeChangeRef","useRef","controllerRef","ThemeController","themeName","controller","snapshot","useSyncExternalStore","useCallback","onStoreChange","useEffect","setTheme","nextTheme","value","useMemo","jsx","useTheme","context","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createCookieSessionStorage } from '@remix-run/node';\n\nconst THEME_SESSION_KEY = 'tokiforge-theme';\n\nexport function createThemeSessionStorage(secret: string) {\n const sessionStorage = createCookieSessionStorage({\n cookie: {\n name: '__tokiforge_theme',\n secrets: [secret],\n sameSite: 'lax',\n path: '/',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n },\n });\n\n return {\n getTheme: async (request: Request): Promise<string | null> => {\n const session = await sessionStorage.getSession(request.headers.get('Cookie'));\n return session.get(THEME_SESSION_KEY) || null;\n },\n\n setTheme: async (request: Request, theme: string): Promise<string> => {\n const session = await sessionStorage.getSession(request.headers.get('Cookie'));\n session.set(THEME_SESSION_KEY, theme);\n return await sessionStorage.commitSession(session);\n },\n };\n}\n","import {\n createContext,\n useContext,\n useEffect,\n useCallback,\n useMemo,\n useRef,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport { ThemeController, type ThemeRuntime, type DesignTokens, type ThemeConfig } from '@tokiforge/core';\n\nexport interface ThemeProviderProps {\n config: ThemeConfig;\n initialTheme?: string;\n selector?: string;\n prefix?: string;\n /** LocalStorage key for persisting selected theme (e.g. 'tokiforge-theme') */\n storageKey?: string;\n /** Whether to read/write theme from storage (default: true) */\n persist?: boolean;\n /** Callback when theme changes (e.g. analytics) */\n onThemeChange?: (themeName: string) => void;\n children: ReactNode;\n}\n\nexport interface ThemeContextType<T extends DesignTokens = DesignTokens> {\n theme: string;\n tokens: T;\n setTheme: (themeName: string) => Promise<void>;\n nextTheme: () => Promise<void>;\n availableThemes: string[];\n runtime: ThemeRuntime;\n}\n\nconst ThemeContext = createContext<ThemeContextType | null>(null);\n\nconst DEFAULT_STORAGE_KEY = 'tokiforge-theme';\n\nexport function ThemeProvider({\n config,\n initialTheme,\n selector = ':root',\n prefix = 'hf',\n storageKey = DEFAULT_STORAGE_KEY,\n persist = true,\n onThemeChange,\n children,\n}: ThemeProviderProps) {\n const onThemeChangeRef = useRef(onThemeChange);\n onThemeChangeRef.current = onThemeChange;\n\n const controllerRef = useRef<ThemeController | null>(null);\n controllerRef.current ??= new ThemeController(config, {\n selector,\n prefix,\n defaultTheme: initialTheme,\n storageKey,\n persist,\n onThemeChange: (themeName) => onThemeChangeRef.current?.(themeName),\n });\n const controller = controllerRef.current;\n\n const snapshot = useSyncExternalStore(\n useCallback((onStoreChange: () => void) => controller.subscribe(onStoreChange), [controller]),\n () => controller.getSnapshot(),\n () => controller.getSnapshot()\n );\n\n useEffect(() => {\n controller.init();\n return () => {\n controller.destroy();\n };\n }, [controller]);\n\n const setTheme = useCallback(\n async (themeName: string) => {\n controller.setTheme(themeName);\n },\n [controller]\n );\n\n const nextTheme = useCallback(async () => {\n controller.nextTheme();\n }, [controller]);\n\n const value = useMemo<ThemeContextType>(\n () => ({\n theme: snapshot.theme,\n tokens: snapshot.tokens,\n setTheme,\n nextTheme,\n availableThemes: controller.getAvailableThemes(),\n runtime: controller.runtime,\n }),\n [snapshot, setTheme, nextTheme, controller]\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\nexport function useTheme<T extends DesignTokens = DesignTokens>(): ThemeContextType<T> {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context as ThemeContextType<T>;\n}\n\n// Re-export server utilities\nexport { createThemeSessionStorage } from './server';\n"]}
@@ -0,0 +1,34 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { DesignTokens, ThemeRuntime, ThemeConfig } from '@tokiforge/core';
4
+
5
+ declare function createThemeSessionStorage(secret: string): {
6
+ getTheme: (request: Request) => Promise<string | null>;
7
+ setTheme: (request: Request, theme: string) => Promise<string>;
8
+ };
9
+
10
+ interface ThemeProviderProps {
11
+ config: ThemeConfig;
12
+ initialTheme?: string;
13
+ selector?: string;
14
+ prefix?: string;
15
+ /** LocalStorage key for persisting selected theme (e.g. 'tokiforge-theme') */
16
+ storageKey?: string;
17
+ /** Whether to read/write theme from storage (default: true) */
18
+ persist?: boolean;
19
+ /** Callback when theme changes (e.g. analytics) */
20
+ onThemeChange?: (themeName: string) => void;
21
+ children: ReactNode;
22
+ }
23
+ interface ThemeContextType<T extends DesignTokens = DesignTokens> {
24
+ theme: string;
25
+ tokens: T;
26
+ setTheme: (themeName: string) => Promise<void>;
27
+ nextTheme: () => Promise<void>;
28
+ availableThemes: string[];
29
+ runtime: ThemeRuntime;
30
+ }
31
+ declare function ThemeProvider({ config, initialTheme, selector, prefix, storageKey, persist, onThemeChange, children, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
32
+ declare function useTheme<T extends DesignTokens = DesignTokens>(): ThemeContextType<T>;
33
+
34
+ export { type ThemeContextType, ThemeProvider, type ThemeProviderProps, createThemeSessionStorage, useTheme };
@@ -0,0 +1,34 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { DesignTokens, ThemeRuntime, ThemeConfig } from '@tokiforge/core';
4
+
5
+ declare function createThemeSessionStorage(secret: string): {
6
+ getTheme: (request: Request) => Promise<string | null>;
7
+ setTheme: (request: Request, theme: string) => Promise<string>;
8
+ };
9
+
10
+ interface ThemeProviderProps {
11
+ config: ThemeConfig;
12
+ initialTheme?: string;
13
+ selector?: string;
14
+ prefix?: string;
15
+ /** LocalStorage key for persisting selected theme (e.g. 'tokiforge-theme') */
16
+ storageKey?: string;
17
+ /** Whether to read/write theme from storage (default: true) */
18
+ persist?: boolean;
19
+ /** Callback when theme changes (e.g. analytics) */
20
+ onThemeChange?: (themeName: string) => void;
21
+ children: ReactNode;
22
+ }
23
+ interface ThemeContextType<T extends DesignTokens = DesignTokens> {
24
+ theme: string;
25
+ tokens: T;
26
+ setTheme: (themeName: string) => Promise<void>;
27
+ nextTheme: () => Promise<void>;
28
+ availableThemes: string[];
29
+ runtime: ThemeRuntime;
30
+ }
31
+ declare function ThemeProvider({ config, initialTheme, selector, prefix, storageKey, persist, onThemeChange, children, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
32
+ declare function useTheme<T extends DesignTokens = DesignTokens>(): ThemeContextType<T>;
33
+
34
+ export { type ThemeContextType, ThemeProvider, type ThemeProviderProps, createThemeSessionStorage, useTheme };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import {createContext,useRef,useSyncExternalStore,useCallback,useEffect,useMemo,useContext}from'react';import {ThemeController}from'@tokiforge/core';import {createCookieSessionStorage}from'@remix-run/node';import {jsx}from'react/jsx-runtime';var l="tokiforge-theme";function y(t){let n=createCookieSessionStorage({cookie:{name:"__tokiforge_theme",secrets:[t],sameSite:"lax",path:"/",httpOnly:true,secure:process.env.NODE_ENV==="production"}});return {getTheme:async o=>(await n.getSession(o.headers.get("Cookie"))).get(l)||null,setTheme:async(o,r)=>{let i=await n.getSession(o.headers.get("Cookie"));return i.set(l,r),await n.commitSession(i)}}}var x=createContext(null),_="tokiforge-theme";function K({config:t,initialTheme:n,selector:o=":root",prefix:r="hf",storageKey:i=_,persist:f=true,onThemeChange:a,children:d}){let c=useRef(a);c.current=a;let m=useRef(null);m.current??(m.current=new ThemeController(t,{selector:o,prefix:r,defaultTheme:n,storageKey:i,persist:f,onThemeChange:s=>c.current?.(s)}));let e=m.current,h=useSyncExternalStore(useCallback(s=>e.subscribe(s),[e]),()=>e.getSnapshot(),()=>e.getSnapshot());useEffect(()=>(e.init(),()=>{e.destroy();}),[e]);let u=useCallback(async s=>{e.setTheme(s);},[e]),g=useCallback(async()=>{e.nextTheme();},[e]),C=useMemo(()=>({theme:h.theme,tokens:h.tokens,setTheme:u,nextTheme:g,availableThemes:e.getAvailableThemes(),runtime:e.runtime}),[h,u,g,e]);return jsx(x.Provider,{value:C,children:d})}function q(){let t=useContext(x);if(!t)throw new Error("useTheme must be used within a ThemeProvider");return t}export{K as ThemeProvider,y as createThemeSessionStorage,q as useTheme};//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/index.tsx"],"names":["THEME_SESSION_KEY","createThemeSessionStorage","secret","sessionStorage","createCookieSessionStorage","request","theme","session","ThemeContext","createContext","DEFAULT_STORAGE_KEY","ThemeProvider","config","initialTheme","selector","prefix","storageKey","persist","onThemeChange","children","onThemeChangeRef","useRef","controllerRef","ThemeController","themeName","controller","snapshot","useSyncExternalStore","useCallback","onStoreChange","useEffect","setTheme","nextTheme","value","useMemo","jsx","useTheme","context","useContext"],"mappings":"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","file":"index.js","sourcesContent":["import { createCookieSessionStorage } from '@remix-run/node';\n\nconst THEME_SESSION_KEY = 'tokiforge-theme';\n\nexport function createThemeSessionStorage(secret: string) {\n const sessionStorage = createCookieSessionStorage({\n cookie: {\n name: '__tokiforge_theme',\n secrets: [secret],\n sameSite: 'lax',\n path: '/',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n },\n });\n\n return {\n getTheme: async (request: Request): Promise<string | null> => {\n const session = await sessionStorage.getSession(request.headers.get('Cookie'));\n return session.get(THEME_SESSION_KEY) || null;\n },\n\n setTheme: async (request: Request, theme: string): Promise<string> => {\n const session = await sessionStorage.getSession(request.headers.get('Cookie'));\n session.set(THEME_SESSION_KEY, theme);\n return await sessionStorage.commitSession(session);\n },\n };\n}\n","import {\n createContext,\n useContext,\n useEffect,\n useCallback,\n useMemo,\n useRef,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport { ThemeController, type ThemeRuntime, type DesignTokens, type ThemeConfig } from '@tokiforge/core';\n\nexport interface ThemeProviderProps {\n config: ThemeConfig;\n initialTheme?: string;\n selector?: string;\n prefix?: string;\n /** LocalStorage key for persisting selected theme (e.g. 'tokiforge-theme') */\n storageKey?: string;\n /** Whether to read/write theme from storage (default: true) */\n persist?: boolean;\n /** Callback when theme changes (e.g. analytics) */\n onThemeChange?: (themeName: string) => void;\n children: ReactNode;\n}\n\nexport interface ThemeContextType<T extends DesignTokens = DesignTokens> {\n theme: string;\n tokens: T;\n setTheme: (themeName: string) => Promise<void>;\n nextTheme: () => Promise<void>;\n availableThemes: string[];\n runtime: ThemeRuntime;\n}\n\nconst ThemeContext = createContext<ThemeContextType | null>(null);\n\nconst DEFAULT_STORAGE_KEY = 'tokiforge-theme';\n\nexport function ThemeProvider({\n config,\n initialTheme,\n selector = ':root',\n prefix = 'hf',\n storageKey = DEFAULT_STORAGE_KEY,\n persist = true,\n onThemeChange,\n children,\n}: ThemeProviderProps) {\n const onThemeChangeRef = useRef(onThemeChange);\n onThemeChangeRef.current = onThemeChange;\n\n const controllerRef = useRef<ThemeController | null>(null);\n controllerRef.current ??= new ThemeController(config, {\n selector,\n prefix,\n defaultTheme: initialTheme,\n storageKey,\n persist,\n onThemeChange: (themeName) => onThemeChangeRef.current?.(themeName),\n });\n const controller = controllerRef.current;\n\n const snapshot = useSyncExternalStore(\n useCallback((onStoreChange: () => void) => controller.subscribe(onStoreChange), [controller]),\n () => controller.getSnapshot(),\n () => controller.getSnapshot()\n );\n\n useEffect(() => {\n controller.init();\n return () => {\n controller.destroy();\n };\n }, [controller]);\n\n const setTheme = useCallback(\n async (themeName: string) => {\n controller.setTheme(themeName);\n },\n [controller]\n );\n\n const nextTheme = useCallback(async () => {\n controller.nextTheme();\n }, [controller]);\n\n const value = useMemo<ThemeContextType>(\n () => ({\n theme: snapshot.theme,\n tokens: snapshot.tokens,\n setTheme,\n nextTheme,\n availableThemes: controller.getAvailableThemes(),\n runtime: controller.runtime,\n }),\n [snapshot, setTheme, nextTheme, controller]\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\nexport function useTheme<T extends DesignTokens = DesignTokens>(): ThemeContextType<T> {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context as ThemeContextType<T>;\n}\n\n// Re-export server utilities\nexport { createThemeSessionStorage } from './server';\n"]}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@tokiforge/remix",
3
+ "version": "2.3.0",
4
+ "description": "Remix integration for TokiForge theming",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./server": {
16
+ "types": "./dist/server.d.ts",
17
+ "import": "./dist/server.js",
18
+ "require": "./dist/server.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "sideEffects": false,
26
+ "peerDependencies": {
27
+ "@remix-run/node": ">=2.0.0",
28
+ "@remix-run/react": ">=2.0.0",
29
+ "@tokiforge/core": "^2.3.0",
30
+ "react": ">=18.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@remix-run/node": "^2.0.0",
34
+ "@remix-run/react": "^2.0.0",
35
+ "@testing-library/react": "^16.3.0",
36
+ "@types/react": "^19.2.14",
37
+ "@vitest/ui": "^4.1.5",
38
+ "jsdom": "^26.1.0",
39
+ "react": "^19.2.5",
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^6.0.3",
42
+ "vitest": "^4.1.5"
43
+ },
44
+ "dependencies": {
45
+ "@tokiforge/core": "^2.3.0"
46
+ },
47
+ "keywords": [
48
+ "remix",
49
+ "theming",
50
+ "design-tokens"
51
+ ],
52
+ "author": "TokiForge Community",
53
+ "license": "AGPL-3.0",
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "clean": "rm -rf dist",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "test:ui": "vitest --ui"
61
+ }
62
+ }