@upstart.gg/vite-plugins 0.1.83 → 0.1.85
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/site-theme.d.ts +55 -0
- package/dist/site-theme.d.ts.map +1 -0
- package/dist/site-theme.js +184 -0
- package/dist/site-theme.js.map +1 -0
- package/dist/upstart-editor-api.d.ts +63 -1
- package/dist/upstart-editor-api.d.ts.map +1 -1
- package/dist/upstart-editor-api.js +233 -1
- package/dist/upstart-editor-api.js.map +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-attrs.js +65 -2
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +2 -24
- package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +7 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js +33 -0
- package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +35 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/labels.js +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/move-controls.js +434 -0
- package/dist/vite-plugin-upstart-editor/runtime/move-controls.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +9 -3
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/site-theme.ts +205 -0
- package/src/tests/move-controls.test.ts +86 -0
- package/src/tests/site-theme.test.ts +122 -0
- package/src/tests/upstart-editor-api-move-element.test.ts +220 -0
- package/src/tests/vite-plugin-upstart-attrs.test.ts +165 -4
- package/src/upstart-editor-api.ts +294 -0
- package/src/vite-plugin-upstart-attrs.ts +93 -2
- package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +2 -40
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +17 -3
- package/src/vite-plugin-upstart-editor/runtime/controls-ui.ts +48 -0
- package/src/vite-plugin-upstart-editor/runtime/index.ts +46 -0
- package/src/vite-plugin-upstart-editor/runtime/labels.ts +10 -0
- package/src/vite-plugin-upstart-editor/runtime/move-controls.ts +536 -0
- package/src/vite-plugin-upstart-editor/runtime/types.ts +9 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/site-theme.d.ts
|
|
4
|
+
/** The theme colors the panel edits, in the theme file's naming. */
|
|
5
|
+
declare const EDITABLE_THEME_COLORS: readonly ["base100", "base200", "base300", "baseContent", "primary", "primaryContent", "secondary", "secondaryContent", "accent", "accentContent", "neutral", "neutralContent"];
|
|
6
|
+
type EditableThemeColor = (typeof EDITABLE_THEME_COLORS)[number];
|
|
7
|
+
type ThemeColors = Partial<Record<EditableThemeColor, string>>;
|
|
8
|
+
declare const payloadGetTheme: z.ZodObject<{
|
|
9
|
+
action: z.ZodLiteral<"getTheme">;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
type PayloadGetTheme = z.infer<typeof payloadGetTheme>;
|
|
12
|
+
declare const payloadSetTheme: z.ZodObject<{
|
|
13
|
+
action: z.ZodLiteral<"setTheme">;
|
|
14
|
+
colors: z.ZodRecord<z.ZodEnum<{
|
|
15
|
+
accent: "accent";
|
|
16
|
+
accentContent: "accentContent";
|
|
17
|
+
base100: "base100";
|
|
18
|
+
base200: "base200";
|
|
19
|
+
base300: "base300";
|
|
20
|
+
baseContent: "baseContent";
|
|
21
|
+
neutral: "neutral";
|
|
22
|
+
neutralContent: "neutralContent";
|
|
23
|
+
primary: "primary";
|
|
24
|
+
primaryContent: "primaryContent";
|
|
25
|
+
secondary: "secondary";
|
|
26
|
+
secondaryContent: "secondaryContent";
|
|
27
|
+
}> & z.core.$partial, z.ZodString>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
type PayloadSetTheme = z.infer<typeof payloadSetTheme>;
|
|
30
|
+
interface ThemePalette {
|
|
31
|
+
/** Stable id: "current" for the chosen mockup, "palette-<n>" for its variants. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** The catalog palette's description, when known (e.g. "Blush base, deep rose text…"). */
|
|
34
|
+
label: string;
|
|
35
|
+
colors: ThemeColors;
|
|
36
|
+
}
|
|
37
|
+
type GetThemeResult = {
|
|
38
|
+
success: true;
|
|
39
|
+
themeId: string;
|
|
40
|
+
colors: ThemeColors;
|
|
41
|
+
palettes: ThemePalette[];
|
|
42
|
+
} | {
|
|
43
|
+
success: false;
|
|
44
|
+
error: string;
|
|
45
|
+
};
|
|
46
|
+
type SetThemeResult = {
|
|
47
|
+
success: true;
|
|
48
|
+
filePaths: string[];
|
|
49
|
+
} | {
|
|
50
|
+
success: false;
|
|
51
|
+
error: string;
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { EDITABLE_THEME_COLORS, EditableThemeColor, GetThemeResult, PayloadGetTheme, PayloadSetTheme, SetThemeResult, ThemeColors, ThemePalette, payloadGetTheme, payloadSetTheme };
|
|
55
|
+
//# sourceMappingURL=site-theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-theme.d.ts","names":[],"sources":["../src/site-theme.ts"],"mappings":";;;;cAiBa,qBAAA;AAAA,KAcD,kBAAA,WAA6B,qBAAA;AAAA,KAC7B,WAAA,GAAc,OAAA,CAAQ,MAAA,CAAO,kBAAA;AAAA,cAS5B,eAAA,EAAe,CAAA,CAAA,SAAA;;;KAChB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,eAAA;AAAA,cAEhC,eAAA,EAAe,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;KAIhB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,eAAA;AAAA,UAE5B,YAAA;;EAEf,EAAA;;EAEA,KAAA;EACA,MAAA,EAAQ,WAAA;AAAA;AAAA,KAGE,cAAA;EACN,OAAA;EAAe,OAAA;EAAiB,MAAA,EAAQ,WAAA;EAAa,QAAA,EAAU,YAAA;AAAA;EAC/D,OAAA;EAAgB,KAAA;AAAA;AAAA,KAEV,cAAA;EAAmB,OAAA;EAAe,SAAA;AAAA;EAA0B,OAAA;EAAgB,KAAA;AAAA"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
//#region src/site-theme.ts
|
|
5
|
+
/**
|
|
6
|
+
* The site's colors, editable from the editor's "Colors" panel.
|
|
7
|
+
*
|
|
8
|
+
* A site's colors live in its theme file (`app/config/themes/<id>.json`, the one `site.json` names
|
|
9
|
+
* as the default), read by the theme plugin at build time. A new site also keeps the palettes the
|
|
10
|
+
* client was offered when choosing its look (`docs/mockups/<direction>/` and its `palettes/<n>/`
|
|
11
|
+
* variants, see the `PaletteVariants` tool): the panel offers them again, next to a free picker.
|
|
12
|
+
*
|
|
13
|
+
* Saving also records in `docs/DESIGN.md` that the user chose the colors themselves: agents read
|
|
14
|
+
* the brief before any UI work, and would otherwise bring the brief's palette back.
|
|
15
|
+
*/
|
|
16
|
+
/** The theme colors the panel edits, in the theme file's naming. */
|
|
17
|
+
const EDITABLE_THEME_COLORS = [
|
|
18
|
+
"base100",
|
|
19
|
+
"base200",
|
|
20
|
+
"base300",
|
|
21
|
+
"baseContent",
|
|
22
|
+
"primary",
|
|
23
|
+
"primaryContent",
|
|
24
|
+
"secondary",
|
|
25
|
+
"secondaryContent",
|
|
26
|
+
"accent",
|
|
27
|
+
"accentContent",
|
|
28
|
+
"neutral",
|
|
29
|
+
"neutralContent"
|
|
30
|
+
];
|
|
31
|
+
/** A CSS color as theme files hold them: oklch(), hex, rgb() or hsl(). */
|
|
32
|
+
const cssColor = z.string().trim().max(80).regex(/^(#[0-9a-fA-F]{3,8}|(oklch|oklab|rgba?|hsla?)\([0-9.,%\s/+-]+\))$/, "Not a CSS color");
|
|
33
|
+
const payloadGetTheme = z.object({ action: z.literal("getTheme") });
|
|
34
|
+
const payloadSetTheme = z.object({
|
|
35
|
+
action: z.literal("setTheme"),
|
|
36
|
+
colors: z.partialRecord(z.enum(EDITABLE_THEME_COLORS), cssColor)
|
|
37
|
+
});
|
|
38
|
+
/** Mockup token (Tailwind config of a mockup) → theme color. */
|
|
39
|
+
const MOCKUP_TOKENS = {
|
|
40
|
+
"base-100": "base100",
|
|
41
|
+
"base-200": "base200",
|
|
42
|
+
"base-300": "base300",
|
|
43
|
+
"base-content": "baseContent",
|
|
44
|
+
primary: "primary",
|
|
45
|
+
"primary-content": "primaryContent",
|
|
46
|
+
secondary: "secondary",
|
|
47
|
+
"secondary-content": "secondaryContent",
|
|
48
|
+
accent: "accent",
|
|
49
|
+
"accent-content": "accentContent",
|
|
50
|
+
neutral: "neutral",
|
|
51
|
+
"neutral-content": "neutralContent"
|
|
52
|
+
};
|
|
53
|
+
/** The colors a mockup sets in its Tailwind config, in the theme's naming. */
|
|
54
|
+
function mockupThemeColors(html) {
|
|
55
|
+
const script = [...html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)].find((m) => m[1].includes("tailwind.config"))?.[1];
|
|
56
|
+
const colors = {};
|
|
57
|
+
if (!script) return colors;
|
|
58
|
+
for (const [token, key] of Object.entries(MOCKUP_TOKENS)) {
|
|
59
|
+
const value = new RegExp(`['"]?${token.replace(/-/g, "\\-")}['"]?\\s*:\\s*(['"])([^'"]*)\\1`).exec(script)?.[2];
|
|
60
|
+
if (value && cssColor.safeParse(value).success) colors[key] = value.trim();
|
|
61
|
+
}
|
|
62
|
+
return colors;
|
|
63
|
+
}
|
|
64
|
+
/** The block `setTheme` keeps up to date at the end of the brief. */
|
|
65
|
+
const BRIEF_START = "<!-- upstart:theme-colors -->";
|
|
66
|
+
const BRIEF_END = "<!-- /upstart:theme-colors -->";
|
|
67
|
+
function briefWithUserColors(brief, themeFile) {
|
|
68
|
+
const block = [
|
|
69
|
+
BRIEF_START,
|
|
70
|
+
"## Colors chosen by the user",
|
|
71
|
+
`The user set the site's colors themselves in the editor. \`${themeFile}\` is the source of truth for colors and wins over any palette above: keep its values, and use its tokens (bg-base-100, text-primary…) rather than new colors.`,
|
|
72
|
+
BRIEF_END
|
|
73
|
+
].join("\n");
|
|
74
|
+
const start = brief.indexOf(BRIEF_START);
|
|
75
|
+
const end = brief.indexOf(BRIEF_END);
|
|
76
|
+
if (start !== -1 && end > start) return `${brief.slice(0, start)}${block}${brief.slice(end + 30)}`;
|
|
77
|
+
return `${brief.trimEnd()}\n\n${block}\n`;
|
|
78
|
+
}
|
|
79
|
+
/** The theme file the site uses by default, from `app/config/site.json`. */
|
|
80
|
+
async function defaultThemeFile(projectRoot) {
|
|
81
|
+
try {
|
|
82
|
+
const site = JSON.parse(await fs.readFile(path.join(projectRoot, "app", "config", "site.json"), "utf-8"));
|
|
83
|
+
const themeId = site.themes?.[site.themes.default ?? "light"] ?? site.themes?.light;
|
|
84
|
+
if (!themeId || !/^[a-z0-9-]+$/.test(themeId)) return null;
|
|
85
|
+
return {
|
|
86
|
+
themeId,
|
|
87
|
+
filePath: path.join(projectRoot, "app", "config", "themes", `${themeId}.json`)
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** The mockup the brief names as its reference (`docs/mockups/<direction>/…`). */
|
|
94
|
+
async function chosenMockup(projectRoot) {
|
|
95
|
+
const brief = await fs.readFile(path.join(projectRoot, "docs", "DESIGN.md"), "utf-8").catch(() => "");
|
|
96
|
+
return /docs\/mockups\/([a-z0-9-]+)\//.exec(brief)?.[1] ?? null;
|
|
97
|
+
}
|
|
98
|
+
/** The palettes the client was offered for the chosen mockup; none for an older site. */
|
|
99
|
+
async function offeredPalettes(projectRoot) {
|
|
100
|
+
const direction = await chosenMockup(projectRoot);
|
|
101
|
+
if (!direction) return [];
|
|
102
|
+
const dir = path.join(projectRoot, "docs", "mockups", direction);
|
|
103
|
+
const readPalette = async (id, folder) => {
|
|
104
|
+
const colors = mockupThemeColors(await fs.readFile(path.join(folder, "index.html"), "utf-8").catch(() => ""));
|
|
105
|
+
if (Object.keys(colors).length < 3) return null;
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
label: (await fs.readFile(path.join(folder, "variant.json"), "utf-8").then((raw) => JSON.parse(raw)).catch(() => ({}))).palette ?? "",
|
|
109
|
+
colors
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const variants = await fs.readdir(path.join(dir, "palettes")).catch(() => []);
|
|
113
|
+
return (await Promise.all([readPalette("current", dir), ...variants.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b)).map((n) => readPalette(`palette-${n}`, path.join(dir, "palettes", n)))])).filter((p) => p !== null);
|
|
114
|
+
}
|
|
115
|
+
async function getSiteTheme(projectRoot) {
|
|
116
|
+
const theme = await defaultThemeFile(projectRoot);
|
|
117
|
+
if (!theme) return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: "This site has no theme in app/config/site.json"
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
const content = JSON.parse(await fs.readFile(theme.filePath, "utf-8"));
|
|
123
|
+
const colors = {};
|
|
124
|
+
for (const key of EDITABLE_THEME_COLORS) {
|
|
125
|
+
const value = content.colors?.[key];
|
|
126
|
+
if (typeof value === "string") colors[key] = value;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
themeId: theme.themeId,
|
|
131
|
+
colors,
|
|
132
|
+
palettes: await offeredPalettes(projectRoot)
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: `The theme file app/config/themes/${theme.themeId}.json is missing`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function setSiteTheme(projectRoot, params) {
|
|
142
|
+
const parsed = payloadSetTheme.safeParse(params);
|
|
143
|
+
if (!parsed.success) return {
|
|
144
|
+
success: false,
|
|
145
|
+
error: `Invalid payload: ${parsed.error.message}`
|
|
146
|
+
};
|
|
147
|
+
if (Object.keys(parsed.data.colors).length === 0) return {
|
|
148
|
+
success: true,
|
|
149
|
+
filePaths: []
|
|
150
|
+
};
|
|
151
|
+
const theme = await defaultThemeFile(projectRoot);
|
|
152
|
+
if (!theme) return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: "This site has no theme in app/config/site.json"
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
const content = JSON.parse(await fs.readFile(theme.filePath, "utf-8"));
|
|
158
|
+
content.colors = {
|
|
159
|
+
...content.colors,
|
|
160
|
+
...parsed.data.colors
|
|
161
|
+
};
|
|
162
|
+
await fs.writeFile(theme.filePath, `${JSON.stringify(content, null, 2)}\n`);
|
|
163
|
+
const filePaths = [theme.filePath];
|
|
164
|
+
const briefPath = path.join(projectRoot, "docs", "DESIGN.md");
|
|
165
|
+
const brief = await fs.readFile(briefPath, "utf-8").catch(() => null);
|
|
166
|
+
if (brief !== null) {
|
|
167
|
+
await fs.writeFile(briefPath, briefWithUserColors(brief, path.relative(projectRoot, theme.filePath)));
|
|
168
|
+
filePaths.push(briefPath);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
filePaths
|
|
173
|
+
};
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: String(err)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { EDITABLE_THEME_COLORS, getSiteTheme, payloadGetTheme, payloadSetTheme, setSiteTheme };
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=site-theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-theme.js","names":[],"sources":["../src/site-theme.ts"],"sourcesContent":["/**\n * The site's colors, editable from the editor's \"Colors\" panel.\n *\n * A site's colors live in its theme file (`app/config/themes/<id>.json`, the one `site.json` names\n * as the default), read by the theme plugin at build time. A new site also keeps the palettes the\n * client was offered when choosing its look (`docs/mockups/<direction>/` and its `palettes/<n>/`\n * variants, see the `PaletteVariants` tool): the panel offers them again, next to a free picker.\n *\n * Saving also records in `docs/DESIGN.md` that the user chose the colors themselves: agents read\n * the brief before any UI work, and would otherwise bring the brief's palette back.\n */\n\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\n\n/** The theme colors the panel edits, in the theme file's naming. */\nexport const EDITABLE_THEME_COLORS = [\n \"base100\",\n \"base200\",\n \"base300\",\n \"baseContent\",\n \"primary\",\n \"primaryContent\",\n \"secondary\",\n \"secondaryContent\",\n \"accent\",\n \"accentContent\",\n \"neutral\",\n \"neutralContent\",\n] as const;\nexport type EditableThemeColor = (typeof EDITABLE_THEME_COLORS)[number];\nexport type ThemeColors = Partial<Record<EditableThemeColor, string>>;\n\n/** A CSS color as theme files hold them: oklch(), hex, rgb() or hsl(). */\nconst cssColor = z\n .string()\n .trim()\n .max(80)\n .regex(/^(#[0-9a-fA-F]{3,8}|(oklch|oklab|rgba?|hsla?)\\([0-9.,%\\s/+-]+\\))$/, \"Not a CSS color\");\n\nexport const payloadGetTheme = z.object({ action: z.literal(\"getTheme\") });\nexport type PayloadGetTheme = z.infer<typeof payloadGetTheme>;\n\nexport const payloadSetTheme = z.object({\n action: z.literal(\"setTheme\"),\n colors: z.partialRecord(z.enum(EDITABLE_THEME_COLORS), cssColor),\n});\nexport type PayloadSetTheme = z.infer<typeof payloadSetTheme>;\n\nexport interface ThemePalette {\n /** Stable id: \"current\" for the chosen mockup, \"palette-<n>\" for its variants. */\n id: string;\n /** The catalog palette's description, when known (e.g. \"Blush base, deep rose text…\"). */\n label: string;\n colors: ThemeColors;\n}\n\nexport type GetThemeResult =\n | { success: true; themeId: string; colors: ThemeColors; palettes: ThemePalette[] }\n | { success: false; error: string };\n\nexport type SetThemeResult = { success: true; filePaths: string[] } | { success: false; error: string };\n\n/** Mockup token (Tailwind config of a mockup) → theme color. */\nconst MOCKUP_TOKENS: Record<string, EditableThemeColor> = {\n \"base-100\": \"base100\",\n \"base-200\": \"base200\",\n \"base-300\": \"base300\",\n \"base-content\": \"baseContent\",\n primary: \"primary\",\n \"primary-content\": \"primaryContent\",\n secondary: \"secondary\",\n \"secondary-content\": \"secondaryContent\",\n accent: \"accent\",\n \"accent-content\": \"accentContent\",\n neutral: \"neutral\",\n \"neutral-content\": \"neutralContent\",\n};\n\n/** The colors a mockup sets in its Tailwind config, in the theme's naming. */\nexport function mockupThemeColors(html: string): ThemeColors {\n const script = [...html.matchAll(/<script\\b[^>]*>([\\s\\S]*?)<\\/script>/g)].find((m) =>\n m[1].includes(\"tailwind.config\"),\n )?.[1];\n const colors: ThemeColors = {};\n if (!script) return colors;\n for (const [token, key] of Object.entries(MOCKUP_TOKENS)) {\n const pattern = new RegExp(`['\"]?${token.replace(/-/g, \"\\\\-\")}['\"]?\\\\s*:\\\\s*(['\"])([^'\"]*)\\\\1`);\n const value = pattern.exec(script)?.[2];\n if (value && cssColor.safeParse(value).success) colors[key] = value.trim();\n }\n return colors;\n}\n\n/** The block `setTheme` keeps up to date at the end of the brief. */\nconst BRIEF_START = \"<!-- upstart:theme-colors -->\";\nconst BRIEF_END = \"<!-- /upstart:theme-colors -->\";\n\nexport function briefWithUserColors(brief: string, themeFile: string): string {\n const block = [\n BRIEF_START,\n \"## Colors chosen by the user\",\n `The user set the site's colors themselves in the editor. \\`${themeFile}\\` is the source of truth for colors and wins over any palette above: keep its values, and use its tokens (bg-base-100, text-primary…) rather than new colors.`,\n BRIEF_END,\n ].join(\"\\n\");\n const start = brief.indexOf(BRIEF_START);\n const end = brief.indexOf(BRIEF_END);\n if (start !== -1 && end > start) {\n return `${brief.slice(0, start)}${block}${brief.slice(end + BRIEF_END.length)}`;\n }\n return `${brief.trimEnd()}\\n\\n${block}\\n`;\n}\n\n/** The theme file the site uses by default, from `app/config/site.json`. */\nasync function defaultThemeFile(projectRoot: string): Promise<{ themeId: string; filePath: string } | null> {\n try {\n const site = JSON.parse(\n await fs.readFile(path.join(projectRoot, \"app\", \"config\", \"site.json\"), \"utf-8\"),\n ) as {\n themes?: { light?: string; dark?: string; default?: \"light\" | \"dark\" };\n };\n const themeId = site.themes?.[site.themes.default ?? \"light\"] ?? site.themes?.light;\n if (!themeId || !/^[a-z0-9-]+$/.test(themeId)) return null;\n return { themeId, filePath: path.join(projectRoot, \"app\", \"config\", \"themes\", `${themeId}.json`) };\n } catch {\n return null;\n }\n}\n\n/** The mockup the brief names as its reference (`docs/mockups/<direction>/…`). */\nasync function chosenMockup(projectRoot: string): Promise<string | null> {\n const brief = await fs.readFile(path.join(projectRoot, \"docs\", \"DESIGN.md\"), \"utf-8\").catch(() => \"\");\n return /docs\\/mockups\\/([a-z0-9-]+)\\//.exec(brief)?.[1] ?? null;\n}\n\n/** The palettes the client was offered for the chosen mockup; none for an older site. */\nexport async function offeredPalettes(projectRoot: string): Promise<ThemePalette[]> {\n const direction = await chosenMockup(projectRoot);\n if (!direction) return [];\n const dir = path.join(projectRoot, \"docs\", \"mockups\", direction);\n const readPalette = async (id: string, folder: string): Promise<ThemePalette | null> => {\n const html = await fs.readFile(path.join(folder, \"index.html\"), \"utf-8\").catch(() => \"\");\n const colors = mockupThemeColors(html);\n if (Object.keys(colors).length < 3) return null;\n const variant = await fs\n .readFile(path.join(folder, \"variant.json\"), \"utf-8\")\n .then((raw) => JSON.parse(raw) as { palette?: string })\n .catch((): { palette?: string } => ({}));\n return { id, label: variant.palette ?? \"\", colors };\n };\n const variants = await fs.readdir(path.join(dir, \"palettes\")).catch((): string[] => []);\n const palettes = await Promise.all([\n readPalette(\"current\", dir),\n ...variants\n .filter((n) => /^\\d+$/.test(n))\n .sort((a, b) => Number(a) - Number(b))\n .map((n) => readPalette(`palette-${n}`, path.join(dir, \"palettes\", n))),\n ]);\n return palettes.filter((p): p is ThemePalette => p !== null);\n}\n\nexport async function getSiteTheme(projectRoot: string): Promise<GetThemeResult> {\n const theme = await defaultThemeFile(projectRoot);\n if (!theme) return { success: false, error: \"This site has no theme in app/config/site.json\" };\n try {\n const content = JSON.parse(await fs.readFile(theme.filePath, \"utf-8\")) as {\n colors?: Record<string, string>;\n };\n const colors: ThemeColors = {};\n for (const key of EDITABLE_THEME_COLORS) {\n const value = content.colors?.[key];\n if (typeof value === \"string\") colors[key] = value;\n }\n return { success: true, themeId: theme.themeId, colors, palettes: await offeredPalettes(projectRoot) };\n } catch {\n return { success: false, error: `The theme file app/config/themes/${theme.themeId}.json is missing` };\n }\n}\n\nexport async function setSiteTheme(projectRoot: string, params: PayloadSetTheme): Promise<SetThemeResult> {\n const parsed = payloadSetTheme.safeParse(params);\n if (!parsed.success) return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n if (Object.keys(parsed.data.colors).length === 0) return { success: true, filePaths: [] };\n const theme = await defaultThemeFile(projectRoot);\n if (!theme) return { success: false, error: \"This site has no theme in app/config/site.json\" };\n try {\n const content = JSON.parse(await fs.readFile(theme.filePath, \"utf-8\")) as {\n colors?: Record<string, string>;\n };\n content.colors = { ...content.colors, ...parsed.data.colors };\n await fs.writeFile(theme.filePath, `${JSON.stringify(content, null, 2)}\\n`);\n const filePaths = [theme.filePath];\n\n const briefPath = path.join(projectRoot, \"docs\", \"DESIGN.md\");\n const brief = await fs.readFile(briefPath, \"utf-8\").catch(() => null);\n if (brief !== null) {\n await fs.writeFile(briefPath, briefWithUserColors(brief, path.relative(projectRoot, theme.filePath)));\n filePaths.push(briefPath);\n }\n return { success: true, filePaths };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EditableEntry } from "./vite-plugin-upstart-attrs.js";
|
|
2
|
+
import { EDITABLE_THEME_COLORS, EditableThemeColor, GetThemeResult, PayloadGetTheme, PayloadSetTheme, SetThemeResult, ThemeColors, ThemePalette, payloadGetTheme, payloadSetTheme } from "./site-theme.js";
|
|
2
3
|
import z from "zod";
|
|
3
4
|
|
|
4
5
|
//#region src/upstart-editor-api.d.ts
|
|
@@ -16,6 +17,38 @@ declare function escapeStringLiteralBody(value: string, quote: string): string;
|
|
|
16
17
|
declare function readLocaleValue(data: Record<string, unknown>, key: string): string | null;
|
|
17
18
|
/** Write a key, keeping the shape it already has; new keys are created flat. */
|
|
18
19
|
declare function writeLocaleValue(data: Record<string, unknown>, key: string, value: string): void;
|
|
20
|
+
interface SourceRange {
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The result of moving a range: the new code, plus what changed so the offsets of other
|
|
26
|
+
* elements can be mapped onto it (see `mapOffset`). Offsets are in the ORIGINAL code, except
|
|
27
|
+
* `moved`, which is the new range of the moved element.
|
|
28
|
+
*/
|
|
29
|
+
interface MoveEdit {
|
|
30
|
+
code: string;
|
|
31
|
+
/** Removed source range. */
|
|
32
|
+
removeStart: number;
|
|
33
|
+
removeEnd: number;
|
|
34
|
+
/** Where the moved text was inserted, and its length. */
|
|
35
|
+
insertAt: number;
|
|
36
|
+
insertLength: number;
|
|
37
|
+
moved: SourceRange;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Move the `src` range before or after the `tgt` range (`tgt` must not be inside `src`).
|
|
41
|
+
* Elements alone on their lines are moved as whole lines, re-indented to the target's level;
|
|
42
|
+
* inline elements are moved as-is. Inserting at the far edge of the target keeps the target's
|
|
43
|
+
* own range untouched even when the source sits inside it (e.g. a <span> out of its <p>).
|
|
44
|
+
*/
|
|
45
|
+
declare function moveRange(code: string, src: SourceRange, tgt: SourceRange, position: "before" | "after"): MoveEdit;
|
|
46
|
+
/**
|
|
47
|
+
* Where an offset of the original code (outside the removed range) lands after `edit`.
|
|
48
|
+
* A start sitting exactly at the insertion point is pushed after the inserted text, an end
|
|
49
|
+
* sitting there stays before it.
|
|
50
|
+
*/
|
|
51
|
+
declare function mapOffset(edit: MoveEdit, offset: number, isEnd: boolean): number;
|
|
19
52
|
declare const payloadEditText: z.ZodObject<{
|
|
20
53
|
action: z.ZodLiteral<"editText">;
|
|
21
54
|
language: z.ZodString;
|
|
@@ -61,6 +94,25 @@ declare const payloadArraySet: z.ZodObject<{
|
|
|
61
94
|
items: z.ZodArray<z.ZodString>;
|
|
62
95
|
}, z.core.$strip>;
|
|
63
96
|
type PayloadArraySet = z.infer<typeof payloadArraySet>;
|
|
97
|
+
declare const payloadMoveElements: z.ZodObject<{
|
|
98
|
+
action: z.ZodLiteral<"moveElements">;
|
|
99
|
+
moves: z.ZodArray<z.ZodObject<{
|
|
100
|
+
id: z.ZodString;
|
|
101
|
+
targetId: z.ZodString;
|
|
102
|
+
position: z.ZodEnum<{
|
|
103
|
+
after: "after";
|
|
104
|
+
before: "before";
|
|
105
|
+
}>;
|
|
106
|
+
}, z.core.$strip>>;
|
|
107
|
+
}, z.core.$strip>;
|
|
108
|
+
type PayloadMoveElements = z.infer<typeof payloadMoveElements>;
|
|
109
|
+
type MoveElementsResult = {
|
|
110
|
+
success: true;
|
|
111
|
+
filePaths: string[];
|
|
112
|
+
} | {
|
|
113
|
+
success: false;
|
|
114
|
+
error: string;
|
|
115
|
+
};
|
|
64
116
|
declare const payloadGetPageMeta: z.ZodObject<{
|
|
65
117
|
action: z.ZodLiteral<"getPageMeta">;
|
|
66
118
|
routeId: z.ZodString;
|
|
@@ -218,6 +270,12 @@ declare class UpstartEditorAPI {
|
|
|
218
270
|
* batched ✓ apply so a whole add/delete session is a single edit + rebuild.
|
|
219
271
|
*/
|
|
220
272
|
arraySet(params: PayloadArraySet): Promise<EditResult>;
|
|
273
|
+
/**
|
|
274
|
+
* Apply a batch of element moves (editor drag & drop), in order. Every element involved is
|
|
275
|
+
* tracked across the moves, so a later move finds elements an earlier one shifted. Files are
|
|
276
|
+
* only written when every move succeeded and every result still parses.
|
|
277
|
+
*/
|
|
278
|
+
moveElements(params: PayloadMoveElements): Promise<MoveElementsResult>;
|
|
221
279
|
/**
|
|
222
280
|
* Read the page metadata shown in the browser tab and in search results.
|
|
223
281
|
*
|
|
@@ -243,6 +301,10 @@ declare class UpstartEditorAPI {
|
|
|
243
301
|
* workspace by the caller, which has bucket access.
|
|
244
302
|
*/
|
|
245
303
|
setSiteMeta(params: PayloadSetSiteMeta): Promise<SetSiteMetaResult>;
|
|
304
|
+
/** The site's colors and, for a new site, the palettes offered with its mockup (see site-theme.ts). */
|
|
305
|
+
getTheme(_params: PayloadGetTheme): Promise<GetThemeResult>;
|
|
306
|
+
/** Write colors to the site's theme file, and record in the brief that the user chose them. */
|
|
307
|
+
setTheme(params: PayloadSetTheme): Promise<SetThemeResult>;
|
|
246
308
|
private readSiteConfig;
|
|
247
309
|
private readRootFile;
|
|
248
310
|
private setMetaExport;
|
|
@@ -278,5 +340,5 @@ declare class UpstartEditorAPI {
|
|
|
278
340
|
getElementsByFile(file: string): Record<string, EditableEntry>;
|
|
279
341
|
}
|
|
280
342
|
//#endregion
|
|
281
|
-
export { EditResult, EditableRegistry, GetPageMetaResult, GetSiteMetaResult, PageMetaField, PayloadArrayItemAdd, PayloadArrayItemDelete, PayloadArraySet, PayloadEditClassName, PayloadEditImage, PayloadEditText, PayloadEditTextDirect, PayloadGetPageMeta, PayloadGetSiteMeta, PayloadSetPageMeta, PayloadSetSiteMeta, SetPageMetaResult, SetSiteMetaResult, UpstartEditorAPI, escapeStringLiteralBody, payloadArrayItemAdd, payloadArrayItemDelete, payloadArraySet, payloadEditClassName, payloadEditImage, payloadEditText, payloadEditTextDirect, payloadGetPageMeta, payloadGetSiteMeta, payloadSetPageMeta, payloadSetSiteMeta, readLocaleValue, writeLocaleValue };
|
|
343
|
+
export { EDITABLE_THEME_COLORS, EditResult, EditableRegistry, type EditableThemeColor, GetPageMetaResult, GetSiteMetaResult, type GetThemeResult, MoveEdit, MoveElementsResult, PageMetaField, PayloadArrayItemAdd, PayloadArrayItemDelete, PayloadArraySet, PayloadEditClassName, PayloadEditImage, PayloadEditText, PayloadEditTextDirect, PayloadGetPageMeta, PayloadGetSiteMeta, type PayloadGetTheme, PayloadMoveElements, PayloadSetPageMeta, PayloadSetSiteMeta, type PayloadSetTheme, SetPageMetaResult, SetSiteMetaResult, type SetThemeResult, type ThemeColors, type ThemePalette, UpstartEditorAPI, escapeStringLiteralBody, mapOffset, moveRange, payloadArrayItemAdd, payloadArrayItemDelete, payloadArraySet, payloadEditClassName, payloadEditImage, payloadEditText, payloadEditTextDirect, payloadGetPageMeta, payloadGetSiteMeta, payloadGetTheme, payloadMoveElements, payloadSetPageMeta, payloadSetSiteMeta, payloadSetTheme, readLocaleValue, writeLocaleValue };
|
|
282
344
|
//# sourceMappingURL=upstart-editor-api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upstart-editor-api.d.ts","names":[],"sources":["../src/upstart-editor-api.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"upstart-editor-api.d.ts","names":[],"sources":["../src/upstart-editor-api.ts"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { analyzeRouteMeta } from "./page-meta.js";
|
|
2
2
|
import { ensureRootSiteMeta } from "./site-meta.js";
|
|
3
|
+
import { EDITABLE_THEME_COLORS, getSiteTheme, payloadGetTheme, payloadSetTheme, setSiteTheme } from "./site-theme.js";
|
|
3
4
|
import MagicString from "magic-string";
|
|
4
5
|
import { parseSync } from "oxc-parser";
|
|
5
6
|
import fs from "node:fs/promises";
|
|
@@ -260,6 +261,109 @@ function printMetaTag(fieldName, value) {
|
|
|
260
261
|
if (fieldName === "title") return !/[{}<>&\n]/.test(value) ? `<title>${value}</title>` : `<title>{"${escapeStringLiteralBody(value, "\"")}"}</title>`;
|
|
261
262
|
return `<meta name="${fieldName}" ${/["\n]/.test(value) ? `content={"${escapeStringLiteralBody(value, "\"")}"}` : `content="${value}"`} />`;
|
|
262
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Current range of an element in `code`: `hint` when the source still matches there, otherwise
|
|
266
|
+
* the occurrence of its source nearest to `hint` (an earlier edit shifted it), and as a last
|
|
267
|
+
* resort a whitespace-insensitive match (a move re-indented it).
|
|
268
|
+
*/
|
|
269
|
+
function locateElement(code, hint) {
|
|
270
|
+
const { start, end, content } = hint;
|
|
271
|
+
if (code.slice(start, end) === content) return hint;
|
|
272
|
+
if (content.trim() === "") return null;
|
|
273
|
+
const nearest = (a, b) => Math.abs(a - start) < Math.abs(b - start);
|
|
274
|
+
let best = -1;
|
|
275
|
+
for (let i = code.indexOf(content); i !== -1; i = code.indexOf(content, i + 1)) if (best === -1 || nearest(i, best)) best = i;
|
|
276
|
+
if (best !== -1) return {
|
|
277
|
+
start: best,
|
|
278
|
+
end: best + content.length,
|
|
279
|
+
content
|
|
280
|
+
};
|
|
281
|
+
const pattern = content.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
282
|
+
let found = null;
|
|
283
|
+
for (const m of code.matchAll(new RegExp(pattern, "g"))) if (!found || nearest(m.index, found.start)) found = {
|
|
284
|
+
start: m.index,
|
|
285
|
+
end: m.index + m[0].length,
|
|
286
|
+
content: m[0]
|
|
287
|
+
};
|
|
288
|
+
return found;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The full lines a range occupies (from the line start to just after its trailing newline)
|
|
292
|
+
* when nothing else than whitespace shares those lines, plus the indentation of the first one.
|
|
293
|
+
*/
|
|
294
|
+
function ownLines(code, range) {
|
|
295
|
+
const lineStart = code.lastIndexOf("\n", range.start - 1) + 1;
|
|
296
|
+
const indent = code.slice(lineStart, range.start);
|
|
297
|
+
if (indent.trim() !== "") return null;
|
|
298
|
+
const newline = code.indexOf("\n", range.end);
|
|
299
|
+
const lineEnd = newline === -1 ? code.length : newline;
|
|
300
|
+
if (code.slice(range.end, lineEnd).trim() !== "") return null;
|
|
301
|
+
return {
|
|
302
|
+
start: lineStart,
|
|
303
|
+
end: newline === -1 ? code.length : newline + 1,
|
|
304
|
+
indent
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Move the `src` range before or after the `tgt` range (`tgt` must not be inside `src`).
|
|
309
|
+
* Elements alone on their lines are moved as whole lines, re-indented to the target's level;
|
|
310
|
+
* inline elements are moved as-is. Inserting at the far edge of the target keeps the target's
|
|
311
|
+
* own range untouched even when the source sits inside it (e.g. a <span> out of its <p>).
|
|
312
|
+
*/
|
|
313
|
+
function moveRange(code, src, tgt, position) {
|
|
314
|
+
const srcLines = ownLines(code, src);
|
|
315
|
+
const tgtLines = ownLines(code, tgt);
|
|
316
|
+
let removeStart = src.start;
|
|
317
|
+
let removeEnd = src.end;
|
|
318
|
+
let insertAt = position === "before" ? tgt.start : tgt.end;
|
|
319
|
+
let inserted = code.slice(src.start, src.end);
|
|
320
|
+
let elementOffset = 0;
|
|
321
|
+
let elementLength = inserted.length;
|
|
322
|
+
if (srcLines && tgtLines) {
|
|
323
|
+
const body = inserted.split("\n").map((line, i) => {
|
|
324
|
+
if (i === 0) return line;
|
|
325
|
+
return line.startsWith(srcLines.indent) ? tgtLines.indent + line.slice(srcLines.indent.length) : line;
|
|
326
|
+
}).join("\n");
|
|
327
|
+
removeStart = srcLines.start;
|
|
328
|
+
removeEnd = srcLines.end;
|
|
329
|
+
elementOffset = tgtLines.indent.length;
|
|
330
|
+
elementLength = body.length;
|
|
331
|
+
if (position === "before") {
|
|
332
|
+
insertAt = tgtLines.start;
|
|
333
|
+
inserted = `${tgtLines.indent}${body}\n`;
|
|
334
|
+
} else {
|
|
335
|
+
insertAt = tgtLines.end;
|
|
336
|
+
const atEofWithoutNewline = tgtLines.end === code.length && !code.endsWith("\n");
|
|
337
|
+
inserted = atEofWithoutNewline ? `\n${tgtLines.indent}${body}` : `${tgtLines.indent}${body}\n`;
|
|
338
|
+
if (atEofWithoutNewline) elementOffset += 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const removed = removeEnd - removeStart;
|
|
342
|
+
const next = insertAt <= removeStart ? code.slice(0, insertAt) + inserted + code.slice(insertAt, removeStart) + code.slice(removeEnd) : code.slice(0, removeStart) + code.slice(removeEnd, insertAt) + inserted + code.slice(insertAt);
|
|
343
|
+
const movedStart = (insertAt <= removeStart ? insertAt : insertAt - removed) + elementOffset;
|
|
344
|
+
return {
|
|
345
|
+
code: next,
|
|
346
|
+
removeStart,
|
|
347
|
+
removeEnd,
|
|
348
|
+
insertAt,
|
|
349
|
+
insertLength: inserted.length,
|
|
350
|
+
moved: {
|
|
351
|
+
start: movedStart,
|
|
352
|
+
end: movedStart + elementLength
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Where an offset of the original code (outside the removed range) lands after `edit`.
|
|
358
|
+
* A start sitting exactly at the insertion point is pushed after the inserted text, an end
|
|
359
|
+
* sitting there stays before it.
|
|
360
|
+
*/
|
|
361
|
+
function mapOffset(edit, offset, isEnd) {
|
|
362
|
+
let mapped = offset;
|
|
363
|
+
if (offset >= edit.removeEnd) mapped -= edit.removeEnd - edit.removeStart;
|
|
364
|
+
if (isEnd ? offset > edit.insertAt : offset >= edit.insertAt) mapped += edit.insertLength;
|
|
365
|
+
return mapped;
|
|
366
|
+
}
|
|
263
367
|
/** Start of the line `offset` sits on, so a removed tag doesn't leave a blank line behind. */
|
|
264
368
|
function lineRangeStart(code, offset) {
|
|
265
369
|
const lineStart = code.lastIndexOf("\n", offset);
|
|
@@ -310,6 +414,14 @@ const payloadArraySet = z.object({
|
|
|
310
414
|
arrayId: z.string().min(1),
|
|
311
415
|
items: z.array(z.string()).min(1)
|
|
312
416
|
});
|
|
417
|
+
const payloadMoveElements = z.object({
|
|
418
|
+
action: z.literal("moveElements"),
|
|
419
|
+
moves: z.array(z.object({
|
|
420
|
+
id: z.string().min(1),
|
|
421
|
+
targetId: z.string().min(1),
|
|
422
|
+
position: z.enum(["before", "after"])
|
|
423
|
+
})).min(1).max(200)
|
|
424
|
+
});
|
|
313
425
|
const routeIdSchema = z.string().min(1).regex(/^[a-zA-Z0-9._$/()[\]+~-]+$/).refine((id) => !id.split("/").includes("..") && !id.startsWith("/"), "Invalid route id");
|
|
314
426
|
const payloadGetPageMeta = z.object({
|
|
315
427
|
action: z.literal("getPageMeta"),
|
|
@@ -716,6 +828,118 @@ var UpstartEditorAPI = class {
|
|
|
716
828
|
}
|
|
717
829
|
}
|
|
718
830
|
/**
|
|
831
|
+
* Apply a batch of element moves (editor drag & drop), in order. Every element involved is
|
|
832
|
+
* tracked across the moves, so a later move finds elements an earlier one shifted. Files are
|
|
833
|
+
* only written when every move succeeded and every result still parses.
|
|
834
|
+
*/
|
|
835
|
+
async moveElements(params) {
|
|
836
|
+
const parsed = payloadMoveElements.safeParse(params);
|
|
837
|
+
if (!parsed.success) return {
|
|
838
|
+
success: false,
|
|
839
|
+
error: `Invalid payload: ${parsed.error.message}`
|
|
840
|
+
};
|
|
841
|
+
if (!this.registry) try {
|
|
842
|
+
await this.loadRegistry();
|
|
843
|
+
} catch (err) {
|
|
844
|
+
return {
|
|
845
|
+
success: false,
|
|
846
|
+
error: `Failed to load registry: ${err}`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const elements = this.registry.elements;
|
|
850
|
+
const codes = /* @__PURE__ */ new Map();
|
|
851
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
852
|
+
try {
|
|
853
|
+
for (const { id, targetId, position } of parsed.data.moves) {
|
|
854
|
+
if (id === targetId) return {
|
|
855
|
+
success: false,
|
|
856
|
+
error: "Cannot move an element onto itself"
|
|
857
|
+
};
|
|
858
|
+
const source = elements[id];
|
|
859
|
+
const target = elements[targetId];
|
|
860
|
+
if (source?.type !== "element") return {
|
|
861
|
+
success: false,
|
|
862
|
+
error: `Element ${id} not found in registry`
|
|
863
|
+
};
|
|
864
|
+
if (target?.type !== "element") return {
|
|
865
|
+
success: false,
|
|
866
|
+
error: `Element ${targetId} not found in registry`
|
|
867
|
+
};
|
|
868
|
+
if (source.file !== target.file) return {
|
|
869
|
+
success: false,
|
|
870
|
+
error: "Elements can only be moved within the same file"
|
|
871
|
+
};
|
|
872
|
+
const file = source.file;
|
|
873
|
+
let code = codes.get(file);
|
|
874
|
+
if (code === void 0) {
|
|
875
|
+
code = await fs.readFile(path.join(this.projectRoot, file), "utf-8");
|
|
876
|
+
codes.set(file, code);
|
|
877
|
+
for (const [elementId, entry] of Object.entries(elements)) {
|
|
878
|
+
if (entry.type !== "element" || entry.file !== file) continue;
|
|
879
|
+
tracked.set(elementId, {
|
|
880
|
+
start: entry.startOffset,
|
|
881
|
+
end: entry.endOffset,
|
|
882
|
+
content: entry.originalContent
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const src = locateElement(code, tracked.get(id));
|
|
887
|
+
const tgt = locateElement(code, tracked.get(targetId));
|
|
888
|
+
if (!src || !tgt) return {
|
|
889
|
+
success: false,
|
|
890
|
+
error: `Element not found in ${file}. The file may have been modified.`
|
|
891
|
+
};
|
|
892
|
+
if (tgt.start >= src.start && tgt.end <= src.end) return {
|
|
893
|
+
success: false,
|
|
894
|
+
error: "Cannot move an element inside itself"
|
|
895
|
+
};
|
|
896
|
+
const edit = moveRange(code, src, tgt, position);
|
|
897
|
+
codes.set(file, edit.code);
|
|
898
|
+
for (const [elementId, el] of tracked) {
|
|
899
|
+
if (elementId === id) continue;
|
|
900
|
+
if (elements[elementId].file !== file) continue;
|
|
901
|
+
if (el.start >= src.start && el.end <= src.end) {
|
|
902
|
+
const delta = edit.moved.start - src.start;
|
|
903
|
+
tracked.set(elementId, {
|
|
904
|
+
...el,
|
|
905
|
+
start: el.start + delta,
|
|
906
|
+
end: el.end + delta
|
|
907
|
+
});
|
|
908
|
+
} else tracked.set(elementId, {
|
|
909
|
+
...el,
|
|
910
|
+
start: mapOffset(edit, el.start, false),
|
|
911
|
+
end: mapOffset(edit, el.end, true)
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
tracked.set(id, {
|
|
915
|
+
...edit.moved,
|
|
916
|
+
content: edit.code.slice(edit.moved.start, edit.moved.end)
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
const changed = [];
|
|
920
|
+
for (const [file, code] of codes) {
|
|
921
|
+
const filePath = path.join(this.projectRoot, file);
|
|
922
|
+
if (code === await fs.readFile(filePath, "utf-8")) continue;
|
|
923
|
+
if (parseSync(file, code, { sourceType: "module" }).errors.length > 0) return {
|
|
924
|
+
success: false,
|
|
925
|
+
error: `Moving the elements would produce invalid code in ${file}`
|
|
926
|
+
};
|
|
927
|
+
changed.push([filePath, code]);
|
|
928
|
+
}
|
|
929
|
+
for (const [filePath, code] of changed) await fs.writeFile(filePath, code);
|
|
930
|
+
if (changed.length > 0) this.registry = null;
|
|
931
|
+
return {
|
|
932
|
+
success: true,
|
|
933
|
+
filePaths: changed.map(([filePath]) => filePath)
|
|
934
|
+
};
|
|
935
|
+
} catch (err) {
|
|
936
|
+
return {
|
|
937
|
+
success: false,
|
|
938
|
+
error: String(err)
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
719
943
|
* Read the page metadata shown in the browser tab and in search results.
|
|
720
944
|
*
|
|
721
945
|
* Two shapes are supported, in this order: React-rendered tags (`<title>{title}</title>`
|
|
@@ -1012,6 +1236,14 @@ var UpstartEditorAPI = class {
|
|
|
1012
1236
|
};
|
|
1013
1237
|
}
|
|
1014
1238
|
}
|
|
1239
|
+
/** The site's colors and, for a new site, the palettes offered with its mockup (see site-theme.ts). */
|
|
1240
|
+
async getTheme(_params) {
|
|
1241
|
+
return getSiteTheme(this.projectRoot);
|
|
1242
|
+
}
|
|
1243
|
+
/** Write colors to the site's theme file, and record in the brief that the user chose them. */
|
|
1244
|
+
async setTheme(params) {
|
|
1245
|
+
return setSiteTheme(this.projectRoot, params);
|
|
1246
|
+
}
|
|
1015
1247
|
async readSiteConfig() {
|
|
1016
1248
|
try {
|
|
1017
1249
|
const raw = await fs.readFile(path.join(this.projectRoot, "app", "config", "site.json"), "utf-8");
|
|
@@ -1342,6 +1574,6 @@ var UpstartEditorAPI = class {
|
|
|
1342
1574
|
}
|
|
1343
1575
|
};
|
|
1344
1576
|
//#endregion
|
|
1345
|
-
export { UpstartEditorAPI, escapeStringLiteralBody, payloadArrayItemAdd, payloadArrayItemDelete, payloadArraySet, payloadEditClassName, payloadEditImage, payloadEditText, payloadEditTextDirect, payloadGetPageMeta, payloadGetSiteMeta, payloadSetPageMeta, payloadSetSiteMeta, readLocaleValue, writeLocaleValue };
|
|
1577
|
+
export { EDITABLE_THEME_COLORS, UpstartEditorAPI, escapeStringLiteralBody, mapOffset, moveRange, payloadArrayItemAdd, payloadArrayItemDelete, payloadArraySet, payloadEditClassName, payloadEditImage, payloadEditText, payloadEditTextDirect, payloadGetPageMeta, payloadGetSiteMeta, payloadGetTheme, payloadMoveElements, payloadSetPageMeta, payloadSetSiteMeta, payloadSetTheme, readLocaleValue, writeLocaleValue };
|
|
1346
1578
|
|
|
1347
1579
|
//# sourceMappingURL=upstart-editor-api.js.map
|