@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.
@@ -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
+ });