@picloom/create-theme 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +36 -2
- package/dist/template/LICENSE +21 -0
- package/dist/template/README.md +60 -0
- package/dist/template/gitignore +7 -0
- package/dist/template/package.json +27 -0
- package/dist/template/public/README.txt +3 -0
- package/dist/template/src/Page.vue +405 -0
- package/dist/template/src/entry.ts +13 -0
- package/dist/template/src/env.d.ts +6 -0
- package/dist/template/src/style.css +121 -0
- package/dist/template/theme.json +108 -0
- package/dist/template/tsconfig.json +27 -0
- package/dist/template/vite.config.ts +23 -0
- package/index.js +63 -0
- package/package.json +29 -4
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,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @picloom/create-theme
|
|
2
2
|
|
|
3
|
-
|
|
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.1 ./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 template maintained in this package.
|
|
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 `template/` into `dist/template/` without reading the host repository. Preview and ZIP builds are provided
|
|
27
|
+
by `@picloom/theme-tools`; generated projects do not contain a copy of the preview host.
|
|
28
|
+
Do not edit `dist/template/`. Use exact npm version 0.1.1.
|
|
29
|
+
|
|
30
|
+
## 中文
|
|
31
|
+
|
|
32
|
+
基于工具链仓库独立维护的模板创建 Vue 3 主题。提供模拟预览、热更新、生产产物预览与 ZIP 打包。
|
|
33
|
+
创建工具只复制文件,不覆盖已有目录、不安装依赖、不初始化 Git、不发布。
|
|
34
|
+
模拟预览不连接真实服务;上传、登录与权限必须在隔离测试实例安装后验证。
|
|
35
|
+
通过 npm 创建项目,也可按文档使用本地 `.tgz` 验证开发版本。编辑 `template/` 维护模板,构建时复制到 `dist/template/`,不依赖主仓库默认主题源码。
|
|
36
|
+
|
|
37
|
+
协议类型来自 `@picloom/theme-sdk`,预览与构建调用 `@picloom/theme-tools`;生成项目不复制通用工具代码。
|
|
@@ -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,60 @@
|
|
|
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
|
+
SDK and tool dependencies use exact versions after publication. During local validation, install the matching local tarballs; the current development versions are not yet published.
|
|
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 typecheck` 单独检查类型;`bun run build` 构建并生成可安装 ZIP;`bun run preview` 检查已有构建产物。
|
|
53
|
+
- 修改主题 ID、显示名称和版本均以 `theme.json` 为准,静态资源放入 `public/assets/`。
|
|
54
|
+
- 入口导入的样式合并为 `style.css`,须保留清单全局样式声明;页面专属样式通过各页面的 `styles` 引用,切页时分别加载和卸载。
|
|
55
|
+
- 主题包不包含开发面板、本地配置、凭据或源码映射。所有主题配置可能公开,不要保存密钥。
|
|
56
|
+
- 在隔离 Picloom 测试实例安装 ZIP 后验证真实上传和登录;模拟成功不代表真实服务已验证。
|
|
57
|
+
- SDK 和开发工具在发布后使用 npm 精确版本;当前未发布版本使用本地包验证,安装时同步更新锁文件。
|
|
58
|
+
|
|
59
|
+
Local preview and ZIP builds are provided by `@picloom/theme-tools` using this theme’s Vite configuration.
|
|
60
|
+
本地预览和 ZIP 构建由 `@picloom/theme-tools` 统一提供,使用本主题的 Vite 配置,不复制预览宿主源码。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-theme",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"bun": ">=1.4.3"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun --bun picloom-theme dev",
|
|
11
|
+
"typecheck": "bun --bun vue-tsc --noEmit",
|
|
12
|
+
"build": "bun --bun picloom-theme build",
|
|
13
|
+
"preview": "bun --bun picloom-theme preview"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@picloom/theme-sdk": "0.1.1",
|
|
17
|
+
"@tailwindcss/vite": "4.3.3",
|
|
18
|
+
"@types/bun": "1.3.10",
|
|
19
|
+
"@vitejs/plugin-vue": "6.0.10",
|
|
20
|
+
"tailwindcss": "4.3.3",
|
|
21
|
+
"typescript": "5.9.3",
|
|
22
|
+
"vite": "8.3.4",
|
|
23
|
+
"vue": "3.5.44",
|
|
24
|
+
"vue-tsc": "3.3.13",
|
|
25
|
+
"@picloom/theme-tools": "0.1.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onUnmounted, computed } from "vue";
|
|
3
|
+
import type { ThemeContext } from "@picloom/theme-sdk";
|
|
4
|
+
const props = defineProps<{ context: ThemeContext }>();
|
|
5
|
+
const ctx = props.context,
|
|
6
|
+
s = ref(ctx.getSnapshot());
|
|
7
|
+
const stop = ctx.subscribe((value) => {
|
|
8
|
+
s.value = value;
|
|
9
|
+
});
|
|
10
|
+
onUnmounted(stop);
|
|
11
|
+
const t = (key: string, params?: Record<string, string | number>) =>
|
|
12
|
+
ctx.t(key, params);
|
|
13
|
+
function size(value: number) {
|
|
14
|
+
const unit = value < 1024 ? 1 : value < 1024 ** 2 ? 1024 : 1024 ** 2;
|
|
15
|
+
const digits = unit === 1 ? 0 : 1;
|
|
16
|
+
return (
|
|
17
|
+
new Intl.NumberFormat(s.value.locale, {
|
|
18
|
+
minimumFractionDigits: digits,
|
|
19
|
+
maximumFractionDigits: digits,
|
|
20
|
+
}).format(value / unit) +
|
|
21
|
+
(unit === 1 ? " B" : unit === 1024 ? " KB" : " MB")
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const input = ref<HTMLInputElement>(),
|
|
25
|
+
username = ref(""),
|
|
26
|
+
password = ref(""),
|
|
27
|
+
dragging = ref(false);
|
|
28
|
+
const profile = computed(() =>
|
|
29
|
+
s.value.profiles.find(
|
|
30
|
+
(p) => p.id === s.value.selectedProfileId && p.available,
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
function files(list: FileList | null) {
|
|
34
|
+
if (list) ctx.uploads.add(Array.from(list));
|
|
35
|
+
}
|
|
36
|
+
function paste(e: ClipboardEvent) {
|
|
37
|
+
if (
|
|
38
|
+
s.value.page !== "home" ||
|
|
39
|
+
(e.target as HTMLElement)?.closest("input,textarea,[contenteditable]")
|
|
40
|
+
)
|
|
41
|
+
return;
|
|
42
|
+
if (e.clipboardData?.files.length) {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
files(e.clipboardData.files);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const urls = e.clipboardData?.getData("text/plain").trim().split(/\s+/);
|
|
48
|
+
if (
|
|
49
|
+
urls?.length &&
|
|
50
|
+
urls.every((u) => {
|
|
51
|
+
try {
|
|
52
|
+
return ["http:", "https:"].includes(new URL(u).protocol);
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
) {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
ctx.uploads.addUrls(urls);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
window.addEventListener("paste", paste);
|
|
63
|
+
onUnmounted(() => window.removeEventListener("paste", paste));
|
|
64
|
+
async function login() {
|
|
65
|
+
const value = password.value;
|
|
66
|
+
password.value = "";
|
|
67
|
+
await ctx.login(username.value, value);
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
<template>
|
|
71
|
+
<div
|
|
72
|
+
class="min-h-screen bg-background text-foreground"
|
|
73
|
+
:class="{ dark: s.dark }"
|
|
74
|
+
>
|
|
75
|
+
<header
|
|
76
|
+
class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-4 px-5 py-6"
|
|
77
|
+
>
|
|
78
|
+
<a
|
|
79
|
+
href="/"
|
|
80
|
+
class="flex items-center gap-2 text-lg font-semibold"
|
|
81
|
+
@click.prevent="ctx.navigate('home')"
|
|
82
|
+
><img
|
|
83
|
+
v-if="s.site.logoUrl"
|
|
84
|
+
:src="s.site.logoUrl"
|
|
85
|
+
alt=""
|
|
86
|
+
class="size-8"
|
|
87
|
+
/>{{ s.site.name }}</a
|
|
88
|
+
>
|
|
89
|
+
<div class="flex items-center gap-4">
|
|
90
|
+
<select
|
|
91
|
+
v-if="s.locales.length > 1"
|
|
92
|
+
:value="s.locale"
|
|
93
|
+
:aria-label="t('themes.language')"
|
|
94
|
+
@change="ctx.setLocale(($event.target as HTMLSelectElement).value)"
|
|
95
|
+
class="native-select"
|
|
96
|
+
>
|
|
97
|
+
<option
|
|
98
|
+
v-for="language in s.languages"
|
|
99
|
+
:key="language.tag"
|
|
100
|
+
:value="language.tag"
|
|
101
|
+
>
|
|
102
|
+
{{ language.name }}
|
|
103
|
+
</option>
|
|
104
|
+
</select>
|
|
105
|
+
<select
|
|
106
|
+
:value="s.mode"
|
|
107
|
+
:aria-label="t('themes.mode')"
|
|
108
|
+
@change="
|
|
109
|
+
ctx.setMode(
|
|
110
|
+
($event.target as HTMLSelectElement).value as
|
|
111
|
+
| 'light'
|
|
112
|
+
| 'dark'
|
|
113
|
+
| 'auto',
|
|
114
|
+
)
|
|
115
|
+
"
|
|
116
|
+
class="native-select"
|
|
117
|
+
>
|
|
118
|
+
<option value="auto">{{ t("themes.auto") }}</option>
|
|
119
|
+
<option value="light">{{ t("ui.lightMode") }}</option>
|
|
120
|
+
<option value="dark">{{ t("ui.darkMode") }}</option>
|
|
121
|
+
</select>
|
|
122
|
+
<a
|
|
123
|
+
v-if="s.page === 'home'"
|
|
124
|
+
:href="s.user ? '/app/images' : '/login?return=/app/images'"
|
|
125
|
+
@click.prevent="ctx.navigate(s.user ? 'app' : 'login')"
|
|
126
|
+
>{{ s.user ? t("ui.app") : t("ui.signIn") }}
|
|
127
|
+
<span aria-hidden="true">→</span></a
|
|
128
|
+
>
|
|
129
|
+
</div>
|
|
130
|
+
</header>
|
|
131
|
+
<main
|
|
132
|
+
v-if="s.page === 'home'"
|
|
133
|
+
class="mx-auto max-w-3xl px-5 py-12 sm:py-20"
|
|
134
|
+
>
|
|
135
|
+
<div
|
|
136
|
+
v-if="s.site.homeTitle || s.site.description"
|
|
137
|
+
class="mb-10 text-center"
|
|
138
|
+
>
|
|
139
|
+
<h1
|
|
140
|
+
v-if="s.site.homeTitle"
|
|
141
|
+
class="text-3xl font-semibold tracking-tight sm:text-4xl"
|
|
142
|
+
>
|
|
143
|
+
{{ s.site.homeTitle }}
|
|
144
|
+
</h1>
|
|
145
|
+
<p v-if="s.site.description" class="mt-4 text-muted-foreground">
|
|
146
|
+
{{ s.site.description }}
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
<p v-if="s.loading" class="subtle">{{ t("ui.loading") }}</p>
|
|
150
|
+
<p v-if="s.error" class="error-box">
|
|
151
|
+
{{ s.error }}
|
|
152
|
+
<button @click="ctx.refresh()">{{ t("ui.retry") }}</button>
|
|
153
|
+
</p>
|
|
154
|
+
<div class="space-y-6">
|
|
155
|
+
<div
|
|
156
|
+
class="cursor-pointer rounded-2xl border-2 border-dashed px-6 py-16 text-center"
|
|
157
|
+
:class="dragging ? 'border-primary bg-primary/5' : 'border-border'"
|
|
158
|
+
role="button"
|
|
159
|
+
tabindex="0"
|
|
160
|
+
:aria-label="t('ui.dropImagesHere')"
|
|
161
|
+
:aria-disabled="!profile"
|
|
162
|
+
@click="profile && input?.click()"
|
|
163
|
+
@keydown.enter.self.prevent="profile && input?.click()"
|
|
164
|
+
@keydown.space.self.prevent="profile && input?.click()"
|
|
165
|
+
@dragover.prevent="dragging = true"
|
|
166
|
+
@dragleave.prevent="dragging = false"
|
|
167
|
+
@drop.prevent="
|
|
168
|
+
dragging = false;
|
|
169
|
+
files($event.dataTransfer?.files ?? null);
|
|
170
|
+
"
|
|
171
|
+
>
|
|
172
|
+
<p class="mb-2 font-medium">{{ t("ui.dropImagesHere") }}</p>
|
|
173
|
+
<p class="mb-5 text-sm text-muted-foreground">
|
|
174
|
+
{{ t("ui.orPasteFromYourClipboard") }}
|
|
175
|
+
</p>
|
|
176
|
+
<button
|
|
177
|
+
class="rounded-lg bg-primary px-5 py-2 text-primary-foreground disabled:opacity-50"
|
|
178
|
+
:disabled="!profile"
|
|
179
|
+
@click.stop="input?.click()"
|
|
180
|
+
>
|
|
181
|
+
{{ t("ui.chooseFiles") }}
|
|
182
|
+
</button>
|
|
183
|
+
<input
|
|
184
|
+
ref="input"
|
|
185
|
+
type="file"
|
|
186
|
+
data-testid="upload-input"
|
|
187
|
+
accept="image/*"
|
|
188
|
+
multiple
|
|
189
|
+
hidden
|
|
190
|
+
@click.stop
|
|
191
|
+
@change="
|
|
192
|
+
files(input?.files ?? null);
|
|
193
|
+
if (input) input.value = '';
|
|
194
|
+
"
|
|
195
|
+
/>
|
|
196
|
+
<p v-if="profile" class="mt-4 text-xs text-muted-foreground">
|
|
197
|
+
{{ t("uploads.limit", { size: size(profile.maxUploadBytes) }) }} ·
|
|
198
|
+
{{ profile.allowedMimeTypes.join(" / ") }}
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
<label class="field"
|
|
202
|
+
>{{ t("ui.uploadProfile")
|
|
203
|
+
}}<select
|
|
204
|
+
class="native-select"
|
|
205
|
+
:aria-label="t('ui.uploadProfile')"
|
|
206
|
+
:value="s.selectedProfileId"
|
|
207
|
+
@change="
|
|
208
|
+
ctx.selectProfile(($event.target as HTMLSelectElement).value)
|
|
209
|
+
"
|
|
210
|
+
>
|
|
211
|
+
<option value="" disabled>{{ t("ui.selectAProfile") }}</option>
|
|
212
|
+
<option
|
|
213
|
+
v-for="p in s.profiles"
|
|
214
|
+
:key="p.id"
|
|
215
|
+
:value="p.id"
|
|
216
|
+
:disabled="!p.available"
|
|
217
|
+
>
|
|
218
|
+
{{ p.name }}{{ p.reason ? " · " + p.reason : "" }}
|
|
219
|
+
</option>
|
|
220
|
+
</select></label
|
|
221
|
+
>
|
|
222
|
+
<p v-if="!s.loading && !profile" class="error-box">
|
|
223
|
+
{{
|
|
224
|
+
s.profiles.some((p) => p.available)
|
|
225
|
+
? t("profiles.required")
|
|
226
|
+
: !s.user
|
|
227
|
+
? t("users.guestUnavailable")
|
|
228
|
+
: t("users.noProfiles")
|
|
229
|
+
}}
|
|
230
|
+
</p>
|
|
231
|
+
<div class="flex flex-wrap items-center gap-4 border-b pb-6">
|
|
232
|
+
<label class="field flex-1"
|
|
233
|
+
>{{ t("uploads.copyFormat")
|
|
234
|
+
}}<select
|
|
235
|
+
class="native-select"
|
|
236
|
+
:aria-label="t('uploads.copyFormat')"
|
|
237
|
+
:value="s.copy.format"
|
|
238
|
+
@change="
|
|
239
|
+
ctx.links.setPreferences(
|
|
240
|
+
($event.target as HTMLSelectElement).value,
|
|
241
|
+
s.copy.auto,
|
|
242
|
+
)
|
|
243
|
+
"
|
|
244
|
+
>
|
|
245
|
+
<option v-for="f in s.copy.formats" :key="f">{{ f }}</option>
|
|
246
|
+
</select></label
|
|
247
|
+
>
|
|
248
|
+
<label
|
|
249
|
+
><input
|
|
250
|
+
type="checkbox"
|
|
251
|
+
:checked="s.copy.auto"
|
|
252
|
+
@change="
|
|
253
|
+
ctx.links.setPreferences(
|
|
254
|
+
s.copy.format,
|
|
255
|
+
($event.target as HTMLInputElement).checked,
|
|
256
|
+
)
|
|
257
|
+
"
|
|
258
|
+
/>
|
|
259
|
+
{{ t("uploads.autoCopy") }}</label
|
|
260
|
+
>
|
|
261
|
+
</div>
|
|
262
|
+
<div v-if="s.uploads.length" class="space-y-3">
|
|
263
|
+
<h2>
|
|
264
|
+
{{ t("ui.uploadQueue") }} ·
|
|
265
|
+
{{ s.uploads.filter((u) => u.status === "success").length }} /
|
|
266
|
+
{{ s.uploads.length }}
|
|
267
|
+
</h2>
|
|
268
|
+
<button @click="ctx.links.copyAll()">
|
|
269
|
+
{{ t("uploads.copyAll") }}
|
|
270
|
+
</button>
|
|
271
|
+
<div
|
|
272
|
+
v-for="u in s.uploads"
|
|
273
|
+
:key="u.id"
|
|
274
|
+
class="rounded-lg border p-3"
|
|
275
|
+
data-testid="upload-row"
|
|
276
|
+
>
|
|
277
|
+
<p class="truncate">{{ u.name }}</p>
|
|
278
|
+
<p class="text-xs text-muted-foreground">
|
|
279
|
+
{{ u.sourceUrl ? t("uploads.fromUrl") : size(u.size) }} ·
|
|
280
|
+
{{ u.profileName }}
|
|
281
|
+
</p>
|
|
282
|
+
<p class="mt-2 text-xs" aria-live="polite">
|
|
283
|
+
{{
|
|
284
|
+
u.error ||
|
|
285
|
+
{
|
|
286
|
+
queued: t("ui.queued"),
|
|
287
|
+
uploading: t("uploads.progress", { progress: u.progress }),
|
|
288
|
+
processing: t("ui.processingOnServer"),
|
|
289
|
+
success: t("ui.uploaded"),
|
|
290
|
+
cancelled: t("uploads.cancelled"),
|
|
291
|
+
failed: "",
|
|
292
|
+
}[u.status]
|
|
293
|
+
}}
|
|
294
|
+
</p>
|
|
295
|
+
<progress
|
|
296
|
+
v-if="u.status === 'uploading'"
|
|
297
|
+
:value="u.progress"
|
|
298
|
+
max="100"
|
|
299
|
+
:aria-label="t('ui.uploadProgress')"
|
|
300
|
+
class="w-full"
|
|
301
|
+
/>
|
|
302
|
+
<div class="mt-2 flex gap-3">
|
|
303
|
+
<button
|
|
304
|
+
v-if="u.status === 'failed' && u.resumable"
|
|
305
|
+
@click="ctx.uploads.resume(u.id)"
|
|
306
|
+
>
|
|
307
|
+
{{ t("uploads.continue") }}</button
|
|
308
|
+
><button
|
|
309
|
+
v-if="['queued', 'uploading'].includes(u.status)"
|
|
310
|
+
@click="ctx.uploads.cancel(u.id)"
|
|
311
|
+
>
|
|
312
|
+
{{ t("ui.cancel") }}</button
|
|
313
|
+
><button
|
|
314
|
+
v-if="['failed', 'cancelled'].includes(u.status)"
|
|
315
|
+
@click="ctx.uploads.dismiss(u.id)"
|
|
316
|
+
>
|
|
317
|
+
{{ t("uploads.dismiss") }}
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
<div
|
|
321
|
+
v-if="u.result"
|
|
322
|
+
role="group"
|
|
323
|
+
:aria-label="t('ui.copy')"
|
|
324
|
+
class="mt-3 flex flex-wrap gap-2"
|
|
325
|
+
>
|
|
326
|
+
<select
|
|
327
|
+
class="native-select"
|
|
328
|
+
:aria-label="t('uploads.copyFormat')"
|
|
329
|
+
:value="s.copy.format"
|
|
330
|
+
@change="
|
|
331
|
+
ctx.links.setPreferences(
|
|
332
|
+
($event.target as HTMLSelectElement).value,
|
|
333
|
+
s.copy.auto,
|
|
334
|
+
)
|
|
335
|
+
"
|
|
336
|
+
>
|
|
337
|
+
<option v-for="format in s.copy.formats" :key="format">
|
|
338
|
+
{{ format }}
|
|
339
|
+
</option></select
|
|
340
|
+
><input
|
|
341
|
+
class="native-select min-w-0 flex-1"
|
|
342
|
+
readonly
|
|
343
|
+
:value="ctx.links.format(u.result.url)"
|
|
344
|
+
:aria-label="t('uploads.result')"
|
|
345
|
+
/><button @click="ctx.links.copy(u.result!.url)">
|
|
346
|
+
{{ t("ui.copy") }}
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</main>
|
|
353
|
+
<main v-else class="mx-auto max-w-sm px-5 py-12">
|
|
354
|
+
<p
|
|
355
|
+
v-if="s.site.description"
|
|
356
|
+
class="mb-5 text-center text-muted-foreground"
|
|
357
|
+
>
|
|
358
|
+
{{ s.site.description }}
|
|
359
|
+
</p>
|
|
360
|
+
<form class="panel space-y-6 p-7" @submit.prevent="login">
|
|
361
|
+
<h1 class="text-xl font-semibold">{{ t("ui.signIn") }}</h1>
|
|
362
|
+
<p v-if="s.login.expired" class="subtle">
|
|
363
|
+
{{ t("ui.sessionExpiredSignInAgain") }}
|
|
364
|
+
</p>
|
|
365
|
+
<p v-if="s.login.unavailable" class="error-box">
|
|
366
|
+
{{ t("ui.cannotReachTheServerTryAgain") }}
|
|
367
|
+
</p>
|
|
368
|
+
<label class="field"
|
|
369
|
+
>{{ t("ui.username")
|
|
370
|
+
}}<input
|
|
371
|
+
v-model="username"
|
|
372
|
+
class="native-select"
|
|
373
|
+
autocomplete="username"
|
|
374
|
+
required
|
|
375
|
+
/></label>
|
|
376
|
+
<label class="field"
|
|
377
|
+
>{{ t("ui.password")
|
|
378
|
+
}}<input
|
|
379
|
+
v-model="password"
|
|
380
|
+
class="native-select"
|
|
381
|
+
type="password"
|
|
382
|
+
autocomplete="current-password"
|
|
383
|
+
required
|
|
384
|
+
/></label>
|
|
385
|
+
<p v-if="s.login.error" role="alert" class="error-box">
|
|
386
|
+
{{ s.login.error }}
|
|
387
|
+
</p>
|
|
388
|
+
<button
|
|
389
|
+
class="w-full rounded-lg bg-primary px-5 py-2 text-primary-foreground"
|
|
390
|
+
type="submit"
|
|
391
|
+
:disabled="s.login.busy"
|
|
392
|
+
>
|
|
393
|
+
{{ s.login.busy ? t("ui.signingIn") : t("ui.signIn") }}
|
|
394
|
+
</button>
|
|
395
|
+
</form>
|
|
396
|
+
</main>
|
|
397
|
+
<div
|
|
398
|
+
v-if="s.notice"
|
|
399
|
+
role="status"
|
|
400
|
+
class="fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-xl border bg-card px-5 py-3"
|
|
401
|
+
>
|
|
402
|
+
{{ s.notice }}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createApp } from "vue";
|
|
2
|
+
import Page from "./Page.vue";
|
|
3
|
+
import type { ThemeContext } from "@picloom/theme-sdk";
|
|
4
|
+
import "./style.css";
|
|
5
|
+
export function mount(container: HTMLElement, context: ThemeContext) {
|
|
6
|
+
const app = createApp(Page, { context });
|
|
7
|
+
app.mount(container);
|
|
8
|
+
return {
|
|
9
|
+
dispose() {
|
|
10
|
+
app.unmount();
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
@import "tailwindcss" source(none);
|
|
2
|
+
@source "./";
|
|
3
|
+
@custom-variant dark (&:is(.dark *));
|
|
4
|
+
@theme inline {
|
|
5
|
+
--color-background: var(--background);
|
|
6
|
+
--color-foreground: var(--foreground);
|
|
7
|
+
--color-card: var(--card);
|
|
8
|
+
--color-card-foreground: var(--foreground);
|
|
9
|
+
--color-popover: var(--card);
|
|
10
|
+
--color-popover-foreground: var(--foreground);
|
|
11
|
+
--color-primary: var(--primary);
|
|
12
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
13
|
+
--color-secondary: var(--muted);
|
|
14
|
+
--color-secondary-foreground: var(--foreground);
|
|
15
|
+
--color-muted: var(--muted);
|
|
16
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
17
|
+
--color-accent: var(--muted);
|
|
18
|
+
--color-accent-foreground: var(--foreground);
|
|
19
|
+
--color-destructive: #d44e55;
|
|
20
|
+
--color-destructive-foreground: #fff;
|
|
21
|
+
--color-border: var(--border);
|
|
22
|
+
--color-input: var(--border);
|
|
23
|
+
--color-ring: var(--primary);
|
|
24
|
+
--radius-sm: 6px;
|
|
25
|
+
--radius-md: 8px;
|
|
26
|
+
--radius-lg: 12px;
|
|
27
|
+
--radius-xl: 16px;
|
|
28
|
+
}
|
|
29
|
+
:root {
|
|
30
|
+
--background: #fff;
|
|
31
|
+
--foreground: #213547;
|
|
32
|
+
--card: #f9f9f9;
|
|
33
|
+
--primary: #287f59;
|
|
34
|
+
--primary-foreground: #fff;
|
|
35
|
+
color-scheme: light;
|
|
36
|
+
accent-color: var(--primary);
|
|
37
|
+
--muted: #f1f1f1;
|
|
38
|
+
--muted-foreground: #666;
|
|
39
|
+
--border: #e5e5e5;
|
|
40
|
+
font-family: Inter, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
41
|
+
}
|
|
42
|
+
.dark {
|
|
43
|
+
color-scheme: dark;
|
|
44
|
+
accent-color: var(--primary);
|
|
45
|
+
--background: #1a1a1a;
|
|
46
|
+
--foreground: #ffffffde;
|
|
47
|
+
--card: #242424;
|
|
48
|
+
--primary: #42b883;
|
|
49
|
+
--primary-foreground: #13261e;
|
|
50
|
+
--muted: #2f2f2f;
|
|
51
|
+
--muted-foreground: #ebebeb99;
|
|
52
|
+
--border: #5454547a;
|
|
53
|
+
}
|
|
54
|
+
@layer base {
|
|
55
|
+
* {
|
|
56
|
+
@apply border-border;
|
|
57
|
+
}
|
|
58
|
+
body {
|
|
59
|
+
@apply bg-background text-foreground;
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
button,
|
|
63
|
+
a,
|
|
64
|
+
input,
|
|
65
|
+
select {
|
|
66
|
+
-webkit-tap-highlight-color: transparent;
|
|
67
|
+
}
|
|
68
|
+
/* Native popup menus may not inherit the select's transparent background. */
|
|
69
|
+
select {
|
|
70
|
+
color-scheme: inherit;
|
|
71
|
+
}
|
|
72
|
+
select option,
|
|
73
|
+
select optgroup {
|
|
74
|
+
background-color: var(--background);
|
|
75
|
+
color: var(--foreground);
|
|
76
|
+
}
|
|
77
|
+
button:not(:disabled),
|
|
78
|
+
a {
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
}
|
|
81
|
+
:focus-visible {
|
|
82
|
+
outline: 2px solid var(--primary);
|
|
83
|
+
outline-offset: 3px;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
@layer components {
|
|
87
|
+
.panel {
|
|
88
|
+
@apply rounded-xl border bg-card dark:bg-transparent;
|
|
89
|
+
}
|
|
90
|
+
.field {
|
|
91
|
+
@apply grid gap-2 text-sm;
|
|
92
|
+
}
|
|
93
|
+
.native-select {
|
|
94
|
+
@apply h-9 min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50;
|
|
95
|
+
}
|
|
96
|
+
input[type="checkbox"] {
|
|
97
|
+
@apply size-4 shrink-0 accent-primary;
|
|
98
|
+
}
|
|
99
|
+
label.field:has(> :required),
|
|
100
|
+
.required-label {
|
|
101
|
+
position: relative;
|
|
102
|
+
}
|
|
103
|
+
label.field:has(> :required)::after,
|
|
104
|
+
.required-label::after {
|
|
105
|
+
content: "*";
|
|
106
|
+
position: absolute;
|
|
107
|
+
right: 0;
|
|
108
|
+
top: 0;
|
|
109
|
+
color: var(--primary);
|
|
110
|
+
}
|
|
111
|
+
[aria-invalid="true"] {
|
|
112
|
+
border-color: var(--color-destructive);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.subtle {
|
|
116
|
+
@apply text-sm text-muted-foreground;
|
|
117
|
+
}
|
|
118
|
+
.error-box {
|
|
119
|
+
@apply rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "example.default",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"apiVersion": 1,
|
|
5
|
+
"defaultLocale": "en-US",
|
|
6
|
+
"supportedLocales": [
|
|
7
|
+
"zh-CN",
|
|
8
|
+
"en-US"
|
|
9
|
+
],
|
|
10
|
+
"nameKey": "package.name",
|
|
11
|
+
"messages": {
|
|
12
|
+
"en-US": {
|
|
13
|
+
"package.name": "Default theme",
|
|
14
|
+
"profiles.required": "No usable default upload profile. Please select a profile.",
|
|
15
|
+
"themes.auto": "System",
|
|
16
|
+
"themes.language": "Language",
|
|
17
|
+
"themes.mode": "Appearance",
|
|
18
|
+
"ui.app": "App",
|
|
19
|
+
"ui.cancel": "Cancel",
|
|
20
|
+
"ui.cannotReachTheServerTryAgain": "Cannot reach the server. Try again.",
|
|
21
|
+
"ui.chooseFiles": "Choose files",
|
|
22
|
+
"ui.copy": "Copy",
|
|
23
|
+
"ui.darkMode": "Dark mode",
|
|
24
|
+
"ui.dropImagesHere": "Drop images here",
|
|
25
|
+
"ui.lightMode": "Light mode",
|
|
26
|
+
"ui.loading": "Loading…",
|
|
27
|
+
"ui.orPasteFromYourClipboard": "or paste images or image URLs",
|
|
28
|
+
"ui.password": "Password",
|
|
29
|
+
"ui.processingOnServer": "Processing on server…",
|
|
30
|
+
"ui.queued": "Queued",
|
|
31
|
+
"ui.retry": "Retry",
|
|
32
|
+
"ui.selectAProfile": "Select a profile",
|
|
33
|
+
"ui.sessionExpiredSignInAgain": "Session expired. Sign in again.",
|
|
34
|
+
"ui.signIn": "Sign in",
|
|
35
|
+
"ui.signingIn": "Signing in…",
|
|
36
|
+
"ui.uploadProfile": "Upload profile",
|
|
37
|
+
"ui.uploadProgress": "Upload progress",
|
|
38
|
+
"ui.uploadQueue": "Upload queue",
|
|
39
|
+
"ui.uploaded": "Uploaded",
|
|
40
|
+
"ui.username": "Username",
|
|
41
|
+
"uploads.autoCopy": "Auto-copy after upload",
|
|
42
|
+
"uploads.cancelled": "Cancelled",
|
|
43
|
+
"uploads.continue": "Continue upload",
|
|
44
|
+
"uploads.copyAll": "Copy all",
|
|
45
|
+
"uploads.copyFormat": "Copy format",
|
|
46
|
+
"uploads.dismiss": "Dismiss",
|
|
47
|
+
"uploads.fromUrl": "From URL",
|
|
48
|
+
"uploads.limit": "Up to {size} per file",
|
|
49
|
+
"uploads.progress": "Uploading {progress}%",
|
|
50
|
+
"uploads.result": "Upload result",
|
|
51
|
+
"users.guestUnavailable": "Guest uploads are unavailable. Sign in to upload.",
|
|
52
|
+
"users.noProfiles": "No upload profiles available. Contact an administrator."
|
|
53
|
+
},
|
|
54
|
+
"zh-CN": {
|
|
55
|
+
"package.name": "默认主题",
|
|
56
|
+
"profiles.required": "没有可用的默认上传配置,请选择上传配置。",
|
|
57
|
+
"themes.auto": "跟随系统",
|
|
58
|
+
"themes.language": "语言",
|
|
59
|
+
"themes.mode": "显示模式",
|
|
60
|
+
"ui.app": "管理后台",
|
|
61
|
+
"ui.cancel": "取消",
|
|
62
|
+
"ui.cannotReachTheServerTryAgain": "无法连接服务器,请重试。",
|
|
63
|
+
"ui.chooseFiles": "选择文件",
|
|
64
|
+
"ui.copy": "复制",
|
|
65
|
+
"ui.darkMode": "深色模式",
|
|
66
|
+
"ui.dropImagesHere": "将图片拖到这里",
|
|
67
|
+
"ui.lightMode": "浅色模式",
|
|
68
|
+
"ui.loading": "正在加载…",
|
|
69
|
+
"ui.orPasteFromYourClipboard": "或粘贴图片、图片链接",
|
|
70
|
+
"ui.password": "密码",
|
|
71
|
+
"ui.processingOnServer": "服务器处理中…",
|
|
72
|
+
"ui.queued": "排队中",
|
|
73
|
+
"ui.retry": "重试",
|
|
74
|
+
"ui.selectAProfile": "选择上传配置",
|
|
75
|
+
"ui.sessionExpiredSignInAgain": "登录已过期,请重新登录。",
|
|
76
|
+
"ui.signIn": "登录",
|
|
77
|
+
"ui.signingIn": "正在登录…",
|
|
78
|
+
"ui.uploadProfile": "上传配置",
|
|
79
|
+
"ui.uploadProgress": "上传进度",
|
|
80
|
+
"ui.uploadQueue": "上传队列",
|
|
81
|
+
"ui.uploaded": "已上传",
|
|
82
|
+
"ui.username": "用户名",
|
|
83
|
+
"uploads.autoCopy": "上传后自动复制",
|
|
84
|
+
"uploads.cancelled": "已取消",
|
|
85
|
+
"uploads.continue": "继续上传",
|
|
86
|
+
"uploads.copyAll": "复制全部",
|
|
87
|
+
"uploads.copyFormat": "复制格式",
|
|
88
|
+
"uploads.dismiss": "关闭",
|
|
89
|
+
"uploads.fromUrl": "通过链接上传",
|
|
90
|
+
"uploads.limit": "单文件最大 {size}",
|
|
91
|
+
"uploads.progress": "正在上传 {progress}%",
|
|
92
|
+
"uploads.result": "上传结果",
|
|
93
|
+
"users.guestUnavailable": "访客上传不可用,请登录后上传。",
|
|
94
|
+
"users.noProfiles": "没有可用的上传配置,请联系管理员。"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"pages": {
|
|
98
|
+
"home": {
|
|
99
|
+
"entry": "entry.js"
|
|
100
|
+
},
|
|
101
|
+
"login": {
|
|
102
|
+
"entry": "entry.js"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"styles": [
|
|
106
|
+
"style.css"
|
|
107
|
+
]
|
|
108
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"lib": [
|
|
13
|
+
"ES2022",
|
|
14
|
+
"DOM",
|
|
15
|
+
"DOM.Iterable"
|
|
16
|
+
],
|
|
17
|
+
"types": [
|
|
18
|
+
"bun",
|
|
19
|
+
"vite/client"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"include": [
|
|
23
|
+
"src/**/*.ts",
|
|
24
|
+
"src/**/*.vue",
|
|
25
|
+
"vite.config.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import vue from "@vitejs/plugin-vue";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
export default defineConfig(({ command }) => ({
|
|
6
|
+
plugins: [vue(), tailwindcss()],
|
|
7
|
+
publicDir: false,
|
|
8
|
+
define:
|
|
9
|
+
command === "build"
|
|
10
|
+
? { "process.env.NODE_ENV": JSON.stringify("production") }
|
|
11
|
+
: {},
|
|
12
|
+
build: {
|
|
13
|
+
outDir: "dist/package",
|
|
14
|
+
emptyOutDir: true,
|
|
15
|
+
sourcemap: false,
|
|
16
|
+
lib: {
|
|
17
|
+
entry: resolve("src/entry.ts"),
|
|
18
|
+
formats: ["es"],
|
|
19
|
+
fileName: () => "entry.js",
|
|
20
|
+
cssFileName: "style",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}));
|
package/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
cp,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
readdir,
|
|
7
|
+
rename,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { basename, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
|
|
15
|
+
console.log("Usage: bun create @picloom/theme <new-directory>");
|
|
16
|
+
} else if (args.length !== 1 || args[0].startsWith("-")) {
|
|
17
|
+
console.error("Usage: bun create @picloom/theme <new-directory>");
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
} else {
|
|
20
|
+
try {
|
|
21
|
+
const target = resolve(args[0]);
|
|
22
|
+
// Refuse even empty existing directories: never merge into a user's project.
|
|
23
|
+
await mkdir(target);
|
|
24
|
+
const template = fileURLToPath(
|
|
25
|
+
new URL("./dist/template/", import.meta.url),
|
|
26
|
+
);
|
|
27
|
+
for (const entry of await readdir(template)) {
|
|
28
|
+
await cp(resolve(template, entry), resolve(target, entry), {
|
|
29
|
+
recursive: true,
|
|
30
|
+
force: false,
|
|
31
|
+
errorOnExist: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Package managers may rename/omit .gitignore when packing, so ship it under a plain name.
|
|
35
|
+
await rename(resolve(target, "gitignore"), resolve(target, ".gitignore"));
|
|
36
|
+
const name =
|
|
37
|
+
basename(target)
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9._-]/g, "-")
|
|
40
|
+
.replace(/^[^a-z]+/, "")
|
|
41
|
+
.slice(0, 80)
|
|
42
|
+
.replace(/\.+$/, "") || "my-theme";
|
|
43
|
+
const manifestPath = resolve(target, "theme.json");
|
|
44
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
45
|
+
manifest.id = `example.${name}`;
|
|
46
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
47
|
+
const packagePath = resolve(target, "package.json");
|
|
48
|
+
const pkg = JSON.parse(await readFile(packagePath, "utf8"));
|
|
49
|
+
pkg.name = name;
|
|
50
|
+
await writeFile(packagePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
51
|
+
const quotedDirectory = "'" + args[0].replaceAll("'", "'\\''") + "'";
|
|
52
|
+
console.log(
|
|
53
|
+
`Created ${target}\n\nNext:\n cd ${quotedDirectory}\n bun install\n bun run typecheck\n bun run dev\n bun run build\n\nUpload dist/${manifest.id}-${manifest.version}.zip to Picloom.`,
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(
|
|
57
|
+
error.code === "EEXIST"
|
|
58
|
+
? "Target already exists. Choose a new directory; no existing files were changed."
|
|
59
|
+
: `Could not create theme: ${error.message}`,
|
|
60
|
+
);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@picloom/create-theme",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Create a standalone Picloom Vue theme with local preview and ZIP packaging",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-theme": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"bun": ">=1.4.3"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun prepare.ts",
|
|
15
|
+
"prepack": "bun run build"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"dist/template",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/llleixx/picloom-tooling.git",
|
|
26
|
+
"directory": "tools/create-theme"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|