revise all

This commit is contained in:
jasinco 2025-01-05 22:42:05 +08:00
commit bf68826881
50 changed files with 4549 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

18
.prettierrc Normal file
View File

@ -0,0 +1,18 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": [
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

14
components.json Normal file
View File

@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

2
justfile Normal file
View File

@ -0,0 +1,2 @@
genpb:
protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/lib/protobuf/ ./src/lib/protobuf/definitions/*

2927
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "niming-v2",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check ."
},
"devDependencies": {
"@iconify/json": "^2.2.291",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
"clsx": "^2.1.1",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.9",
"typescript": "^5.0.0",
"unplugin-icons": "^0.22.0",
"vite": "^5.4.11"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"dependencies": {
"@protobuf-ts/plugin": "^2.9.4",
"axios": "^1.7.9",
"bits-ui": "^0.22.0",
"dompurify": "^3.2.3",
"mode-watcher": "^0.5.0",
"ts-proto": "^2.6.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

31
src/app.css Normal file
View File

@ -0,0 +1,31 @@
@import "./color.css";
@import "./fonts.css";
@import "./bits-ui.css";
@import "https://cdn.plyr.io/3.7.8/plyr.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-border;
}
body {
/* @apply bg-background text-foreground; */
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: "PingFang";
width: 100dvw;
min-height: 100dvh;
box-sizing: border-box;
a {
color: hsl(var(--secondary))
}
margin:0px;
}
}

14
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
import 'unplugin-icons/types/svelte'
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export { };

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

30
src/bits-ui.css Normal file
View File

@ -0,0 +1,30 @@
@import "./color.css";
.post_trigger {
padding: 10px 20px;
max-height: 60dvh;
text-align: left;
width: 100%;
font-size: 15px;
box-sizing: border-box;
}
.post_overlay {
position: fixed;
inset: 0;
z-index: 40;
backdrop-filter: grayscale(30%) brightness(50%);
}
.post_content {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 50;
background-color: #0f291f;
}
.scroll>div {
width: 100%;
}

65
src/color.css Normal file
View File

@ -0,0 +1,65 @@
@layer base {
:root {
--background: 192, 9.0%, 32.7%;
--foreground: 47, 60.0%, 89.2%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 105 7% 43%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
--radius: 0.5rem;
}
.dark {
--background: 192, 9.0%, 32.7%;
--foreground: 47, 60.0%, 89.2%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 105 7% 43%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
}
}

35
src/fonts.css Normal file
View File

@ -0,0 +1,35 @@
@font-face {
font-family: "PingFang";
src:
local('PingFang Bold'), src("/PingFang Bold.woff2") type("woff2");
font-weight: bold;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Medium'), src("/PingFang Medium.woff2") type("woff2");
font-weight: normal;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Heavy'), src("/PingFang Heavy.woff2") type("woff2");
font-weight: bolder;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Light'), src("/PingFang Light.woff2") type("woff2");
font-weight: lighter;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Regular'), src("/PingFang Regular.woff2") type("woff2");
font-weight: normal;
}

21
src/lib/Button.svelte Normal file
View File

@ -0,0 +1,21 @@
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip';
import type { Snippet } from 'svelte';
import type { MouseEventHandler } from 'svelte/elements';
let {
children,
onclick = undefined,
description = 'default text'
}: {
onclick: MouseEventHandler<HTMLButtonElement> | undefined;
children: Snippet;
description: string;
} = $props();
</script>
<Tooltip.Root>
<Tooltip.Trigger>
<button {onclick}> {@render children()}</button>
</Tooltip.Trigger>
<Tooltip.Content>{description}</Tooltip.Content>
</Tooltip.Root>

136
src/lib/Editor.svelte Normal file
View File

