@expo-forge/forge-ui 0.0.47
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 +385 -0
- package/babel-plugin.js +3137 -0
- package/bin/forge.js +52 -0
- package/bin/install.js +87 -0
- package/bin/native/forge-win-x64.exe +0 -0
- package/bin/prepare-publish.js +77 -0
- package/package.json +77 -0
- package/theme.js +379 -0
- package/types/index.d.ts +140 -0
- package/types/theme.d.ts +93 -0
package/bin/forge.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ForgeUI CLI bootstrap
|
|
3
|
+
// Resolves the correct platform binary and runs it.
|
|
4
|
+
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
|
|
11
|
+
const binaryPath = getBinaryPath();
|
|
12
|
+
|
|
13
|
+
if (!binaryPath) {
|
|
14
|
+
console.error(
|
|
15
|
+
"[forge] Could not find the forge binary.\n" +
|
|
16
|
+
" Run: npm install @expo-forge/forge-ui (re-runs postinstall)\n" +
|
|
17
|
+
" Or download manually from: https://github.com/semsakadanupol/forge-ui/releases",
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
23
|
+
stdio: "inherit",
|
|
24
|
+
});
|
|
25
|
+
process.exit(result.status ?? 1);
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function getBinaryPath() {
|
|
30
|
+
const binDir = path.join(__dirname, "native");
|
|
31
|
+
const name = platformBinaryName();
|
|
32
|
+
if (!name) return null;
|
|
33
|
+
|
|
34
|
+
const localBin = path.join(binDir, name);
|
|
35
|
+
if (fs.existsSync(localBin)) return localBin;
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function platformBinaryName() {
|
|
41
|
+
const p = process.platform;
|
|
42
|
+
const a = process.arch;
|
|
43
|
+
|
|
44
|
+
if (p === "win32" && a === "x64") return "forge-win-x64.exe";
|
|
45
|
+
if (p === "darwin" && a === "x64") return "forge-mac-x64";
|
|
46
|
+
if (p === "darwin" && a === "arm64") return "forge-mac-arm64";
|
|
47
|
+
if (p === "linux" && a === "x64") return "forge-linux-x64";
|
|
48
|
+
if (p === "linux" && a === "arm64") return "forge-linux-arm64";
|
|
49
|
+
|
|
50
|
+
console.error(`[forge] Unsupported platform: ${p}/${a}`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs after `npm install @expo-forge/forge-ui`.
|
|
3
|
+
// Downloads the correct platform binary into bin/native/.
|
|
4
|
+
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const https = require("https");
|
|
8
|
+
const http = require("http");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { execFileSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const VERSION = require("../package.json").version;
|
|
14
|
+
const BASE_URL = `https://github.com/semsakadanupol/forge-ui/releases/download/v${VERSION}`;
|
|
15
|
+
|
|
16
|
+
const PLATFORM_MAP = {
|
|
17
|
+
"win32-x64": "forge-win-x64.exe",
|
|
18
|
+
"darwin-x64": "forge-mac-x64",
|
|
19
|
+
"darwin-arm64": "forge-mac-arm64",
|
|
20
|
+
"linux-x64": "forge-linux-x64",
|
|
21
|
+
"linux-arm64": "forge-linux-arm64",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const key = `${process.platform}-${process.arch}`;
|
|
25
|
+
const binName = PLATFORM_MAP[key];
|
|
26
|
+
|
|
27
|
+
if (!binName) {
|
|
28
|
+
console.warn(
|
|
29
|
+
`[forge] No pre-built binary for ${key}. Build from source: https://github.com/semsakadanupol/forge-ui`,
|
|
30
|
+
);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const outDir = path.join(__dirname, "native");
|
|
35
|
+
const outPath = path.join(outDir, binName);
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(outPath)) {
|
|
38
|
+
console.log("[forge] Binary already installed.");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
const url = `${BASE_URL}/${binName}`;
|
|
45
|
+
console.log(`[forge] Downloading ${binName} from GitHub releases…`);
|
|
46
|
+
|
|
47
|
+
downloadFile(url, outPath)
|
|
48
|
+
.then(() => {
|
|
49
|
+
if (process.platform !== "win32") {
|
|
50
|
+
fs.chmodSync(outPath, 0o755);
|
|
51
|
+
}
|
|
52
|
+
console.log(`[forge] Installed → bin/native/${binName}`);
|
|
53
|
+
})
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
console.error(`[forge] Download failed: ${err.message}`);
|
|
56
|
+
console.error(" Build from source: cargo build --release");
|
|
57
|
+
fs.rmSync(outPath, { force: true });
|
|
58
|
+
process.exit(0); // Don't block npm install — user can build manually
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function downloadFile(url, dest) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const file = fs.createWriteStream(dest);
|
|
66
|
+
const get = url.startsWith("https") ? https.get : http.get;
|
|
67
|
+
|
|
68
|
+
function request(currentUrl) {
|
|
69
|
+
get(currentUrl, (res) => {
|
|
70
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
71
|
+
// Follow redirect
|
|
72
|
+
request(res.headers.location);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (res.statusCode !== 200) {
|
|
76
|
+
reject(new Error(`HTTP ${res.statusCode} for ${currentUrl}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.pipe(file);
|
|
80
|
+
file.on("finish", () => file.close(resolve));
|
|
81
|
+
file.on("error", reject);
|
|
82
|
+
}).on("error", reject);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
request(url);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
10
|
+
const outDir = path.join(__dirname, "native");
|
|
11
|
+
|
|
12
|
+
const platformMap = {
|
|
13
|
+
"win32-x64": "forge-win-x64.exe",
|
|
14
|
+
"darwin-x64": "forge-mac-x64",
|
|
15
|
+
"darwin-arm64": "forge-mac-arm64",
|
|
16
|
+
"linux-x64": "forge-linux-x64",
|
|
17
|
+
"linux-arm64": "forge-linux-arm64",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const key = `${process.platform}-${process.arch}`;
|
|
21
|
+
const packagedName = platformMap[key];
|
|
22
|
+
|
|
23
|
+
function resolveCargoCommand() {
|
|
24
|
+
if (process.env.CARGO) {
|
|
25
|
+
return process.env.CARGO;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const homeCargo = path.join(
|
|
29
|
+
os.homedir(),
|
|
30
|
+
".cargo",
|
|
31
|
+
"bin",
|
|
32
|
+
process.platform === "win32" ? "cargo.exe" : "cargo",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (fs.existsSync(homeCargo)) {
|
|
36
|
+
return homeCargo;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return "cargo";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!packagedName) {
|
|
43
|
+
console.error(`[forge] Unsupported publish platform: ${key}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cargoResult = spawnSync(resolveCargoCommand(), ["build", "--release"], {
|
|
48
|
+
cwd: rootDir,
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
shell: process.platform === "win32",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (cargoResult.status !== 0) {
|
|
54
|
+
process.exit(cargoResult.status || 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const builtBinary = path.join(
|
|
58
|
+
rootDir,
|
|
59
|
+
"target",
|
|
60
|
+
"release",
|
|
61
|
+
process.platform === "win32" ? "forge.exe" : "forge",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(builtBinary)) {
|
|
65
|
+
console.error(`[forge] Built binary not found: ${builtBinary}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
70
|
+
const packagedBinary = path.join(outDir, packagedName);
|
|
71
|
+
fs.copyFileSync(builtBinary, packagedBinary);
|
|
72
|
+
|
|
73
|
+
if (process.platform !== "win32") {
|
|
74
|
+
fs.chmodSync(packagedBinary, 0o755);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`[forge] Bundled binary updated → bin/native/${packagedName}`);
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expo-forge/forge-ui",
|
|
3
|
+
"version": "0.0.47",
|
|
4
|
+
"description": "ForgeUI — utility-first CSS/React Native style engine",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/semsakadanupol/forge-ui.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/semsakadanupol/forge-ui#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/semsakadanupol/forge-ui/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"forge-ui",
|
|
16
|
+
"css",
|
|
17
|
+
"utility-first",
|
|
18
|
+
"react-native",
|
|
19
|
+
"expo",
|
|
20
|
+
"babel-plugin"
|
|
21
|
+
],
|
|
22
|
+
"types": "types/index.d.ts",
|
|
23
|
+
"main": "theme.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./types/index.d.ts",
|
|
27
|
+
"default": "./theme.js"
|
|
28
|
+
},
|
|
29
|
+
"./theme": {
|
|
30
|
+
"types": "./types/theme.d.ts",
|
|
31
|
+
"default": "./theme.js"
|
|
32
|
+
},
|
|
33
|
+
"./babel-plugin": "./babel-plugin.js",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"typesVersions": {
|
|
37
|
+
"*": {
|
|
38
|
+
"theme": [
|
|
39
|
+
"types/theme.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"*": [
|
|
42
|
+
"types/index.d.ts"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"bin": {
|
|
47
|
+
"forge": "bin/forge.js"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=18",
|
|
51
|
+
"react-native": ">=0.72"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"react": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"react-native": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"postinstall": "node bin/install.js",
|
|
63
|
+
"prepack": "node bin/prepare-publish.js"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"bin/",
|
|
70
|
+
"babel-plugin.js",
|
|
71
|
+
"theme.js",
|
|
72
|
+
"types/"
|
|
73
|
+
],
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/theme.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const React = require("react");
|
|
4
|
+
const { createContext, useContext, useState, useEffect } = React;
|
|
5
|
+
|
|
6
|
+
let Appearance, useColorScheme, AsyncStorage;
|
|
7
|
+
let isReactNative = false;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const RN = require("react-native");
|
|
11
|
+
Appearance = RN.Appearance;
|
|
12
|
+
useColorScheme = RN.useColorScheme;
|
|
13
|
+
isReactNative = !!RN;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// Non-native environment fallback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
AsyncStorage = require("@react-native-async-storage/async-storage").default;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// AsyncStorage not available
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isBrowserDom =
|
|
25
|
+
typeof window !== "undefined" && typeof document !== "undefined";
|
|
26
|
+
|
|
27
|
+
const hasMatchMedia =
|
|
28
|
+
typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
29
|
+
|
|
30
|
+
const THEME_STORAGE_KEY = "FORGE_THEME_PREFERENCE";
|
|
31
|
+
|
|
32
|
+
function setGlobalThemeOverride(value) {
|
|
33
|
+
if (typeof globalThis !== "undefined") {
|
|
34
|
+
if (value === "light" || value === "dark") {
|
|
35
|
+
globalThis.__forgeThemeOverride = value;
|
|
36
|
+
} else {
|
|
37
|
+
delete globalThis.__forgeThemeOverride;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isLocalStorageAvailable() {
|
|
43
|
+
try {
|
|
44
|
+
if (!isBrowserDom) return false;
|
|
45
|
+
let storage;
|
|
46
|
+
try {
|
|
47
|
+
storage = window["localStorage"];
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!storage) return false;
|
|
52
|
+
const testKey = "__forge_test__";
|
|
53
|
+
try {
|
|
54
|
+
storage.setItem(testKey, "1");
|
|
55
|
+
storage.removeItem(testKey);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getSavedTheme() {
|
|
66
|
+
try {
|
|
67
|
+
if (isReactNative && AsyncStorage) {
|
|
68
|
+
const saved = await AsyncStorage.getItem(THEME_STORAGE_KEY);
|
|
69
|
+
if (saved === "light" || saved === "dark") {
|
|
70
|
+
return saved;
|
|
71
|
+
}
|
|
72
|
+
} else if (isLocalStorageAvailable()) {
|
|
73
|
+
let storage;
|
|
74
|
+
try {
|
|
75
|
+
storage = window["localStorage"];
|
|
76
|
+
} catch (e) {
|
|
77
|
+
storage = null;
|
|
78
|
+
}
|
|
79
|
+
if (storage) {
|
|
80
|
+
const saved = storage.getItem(THEME_STORAGE_KEY);
|
|
81
|
+
if (saved === "light" || saved === "dark") {
|
|
82
|
+
return saved;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Fallback if storage fails
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function saveTheme(themeValue) {
|
|
93
|
+
try {
|
|
94
|
+
if (isReactNative && AsyncStorage) {
|
|
95
|
+
if (themeValue === "light" || themeValue === "dark") {
|
|
96
|
+
await AsyncStorage.setItem(THEME_STORAGE_KEY, themeValue);
|
|
97
|
+
} else {
|
|
98
|
+
await AsyncStorage.removeItem(THEME_STORAGE_KEY);
|
|
99
|
+
}
|
|
100
|
+
} else if (isLocalStorageAvailable()) {
|
|
101
|
+
let storage;
|
|
102
|
+
try {
|
|
103
|
+
storage = window["localStorage"];
|
|
104
|
+
} catch (e) {
|
|
105
|
+
storage = null;
|
|
106
|
+
}
|
|
107
|
+
if (storage) {
|
|
108
|
+
if (themeValue === "light" || themeValue === "dark") {
|
|
109
|
+
storage.setItem(THEME_STORAGE_KEY, themeValue);
|
|
110
|
+
} else {
|
|
111
|
+
storage.removeItem(THEME_STORAGE_KEY);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// Fallback if storage fails
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getNativeSystemTheme() {
|
|
121
|
+
if (
|
|
122
|
+
isReactNative &&
|
|
123
|
+
Appearance &&
|
|
124
|
+
typeof Appearance.getColorScheme === "function"
|
|
125
|
+
) {
|
|
126
|
+
return Appearance.getColorScheme() || "light";
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getWebSystemTheme() {
|
|
132
|
+
if (hasMatchMedia) {
|
|
133
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
134
|
+
? "dark"
|
|
135
|
+
: "light";
|
|
136
|
+
}
|
|
137
|
+
return "light";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Get initial theme based on environment
|
|
141
|
+
function getInitialTheme() {
|
|
142
|
+
const nativeTheme = getNativeSystemTheme();
|
|
143
|
+
if (nativeTheme) {
|
|
144
|
+
return nativeTheme;
|
|
145
|
+
}
|
|
146
|
+
if (isBrowserDom) {
|
|
147
|
+
return getWebSystemTheme();
|
|
148
|
+
}
|
|
149
|
+
return "light";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ThemeCtx = createContext({
|
|
153
|
+
scheme: getInitialTheme(),
|
|
154
|
+
toggleTheme: () => {},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Wrap your app with <ThemeProvider> so dark: classes re-render
|
|
159
|
+
* automatically when the device color scheme changes.
|
|
160
|
+
*
|
|
161
|
+
* You can also pass a manual `theme` prop to force light/dark:
|
|
162
|
+
* <ThemeProvider theme="dark">
|
|
163
|
+
*/
|
|
164
|
+
function ThemeProvider({ children, theme }) {
|
|
165
|
+
const rnSystemScheme =
|
|
166
|
+
typeof useColorScheme === "function" ? useColorScheme() : null;
|
|
167
|
+
const nativeSystemScheme = getNativeSystemTheme();
|
|
168
|
+
const webSystemScheme =
|
|
169
|
+
!rnSystemScheme && isBrowserDom ? getWebSystemTheme() : null;
|
|
170
|
+
const systemScheme =
|
|
171
|
+
rnSystemScheme || nativeSystemScheme || webSystemScheme || "light";
|
|
172
|
+
|
|
173
|
+
const [scheme, setScheme] = useState(theme || systemScheme);
|
|
174
|
+
const [manualOverride, setManualOverride] = useState(!!theme);
|
|
175
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
176
|
+
|
|
177
|
+
// Load saved theme preference on mount
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const loadSavedTheme = async () => {
|
|
180
|
+
if (!theme) {
|
|
181
|
+
const saved = await getSavedTheme();
|
|
182
|
+
if (saved) {
|
|
183
|
+
setScheme(saved);
|
|
184
|
+
setManualOverride(true);
|
|
185
|
+
setGlobalThemeOverride(saved);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
setIsHydrated(true);
|
|
189
|
+
};
|
|
190
|
+
loadSavedTheme();
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
const toggleTheme = () => {
|
|
194
|
+
const next = scheme === "dark" ? "light" : "dark";
|
|
195
|
+
setScheme(next);
|
|
196
|
+
setManualOverride(true);
|
|
197
|
+
setGlobalThemeOverride(next);
|
|
198
|
+
// Save theme preference
|
|
199
|
+
saveTheme(next);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Keep web DOM class in sync with theme.
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (isBrowserDom) {
|
|
205
|
+
document.documentElement.classList.toggle("dark", scheme === "dark");
|
|
206
|
+
}
|
|
207
|
+
}, [scheme]);
|
|
208
|
+
|
|
209
|
+
// Manual theme override via prop.
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (theme) {
|
|
212
|
+
setScheme(theme);
|
|
213
|
+
setManualOverride(true);
|
|
214
|
+
setGlobalThemeOverride(theme);
|
|
215
|
+
} else {
|
|
216
|
+
setManualOverride(false);
|
|
217
|
+
setGlobalThemeOverride(null);
|
|
218
|
+
}
|
|
219
|
+
}, [theme]);
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!theme && !manualOverride && isHydrated) {
|
|
223
|
+
setScheme(systemScheme);
|
|
224
|
+
}
|
|
225
|
+
}, [theme, manualOverride, systemScheme, isHydrated]);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (manualOverride) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
isReactNative &&
|
|
234
|
+
Appearance &&
|
|
235
|
+
typeof Appearance.addChangeListener === "function"
|
|
236
|
+
) {
|
|
237
|
+
const current = getNativeSystemTheme();
|
|
238
|
+
if (current) setScheme(current);
|
|
239
|
+
|
|
240
|
+
const sub = Appearance.addChangeListener((event) => {
|
|
241
|
+
const next =
|
|
242
|
+
(event && event.colorScheme) || getNativeSystemTheme() || "light";
|
|
243
|
+
setScheme(next);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return () => {
|
|
247
|
+
if (sub && typeof sub.remove === "function") {
|
|
248
|
+
sub.remove();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (isBrowserDom && hasMatchMedia) {
|
|
254
|
+
// Web: listen to matchMedia changes
|
|
255
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
256
|
+
const handler = (e) => {
|
|
257
|
+
const newScheme = e.matches ? "dark" : "light";
|
|
258
|
+
setScheme(newScheme);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// New API
|
|
262
|
+
if (typeof mq.addEventListener === "function") {
|
|
263
|
+
mq.addEventListener("change", handler);
|
|
264
|
+
return () => mq.removeEventListener("change", handler);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Safari fallback
|
|
268
|
+
if (typeof mq.addListener === "function") {
|
|
269
|
+
mq.addListener(handler);
|
|
270
|
+
return () => mq.removeListener(handler);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}, [manualOverride]);
|
|
274
|
+
|
|
275
|
+
const contextValue = {
|
|
276
|
+
scheme,
|
|
277
|
+
toggleTheme,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return React.createElement(
|
|
281
|
+
ThemeCtx.Provider,
|
|
282
|
+
{ value: contextValue },
|
|
283
|
+
children,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns the current theme: "light" | "dark".
|
|
289
|
+
* Requires <ThemeProvider> to be in the tree for reactive updates.
|
|
290
|
+
*/
|
|
291
|
+
function useForgeTheme() {
|
|
292
|
+
const ctx = useContext(ThemeCtx);
|
|
293
|
+
return ctx.scheme;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Returns the current theme and a toggle function.
|
|
298
|
+
* Requires <ThemeProvider> to be in the tree for reactive updates.
|
|
299
|
+
*/
|
|
300
|
+
function useTheme() {
|
|
301
|
+
const ctx = useContext(ThemeCtx);
|
|
302
|
+
return {
|
|
303
|
+
theme: ctx.scheme,
|
|
304
|
+
toggleTheme: ctx.toggleTheme,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Forge Color Palettes ──────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
const COLORS = {
|
|
311
|
+
slate: ["#f8fafc", "#f1f5f9", "#e2e8f0", "#cbd5e1", "#94a3b8", "#64748b", "#475569", "#334155", "#1e293b", "#0f172a", "#020617"],
|
|
312
|
+
gray: ["#f9fafb", "#f3f4f6", "#e5e7eb", "#d1d5db", "#9ca3af", "#6b7280", "#4b5563", "#374151", "#1f2937", "#111827", "#030712"],
|
|
313
|
+
zinc: ["#fafafa", "#f4f4f5", "#e4e4e7", "#d4d4d8", "#a1a1aa", "#71717a", "#52525b", "#3f3f46", "#27272a", "#18181b", "#09090b"],
|
|
314
|
+
neutral: ["#fafafa", "#f5f5f5", "#e5e5e5", "#d4d4d4", "#a3a3a3", "#737373", "#525252", "#404040", "#262626", "#171717", "#0a0a0a"],
|
|
315
|
+
stone: ["#fafaf9", "#f5f5f4", "#e7e5e4", "#d6d3d1", "#a8a29e", "#78716c", "#57534e", "#44403c", "#292524", "#1c1917", "#0c0a09"],
|
|
316
|
+
red: ["#fef2f2", "#fee2e2", "#fecaca", "#fca5a5", "#f87171", "#ef4444", "#dc2626", "#b91c1c", "#991b1b", "#7f1d1d", "#450a0a"],
|
|
317
|
+
orange: ["#fff7ed", "#ffedd5", "#fed7aa", "#fdba74", "#fb923c", "#f97316", "#ea580c", "#c2410c", "#9a3412", "#7c2d12", "#431407"],
|
|
318
|
+
amber: ["#fffbeb", "#fef3c7", "#fde68a", "#fcd34d", "#fbbf24", "#f59e0b", "#d97706", "#b45309", "#92400e", "#78350f", "#451a03"],
|
|
319
|
+
yellow: ["#fefce8", "#fef9c3", "#fef08a", "#fde047", "#facc15", "#eab308", "#ca8a04", "#a16207", "#854d0e", "#713f12", "#422006"],
|
|
320
|
+
lime: ["#f7fee7", "#ecfccb", "#d9f99d", "#bef264", "#a3e635", "#84cc16", "#65a30d", "#4d7c0f", "#3f6212", "#365314", "#1a2e05"],
|
|
321
|
+
green: ["#f0fdf4", "#dcfce7", "#bbf7d0", "#86efac", "#4ade80", "#22c55e", "#16a34a", "#15803d", "#166534", "#14532d", "#052e16"],
|
|
322
|
+
emerald: ["#ecfdf5", "#d1fae5", "#a7f3d0", "#6ee7b7", "#34d399", "#10b981", "#059669", "#047857", "#065f46", "#064e3b", "#022c22"],
|
|
323
|
+
teal: ["#f0fdfa", "#ccfbf1", "#99f6e4", "#5eead4", "#2dd4bf", "#14b8a6", "#0d9488", "#0f766e", "#115e59", "#134e4a", "#042f2e"],
|
|
324
|
+
cyan: ["#ecfeff", "#cffafe", "#a5f3fc", "#67e8f9", "#22d3ee", "#06b6d4", "#0891b2", "#0e7490", "#155e75", "#164e63", "#083344"],
|
|
325
|
+
sky: ["#f0f9ff", "#e0f2fe", "#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#0284c7", "#0369a1", "#075985", "#0c4a6e", "#082f49"],
|
|
326
|
+
blue: ["#eff6ff", "#dbeafe", "#bfdbfe", "#93c5fd", "#60a5fa", "#3b82f6", "#2563eb", "#1d4ed8", "#1e40af", "#1e3a8a", "#172554"],
|
|
327
|
+
indigo: ["#eef2ff", "#e0e7ff", "#c7d2fe", "#a5b4fc", "#818cf8", "#6366f1", "#4f46e5", "#4338ca", "#3730a3", "#312e81", "#1e1b4b"],
|
|
328
|
+
violet: ["#f5f3ff", "#ede9fe", "#ddd6fe", "#c4b5fd", "#a78bfa", "#8b5cf6", "#7c3aed", "#6d28d9", "#5b21b6", "#4c1d95", "#2e1065"],
|
|
329
|
+
purple: ["#faf5ff", "#f3e8ff", "#e9d5ff", "#d8b4fe", "#c084fc", "#a855f7", "#9333ea", "#7e22ce", "#6b21a8", "#581c87", "#3b0764"],
|
|
330
|
+
fuchsia: ["#fdf4ff", "#fae8ff", "#f5d0fe", "#f0abfc", "#e879f9", "#d946ef", "#c026d3", "#a21caf", "#86198f", "#701a75", "#4a044e"],
|
|
331
|
+
pink: ["#fdf2f8", "#fce7f3", "#fbcfe8", "#f9a8d4", "#f472b6", "#ec4899", "#db2777", "#be185d", "#9d174d", "#831843", "#500724"],
|
|
332
|
+
rose: ["#fff1f2", "#ffe4e6", "#fecdd3", "#fda4af", "#fb7185", "#f43f5e", "#e11d48", "#be123c", "#9f1239", "#881337", "#4c0519"],
|
|
333
|
+
brown: ["#fdf8f6", "#f2e8e5", "#eaddd7", "#d2bab0", "#bfa094", "#a07856", "#7c5e45", "#5c4033", "#3e2723", "#2d1b10", "#1a0f07"],
|
|
334
|
+
tan: ["#fefce8", "#fef9c3", "#fef08a", "#f5deb3", "#deb887", "#c8a96e", "#b8934a", "#a07828", "#7c5c1e", "#5c4214", "#3b280a"],
|
|
335
|
+
sand: ["#fefdf5", "#fdf8e1", "#fbf0c0", "#f5e49c", "#edd57a", "#d4b858", "#b89a3e", "#9c7e28", "#7c6218", "#5c480e", "#3b2e06"],
|
|
336
|
+
chocolate: ["#fff8f5", "#feecd8", "#fdd5b0", "#f5b87a", "#e89244", "#c0622a", "#9a4520", "#7a3018", "#5a2012", "#3e140c", "#220a06"],
|
|
337
|
+
sienna: ["#fff5f2", "#fee8e0", "#fcc9b8", "#f8a080", "#f07050", "#c85a30", "#a04020", "#7a2c12", "#5c1e0e", "#3e1208", "#220a04"],
|
|
338
|
+
mint: ["#f0fff4", "#dcfce7", "#bbf7d0", "#86efac", "#4ade80", "#34d399", "#10b981", "#059669", "#047857", "#065f46", "#022c22"],
|
|
339
|
+
sage: ["#f6f8f4", "#e8efe4", "#cdddc8", "#adc8a4", "#87a87c", "#6b8c60", "#527048", "#3e5434", "#2c3c24", "#1c2818", "#0e140c"],
|
|
340
|
+
olive: ["#f8f8e8", "#eeeec8", "#d8d898", "#bcbc68", "#9c9c44", "#808028", "#686818", "#505010", "#3a3a08", "#282804", "#181802"],
|
|
341
|
+
moss: ["#f4f7f0", "#e0e8d8", "#c0d4b0", "#98b884", "#70985c", "#547c3e", "#3e6028", "#2c4818", "#1e320e", "#122008", "#081004"],
|
|
342
|
+
forest: ["#f0f8f0", "#d8ecd8", "#a8d4a8", "#78b878", "#4c9a4c", "#2d7a2d", "#1e5e1e", "#154615", "#0e300e", "#081e08", "#041004"],
|
|
343
|
+
navy: ["#f0f4ff", "#dce8ff", "#b8d0ff", "#84a8f8", "#5070e0", "#2a4ab8", "#1c348e", "#142264", "#0e1646", "#080e2e", "#040818"],
|
|
344
|
+
cobalt: ["#f0f4ff", "#dce4ff", "#b8cafe", "#84a4fc", "#4c78f0", "#1c50d8", "#1238a8", "#0c2680", "#081858", "#040e36", "#020618"],
|
|
345
|
+
royal: ["#f4f0ff", "#e4d8ff", "#c8b0ff", "#a884ff", "#8054f0", "#5c28d8", "#4018a8", "#2e0c80", "#1e085a", "#120438", "#08021a"],
|
|
346
|
+
cerulean: ["#f0f8ff", "#dceeff", "#b8daff", "#84bef8", "#4898ec", "#1c74cc", "#145aa8", "#0e4280", "#082e5a", "#041e38", "#020e1c"],
|
|
347
|
+
crimson: ["#fff0f2", "#ffd8de", "#ffaaba", "#f87892", "#e84068", "#c01848", "#940c34", "#6e0424", "#4e0218", "#30010e", "#180006"],
|
|
348
|
+
ruby: ["#fff1f3", "#ffdbdf", "#ffb0b8", "#f87880", "#e84050", "#c01830", "#94081e", "#6e0414", "#4e020c", "#300106", "#180003"],
|
|
349
|
+
wine: ["#fff0f4", "#ffd8e4", "#ffaac4", "#f878a0", "#e04070", "#b81848", "#8e0c30", "#680420", "#4a0214", "#2e010c", "#160004"],
|
|
350
|
+
burgundy: ["#fff0f2", "#ffd4da", "#ffa0ae", "#e86c82", "#c83850", "#9e1430", "#780a20", "#540414", "#3a020c", "#240108", "#100004"],
|
|
351
|
+
maroon: ["#fff0ef", "#ffd6d4", "#ffa8a4", "#e87870", "#c84040", "#9e1c1c", "#780c0c", "#540404", "#3a0202", "#240101", "#100000"],
|
|
352
|
+
gold: ["#fffdf0", "#fffadc", "#fff0a8", "#ffe068", "#f0c020", "#c89800", "#a07800", "#7c5c00", "#5c4400", "#3c2c00", "#201800"],
|
|
353
|
+
bronze: ["#fdf8f0", "#f8edd8", "#efd4a8", "#dfb070", "#c8883c", "#a86420", "#844c10", "#623808", "#462804", "#2e1a02", "#180e00"],
|
|
354
|
+
copper: ["#fdf6f0", "#fae8d8", "#f4c8a8", "#e8a070", "#d47038", "#b44c14", "#8c3808", "#682804", "#4a1c02", "#2e1001", "#180800"],
|
|
355
|
+
lavender: ["#faf6ff", "#f0e8ff", "#dfd0ff", "#c8acff", "#ae84f8", "#905ae0", "#7038c0", "#501c9c", "#380e78", "#240650", "#120230"],
|
|
356
|
+
lilac: ["#fdf5ff", "#f9eaff", "#f0d0ff", "#e0acff", "#cc84f8", "#b05ce0", "#8c3cc0", "#681ea0", "#4c0e80", "#300460", "#18023a"],
|
|
357
|
+
mauve: ["#fdf4ff", "#f8e6ff", "#edccff", "#dba4ff", "#c074f4", "#a048d8", "#7c28b0", "#5c1290", "#40086e", "#280448", "#12022a"],
|
|
358
|
+
orchid: ["#fdf2ff", "#fae0ff", "#f0baff", "#e088ff", "#cc4ef8", "#a818e0", "#8408b8", "#620094", "#460070", "#2c0048", "#14002c"],
|
|
359
|
+
coral: ["#fff4f0", "#ffe4dc", "#ffbcac", "#ff8e78", "#f85f48", "#e03020", "#b81c10", "#8e0c06", "#660402", "#400200", "#200100"],
|
|
360
|
+
salmon: ["#fff5f3", "#ffe6e0", "#ffc4b8", "#ff9888", "#f86a58", "#e04030", "#b82820", "#8e1412", "#660808", "#400404", "#200202"],
|
|
361
|
+
peach: ["#fff8f3", "#ffedd8", "#ffd4a8", "#ffb070", "#f88a3c", "#e06018", "#b84008", "#8c2804", "#641802", "#3e0c00", "#200600"],
|
|
362
|
+
turquoise: ["#f0fffe", "#ccfefa", "#98fcf4", "#50f0e8", "#14d8d0", "#00b0a8", "#008880", "#006860", "#004844", "#002e2c", "#001816"],
|
|
363
|
+
aqua: ["#f0fffe", "#ccfefa", "#98fdfa", "#52f0f2", "#14d8e0", "#00b4be", "#008c94", "#006870", "#00484e", "#002e34", "#00181c"],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Returns Forge color palettes for use in native components.
|
|
368
|
+
* Each color has 11 shades (0-10).
|
|
369
|
+
*
|
|
370
|
+
* Usage:
|
|
371
|
+
* const colors = useColors();
|
|
372
|
+
* <View style={{ backgroundColor: colors.blue[5] }} />
|
|
373
|
+
*/
|
|
374
|
+
function useColors() {
|
|
375
|
+
return COLORS;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
module.exports = { ThemeProvider, useForgeTheme, useTheme, useColors };
|
|
379
|
+
|