@picloom/create-theme 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lllei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,34 @@
1
- # Temporary Holding Version
1
+ # @picloom/create-theme
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Create a standalone Vue 3 Picloom theme with Bun 1.4.3+, simulated preview,
4
+ component/style HMR and ZIP packaging. No application checkout, Node/npm CLI or
5
+ system `zip` is required.
6
+
7
+ Create a project:
8
+
9
+ ```sh
10
+ bun create @picloom/theme@0.1.0 ./my-theme
11
+ cd my-theme
12
+ bun install
13
+ bun run dev
14
+ bun run build
15
+ bun run preview
16
+ ```
17
+
18
+ The target must not exist; its parent must exist. The creator copies files only;
19
+ it does not install dependencies, initialize Git or publish. Both home and login
20
+ pages start from the same source used by Picloom's built-in default theme.
21
+ `--help` prints usage.
22
+
23
+ To test local packages, use the `.tgz` workflow in
24
+ [theme tooling](https://llleixx.github.io/picloom/development/theme-tooling.html).
25
+ Maintainers run `bun run build:create-theme` before `bun pm pack --ignore-scripts`.
26
+ The build copies `themes/default/` into the distribution template and bundles
27
+ shared pure helpers into private preview tooling. Do not edit `dist/template/`.
28
+
29
+ ## 中文
30
+
31
+ 基于宿主同一份默认主题源码创建独立 Vue 3 主题。提供模拟预览、热更新、生产产物预览与 ZIP 打包。
32
+ 创建工具只复制文件,不覆盖已有目录、不安装依赖、不初始化 Git、不发布。
33
+ 模拟预览不连接真实服务;上传、登录与权限必须在隔离测试实例安装后验证。
34
+ 通过 npm 创建项目,也可按文档使用本地 `.tgz` 验证开发版本。模板在创建工具构建时自动生成,不手工维护第二份默认主题。
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lllei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # Picloom theme / 主题开发
2
+
3
+ Requires Bun 1.4.3+. This project starts from Picloom's built-in default theme,
4
+ uses only the public Theme SDK, and bundles its own Vue runtime.
5
+
6
+ ```sh
7
+ bun install
8
+ bun run dev
9
+ bun run typecheck
10
+ bun run build
11
+ bun run preview
12
+ ```
13
+
14
+ `dev` listens on http://127.0.0.1:5173; use `bun run dev --port 5174` if occupied.
15
+ The collapsible preview panel switches pages, languages, appearance, identity and
16
+ simulated states. Uploads and login are **simulations**, never real API calls.
17
+ Do not use preview success as evidence that a storage service or authentication works.
18
+
19
+ Edit `src/Page.vue`, `src/style.css` and `theme.json`. All pages use `src/entry.ts`
20
+ (`entry.js` in the package). Put static package resources in `public/assets/` and
21
+ resolve them with `context.asset("assets/...")`. Imported image/CSS resources are
22
+ also processed by Vite. Keep `style.css` in the manifest's global `styles`: it
23
+ contains all CSS imported by the source entry. Additional page-only styles belong
24
+ in `public/assets/` and the corresponding page's `styles`; preview loads and
25
+ unloads them when changing pages. Arbitrary files outside these inputs are not packaged.
26
+
27
+ Vue components and CSS support HMR. Entry changes reload the preview. Manifest
28
+ and `dev/config.json` changes reload and remount the theme. Invalid JSON shows a
29
+ development error and is never overwritten. The first preview initializes a
30
+ missing config from schema defaults; later edits never restore removed values.
31
+ There is initially no custom config schema, so the generated config is `{}`.
32
+
33
+ `preview` loads the existing `dist/package/` JavaScript and styles through the
34
+ same simulated host, without theme source HMR. Build first; source edits are not
35
+ visible until you rebuild. The preview toolbar itself is still a local tool.
36
+
37
+ The distributable is `dist/<theme-id>-<version>.zip`. Upload it to Picloom's Themes
38
+ page, confirm trust, preview and apply. Test real uploads, login and permissions
39
+ there. Increase `theme.json.version` before distributing changed content; package
40
+ versions are immutable on the host. The package builder checks structure and
41
+ resources; full configuration/manifest validation still runs on installation.
42
+
43
+ The SDK dependency is pinned to a published npm version.
44
+ Commit your generated `bun.lock` to your own repository after installation.
45
+
46
+ ## 中文
47
+
48
+ - 使用 Bun 1.4.3+,无需 Node/npm CLI、主仓库或系统 `zip`。
49
+ - `bun run dev` 提供模拟首页、登录页、上传状态、语言和明暗模式;只用于界面开发。
50
+ - 修改 Vue 组件及样式会热更新;入口、清单和本地配置修改后刷新预览。
51
+ - 首次启动时按 Schema 默认值创建 `dev/config.json`,之后清空或删除值不会自动补回。
52
+ - `bun run build` 先检查类型,再构建并生成可安装 ZIP;`bun run preview` 检查已有构建产物。
53
+ - 修改主题 ID、显示名称和版本均以 `theme.json` 为准,静态资源放入 `public/assets/`。
54
+ - 入口导入的样式合并为 `style.css`,须保留清单全局样式声明;页面专属样式通过各页面的 `styles` 引用,切页时分别加载和卸载。
55
+ - 主题包不包含开发面板、本地配置、凭据或源码映射。所有主题配置可能公开,不要保存密钥。
56
+ - 在隔离 Picloom 测试实例安装 ZIP 后验证真实上传和登录;模拟成功不代表真实服务已验证。
57
+ - SDK 依赖固定到 npm 版本;首次安装后将生成的 `bun.lock` 提交到自己的仓库。
@@ -0,0 +1,363 @@
1
+ import type {
2
+ Config,
3
+ Manifest,
4
+ Page,
5
+ Snapshot,
6
+ ThemeContext,
7
+ Upload,
8
+ } from "@picloom/theme-sdk";
9
+ import { languages, messageText } from "./helpers.js";
10
+ export const scenarios = [
11
+ "ready",
12
+ "loading",
13
+ "uploading",
14
+ "success",
15
+ "failed",
16
+ "no-profiles",
17
+ "login-error",
18
+ ] as const;
19
+ export type Scenario = (typeof scenarios)[number];
20
+ const copy = <T>(value: T): T => structuredClone(value);
21
+ export function createPreview(
22
+ manifest: Manifest,
23
+ config: Config,
24
+ navigate: (page: Page) => void,
25
+ ) {
26
+ const listeners = new Set<(snapshot: Snapshot) => void>();
27
+ const timers = new Set<ReturnType<typeof setTimeout>>();
28
+ const motion = matchMedia("(prefers-color-scheme: dark)");
29
+ const state: Snapshot = {
30
+ page: location.pathname === "/login" ? "login" : "home",
31
+ site: {
32
+ name: "Preview gallery",
33
+ homeTitle: "Preview gallery",
34
+ description: "SIMULATED uploads and login · 模拟上传与登录",
35
+ logoUrl: "",
36
+ faviconUrl: "",
37
+ },
38
+ user: null,
39
+ locale: manifest.defaultLocale,
40
+ locales: manifest.supportedLocales,
41
+ languages: languages.filter((language) =>
42
+ manifest.supportedLocales.includes(language.tag),
43
+ ),
44
+ defaultLocale: manifest.defaultLocale,
45
+ mode: "auto",
46
+ dark: motion.matches,
47
+ profiles: [
48
+ {
49
+ id: "mock",
50
+ name: "Simulated / 模拟上传",
51
+ enabled: true,
52
+ available: true,
53
+ maxUploadBytes: 50 * 1024 ** 2,
54
+ allowedMimeTypes: [
55
+ "image/jpeg",
56
+ "image/png",
57
+ "image/webp",
58
+ "image/svg+xml",
59
+ ],
60
+ reason: null,
61
+ },
62
+ ],
63
+ selectedProfileId: "mock",
64
+ uploads: [],
65
+ loading: false,
66
+ error: "",
67
+ login: { busy: false, error: "", expired: false, unavailable: false },
68
+ copy: {
69
+ format: "URL",
70
+ auto: false,
71
+ formats: ["URL", "Markdown", "HTML", "BBCode"],
72
+ },
73
+ notice: "",
74
+ };
75
+ let scenario: Scenario = "ready",
76
+ alive = true;
77
+ const text = (en: string, zh: string) => (state.locale === "zh-CN" ? zh : en);
78
+ function emit() {
79
+ if (alive) for (const listener of listeners) listener(copy(state));
80
+ }
81
+ function later(callback: () => void, delay: number) {
82
+ const timer = setTimeout(() => {
83
+ timers.delete(timer);
84
+ if (alive) callback();
85
+ }, delay);
86
+ timers.add(timer);
87
+ }
88
+ function clear() {
89
+ for (const timer of timers) clearTimeout(timer);
90
+ timers.clear();
91
+ }
92
+ function notify(en: string, zh: string) {
93
+ state.notice = text(en, zh);
94
+ emit();
95
+ }
96
+ function result(id: string) {
97
+ return { id, url: `https://example.invalid/simulated/${id}.png` };
98
+ }
99
+ function format(url: string) {
100
+ switch (state.copy.format) {
101
+ case "Markdown":
102
+ return `![](${url})`;
103
+ case "HTML":
104
+ return `<img src="${url}" alt="">`;
105
+ case "BBCode":
106
+ return `[img]${url}[/img]`;
107
+ default:
108
+ return url;
109
+ }
110
+ }
111
+ async function copyText(value: string) {
112
+ try {
113
+ await navigator.clipboard.writeText(value);
114
+ notify("Copied", "已复制");
115
+ } catch {
116
+ notify("Clipboard unavailable", "剪贴板不可用");
117
+ }
118
+ }
119
+ function run(item: Upload) {
120
+ later(() => {
121
+ if (
122
+ !state.uploads.includes(item) ||
123
+ !["queued", "uploading"].includes(item.status)
124
+ )
125
+ return;
126
+ item.status = "uploading";
127
+ item.progress = Math.min(100, item.progress + 25);
128
+ if (item.progress === 100) {
129
+ item.status = "success";
130
+ item.result = result(item.id);
131
+ if (state.copy.auto) void copyText(format(item.result.url));
132
+ } else run(item);
133
+ emit();
134
+ }, 400);
135
+ }
136
+ function add(name: string, size: number, sourceUrl?: string) {
137
+ if (
138
+ !state.profiles.some(
139
+ (profile) =>
140
+ profile.id === state.selectedProfileId && profile.available,
141
+ )
142
+ ) {
143
+ notify(
144
+ "Choose an available simulated profile",
145
+ "请选择可用的模拟上传方案",
146
+ );
147
+ return;
148
+ }
149
+ const item: Upload = {
150
+ id: crypto.randomUUID(),
151
+ name,
152
+ size,
153
+ profileName: state.profiles[0]!.name,
154
+ sourceUrl,
155
+ status: "queued",
156
+ progress: 0,
157
+ };
158
+ state.uploads = [...state.uploads, item];
159
+ run(item);
160
+ emit();
161
+ }
162
+ function setScenario(value: Scenario) {
163
+ clear();
164
+ scenario = value;
165
+ state.uploads = [];
166
+ state.loading = value === "loading";
167
+ state.login.busy = false;
168
+ state.login.error =
169
+ value === "login-error"
170
+ ? text("Simulated login failure", "模拟登录失败")
171
+ : "";
172
+ state.notice = "";
173
+ state.profiles = state.profiles.map((profile) => ({
174
+ ...profile,
175
+ available: value !== "no-profiles",
176
+ }));
177
+ state.selectedProfileId = value === "no-profiles" ? "" : "mock";
178
+ if (["uploading", "success", "failed"].includes(value)) {
179
+ const item: Upload = {
180
+ id: "preview-image",
181
+ name: "simulated-image.png",
182
+ size: 128 * 1024,
183
+ profileName: state.profiles[0]!.name,
184
+ status: value as Upload["status"],
185
+ progress: value === "uploading" ? 50 : value === "success" ? 100 : 0,
186
+ };
187
+ if (value === "success") item.result = result(item.id);
188
+ if (value === "failed") {
189
+ item.error = text("Simulated upload failure", "模拟上传失败");
190
+ item.resumable = true;
191
+ }
192
+ state.uploads = [item];
193
+ }
194
+ emit();
195
+ }
196
+ function appearance() {
197
+ state.dark = state.mode === "auto" ? motion.matches : state.mode === "dark";
198
+ document.documentElement.classList.toggle("dark", state.dark);
199
+ emit();
200
+ }
201
+ motion.addEventListener("change", appearance);
202
+ const ctx: ThemeContext = {
203
+ apiVersion: 1,
204
+ config: copy(config),
205
+ getSnapshot: () => copy(state),
206
+ subscribe(listener) {
207
+ listeners.add(listener);
208
+ listener(copy(state));
209
+ return () => {
210
+ listeners.delete(listener);
211
+ };
212
+ },
213
+ asset(path) {
214
+ if (
215
+ !path ||
216
+ path.startsWith("/") ||
217
+ /[\\:?*<>"|]/.test(path) ||
218
+ path.split("/").some((part) => !part || part === "." || part === "..")
219
+ )
220
+ throw Error("Invalid package asset path");
221
+ return new URL("/" + path, location.origin).href;
222
+ },
223
+ t(key, params = {}) {
224
+ return messageText(manifest, state.locale, key).replace(
225
+ /\{(\w+)\}/g,
226
+ (match, name: string) => String(params[name] ?? match),
227
+ );
228
+ },
229
+ localized(value) {
230
+ if (typeof value === "string") return value;
231
+ if (!value || typeof value !== "object" || Array.isArray(value))
232
+ return "";
233
+ return typeof value[state.locale] === "string"
234
+ ? (value[state.locale] as string)
235
+ : "";
236
+ },
237
+ setLocale(language) {
238
+ if (!state.locales.includes(language))
239
+ throw Error("Language is not supported");
240
+ state.locale = language;
241
+ document.documentElement.lang = language;
242
+ if (state.login.error)
243
+ state.login.error = text("Simulated login failure", "模拟登录失败");
244
+ for (const item of state.uploads)
245
+ if (item.error)
246
+ item.error = text("Simulated upload failure", "模拟上传失败");
247
+ emit();
248
+ },
249
+ setMode(mode) {
250
+ state.mode = mode;
251
+ appearance();
252
+ },
253
+ navigate(page) {
254
+ if (page === "app")
255
+ notify(
256
+ "Workspace is unavailable in simulated preview",
257
+ "模拟预览不提供管理后台",
258
+ );
259
+ else navigate(page);
260
+ },
261
+ selectProfile(id) {
262
+ state.selectedProfileId = id;
263
+ emit();
264
+ },
265
+ async refresh() {
266
+ state.error = "";
267
+ setScenario("ready");
268
+ },
269
+ uploads: {
270
+ add(files) {
271
+ for (const file of files) add(file.name, file.size);
272
+ },
273
+ addUrls(urls) {
274
+ for (const sourceUrl of urls) {
275
+ try {
276
+ const url = new URL(sourceUrl);
277
+ if (!["http:", "https:"].includes(url.protocol)) throw Error();
278
+ add(
279
+ url.pathname.split("/").pop() || "remote-image",
280
+ 1024,
281
+ sourceUrl,
282
+ );
283
+ } catch {
284
+ notify("Invalid image URL", "图片地址无效");
285
+ }
286
+ }
287
+ },
288
+ async cancel(id) {
289
+ const item = state.uploads.find((item) => item.id === id);
290
+ if (item && ["queued", "uploading"].includes(item.status))
291
+ item.status = "cancelled";
292
+ emit();
293
+ },
294
+ resume(id) {
295
+ const item = state.uploads.find((item) => item.id === id);
296
+ if (item?.resumable) {
297
+ item.status = "queued";
298
+ item.error = undefined;
299
+ run(item);
300
+ emit();
301
+ }
302
+ },
303
+ async dismiss(id) {
304
+ state.uploads = state.uploads.filter((item) => item.id !== id);
305
+ emit();
306
+ },
307
+ },
308
+ links: {
309
+ format,
310
+ async copy(url) {
311
+ await copyText(format(url));
312
+ },
313
+ async copyAll() {
314
+ await copyText(
315
+ state.uploads
316
+ .flatMap((item) => (item.result ? [format(item.result.url)] : []))
317
+ .join("\n"),
318
+ );
319
+ },
320
+ setPreferences(format, auto) {
321
+ if (!state.copy.formats.includes(format))
322
+ throw Error("Unknown copy format");
323
+ state.copy = { ...state.copy, format, auto };
324
+ emit();
325
+ },
326
+ },
327
+ async login(username, _password) {
328
+ state.login.busy = true;
329
+ state.login.error = "";
330
+ emit();
331
+ await new Promise<void>((resolve) => setTimeout(resolve, 150));
332
+ if (!alive) return;
333
+ state.login.busy = false;
334
+ if (scenario === "login-error")
335
+ state.login.error = text("Simulated login failure", "模拟登录失败");
336
+ else {
337
+ state.user = { name: username || "Preview user" };
338
+ navigate("home");
339
+ }
340
+ emit();
341
+ },
342
+ };
343
+ appearance();
344
+ document.documentElement.lang = state.locale;
345
+ return {
346
+ ctx,
347
+ setScenario,
348
+ setPage(page: Page) {
349
+ state.page = page;
350
+ emit();
351
+ },
352
+ setUser(signedIn: boolean) {
353
+ state.user = signedIn ? { name: "Preview user" } : null;
354
+ emit();
355
+ },
356
+ dispose() {
357
+ alive = false;
358
+ clear();
359
+ listeners.clear();
360
+ motion.removeEventListener("change", appearance);
361
+ },
362
+ };
363
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="vite/client" />
2
+ declare module "*.vue" {
3
+ import type { DefineComponent } from "vue";
4
+ const component: DefineComponent;
5
+ export default component;
6
+ }
@@ -0,0 +1,18 @@
1
+ import type { Field, Json } from "@picloom/theme-sdk";
2
+ export function initialValue(schema: Field, value?: Json): Json | undefined;
3
+ export function messageText(
4
+ catalog: {
5
+ defaultLocale: string;
6
+ messages: Record<string, Record<string, string>>;
7
+ },
8
+ locale: string,
9
+ key: string,
10
+ ): string;
11
+ export const supportedLocales: readonly string[];
12
+ export const languages: readonly {
13
+ tag: string;
14
+ name: string;
15
+ direction: string;
16
+ }[];
17
+ export function safeName(value: unknown): value is string;
18
+ export function assetPath(value: unknown): value is string;
@@ -0,0 +1,114 @@
1
+ // ../../shared/field-schema.ts
2
+ function cloneJson(value) {
3
+ return JSON.parse(JSON.stringify(value));
4
+ }
5
+ function pointer(key) {
6
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
7
+ }
8
+ function sensitivePaths(schema, path = "") {
9
+ if (schema["x-sensitive"] === true || schema.type === "array" && schema.items && sensitivePaths(schema.items).length > 0)
10
+ return [path];
11
+ return Object.entries(schema.properties ?? {}).flatMap(([key, rule]) => sensitivePaths(rule, path + "/" + pointer(key)));
12
+ }
13
+ var keywords = new Set([
14
+ "$schema",
15
+ "type",
16
+ "properties",
17
+ "required",
18
+ "additionalProperties",
19
+ "items",
20
+ "enum",
21
+ "const",
22
+ "default",
23
+ "minimum",
24
+ "maximum",
25
+ "exclusiveMinimum",
26
+ "exclusiveMaximum",
27
+ "multipleOf",
28
+ "minLength",
29
+ "maxLength",
30
+ "pattern",
31
+ "format",
32
+ "minItems",
33
+ "maxItems",
34
+ "uniqueItems",
35
+ "if",
36
+ "then",
37
+ "else",
38
+ "allOf",
39
+ "anyOf",
40
+ "oneOf",
41
+ "not",
42
+ "x-localized",
43
+ "x-sensitive",
44
+ "x-immutable",
45
+ "x-ui"
46
+ ]);
47
+ function initialValue(schema, value) {
48
+ if (schema["x-sensitive"] === true || schema.type === "array" && sensitivePaths(schema).length)
49
+ return;
50
+ const seed = value === undefined ? schema.default : value;
51
+ if (schema.type === "object") {
52
+ if (seed !== undefined && (!seed || typeof seed !== "object" || Array.isArray(seed)))
53
+ return cloneJson(seed);
54
+ const result = seed === undefined ? {} : cloneJson(seed);
55
+ for (const [key, rule] of Object.entries(schema.properties ?? {})) {
56
+ const child = initialValue(rule, Object.hasOwn(result, key) ? result[key] : undefined);
57
+ if (child !== undefined)
58
+ result[key] = child;
59
+ else
60
+ delete result[key];
61
+ }
62
+ return seed !== undefined || Object.keys(result).length ? result : undefined;
63
+ }
64
+ if (schema.type === "array" && Array.isArray(seed))
65
+ return seed.map((item) => initialValue(schema.items, item) ?? null);
66
+ return seed === undefined ? undefined : cloneJson(seed);
67
+ }
68
+ // ../../shared/package-localization.ts
69
+ function localeCandidates(locale, fallback) {
70
+ const tag = Intl.getCanonicalLocales(locale)[0];
71
+ return [
72
+ ...new Set([
73
+ tag,
74
+ tag.split("-")[0],
75
+ Intl.getCanonicalLocales(fallback)[0]
76
+ ])
77
+ ];
78
+ }
79
+ function messageText(resource, locale, key) {
80
+ for (const tag of localeCandidates(locale, resource.defaultLocale)) {
81
+ const dictionary = resource.messages[tag];
82
+ const value = dictionary && Object.hasOwn(dictionary, key) ? dictionary[key] : undefined;
83
+ if (value !== undefined)
84
+ return value;
85
+ }
86
+ return key;
87
+ }
88
+ // ../../shared/locales.ts
89
+ var languages = [
90
+ {
91
+ tag: "zh-CN",
92
+ name: "简体中文",
93
+ direction: "ltr",
94
+ aliases: ["zh"]
95
+ },
96
+ {
97
+ tag: "en-US",
98
+ name: "English",
99
+ direction: "ltr",
100
+ aliases: ["en"]
101
+ }
102
+ ];
103
+ var supportedLocales = languages.map((language) => language.tag);
104
+ // ../../shared/theme-package.ts
105
+ var safeName = (v) => typeof v === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/.test(v) && !v.endsWith(".");
106
+ var assetPath = (v) => typeof v === "string" && v.length <= 240 && !/[\\:< >"|?*\x00-\x1f]/.test(v) && v.split("/").every((p) => !!p && p !== "." && p !== ".." && !/[. ]$/.test(p));
107
+ export {
108
+ assetPath,
109
+ initialValue,
110
+ languages,
111
+ messageText,
112
+ safeName,
113
+ supportedLocales
114
+ };
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Picloom theme preview</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/dev/main.ts"></script>
11
+ </body>
12
+ </html>