@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 +21 -0
- package/README.md +33 -2
- package/dist/template/LICENSE +21 -0
- package/dist/template/README.md +57 -0
- package/dist/template/dev/context.ts +363 -0
- package/dist/template/dev/env.d.ts +6 -0
- package/dist/template/dev/helpers.d.ts +18 -0
- package/dist/template/dev/helpers.js +114 -0
- package/dist/template/dev/index.html +12 -0
- package/dist/template/dev/main.ts +179 -0
- package/dist/template/dev/virtual.d.ts +5 -0
- package/dist/template/gitignore +4 -0
- package/dist/template/package.json +27 -0
- package/dist/template/public/README.txt +3 -0
- package/dist/template/scripts/build.ts +119 -0
- package/dist/template/scripts/manifest.ts +61 -0
- package/dist/template/scripts/serve.ts +127 -0
- package/dist/template/src/Page.vue +405 -0
- package/dist/template/src/entry.ts +13 -0
- package/dist/template/src/style.css +121 -0
- package/dist/template/theme.json +108 -0
- package/dist/template/tsconfig.json +22 -0
- package/dist/template/vite.config.ts +20 -0
- package/index.js +63 -0
- package/package.json +29 -4
|
@@ -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,22 @@
|
|
|
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": ["ES2022", "DOM", "DOM.Iterable"],
|
|
13
|
+
"types": ["bun", "vite/client"]
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src/**/*.ts",
|
|
17
|
+
"src/**/*.vue",
|
|
18
|
+
"dev/**/*.ts",
|
|
19
|
+
"scripts/**/*.ts",
|
|
20
|
+
"vite.config.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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({
|
|
6
|
+
plugins: [vue(), tailwindcss()],
|
|
7
|
+
publicDir: false,
|
|
8
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
9
|
+
build: {
|
|
10
|
+
outDir: "dist/package",
|
|
11
|
+
emptyOutDir: true,
|
|
12
|
+
sourcemap: false,
|
|
13
|
+
lib: {
|
|
14
|
+
entry: resolve("src/entry.ts"),
|
|
15
|
+
formats: ["es"],
|
|
16
|
+
fileName: () => "entry.js",
|
|
17
|
+
cssFileName: "style",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|