@reforma/site 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ambient.d.ts +16 -0
- package/dist/ambient.d.ts.map +1 -0
- package/dist/ambient.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/metadata.d.ts +30 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +28 -0
- package/dist/postcss.d.ts +16 -0
- package/dist/postcss.d.ts.map +1 -0
- package/dist/postcss.js +206 -0
- package/dist/root.d.ts +34 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +109 -0
- package/package.json +59 -5
- package/README.md +0 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
declare global {
|
|
3
|
+
/**
|
|
4
|
+
* Branded prop types for the Reforma properties panel.
|
|
5
|
+
* Prefer these over bare `string` when the prop should open Color / Image / Link UI.
|
|
6
|
+
*/
|
|
7
|
+
namespace Reforma {
|
|
8
|
+
/** CSS color string → Color control. */
|
|
9
|
+
type Color = string;
|
|
10
|
+
/** Image / media URL → Image control. */
|
|
11
|
+
type ImageUrl = string;
|
|
12
|
+
/** Page path or URL → Link control (pages picker). */
|
|
13
|
+
type Href = string;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=ambient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ambient.d.ts","sourceRoot":"","sources":["../src/ambient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC;AAEV,OAAO,CAAC,MAAM,CAAC;IACX;;;OAGG;IACH,UAAU,OAAO,CAAC;QACd,wCAAwC;QACxC,KAAK,KAAK,GAAG,MAAM,CAAC;QAEpB,yCAAyC;QACzC,KAAK,QAAQ,GAAG,MAAM,CAAC;QAEvB,sDAAsD;QACtD,KAAK,IAAI,GAAG,MAAM,CAAC;KACtB;CACJ"}
|
package/dist/ambient.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare function projectMetadata(fallback: {
|
|
2
|
+
title: string;
|
|
3
|
+
description: string;
|
|
4
|
+
}): {
|
|
5
|
+
title: {
|
|
6
|
+
template: string;
|
|
7
|
+
default: string;
|
|
8
|
+
};
|
|
9
|
+
description: string;
|
|
10
|
+
icons: {
|
|
11
|
+
icon: string | {
|
|
12
|
+
url: string;
|
|
13
|
+
type: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
robots: {
|
|
17
|
+
index: boolean;
|
|
18
|
+
follow: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function projectRobots(): {
|
|
22
|
+
rules: {
|
|
23
|
+
userAgent: string;
|
|
24
|
+
disallow: string;
|
|
25
|
+
} | {
|
|
26
|
+
userAgent: string;
|
|
27
|
+
allow: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE;;QAMpE,QAAQ;QACR,OAAO;;;;QAIP,IAAI;;;;;;;;;EAQf;AAED,wBAAgB,aAAa;IAIrB,KAAK;;;;;;;EAKZ"}
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function projectMetadata(fallback) {
|
|
2
|
+
const faviconUrl = process.env.REFORMA_FAVICON_URL || '/favicon.svg';
|
|
3
|
+
const indexingOn = process.env.REFORMA_INDEXING !== '0';
|
|
4
|
+
return {
|
|
5
|
+
title: {
|
|
6
|
+
template: '%s',
|
|
7
|
+
default: process.env.REFORMA_DEFAULT_TITLE || fallback.title,
|
|
8
|
+
},
|
|
9
|
+
description: process.env.REFORMA_DEFAULT_DESCRIPTION || fallback.description,
|
|
10
|
+
icons: {
|
|
11
|
+
icon: faviconUrl.endsWith('.svg')
|
|
12
|
+
? { url: faviconUrl, type: 'image/svg+xml' }
|
|
13
|
+
: faviconUrl,
|
|
14
|
+
},
|
|
15
|
+
robots: indexingOn
|
|
16
|
+
? { index: true, follow: true }
|
|
17
|
+
: { index: false, follow: false },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function projectRobots() {
|
|
21
|
+
const blocked = process.env.REFORMA_INDEXING === '0';
|
|
22
|
+
return {
|
|
23
|
+
rules: {
|
|
24
|
+
userAgent: '*',
|
|
25
|
+
...(blocked ? { disallow: '/' } : { allow: '/' }),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite declaration `url()` values the Next bundler would fail to resolve.
|
|
3
|
+
* Empty values become an empty data url. A missing relative file becomes a
|
|
4
|
+
* site-root url when the CSS file path is known. Fragment, scheme, and
|
|
5
|
+
* site-root urls stay. Colocated files that exist stay so they still bundle.
|
|
6
|
+
*/
|
|
7
|
+
export declare function rewriteDeclarationUrls(value: any, options?: {}): string;
|
|
8
|
+
declare function plugin(options?: {}): {
|
|
9
|
+
postcssPlugin: string;
|
|
10
|
+
OnceExit(root: any): void;
|
|
11
|
+
};
|
|
12
|
+
declare namespace plugin {
|
|
13
|
+
var postcss: boolean;
|
|
14
|
+
}
|
|
15
|
+
export default plugin;
|
|
16
|
+
//# sourceMappingURL=postcss.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postcss.d.ts","sourceRoot":"","sources":["../src/postcss.js"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,KAAA,EAAE,OAAO,KAAK,UAiDzD;AA0LD,iBAAS,MAAM,CAAC,OAAO,KAAK;;;EAoB3B;kBApBQ,MAAM;;;eAwBA,MAAM"}
|
package/dist/postcss.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
4
|
+
const EMPTY_URL = 'url("data:,")';
|
|
5
|
+
/**
|
|
6
|
+
* Rewrite declaration `url()` values the Next bundler would fail to resolve.
|
|
7
|
+
* Empty values become an empty data url. A missing relative file becomes a
|
|
8
|
+
* site-root url when the CSS file path is known. Fragment, scheme, and
|
|
9
|
+
* site-root urls stay. Colocated files that exist stay so they still bundle.
|
|
10
|
+
*/
|
|
11
|
+
export function rewriteDeclarationUrls(value, options = {}) {
|
|
12
|
+
const exists = options.exists ?? existsSync;
|
|
13
|
+
const from = knownCssPath(options.from);
|
|
14
|
+
let out = '';
|
|
15
|
+
let quote = '';
|
|
16
|
+
let i = 0;
|
|
17
|
+
while (i < value.length) {
|
|
18
|
+
const ch = value[i];
|
|
19
|
+
if (quote) {
|
|
20
|
+
out += ch;
|
|
21
|
+
if (ch === '\\' && i + 1 < value.length) {
|
|
22
|
+
out += value[i + 1];
|
|
23
|
+
i += 2;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (ch === quote) {
|
|
27
|
+
quote = '';
|
|
28
|
+
}
|
|
29
|
+
i += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '"' || ch === '\'') {
|
|
33
|
+
quote = ch;
|
|
34
|
+
out += ch;
|
|
35
|
+
i += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (isUrlCall(value, i)) {
|
|
39
|
+
const parsed = readUrlCall(value, i);
|
|
40
|
+
if (parsed) {
|
|
41
|
+
out += rewriteUrl(parsed.inner, from, exists) ?? value.slice(i, parsed.end);
|
|
42
|
+
i = parsed.end;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
out += ch;
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function knownCssPath(from) {
|
|
52
|
+
if (typeof from !== 'string') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const trimmed = from.trim();
|
|
56
|
+
if (!trimmed || trimmed.startsWith('<')) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
function isUrlCall(value, index) {
|
|
62
|
+
if (value.slice(index, index + 4).toLowerCase() !== 'url(') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return index === 0 || !/[a-z0-9_-]/i.test(value[index - 1]);
|
|
66
|
+
}
|
|
67
|
+
function readUrlCall(value, index) {
|
|
68
|
+
let cursor = index + 4;
|
|
69
|
+
while (cursor < value.length && /\s/.test(value[cursor])) {
|
|
70
|
+
cursor += 1;
|
|
71
|
+
}
|
|
72
|
+
if (cursor >= value.length) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const quote = value[cursor];
|
|
76
|
+
let inner = '';
|
|
77
|
+
let end = cursor;
|
|
78
|
+
if (quote === '"' || quote === '\'') {
|
|
79
|
+
const string = readString(value, quote, cursor + 1);
|
|
80
|
+
if (!string) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
inner = string.value;
|
|
84
|
+
end = string.end;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const close = value.indexOf(')', cursor);
|
|
88
|
+
if (close < 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
inner = value.slice(cursor, close).trim();
|
|
92
|
+
end = close;
|
|
93
|
+
}
|
|
94
|
+
while (end < value.length && /\s/.test(value[end])) {
|
|
95
|
+
end += 1;
|
|
96
|
+
}
|
|
97
|
+
if (value[end] !== ')') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return { inner, end: end + 1 };
|
|
101
|
+
}
|
|
102
|
+
function readString(value, quote, start) {
|
|
103
|
+
let out = '';
|
|
104
|
+
for (let i = start; i < value.length; i += 1) {
|
|
105
|
+
const ch = value[i];
|
|
106
|
+
if (ch === '\\' && i + 1 < value.length) {
|
|
107
|
+
out += value[i + 1];
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (ch === quote) {
|
|
112
|
+
return { value: out, end: i + 1 };
|
|
113
|
+
}
|
|
114
|
+
out += ch;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function rewriteUrl(inner, from, exists) {
|
|
119
|
+
const trimmed = inner.trim();
|
|
120
|
+
if (!trimmed) {
|
|
121
|
+
return EMPTY_URL;
|
|
122
|
+
}
|
|
123
|
+
if (trimmed.startsWith('#')
|
|
124
|
+
|| trimmed.startsWith('/')
|
|
125
|
+
|| trimmed.startsWith('~')
|
|
126
|
+
|| SCHEME.test(trimmed)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!from || exists(candidatePath(from, trimmed))) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const rooted = toSiteRoot(trimmed);
|
|
133
|
+
if (!rooted) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return formatCssUrl(rooted);
|
|
137
|
+
}
|
|
138
|
+
function candidatePath(from, raw) {
|
|
139
|
+
const query = raw.indexOf('?');
|
|
140
|
+
const hash = raw.indexOf('#');
|
|
141
|
+
let end = raw.length;
|
|
142
|
+
if (query >= 0) {
|
|
143
|
+
end = Math.min(end, query);
|
|
144
|
+
}
|
|
145
|
+
if (hash >= 0) {
|
|
146
|
+
end = Math.min(end, hash);
|
|
147
|
+
}
|
|
148
|
+
let pathname = raw.slice(0, end);
|
|
149
|
+
try {
|
|
150
|
+
pathname = decodeURIComponent(pathname);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Keep the raw path when it is not valid encoding.
|
|
154
|
+
}
|
|
155
|
+
return path.resolve(path.dirname(from), pathname);
|
|
156
|
+
}
|
|
157
|
+
function toSiteRoot(raw) {
|
|
158
|
+
let url;
|
|
159
|
+
try {
|
|
160
|
+
url = new URL(raw, 'https://assets.invalid/');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (url.origin !== 'https://assets.invalid') {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
let pathname = url.pathname;
|
|
169
|
+
try {
|
|
170
|
+
pathname = decodeURIComponent(pathname);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Keep the encoded pathname.
|
|
174
|
+
}
|
|
175
|
+
return `${pathname}${url.search}${url.hash}`;
|
|
176
|
+
}
|
|
177
|
+
function formatCssUrl(raw) {
|
|
178
|
+
if (/^[^\s"'()\\]+$/.test(raw)) {
|
|
179
|
+
return `url(${raw})`;
|
|
180
|
+
}
|
|
181
|
+
const escaped = raw
|
|
182
|
+
.replace(/\\/g, '\\\\')
|
|
183
|
+
.replace(/"/g, '\\"')
|
|
184
|
+
.replace(/\n/g, '\\a ');
|
|
185
|
+
return `url("${escaped}")`;
|
|
186
|
+
}
|
|
187
|
+
function plugin(options = {}) {
|
|
188
|
+
const exists = options.exists ?? existsSync;
|
|
189
|
+
return {
|
|
190
|
+
postcssPlugin: 'reforma-asset-url',
|
|
191
|
+
OnceExit(root) {
|
|
192
|
+
root.walkDecls((decl) => {
|
|
193
|
+
if (!decl.value || !/url\(/i.test(decl.value)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const from = decl.source?.input?.file || decl.source?.input?.from;
|
|
197
|
+
const next = rewriteDeclarationUrls(decl.value, { from, exists });
|
|
198
|
+
if (next !== decl.value) {
|
|
199
|
+
decl.value = next;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
plugin.postcss = true;
|
|
206
|
+
export default plugin;
|
package/dist/root.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type Appearance = 'light' | 'dark';
|
|
3
|
+
export type ModeDocument = {
|
|
4
|
+
default?: string | null;
|
|
5
|
+
modifiers?: {
|
|
6
|
+
name: string;
|
|
7
|
+
contexts?: string[];
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
export type ModeAppearances = Record<string, Appearance>;
|
|
11
|
+
export type SystemModes = {
|
|
12
|
+
light: string;
|
|
13
|
+
dark: string;
|
|
14
|
+
} | null;
|
|
15
|
+
type ModeState = {
|
|
16
|
+
mode: string;
|
|
17
|
+
siteMode: string;
|
|
18
|
+
preview: string | null;
|
|
19
|
+
preference: string | null;
|
|
20
|
+
appearance: Appearance;
|
|
21
|
+
};
|
|
22
|
+
type ModeContextValue = ModeState & {
|
|
23
|
+
setMode: (mode: string | null) => void;
|
|
24
|
+
};
|
|
25
|
+
/** Visitor mode. Without `SiteRoot`, returns the default light appearance. */
|
|
26
|
+
export declare function useMode(): ModeContextValue;
|
|
27
|
+
export declare function SiteRoot({ children, modes, appearances, systemModes, }: {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
modes: ModeDocument;
|
|
30
|
+
appearances?: ModeAppearances;
|
|
31
|
+
systemModes?: SystemModes;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAgE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAErG,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C,MAAM,MAAM,YAAY,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEzD,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAEjE,KAAK,SAAS,GAAG;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,UAAU,CAAC;CAC1B,CAAC;AAEF,KAAK,gBAAgB,GAAG,SAAS,GAAG;IAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAAE,CAAC;AAe/E,8EAA8E;AAC9E,wBAAgB,OAAO,qBAEtB;AAMD,wBAAgB,QAAQ,CAAC,EACrB,QAAQ,EACR,KAAK,EACL,WAAgC,EAChC,WAAkB,GACrB,EAAE;IACC,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;CAC7B,+BA6FA"}
|
package/dist/root.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
const defaultAppearances = { light: 'light', dark: 'dark' };
|
|
5
|
+
const detachedMode = {
|
|
6
|
+
mode: '',
|
|
7
|
+
siteMode: '',
|
|
8
|
+
preview: null,
|
|
9
|
+
preference: null,
|
|
10
|
+
appearance: 'light',
|
|
11
|
+
setMode: () => { },
|
|
12
|
+
};
|
|
13
|
+
const ModeContext = createContext(null);
|
|
14
|
+
/** Visitor mode. Without `SiteRoot`, returns the default light appearance. */
|
|
15
|
+
export function useMode() {
|
|
16
|
+
return useContext(ModeContext) ?? detachedMode;
|
|
17
|
+
}
|
|
18
|
+
function modeNames(modes) {
|
|
19
|
+
return modes.modifiers?.find(modifier => modifier.name === 'mode')?.contexts ?? [];
|
|
20
|
+
}
|
|
21
|
+
export function SiteRoot({ children, modes, appearances = defaultAppearances, systemModes = null, }) {
|
|
22
|
+
const defaultMode = modes.default ?? '';
|
|
23
|
+
const [modeState, setModeState] = useState({
|
|
24
|
+
mode: defaultMode,
|
|
25
|
+
siteMode: defaultMode,
|
|
26
|
+
preview: null,
|
|
27
|
+
preference: null,
|
|
28
|
+
appearance: appearances[defaultMode] ?? 'light',
|
|
29
|
+
});
|
|
30
|
+
const latestState = useRef(null);
|
|
31
|
+
const selectVisitorMode = useRef(null);
|
|
32
|
+
// Hydrate the server default first, then apply visitor preferences without masking mismatches.
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
const storageKey = 'project-mode';
|
|
35
|
+
const names = modeNames(modes);
|
|
36
|
+
const defaultMode = modes.default ?? '';
|
|
37
|
+
const valid = (mode) => mode !== null && names.includes(mode) ? mode : null;
|
|
38
|
+
const media = systemModes ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
|
39
|
+
let preference = latestState.current?.preference ?? null;
|
|
40
|
+
if (!latestState.current) {
|
|
41
|
+
try {
|
|
42
|
+
preference = localStorage.getItem(storageKey);
|
|
43
|
+
}
|
|
44
|
+
catch { /* Storage may be blocked; in-memory selection still works. */ }
|
|
45
|
+
}
|
|
46
|
+
preference = valid(preference);
|
|
47
|
+
let preview = process.env.NODE_ENV !== 'production' ? valid(latestState.current?.preview ?? null) : null;
|
|
48
|
+
const publishMode = () => {
|
|
49
|
+
const systemMode = systemModes && media
|
|
50
|
+
? valid(systemModes[media.matches ? 'dark' : 'light'])
|
|
51
|
+
: null;
|
|
52
|
+
const siteMode = preference ?? systemMode ?? defaultMode;
|
|
53
|
+
const mode = preview ?? siteMode;
|
|
54
|
+
const appearance = appearances[mode] === 'dark' ? 'dark' : 'light';
|
|
55
|
+
const next = { mode, siteMode, preview, preference, appearance };
|
|
56
|
+
document.documentElement.setAttribute('data-theme', mode);
|
|
57
|
+
document.documentElement.setAttribute('data-appearance', appearance);
|
|
58
|
+
document.documentElement.style.colorScheme = appearance;
|
|
59
|
+
latestState.current = next;
|
|
60
|
+
setModeState(next);
|
|
61
|
+
window.dispatchEvent(new CustomEvent('project:mode-change', { detail: next }));
|
|
62
|
+
};
|
|
63
|
+
const onStorage = (event) => {
|
|
64
|
+
if (event.storageArea && event.storageArea !== window.localStorage)
|
|
65
|
+
return;
|
|
66
|
+
if (event.key !== storageKey && event.key !== null)
|
|
67
|
+
return;
|
|
68
|
+
preference = valid(event.newValue);
|
|
69
|
+
publishMode();
|
|
70
|
+
};
|
|
71
|
+
selectVisitorMode.current = mode => {
|
|
72
|
+
preference = valid(mode);
|
|
73
|
+
try {
|
|
74
|
+
if (preference === null)
|
|
75
|
+
localStorage.removeItem(storageKey);
|
|
76
|
+
else
|
|
77
|
+
localStorage.setItem(storageKey, preference);
|
|
78
|
+
}
|
|
79
|
+
catch { /* Keep selection usable when persistence is unavailable. */ }
|
|
80
|
+
publishMode();
|
|
81
|
+
};
|
|
82
|
+
const onPreview = (event) => {
|
|
83
|
+
const request = event.detail;
|
|
84
|
+
if (request.preview !== undefined) {
|
|
85
|
+
if (request.preview !== null && !names.includes(request.preview)) {
|
|
86
|
+
request.error = 'This mode is no longer available. Reload the project variables.';
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
preview = request.preview;
|
|
90
|
+
publishMode();
|
|
91
|
+
}
|
|
92
|
+
if (latestState.current) {
|
|
93
|
+
request.state = latestState.current;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
window.addEventListener('storage', onStorage);
|
|
97
|
+
media?.addEventListener('change', publishMode);
|
|
98
|
+
if (process.env.NODE_ENV !== 'production')
|
|
99
|
+
window.addEventListener('reforma:token-mode', onPreview);
|
|
100
|
+
publishMode();
|
|
101
|
+
return () => {
|
|
102
|
+
window.removeEventListener('storage', onStorage);
|
|
103
|
+
selectVisitorMode.current = null;
|
|
104
|
+
media?.removeEventListener('change', publishMode);
|
|
105
|
+
window.removeEventListener('reforma:token-mode', onPreview);
|
|
106
|
+
};
|
|
107
|
+
}, [appearances, modes, systemModes]);
|
|
108
|
+
return (_jsx(ModeContext, { value: { ...modeState, setMode: mode => selectVisitorMode.current?.(mode) }, children: children }));
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@reforma/site",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Visitor runtime for a Reforma site: mode, publish metadata, and the PostCSS url guard.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": "Reforma, Inc. <dev@reforma.ai>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/reforma-ai/reforma",
|
|
11
|
+
"directory": "packages/site"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./postcss": {
|
|
19
|
+
"types": "./dist/postcss.d.ts",
|
|
20
|
+
"default": "./dist/postcss.js"
|
|
21
|
+
},
|
|
22
|
+
"./ambient": {
|
|
23
|
+
"types": "./dist/ambient.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"dev:prepare": "bun run build",
|
|
38
|
+
"build:publish": "bun run build",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
41
|
+
"lint": "eslint src",
|
|
42
|
+
"lint:fix": "eslint src --fix"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": "^19.2.8",
|
|
46
|
+
"react-dom": "^19.2.8"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@reforma/config-typescript": "workspace:*",
|
|
50
|
+
"@reforma/eslint-config": "workspace:*",
|
|
51
|
+
"@types/node": "^24",
|
|
52
|
+
"@types/react": "^19",
|
|
53
|
+
"@types/react-dom": "^19",
|
|
54
|
+
"happy-dom": "20.10.6",
|
|
55
|
+
"react": "^19.2.8",
|
|
56
|
+
"react-dom": "^19.2.8",
|
|
57
|
+
"typescript": "^7",
|
|
58
|
+
"vitest": "^5.0.1"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/README.md
DELETED