@mfeflow/theme 0.0.0-stage → 0.1.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 Sanjeev Sharma
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 CHANGED
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # @mfeflow/theme
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Design tokens as CSS variables for micro-frontends. The shell picks the theme once and writes `--mf-<token>` variables, `data-theme` and `color-scheme` on `<html>`; every remote inherits them. The choice is persisted, and `noFlashScript` applies it before first paint.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @mfeflow/theme @mfeflow/storage
9
+ ```
10
+
11
+ `react` (19) is an optional peer dependency, needed only for `@mfeflow/theme/react`.
12
+
13
+ ## Usage
14
+
15
+ In the shell:
16
+
17
+ ```tsx
18
+ import { defineTheme, noFlashScript } from '@mfeflow/theme';
19
+ import { ThemeProvider, useTheme } from '@mfeflow/theme/react';
20
+
21
+ export const themes = [
22
+ defineTheme('light', { 'color-bg': '#ffffff', 'color-fg': '#111111' }),
23
+ defineTheme('dark', { 'color-bg': '#111111', 'color-fg': '#f5f5f5' }, { colorScheme: 'dark' }),
24
+ ];
25
+
26
+ // <ThemeProvider themes={themes} defaultTheme="light"> ... </ThemeProvider>
27
+ // Put noFlashScript(themes, { defaultTheme: 'light' }) in an inline <script> in the <head>.
28
+ ```
29
+
30
+ In a remote, read the tokens:
31
+
32
+ ```css
33
+ .card {
34
+ background: var(--mf-color-bg);
35
+ }
36
+ ```
37
+
38
+ Or in TypeScript, `cssVar('color-bg', '#fff')` returns `var(--mf-color-bg, #fff)`. `useTheme()` returns `{ theme, themes, setTheme }`.
39
+
40
+ ## Module Federation sharing
41
+
42
+ Bucket: **singleton**. This package keeps page-wide state, so the shell and every remote must use one copy. `@mfeflow/rsbuild` shares it as a Module Federation singleton by default (`createShellConfig` and `createRemoteConfig`); if you configure sharing by hand, mark it `singleton: true`.
43
+
44
+ ## Links
45
+
46
+ - [mfeflow repository](https://github.com/mfeflow/mfeflow) and its README
47
+ - [Documentation](https://mfeflow.github.io/mfeflow/)
48
+
49
+ MIT licensed.
@@ -0,0 +1,39 @@
1
+ /** Token name → CSS value, e.g. `{ 'color-primary': '#2563eb', 'radius-md': '8px' }`. */
2
+ export type ThemeTokens = Readonly<Record<string, string>>;
3
+ export interface Theme {
4
+ readonly name: string;
5
+ readonly colorScheme: 'light' | 'dark';
6
+ readonly tokens: ThemeTokens;
7
+ }
8
+ /** Every token becomes `--mf-<token>` on `:root`. */
9
+ export declare const CSS_VAR_PREFIX = "--mf-";
10
+ /** Storage key (under the `shell` micro-frontend) for the chosen theme. */
11
+ export declare const THEME_STORAGE_KEY = "theme";
12
+ export declare const THEME_STORAGE_MFE = "shell";
13
+ export declare function cssVarName(token: string): string;
14
+ /** `var(--mf-<token>)`, for remotes that style with tokens. */
15
+ export declare function cssVar(token: string, fallback?: string): string;
16
+ export declare function defineTheme(name: string, tokens: ThemeTokens, options?: {
17
+ colorScheme?: 'light' | 'dark';
18
+ }): Theme;
19
+ /** CSS custom properties for a theme, keyed by variable name. */
20
+ export declare function themeToCssVars(theme: Theme): Record<string, string>;
21
+ /**
22
+ * Writes a theme onto `root` (default `<html>`): CSS variables, `data-theme`
23
+ * and `color-scheme`. Variables from the previous theme that the new one does
24
+ * not define are removed. Only the shell calls this; remotes just read tokens.
25
+ */
26
+ export declare function applyTheme(theme: Theme, root?: HTMLElement): void;
27
+ /** Picks the theme to show: stored choice, then the system dark preference, then the default. */
28
+ export declare function resolveTheme(themes: readonly Theme[], stored: string | undefined, defaultTheme: string, prefersDark?: boolean): Theme;
29
+ export interface NoFlashOptions {
30
+ defaultTheme: string;
31
+ /** Use the OS dark-mode preference when nothing is stored. Default true. */
32
+ followSystem?: boolean;
33
+ }
34
+ /**
35
+ * Source for an inline `<script>` in the shell's `<head>`. It applies the
36
+ * stored (or system) theme before first paint, so reloads never flash the
37
+ * default theme. It never throws.
38
+ */
39
+ export declare function noFlashScript(themes: readonly Theme[], options: NoFlashOptions): string;
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ const CSS_VAR_PREFIX = '--mf-';
2
+ const THEME_STORAGE_KEY = 'theme';
3
+ const THEME_STORAGE_MFE = 'shell';
4
+ const TOKEN = /^[a-z][a-z0-9-]*$/;
5
+ const THEME_NAME = /^[a-z][a-z0-9-]*$/;
6
+ function cssVarName(token) {
7
+ if (!TOKEN.test(token)) throw new Error(`Invalid token name "${token}": use lowercase letters, digits and dashes`);
8
+ return CSS_VAR_PREFIX + token;
9
+ }
10
+ function cssVar(token, fallback) {
11
+ return void 0 === fallback ? `var(${cssVarName(token)})` : `var(${cssVarName(token)}, ${fallback})`;
12
+ }
13
+ function defineTheme(name, tokens, options = {}) {
14
+ if (!THEME_NAME.test(name)) throw new Error(`Invalid theme name "${name}"`);
15
+ for (const token of Object.keys(tokens))cssVarName(token);
16
+ return Object.freeze({
17
+ name,
18
+ colorScheme: options.colorScheme ?? 'light',
19
+ tokens: Object.freeze({
20
+ ...tokens
21
+ })
22
+ });
23
+ }
24
+ function themeToCssVars(theme) {
25
+ const out = {};
26
+ for (const [token, value] of Object.entries(theme.tokens))out[cssVarName(token)] = value;
27
+ return out;
28
+ }
29
+ const applied = new WeakMap();
30
+ function applyTheme(theme, root = document.documentElement) {
31
+ const vars = themeToCssVars(theme);
32
+ const previous = applied.get(root) ?? new Set();
33
+ for (const name of previous)if (!(name in vars)) root.style.removeProperty(name);
34
+ for (const [name, value] of Object.entries(vars))root.style.setProperty(name, value);
35
+ applied.set(root, new Set(Object.keys(vars)));
36
+ root.dataset.theme = theme.name;
37
+ root.style.colorScheme = theme.colorScheme;
38
+ }
39
+ function resolveTheme(themes, stored, defaultTheme, prefersDark = false) {
40
+ const byName = new Map(themes.map((t)=>[
41
+ t.name,
42
+ t
43
+ ]));
44
+ const fallback = byName.get(defaultTheme);
45
+ if (!fallback) throw new Error(`Default theme "${defaultTheme}" is not in the theme list`);
46
+ if (void 0 !== stored) {
47
+ const hit = byName.get(stored);
48
+ if (hit) return hit;
49
+ }
50
+ if (prefersDark) {
51
+ const dark = themes.find((t)=>'dark' === t.colorScheme);
52
+ if (dark) return dark;
53
+ }
54
+ return fallback;
55
+ }
56
+ function noFlashScript(themes, options) {
57
+ resolveTheme(themes, void 0, options.defaultTheme);
58
+ const data = JSON.stringify({
59
+ k: `${THEME_STORAGE_MFE}:${THEME_STORAGE_KEY}`,
60
+ d: options.defaultTheme,
61
+ s: options.followSystem ?? true,
62
+ t: Object.fromEntries(themes.map((t)=>[
63
+ t.name,
64
+ {
65
+ c: t.colorScheme,
66
+ v: themeToCssVars(t)
67
+ }
68
+ ]))
69
+ }).replace(/</g, '\\u003c');
70
+ return `(function(){try{var o=${data};var n;try{n=JSON.parse(localStorage.getItem(o.k)||'null')}catch(e){}if(!o.t[n]){n=o.d;if(o.s&&window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches){for(var x in o.t){if(o.t[x].c==='dark'){n=x;break}}}}var t=o.t[n],r=document.documentElement;for(var v in t.v){r.style.setProperty(v,t.v[v])}r.setAttribute('data-theme',n);r.style.colorScheme=t.c}catch(e){}})();`;
71
+ }
72
+ export { CSS_VAR_PREFIX, THEME_STORAGE_KEY, THEME_STORAGE_MFE, applyTheme, cssVar, cssVarName, defineTheme, noFlashScript, resolveTheme, themeToCssVars };
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type MfeStorage } from '@mfeflow/storage';
3
+ import { type Theme } from './index.js';
4
+ export interface ThemeContextValue {
5
+ theme: Theme;
6
+ themes: readonly string[];
7
+ setTheme: (name: string) => void;
8
+ }
9
+ export interface ThemeProviderProps {
10
+ themes: readonly Theme[];
11
+ defaultTheme: string;
12
+ /** Use the OS dark-mode preference when nothing is stored. Default true. */
13
+ followSystem?: boolean;
14
+ /** Defaults to the shell's storage. */
15
+ storage?: MfeStorage;
16
+ /** Where CSS variables are written. Defaults to `<html>`. */
17
+ root?: HTMLElement;
18
+ children?: ReactNode;
19
+ }
20
+ /** Mount once, in the shell. Remotes call `useTheme()` or just use `cssVar()` tokens. */
21
+ export declare function ThemeProvider({ themes, defaultTheme, followSystem, storage, root, children, }: ThemeProviderProps): import("react").JSX.Element;
22
+ /** The active theme and a setter. Works in the shell and in any remote rendered inside it. */
23
+ export declare function useTheme(): ThemeContextValue;
package/dist/react.js ADDED
@@ -0,0 +1,47 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useLayoutEffect, useMemo } from "react";
3
+ import { createStorage } from "@mfeflow/storage";
4
+ import { useStoredState } from "@mfeflow/storage/react";
5
+ import { THEME_STORAGE_KEY, THEME_STORAGE_MFE, applyTheme, resolveTheme } from "./index.js";
6
+ const ThemeContext = /*#__PURE__*/ createContext(null);
7
+ function isThemeName(value) {
8
+ return null === value || 'string' == typeof value;
9
+ }
10
+ function prefersDark() {
11
+ return "u" > typeof window && 'function' == typeof window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)').matches : false;
12
+ }
13
+ function ThemeProvider({ themes, defaultTheme, followSystem = true, storage, root, children }) {
14
+ const store = useMemo(()=>storage ?? createStorage(THEME_STORAGE_MFE), [
15
+ storage
16
+ ]);
17
+ const [stored, setStored] = useStoredState(store, THEME_STORAGE_KEY, null, isThemeName);
18
+ const theme = resolveTheme(themes, stored ?? void 0, defaultTheme, followSystem && null === stored && prefersDark());
19
+ useLayoutEffect(()=>{
20
+ applyTheme(theme, root);
21
+ }, [
22
+ theme,
23
+ root
24
+ ]);
25
+ const value = useMemo(()=>({
26
+ theme,
27
+ themes: themes.map((t)=>t.name),
28
+ setTheme: (name)=>{
29
+ if (!themes.some((t)=>t.name === name)) throw new Error(`Unknown theme "${name}"`);
30
+ setStored(name);
31
+ }
32
+ }), [
33
+ theme,
34
+ themes,
35
+ setStored
36
+ ]);
37
+ return /*#__PURE__*/ jsx(ThemeContext.Provider, {
38
+ value: value,
39
+ children: children
40
+ });
41
+ }
42
+ function useTheme() {
43
+ const value = useContext(ThemeContext);
44
+ if (!value) throw new Error('useTheme must be used inside <ThemeProvider> (mounted by the shell)');
45
+ return value;
46
+ }
47
+ export { ThemeProvider, useTheme };
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@mfeflow/theme",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Design tokens as CSS variables for micro-frontends: the shell sets the theme once, every remote inherits it. ThemeProvider, useTheme, persisted choice, no-flash script.",
5
+ "keywords": [
6
+ "micro-frontend",
7
+ "mfe",
8
+ "module-federation",
9
+ "theme",
10
+ "design-tokens",
11
+ "css-variables",
12
+ "dark-mode",
13
+ "react",
14
+ "rsbuild"
15
+ ],
16
+ "homepage": "https://mfeflow.github.io/mfeflow/",
17
+ "bugs": "https://github.com/mfeflow/mfeflow/issues",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mfeflow/mfeflow.git",
21
+ "directory": "packages/theme"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Sanjeev Sharma <speedsharma.ai@gmail.com>",
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./react": {
34
+ "types": "./dist/react.d.ts",
35
+ "import": "./dist/react.js",
36
+ "default": "./dist/react.js"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22.12"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": true
48
+ },
49
+ "dependencies": {
50
+ "@mfeflow/storage": "0.1.0"
51
+ },
52
+ "peerDependencies": {
53
+ "react": "^19.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "react": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@rsbuild/plugin-react": "2.1.1",
62
+ "@rslib/core": "1.0.5",
63
+ "@testing-library/dom": "10.4.2",
64
+ "@testing-library/react": "16.3.3",
65
+ "@types/node": "26.6.6",
66
+ "@types/react": "19.3.0",
67
+ "@types/react-dom": "19.3.0",
68
+ "jsdom": "30.1.3",
69
+ "react": "19.3.1",
70
+ "react-dom": "19.3.1",
71
+ "typescript": "7.0.2"
72
+ },
73
+ "mfeflow": {
74
+ "bucket": "singleton"
75
+ },
76
+ "scripts": {
77
+ "build": "rslib build",
78
+ "typecheck": "tsc --noEmit"
79
+ }
6
80
  }