@ -0,0 +1,136 @@
<script lang="ts">
import Button from './Button.svelte';
import ScrollArea from './components/ui/scroll-area/scroll-area.svelte';
import Textarea from './components/ui/textarea/textarea.svelte';
import { Dialog } from 'bits-ui';
import MynauiImageRectangle from '~icons/mynaui/image-rectangle';
import MynauiAddQueueSolid from '~icons/mynaui/add-queue-solid';
import { SvelteMap } from 'svelte/reactivity';
let { mode } = $props();
let post_message = $state('');
const save_draft = () => {
//determine cookie id
let kv_pair = document.cookie.split(';').map((e) => e.trimStart());
let last_id = kv_pair.findLast((e) => e.startsWith('postdraft'));
//default id = 0
let id = 0;
// find last id and plus one
if (last_id) {
id = Number.parseInt(last_id.split('=')[0].split('_')[1]) + 1;
}
let draft_message = window.btoa(post_message).replaceAll('=', '#');
//save
console.log(`postdraft_${id}=${draft_message}`);
document.cookie = `postdraft_${id}=${draft_message}`;
};
let locker = false;
const debounce = (callback: Function) => {
if (!locker) {
locker = true;
callback();
setTimeout(() => {
locker = false;
}, 2000);
}
return null;
};
let selected_images = new SvelteMap<string, File>();
const image_to_url = $derived.by(() => {
let name_with_url: Map<string, string> = new Map<string, string>();
for (const file of selected_images.values()) {
name_with_url.set(file.name, URL.createObjectURL(file));
}
return name_with_url;
});
const add_image = (_: MouseEvent) => {
console.log('add_image');
// pseudo input element
let file_input = document.createElement('input');
file_input.type = 'file';
file_input.accept = '.gif,.png,.jpeg,.jpg,.webm,.heic,.heif,.mp4,.webp';
file_input.multiple = true;
file_input.classList.add('hidden');
file_input.onchange = () => {
for (let iter = 0; iter < (file_input.files ? file_input.files.length : 0); iter++) {
const file = file_input.files?.item(iter);
if (file) {
selected_images.set(file.name, file);
}
}
};
file_input.onclose = () => {
file_input.remove();
};
file_input.click();
};
</script>
{#if mode === 'wide'}
<div
class="border-box mb-7 h-[60dvh] self-end rounded-md bg-primary px-3 pt-1.5 text-primary-foreground"
style="margin-inline: 7.5%"
>
<div class="relative h-10">
<button class="absolute left-0" onclickcapture={() => debounce(save_draft)}>儲存</button>
<button class="absolute right-0">送出</button>
</div>
<Textarea placeholder="匿名訊息" class="h-2/3 resize-none" bind:value={post_message}></Textarea>
<div class="text-2xl">
<!-- <button aria-label="add image"><MynauiImageRectangle></MynauiImageRectangle></button> -->
<Button description="附加圖片" onclick={add_image}
><MynauiImageRectangle></MynauiImageRectangle></Button
>
</div>
<div>
<ScrollArea>
<div class="flex w-max flex-row gap-3">
{#each image_to_url as map}
<img src={map[1]} class="rounded-md md:h-10" alt={map[0]} />
{/each}
</div>
</ScrollArea>
</div>
</div>
{:else if mode === 'compact'}
<Dialog.Root>
<Dialog.Trigger class="fixed bottom-10 right-10">
<MynauiAddQueueSolid class="text-3xl" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 z-50 bg-black/50" />
<Dialog.Content
class="fixed left-[50%] top-[50%] z-50 h-fit w-[85dvw] -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-popover px-3 py-2"
>
<div class="relative h-10">
<button class="absolute left-0" onclickcapture={() => debounce(save_draft)}>儲存</button>
<button class="absolute right-0">送出</button>
</div>
<Textarea placeholder="匿名訊息" class="min-h-40 resize-none" bind:value={post_message}
></Textarea>
<div class="text-2xl">
<!-- <button aria-label="add image"><MynauiImageRectangle></MynauiImageRectangle></button> -->
<Button description="附加圖片" onclick={add_image}
><MynauiImageRectangle></MynauiImageRectangle></Button
>
</div>
<div>
<ScrollArea orientation="horizontal">
<div class="flex w-max flex-row gap-3">
{#each image_to_url as map}
<img src={map[1]} class="h-52 rounded-md" alt={map[0]} />
{/each}
</div>
</ScrollArea>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
{/if}

52
src/lib/Images.svelte Normal file
View File

@ -0,0 +1,52 @@
<script lang="ts">
import axios from 'axios';
import { SvelteMap } from 'svelte/reactivity';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
let { hashIds }: { hashIds: Array<string> } = $props();
let filetype_url_map = new SvelteMap<string, string>();
let len = $state(0);
for (const hash of hashIds) {
axios.head(`/article/file/${hash}`).then((e) => {
const type = e.headers['content-type']?.toString();
if (type) {
filetype_url_map.set(`/article/file/${hash}`, type);
len++;
}
});
}
const single = (): [string, string] => {
return filetype_url_map.entries().toArray()[0];
};
</script>
{#if len > 1}
<ScrollArea
class="my-auto h-fit w-fit max-w-[90%] whitespace-nowrap rounded-md border-0"
orientation="horizontal"
>
<div class="flex h-36 w-max items-center space-x-4 lg:h-64">
{#each filetype_url_map as map}
{#if map[1].startsWith('image')}
<img src={map[0]} class="top-0 h-full object-contain" alt="Pic" />
{:else if map[1].startsWith('video')}
<video class="h-36 lg:h-64" id="player" playsinline controls>
<source src={map[0]} type={map[1]} />
</video>
{/if}
{/each}
</div>
</ScrollArea>
{:else if len == 1}
{#if single()[1].startsWith('image')}
<img src={single()[0]} class="h-36 w-fit rounded-md object-contain lg:h-64" alt="Pic" />
{:else if single()[1].startsWith('video')}
<video class="h-36 lg:h-64" src={single()[0]}></video>
{/if}
{/if}
<style>
</style>

43
src/lib/Navigation.svelte Normal file
View File

@ -0,0 +1,43 @@
<script lang="ts">
import { ModeWatcher } from 'mode-watcher';
let { children } = $props();
</script>
<div id="container">
<a href="/" class="left"><h1>匿名中工</h1> </a>
<ModeWatcher />
<a href="/tos">服務條款</a>
<a href="/" target="_blank">IG</a>
{@render children()}
</div>
<style>
#container {
width: 100dvw;
height: 10dvh;
display: flex;
align-items: center;
flex-direction: row;
justify-content: flex-end;
box-sizing: border-box;
padding: 0px 10px;
position: sticky;
top: 0px;
z-index: 50;
a {
color: var(--text);
text-decoration: none;
margin-top: auto;
margin-bottom: auto;
h1 {
margin: auto 0 auto;
font-size: 2.5rem;
}
}
gap: 20px;
}
.left {
margin-right: auto;
}
</style>

46
src/lib/Post.svelte Normal file
View File

@ -0,0 +1,46 @@
<script lang="ts">
import { FetchResponse_Message } from './protobuf/niming';
import { Dialog, ScrollArea } from 'bits-ui';
import DOMPurify from 'dompurify';
import { fade } from 'svelte/transition';
import Images from './Images.svelte';
let { post }: { post: FetchResponse_Message } = $props();
const url = /(https|http):\/\/[\S]*/g;
const preprocess = (text: string) => {
// Replace https:// or http:// text with <a href="">
text.replaceAll(url, (sub) => {
return `<a href=${sub}>${sub}</a>`;
});
return DOMPurify.sanitize(text);
};
</script>
<div id="container">
<Dialog.Root>
<Dialog.Trigger class="post_trigger mb-10 rounded-lg bg-card text-card-foreground">
<p class="h-fit max-h-[40dvh] overflow-y-hidden text-ellipsis text-pretty break-words">
{@html preprocess(post.content)}
</p>
<!-- <div class="pt-10"></div> -->
<Images hashIds={post.filesHash}></Images>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay transition={fade} transitionConfig={{ duration: 150 }} class="post_overlay" />
<Dialog.Content class="post_content h-fit min-h-[40dvh] w-[85dvw] rounded-lg lg:w-[50dvw]">
<Dialog.Close>Close</Dialog.Close>
<p class="h-fit text-pretty break-words">
{@html preprocess(post.content)}
</p>
<Images hashIds={post.filesHash}></Images>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
<style>
#container {
display: contents;
}
</style>

81
src/lib/api.ts Normal file
View File

@ -0,0 +1,81 @@
import { FetchResponse, Post, PostResponse } from '$lib/protobuf/niming';
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
export class Result<T> {
status: number
enclose?: T
message?: string
constructor(status: number, enclose?: T, message?: string) {
this.status = status;
this.enclose = enclose;
this.message = message;
}
ok(): boolean { return this.status == 1 }
set_status(status: number) {
this.status = status
}
}
function axios_request(method: string, url: string, params?: Map<string, string> | Object, data?: Uint8Array): Promise<AxiosResponse<ArrayBuffer, any>> {
let cfg: AxiosRequestConfig<Uint8Array> = {
method,
url,
params,
data,
responseType: "arraybuffer",
headers: { 'Content-Type': 'application/x-protobuf' },
}
return axios(cfg)
}
export class API {
page: number
constructor() {
this.page = 0
}
async send_post(content: string, refer: bigint | undefined, files: File[] | undefined) {
let files_buf: Uint8Array[] = [];
files?.forEach((e) => {
e.arrayBuffer().then((arrbuf) => {
files_buf.push(new Uint8Array(arrbuf));
});
});
let ctx = Post.create({ content: content, files: files_buf });
if (refer !== undefined) {
ctx.ref = refer;
}
let result = new Result<PostResponse>(0)
await axios_request('post', '/article', undefined, Post.toBinary(ctx))
.then(e => {
result.enclose = PostResponse.fromBinary(new Uint8Array(e.data))
result.set_status(1);
}).catch(e => {
result.message = e.response?.data
});
return result;
}
async fetch(): Promise<Result<FetchResponse>> {
let result = new Result<FetchResponse>(0)
await axios_request('get', '/article/list', { page: this.page })
.then(e => {
result.enclose = FetchResponse.fromBinary(new Uint8Array(e.data))
result.set_status(1);
this.page += 1;
})
.catch(e => {
result.message = e.response?.data
})
return result;
}
}
let api_inst = new API();
export default api_inst;

View File

@ -0,0 +1,10 @@
import Scrollbar from "./scroll-area-scrollbar.svelte";
import Root from "./scroll-area.svelte";
export {
Root,
Scrollbar,
//,
Root as ScrollArea,
Scrollbar as ScrollAreaScrollbar,
};

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ScrollAreaPrimitive.ScrollbarProps & {
orientation?: "vertical" | "horizontal";
};
let className: $$Props["class"] = undefined;
export let orientation: $$Props["orientation"] = "vertical";
export { className as class };
</script>
<ScrollAreaPrimitive.Scrollbar
{orientation}
class={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px",
orientation === "horizontal" && "h-2.5 w-full border-t border-t-transparent p-px",
className
)}
>
<slot />
<ScrollAreaPrimitive.Thumb
class={cn("bg-border relative rounded-full", orientation === "vertical" && "flex-1")}
/>
</ScrollAreaPrimitive.Scrollbar>

View File

@ -0,0 +1,38 @@
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
import { Scrollbar } from './index.js';
import { cn } from '$lib/utils.js';
type $$Props = ScrollAreaPrimitive.Props & {
orientation?: 'vertical' | 'horizontal' | 'both';
scrollbarXClasses?: string;
scrollbarYClasses?: string;
};
let className: $$Props['class'] = undefined;
export { className as class };
export let orientation = 'vertical';
export let scrollbarXClasses: string = '';
export let scrollbarYClasses: string = '';
</script>
<ScrollAreaPrimitive.Root {...$$restProps} class={cn('relative overflow-hidden', className)}>
<ScrollAreaPrimitive.Viewport class="h-full w-full rounded-[inherit]">
{#if orientation === 'vertical'}
<ScrollAreaPrimitive.Content class="block w-full" style="">
<slot />
</ScrollAreaPrimitive.Content>
{:else}
<ScrollAreaPrimitive.Content>
<slot />
</ScrollAreaPrimitive.Content>
{/if}
</ScrollAreaPrimitive.Viewport>
{#if orientation === 'vertical' || orientation === 'both'}
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
{/if}
{#if orientation === 'horizontal' || orientation === 'both'}
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
{/if}
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>

View File

@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};

View File

@ -0,0 +1,22 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = SeparatorPrimitive.Props;
let className: $$Props["class"] = undefined;
export let orientation: $$Props["orientation"] = "horizontal";
export let decorative: $$Props["decorative"] = undefined;
export { className as class };
</script>
<SeparatorPrimitive.Root
class={cn(
"bg-border shrink-0",
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
className
)}
{orientation}
{decorative}
{...$$restProps}
/>

View File

@ -0,0 +1,28 @@
import Root from "./textarea.svelte";
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
};

View File

@ -0,0 +1,38 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements";
import type { TextareaEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<textarea
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

View File

@ -0,0 +1,15 @@
import { Tooltip as TooltipPrimitive } from "bits-ui";
import Content from "./tooltip-content.svelte";
const Root = TooltipPrimitive.Root;
const Trigger = TooltipPrimitive.Trigger;
export {
Root,
Trigger,
Content,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
};

View File

@ -0,0 +1,28 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = TooltipPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
y: 8,
duration: 150,
};
export { className as class };
</script>
<TooltipPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"bg-popover text-popover-foreground z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
className
)}
{...$$restProps}
>
<slot />
</TooltipPrimitive.Content>

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -0,0 +1,38 @@
syntax = "proto3";
// This is for posting a paragraph.
message Post {
string content = 1;
// reply to a post, like a mail chat.
optional int64 ref = 2;
repeated bytes files = 3;
}
enum Status {
Failed = 0;
Success = 1;
}
// The response of the posting, defining what should return.
message PostResponse {
Status status = 1;
string hash = 2;
uint64 id = 3;
optional string failed_message = 4;
}
message FetchResponse {
message Message {
uint64 id = 1;
string content = 2;
// reply to a post, like a mail chat.
// optional uint64 ref = 3;
// request files through /article/file/<id> with MIME type.
// See it as a BLOB url;
repeated string files_hash = 3;
optional string igid = 4;
repeated string comments_hash = 5;
}
// Several post info
repeated Message posts = 1;
}

367
src/lib/protobuf/niming.ts Normal file
View File

@ -0,0 +1,367 @@
// @generated by protobuf-ts 2.9.4
// @generated from protobuf file "niming.proto" (syntax proto3)
// tslint:disable
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
import type { IBinaryWriter } from "@protobuf-ts/runtime";
import { WireType } from "@protobuf-ts/runtime";
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
import type { IBinaryReader } from "@protobuf-ts/runtime";
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
/**
* This is for posting a paragraph.
*
* @generated from protobuf message Post
*/
export interface Post {
/**
* @generated from protobuf field: string content = 1;
*/
content: string;
/**
* reply to a post, like a mail chat.
*
* @generated from protobuf field: optional int64 ref = 2;
*/
ref?: bigint;
/**
* @generated from protobuf field: repeated bytes files = 3;
*/
files: Uint8Array[];
}
/**
* The response of the posting, defining what should return.
*
* @generated from protobuf message PostResponse
*/
export interface PostResponse {
/**
* @generated from protobuf field: Status status = 1;
*/
status: Status;
/**
* @generated from protobuf field: string hash = 2;
*/
hash: string;
/**
* @generated from protobuf field: uint64 id = 3;
*/
id: bigint;
/**
* @generated from protobuf field: optional string failed_message = 4;
*/
failedMessage?: string;
}
/**
* @generated from protobuf message FetchResponse
*/
export interface FetchResponse {
/**
* Several post info
*
* @generated from protobuf field: repeated FetchResponse.Message posts = 1;
*/
posts: FetchResponse_Message[];
}
/**
* @generated from protobuf message FetchResponse.Message
*/
export interface FetchResponse_Message {
/**
* @generated from protobuf field: uint64 id = 1;
*/
id: bigint;
/**
* @generated from protobuf field: string content = 2;
*/
content: string;
/**
* reply to a post, like a mail chat.
* optional uint64 ref = 3;
* request files through /article/file/<id> with MIME type.
* See it as a BLOB url;
*
* @generated from protobuf field: repeated string files_hash = 3;
*/
filesHash: string[];
/**
* @generated from protobuf field: optional string igid = 4;
*/
igid?: string;
/**
* @generated from protobuf field: repeated string comments_hash = 5;
*/
commentsHash: string[];
}
/**
* @generated from protobuf enum Status
*/
export enum Status {
/**
* @generated from protobuf enum value: Failed = 0;
*/
Failed = 0,
/**
* @generated from protobuf enum value: Success = 1;
*/
Success = 1
}
// @generated message type with reflection information, may provide speed optimized methods
class Post$Type extends MessageType<Post> {
constructor() {
super("Post", [
{ no: 1, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "ref", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 3, name: "files", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<Post>): Post {
const message = globalThis.Object.create((this.messagePrototype!));
message.content = "";
message.files = [];
if (value !== undefined)
reflectionMergePartial<Post>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Post): Post {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string content */ 1:
message.content = reader.string();
break;
case /* optional int64 ref */ 2:
message.ref = reader.int64().toBigInt();
break;
case /* repeated bytes files */ 3:
message.files.push(reader.bytes());
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Post, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string content = 1; */
if (message.content !== "")
writer.tag(1, WireType.LengthDelimited).string(message.content);
/* optional int64 ref = 2; */
if (message.ref !== undefined)
writer.tag(2, WireType.Varint).int64(message.ref);
/* repeated bytes files = 3; */
for (let i = 0; i < message.files.length; i++)
writer.tag(3, WireType.LengthDelimited).bytes(message.files[i]);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message Post
*/
export const Post = new Post$Type();
// @generated message type with reflection information, may provide speed optimized methods
class PostResponse$Type extends MessageType<PostResponse> {
constructor() {
super("PostResponse", [
{ no: 1, name: "status", kind: "enum", T: () => ["Status", Status] },
{ no: 2, name: "hash", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 4, name: "failed_message", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<PostResponse>): PostResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.status = 0;
message.hash = "";
message.id = 0n;
if (value !== undefined)
reflectionMergePartial<PostResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: PostResponse): PostResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* Status status */ 1:
message.status = reader.int32();
break;
case /* string hash */ 2:
message.hash = reader.string();
break;
case /* uint64 id */ 3:
message.id = reader.uint64().toBigInt();
break;
case /* optional string failed_message */ 4:
message.failedMessage = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: PostResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* Status status = 1; */
if (message.status !== 0)
writer.tag(1, WireType.Varint).int32(message.status);
/* string hash = 2; */
if (message.hash !== "")
writer.tag(2, WireType.LengthDelimited).string(message.hash);
/* uint64 id = 3; */
if (message.id !== 0n)
writer.tag(3, WireType.Varint).uint64(message.id);
/* optional string failed_message = 4; */
if (message.failedMessage !== undefined)
writer.tag(4, WireType.LengthDelimited).string(message.failedMessage);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message PostResponse
*/
export const PostResponse = new PostResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FetchResponse$Type extends MessageType<FetchResponse> {
constructor() {
super("FetchResponse", [
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => FetchResponse_Message }
]);
}
create(value?: PartialMessage<FetchResponse>): FetchResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.posts = [];
if (value !== undefined)
reflectionMergePartial<FetchResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FetchResponse): FetchResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* repeated FetchResponse.Message posts */ 1:
message.posts.push(FetchResponse_Message.internalBinaryRead(reader, reader.uint32(), options));
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FetchResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated FetchResponse.Message posts = 1; */
for (let i = 0; i < message.posts.length; i++)
FetchResponse_Message.internalBinaryWrite(message.posts[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message FetchResponse
*/
export const FetchResponse = new FetchResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FetchResponse_Message$Type extends MessageType<FetchResponse_Message> {
constructor() {
super("FetchResponse.Message", [
{ no: 1, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 2, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "files_hash", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 4, name: "igid", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "comments_hash", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<FetchResponse_Message>): FetchResponse_Message {
const message = globalThis.Object.create((this.messagePrototype!));
message.id = 0n;
message.content = "";
message.filesHash = [];
message.commentsHash = [];
if (value !== undefined)
reflectionMergePartial<FetchResponse_Message>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FetchResponse_Message): FetchResponse_Message {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* uint64 id */ 1:
message.id = reader.uint64().toBigInt();
break;
case /* string content */ 2:
message.content = reader.string();
break;
case /* repeated string files_hash */ 3:
message.filesHash.push(reader.string());
break;
case /* optional string igid */ 4:
message.igid = reader.string();
break;
case /* repeated string comments_hash */ 5:
message.commentsHash.push(reader.string());
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FetchResponse_Message, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* uint64 id = 1; */
if (message.id !== 0n)
writer.tag(1, WireType.Varint).uint64(message.id);
/* string content = 2; */
if (message.content !== "")
writer.tag(2, WireType.LengthDelimited).string(message.content);
/* repeated string files_hash = 3; */
for (let i = 0; i < message.filesHash.length; i++)
writer.tag(3, WireType.LengthDelimited).string(message.filesHash[i]);
/* optional string igid = 4; */
if (message.igid !== undefined)
writer.tag(4, WireType.LengthDelimited).string(message.igid);
/* repeated string comments_hash = 5; */
for (let i = 0; i < message.commentsHash.length; i++)
writer.tag(5, WireType.LengthDelimited).string(message.commentsHash[i]);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message FetchResponse.Message
*/
export const FetchResponse_Message = new FetchResponse_Message$Type();

62
src/lib/utils.ts Normal file
View File

@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

13
src/routes/+error.svelte Normal file
View File

@ -0,0 +1,13 @@
<div id="container">
<h1>虛空之境,杳無人煙</h1>
</div>
<style>
h1 {
font-size: 3rem;
}
#container {
display: grid;
place-items: center;
}
</style>

View File

@ -0,0 +1,6 @@
<script>
let { children } = $props();
import '../app.css';
</script>
{@render children()}

76
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,76 @@
<script lang="ts">
import Navigation from '$lib/Navigation.svelte';
import api_inst from '$lib/api';
import { FetchResponse_Message } from '$lib/protobuf/niming';
import { onMount } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
import Post from '$lib/Post.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import Editor from '$lib/Editor.svelte';
let posts = new SvelteSet<FetchResponse_Message>();
let width = $state(0);
onMount(() => {
let trigger = document.querySelector('#trigger');
let observe = new IntersectionObserver(
() => {
api_inst.fetch().then((e) => {
if (e.enclose?.posts !== undefined) {
for (const i of e.enclose?.posts) {
posts.add(i);
}
}
});
},
{ root: null, rootMargin: '0px', threshold: 1.0 }
);
if (trigger) {
observe.observe(trigger);
}
});
</script>
<svelte:body bind:clientWidth={width} />
<Navigation><div></div></Navigation>
<!-- Posts -->
<div id="container" class:layout1={width >= 950} class:layout2={width < 950}>
{#if width >= 950}
<ScrollArea class="h-full w-full pl-[3%] pr-[7.5%]" orientation="vertical">
<div class="flex h-max flex-col">
{#each posts as post}
<Post {post} />
{/each}
<p id="trigger"></p>
</div>
</ScrollArea>
<Editor mode="wide" />
{:else}
<ScrollArea class="h-full w-full pl-[3%] pr-[3%]" orientation="vertical">
<div class="flex h-max flex-col">
{#each posts as post}
<Post {post} />
{/each}
<p id="trigger"></p>
</div>
</ScrollArea>
<Editor mode="compact" />
{/if}
</div>
<style>
#container {
width: 100dvw;
box-sizing: border-box;
overflow-x: hidden;
height: 90dvh;
}
.layout1 {
display: grid;
grid-template-columns: 8fr 4fr;
}
.layout2 {
position: relative;
}
</style>

View File

@ -0,0 +1,11 @@
<script>
import Separator from '$lib/components/ui/separator/separator.svelte';
import Navigation from '$lib/Navigation.svelte';
</script>
<Navigation><div></div></Navigation>
<div class="h-fit w-dvw px-[15%] text-center">
<h1 class="text-4xl font-bold">服務條款</h1>
<Separator class="my-8 text-foreground"></Separator>
<ol>Termainl Is the Best Stuff</ol>
</div>

3
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />
/// <reference types="unplugin-icons/types/svelte" />

BIN
static/PingFang Bold.woff2 Normal file

Binary file not shown.

BIN
static/PingFang Heavy.woff2 Normal file

Binary file not shown.

BIN
static/PingFang Light.woff2 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View File

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter({ pages: 'build', assets: 'build', preprocess: false, strict: true })
}
};
export default config;

61
tailwind.config.ts Normal file
View File

@ -0,0 +1,61 @@
import { fontFamily } from "tailwindcss/defaultTheme";
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: "selector",
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
}
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
}
},
};
export default config;

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

18
vite.config.ts Normal file
View File

@ -0,0 +1,18 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import Icons from 'unplugin-icons/vite';
export default defineConfig({
plugins: [sveltekit(), Icons({
compiler: 'svelte',
})],
server: {
proxy: {
'/article': {
target: "http://10.16.20.17:5000",
changeOrigin: true,
}
}
}
});