Stage2
This commit is contained in:
parent
0a754c17a3
commit
5eb5e81f3f
5
.gitignore
vendored
5
.gitignore
vendored
@ -19,3 +19,8 @@ Thumbs.db
|
|||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# lock files
|
||||||
|
bun.lockb
|
||||||
|
yarn.lock
|
||||||
|
.yarn/
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@types/dompurify": "^3",
|
"@types/dompurify": "^3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"bits-ui": "^0.21.16",
|
"bits-ui": "^0.22.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-svelte": "^0.454.0",
|
"lucide-svelte": "^0.454.0",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
|
160
src/app.css
160
src/app.css
@ -3,139 +3,141 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 224 71.4% 4.1%;
|
--foreground: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--muted: 220 14.3% 95.9%;
|
--muted: 220 14.3% 95.9%;
|
||||||
--muted-foreground: 220 8.9% 46.1%;
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 224 71.4% 4.1%;
|
--popover-foreground: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 224 71.4% 4.1%;
|
--card-foreground: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--border: 220 13% 91%;
|
--border: 220 13% 91%;
|
||||||
--input: 220 13% 91%;
|
--input: 220 13% 91%;
|
||||||
|
|
||||||
--primary: 33deg 43% 91%;
|
--primary: 33deg 43% 91%;
|
||||||
--primary-foreground: 210 20% 98%;
|
--primary-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--secondary: 220 14.3% 95.9%;
|
--secondary: 220 14.3% 95.9%;
|
||||||
--secondary-foreground: 220.9 39.3% 11%;
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
|
||||||
--accent: 220 14.3% 95.9%;
|
--accent: 220 14.3% 95.9%;
|
||||||
--accent-foreground: 220.9 39.3% 11%;
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
|
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 72.2% 50.6%;
|
||||||
--destructive-foreground: 210 20% 98%;
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--ring: 224 71.4% 4.1%;
|
--ring: 224 71.4% 4.1%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 249 22% 12%;
|
--background: 249 22% 12%;
|
||||||
--foreground: 245 50% 91%;
|
--foreground: 245 50% 91%;
|
||||||
|
|
||||||
--muted: 215 27.9% 16.9%;
|
--muted: 215 27.9% 16.9%;
|
||||||
--muted-foreground: 217.9 10.6% 64.9%;
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
|
|
||||||
--popover: 224 71.4% 4.1%;
|
--popover: 224 71.4% 4.1%;
|
||||||
--popover-foreground: 210 20% 98%;
|
--popover-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--card: 35 88% 72%;
|
--card: 35 88% 72%;
|
||||||
--card-foreground: 248 13% 36%;
|
--card-foreground: 248 13% 36%;
|
||||||
|
|
||||||
--border: 248 13% 36%;
|
--border: 248 13% 36%;
|
||||||
--input: 215 27.9% 16.9%;
|
--input: 215 27.9% 16.9%;
|
||||||
|
|
||||||
--primary: 248deg 25% 18%;
|
--primary: 248deg 25% 18%;
|
||||||
--primary-foreground: 248deg 15% 61%;
|
--primary-foreground: 248deg 15% 61%;
|
||||||
|
|
||||||
--secondary: 249deg 15% 28%;
|
--secondary: 249deg 15% 28%;
|
||||||
--secondary-foreground: 210 20% 98%;
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--accent: 215 27.9% 16.9%;
|
--accent: 215 27.9% 16.9%;
|
||||||
--accent-foreground: 210 20% 98%;
|
--accent-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 20% 98%;
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
|
||||||
--ring: 216 12.2% 83.9%;
|
--ring: 216 12.2% 83.9%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body {
|
body {
|
||||||
@apply dark;
|
@apply dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "PingFang";
|
font-family: "PingFang";
|
||||||
src:
|
src:
|
||||||
local('PingFang Bold'), src("/PingFang Bold.woff2") type("woff2");
|
local('PingFang Bold'), src("/PingFang Bold.woff2") type("woff2");
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "PingFang";
|
font-family: "PingFang";
|
||||||
src:
|
src:
|
||||||
local('PingFang Medium'), src("/PingFang Medium.woff2") type("woff2");
|
local('PingFang Medium'), src("/PingFang Medium.woff2") type("woff2");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "PingFang";
|
font-family: "PingFang";
|
||||||
src:
|
src:
|
||||||
local('PingFang Heavy'), src("/PingFang Heavy.woff2") type("woff2");
|
local('PingFang Heavy'), src("/PingFang Heavy.woff2") type("woff2");
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "PingFang";
|
font-family: "PingFang";
|
||||||
src:
|
src:
|
||||||
local('PingFang Light'), src("/PingFang Light.woff2") type("woff2");
|
local('PingFang Light'), src("/PingFang Light.woff2") type("woff2");
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "PingFang";
|
font-family: "PingFang";
|
||||||
src:
|
src:
|
||||||
local('PingFang Regular'), src("/PingFang Regular.woff2") type("woff2");
|
local('PingFang Regular'), src("/PingFang Regular.woff2") type("woff2");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: "PingFang";
|
font-family: "PingFang";
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
width: 100dvw;
|
||||||
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background-color: hsl(var(--muted))
|
background-color: hsl(var(--muted))
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: hsl(248deg, 13%, 36%);
|
background-color: hsl(248deg, 13%, 36%);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
124
src/lib/api.ts
124
src/lib/api.ts
@ -1,63 +1,81 @@
|
|||||||
import { Fetch, Post, PostResponse } from '$lib/protobuf/niming';
|
import { FetchResponse, Post, PostResponse } from '$lib/protobuf/niming';
|
||||||
import axios from 'axios';
|
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
|
||||||
|
|
||||||
export const ResultEnum = {
|
|
||||||
Ok: 1,
|
|
||||||
Err: 2
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type ResultEnum = typeof ResultEnum[keyof typeof ResultEnum];
|
|
||||||
|
|
||||||
export interface Result<T> {
|
export class Result<T> {
|
||||||
kind: ResultEnum;
|
status: number
|
||||||
message: string
|
enclose?: T
|
||||||
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 {
|
export class API {
|
||||||
page: number
|
page: number
|
||||||
constructor() {
|
constructor() {
|
||||||
this.page = 0
|
this.page = 0
|
||||||
}
|
}
|
||||||
async send_post(content: string, refer: bigint | undefined, files: File[] | undefined) {
|
async send_post(content: string, refer: bigint | undefined, files: File[] | undefined) {
|
||||||
let readed: Uint8Array[] = [];
|
let files_buf: Uint8Array[] = [];
|
||||||
files?.forEach((e) => {
|
files?.forEach((e) => {
|
||||||
e.arrayBuffer().then((arrbuf) => {
|
e.arrayBuffer().then((arrbuf) => {
|
||||||
readed.push(new Uint8Array(arrbuf));
|
files_buf.push(new Uint8Array(arrbuf));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let ctx = Post.create({ content: content, files: readed });
|
|
||||||
if (refer !== undefined) {
|
|
||||||
ctx.ref = refer;
|
|
||||||
}
|
|
||||||
let result: Result<PostResponse> = { kind: ResultEnum.Err, message: "" };
|
|
||||||
await axios
|
|
||||||
.post('/article/', Post.toBinary(ctx), {
|
|
||||||
headers: { 'Content-Type': 'application/x-protobuf' },
|
|
||||||
responseType: "arraybuffer"
|
|
||||||
})
|
|
||||||
.then((e) => {
|
|
||||||
result.kind = ResultEnum.Ok;
|
|
||||||
result.enclose = PostResponse.fromBinary(e.data);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.response) {
|
|
||||||
result.message = err.response.data.error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
async fetch() {
|
|
||||||
let result: Result<Fetch> = { kind: ResultEnum.Err, message: "" };
|
|
||||||
axios.get("/article/list", { responseType: "arraybuffer" }).then((e) => {
|
|
||||||
result.kind = ResultEnum.Ok;
|
|
||||||
result.enclose = Fetch.fromBinary(e.data);
|
|
||||||
}).catch((e) => {
|
|
||||||
|
|
||||||
});
|
let ctx = Post.create({ content: content, files: files_buf });
|
||||||
this.page++;
|
if (refer !== undefined) {
|
||||||
return result;
|
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();
|
let api_inst = new API();
|
||||||
export default api_inst;
|
export default api_inst;
|
||||||
|
25
src/lib/components/ui/button/button.svelte
Normal file
25
src/lib/components/ui/button/button.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button as ButtonPrimitive } from "bits-ui";
|
||||||
|
import { type Events, type Props, buttonVariants } from "./index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = Props;
|
||||||
|
type $$Events = Events;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let variant: $$Props["variant"] = "default";
|
||||||
|
export let size: $$Props["size"] = "default";
|
||||||
|
export let builders: $$Props["builders"] = [];
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonPrimitive.Root
|
||||||
|
{builders}
|
||||||
|
class={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
type="button"
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ButtonPrimitive.Root>
|
49
src/lib/components/ui/button/index.ts
Normal file
49
src/lib/components/ui/button/index.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
import type { Button as ButtonPrimitive } from "bits-ui";
|
||||||
|
import Root from "./button.svelte";
|
||||||
|
|
||||||
|
const buttonVariants = tv({
|
||||||
|
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type Variant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
type Size = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
type Props = ButtonPrimitive.Props & {
|
||||||
|
variant?: Variant;
|
||||||
|
size?: Size;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Events = ButtonPrimitive.Events;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type Props,
|
||||||
|
type Events,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
type Props as ButtonProps,
|
||||||
|
type Events as ButtonEvents,
|
||||||
|
buttonVariants,
|
||||||
|
};
|
10
src/lib/components/ui/scroll-area/index.ts
Normal file
10
src/lib/components/ui/scroll-area/index.ts
Normal 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,
|
||||||
|
};
|
@ -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>
|
32
src/lib/components/ui/scroll-area/scroll-area.svelte
Normal file
32
src/lib/components/ui/scroll-area/scroll-area.svelte
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<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]">
|
||||||
|
<ScrollAreaPrimitive.Content>
|
||||||
|
<slot />
|
||||||
|
</ScrollAreaPrimitive.Content>
|
||||||
|
</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>
|
15
src/lib/components/ui/tooltip/index.ts
Normal file
15
src/lib/components/ui/tooltip/index.ts
Normal 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,
|
||||||
|
};
|
28
src/lib/components/ui/tooltip/tooltip-content.svelte
Normal file
28
src/lib/components/ui/tooltip/tooltip-content.svelte
Normal 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,2 +1 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
export * from './textmark'
|
|
||||||
|
@ -8,21 +8,30 @@ message Post {
|
|||||||
repeated bytes files = 3;
|
repeated bytes files = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The response of the posting, defining what should return.
|
enum Status {
|
||||||
message PostResponse {
|
Failed = 0;
|
||||||
string hash = 1;
|
Success = 1;
|
||||||
uint64 id = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Fetch {
|
// 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 {
|
message Message {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
string content = 2;
|
string content = 2;
|
||||||
// reply to a post, like a mail chat.
|
// reply to a post, like a mail chat.
|
||||||
optional uint64 ref = 3;
|
// optional uint64 ref = 3;
|
||||||
// request files through /article/file/<id> with MIME type.
|
// request files through /article/file/<id> with MIME type.
|
||||||
// See it as a BLOB url;
|
// See it as a BLOB url;
|
||||||
repeated uint64 files_id = 4;
|
repeated string files_hash = 3;
|
||||||
|
optional string igid = 4;
|
||||||
|
repeated string comments_hash = 5;
|
||||||
}
|
}
|
||||||
// Several post info
|
// Several post info
|
||||||
repeated Message posts = 1;
|
repeated Message posts = 1;
|
||||||
|
@ -38,29 +38,37 @@ export interface Post {
|
|||||||
*/
|
*/
|
||||||
export interface PostResponse {
|
export interface PostResponse {
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf field: string hash = 1;
|
* @generated from protobuf field: Status status = 1;
|
||||||
|
*/
|
||||||
|
status: Status;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string hash = 2;
|
||||||
*/
|
*/
|
||||||
hash: string;
|
hash: string;
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf field: uint64 id = 2;
|
* @generated from protobuf field: uint64 id = 3;
|
||||||
*/
|
*/
|
||||||
id: bigint;
|
id: bigint;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: optional string failed_message = 4;
|
||||||
|
*/
|
||||||
|
failedMessage?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf message Fetch
|
* @generated from protobuf message FetchResponse
|
||||||
*/
|
*/
|
||||||
export interface Fetch {
|
export interface FetchResponse {
|
||||||
/**
|
/**
|
||||||
* Several post info
|
* Several post info
|
||||||
*
|
*
|
||||||
* @generated from protobuf field: repeated Fetch.Message posts = 1;
|
* @generated from protobuf field: repeated FetchResponse.Message posts = 1;
|
||||||
*/
|
*/
|
||||||
posts: Fetch_Message[];
|
posts: FetchResponse_Message[];
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf message Fetch.Message
|
* @generated from protobuf message FetchResponse.Message
|
||||||
*/
|
*/
|
||||||
export interface Fetch_Message {
|
export interface FetchResponse_Message {
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf field: uint64 id = 1;
|
* @generated from protobuf field: uint64 id = 1;
|
||||||
*/
|
*/
|
||||||
@ -71,17 +79,34 @@ export interface Fetch_Message {
|
|||||||
content: string;
|
content: string;
|
||||||
/**
|
/**
|
||||||
* reply to a post, like a mail chat.
|
* reply to a post, like a mail chat.
|
||||||
*
|
* optional uint64 ref = 3;
|
||||||
* @generated from protobuf field: optional uint64 ref = 3;
|
|
||||||
*/
|
|
||||||
ref?: bigint;
|
|
||||||
/**
|
|
||||||
* request files through /article/file/<id> with MIME type.
|
* request files through /article/file/<id> with MIME type.
|
||||||
* See it as a BLOB url;
|
* See it as a BLOB url;
|
||||||
*
|
*
|
||||||
* @generated from protobuf field: repeated uint64 files_id = 4;
|
* @generated from protobuf field: repeated string files_hash = 3;
|
||||||
*/
|
*/
|
||||||
filesId: bigint[];
|
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
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class Post$Type extends MessageType<Post> {
|
class Post$Type extends MessageType<Post> {
|
||||||
@ -149,12 +174,15 @@ export const Post = new Post$Type();
|
|||||||
class PostResponse$Type extends MessageType<PostResponse> {
|
class PostResponse$Type extends MessageType<PostResponse> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("PostResponse", [
|
super("PostResponse", [
|
||||||
{ no: 1, name: "hash", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
{ no: 1, name: "status", kind: "enum", T: () => ["Status", Status] },
|
||||||
{ no: 2, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
|
{ 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 {
|
create(value?: PartialMessage<PostResponse>): PostResponse {
|
||||||
const message = globalThis.Object.create((this.messagePrototype!));
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.status = 0;
|
||||||
message.hash = "";
|
message.hash = "";
|
||||||
message.id = 0n;
|
message.id = 0n;
|
||||||
if (value !== undefined)
|
if (value !== undefined)
|
||||||
@ -166,12 +194,18 @@ class PostResponse$Type extends MessageType<PostResponse> {
|
|||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
let [fieldNo, wireType] = reader.tag();
|
let [fieldNo, wireType] = reader.tag();
|
||||||
switch (fieldNo) {
|
switch (fieldNo) {
|
||||||
case /* string hash */ 1:
|
case /* Status status */ 1:
|
||||||
|
message.status = reader.int32();
|
||||||
|
break;
|
||||||
|
case /* string hash */ 2:
|
||||||
message.hash = reader.string();
|
message.hash = reader.string();
|
||||||
break;
|
break;
|
||||||
case /* uint64 id */ 2:
|
case /* uint64 id */ 3:
|
||||||
message.id = reader.uint64().toBigInt();
|
message.id = reader.uint64().toBigInt();
|
||||||
break;
|
break;
|
||||||
|
case /* optional string failed_message */ 4:
|
||||||
|
message.failedMessage = reader.string();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
let u = options.readUnknownField;
|
let u = options.readUnknownField;
|
||||||
if (u === "throw")
|
if (u === "throw")
|
||||||
@ -184,12 +218,18 @@ class PostResponse$Type extends MessageType<PostResponse> {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
internalBinaryWrite(message: PostResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
internalBinaryWrite(message: PostResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
/* string hash = 1; */
|
/* Status status = 1; */
|
||||||
|
if (message.status !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).int32(message.status);
|
||||||
|
/* string hash = 2; */
|
||||||
if (message.hash !== "")
|
if (message.hash !== "")
|
||||||
writer.tag(1, WireType.LengthDelimited).string(message.hash);
|
writer.tag(2, WireType.LengthDelimited).string(message.hash);
|
||||||
/* uint64 id = 2; */
|
/* uint64 id = 3; */
|
||||||
if (message.id !== 0n)
|
if (message.id !== 0n)
|
||||||
writer.tag(2, WireType.Varint).uint64(message.id);
|
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;
|
let u = options.writeUnknownFields;
|
||||||
if (u !== false)
|
if (u !== false)
|
||||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
@ -201,26 +241,26 @@ class PostResponse$Type extends MessageType<PostResponse> {
|
|||||||
*/
|
*/
|
||||||
export const PostResponse = new PostResponse$Type();
|
export const PostResponse = new PostResponse$Type();
|
||||||
// @generated message type with reflection information, may provide speed optimized methods
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class Fetch$Type extends MessageType<Fetch> {
|
class FetchResponse$Type extends MessageType<FetchResponse> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Fetch", [
|
super("FetchResponse", [
|
||||||
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Fetch_Message }
|
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => FetchResponse_Message }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
create(value?: PartialMessage<Fetch>): Fetch {
|
create(value?: PartialMessage<FetchResponse>): FetchResponse {
|
||||||
const message = globalThis.Object.create((this.messagePrototype!));
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
message.posts = [];
|
message.posts = [];
|
||||||
if (value !== undefined)
|
if (value !== undefined)
|
||||||
reflectionMergePartial<Fetch>(this, message, value);
|
reflectionMergePartial<FetchResponse>(this, message, value);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch): Fetch {
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FetchResponse): FetchResponse {
|
||||||
let message = target ?? this.create(), end = reader.pos + length;
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
let [fieldNo, wireType] = reader.tag();
|
let [fieldNo, wireType] = reader.tag();
|
||||||
switch (fieldNo) {
|
switch (fieldNo) {
|
||||||
case /* repeated Fetch.Message posts */ 1:
|
case /* repeated FetchResponse.Message posts */ 1:
|
||||||
message.posts.push(Fetch_Message.internalBinaryRead(reader, reader.uint32(), options));
|
message.posts.push(FetchResponse_Message.internalBinaryRead(reader, reader.uint32(), options));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
let u = options.readUnknownField;
|
let u = options.readUnknownField;
|
||||||
@ -233,10 +273,10 @@ class Fetch$Type extends MessageType<Fetch> {
|
|||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
internalBinaryWrite(message: Fetch, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
internalBinaryWrite(message: FetchResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
/* repeated Fetch.Message posts = 1; */
|
/* repeated FetchResponse.Message posts = 1; */
|
||||||
for (let i = 0; i < message.posts.length; i++)
|
for (let i = 0; i < message.posts.length; i++)
|
||||||
Fetch_Message.internalBinaryWrite(message.posts[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
|
FetchResponse_Message.internalBinaryWrite(message.posts[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
|
||||||
let u = options.writeUnknownFields;
|
let u = options.writeUnknownFields;
|
||||||
if (u !== false)
|
if (u !== false)
|
||||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
@ -244,29 +284,31 @@ class Fetch$Type extends MessageType<Fetch> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated MessageType for protobuf message Fetch
|
* @generated MessageType for protobuf message FetchResponse
|
||||||
*/
|
*/
|
||||||
export const Fetch = new Fetch$Type();
|
export const FetchResponse = new FetchResponse$Type();
|
||||||
// @generated message type with reflection information, may provide speed optimized methods
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class Fetch_Message$Type extends MessageType<Fetch_Message> {
|
class FetchResponse_Message$Type extends MessageType<FetchResponse_Message> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Fetch.Message", [
|
super("FetchResponse.Message", [
|
||||||
{ no: 1, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
|
{ 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: 2, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
{ no: 3, name: "ref", kind: "scalar", opt: true, T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
|
{ no: 3, name: "files_hash", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
|
||||||
{ no: 4, name: "files_id", kind: "scalar", repeat: 1 /*RepeatType.PACKED*/, T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
|
{ 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<Fetch_Message>): Fetch_Message {
|
create(value?: PartialMessage<FetchResponse_Message>): FetchResponse_Message {
|
||||||
const message = globalThis.Object.create((this.messagePrototype!));
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
message.id = 0n;
|
message.id = 0n;
|
||||||
message.content = "";
|
message.content = "";
|
||||||
message.filesId = [];
|
message.filesHash = [];
|
||||||
|
message.commentsHash = [];
|
||||||
if (value !== undefined)
|
if (value !== undefined)
|
||||||
reflectionMergePartial<Fetch_Message>(this, message, value);
|
reflectionMergePartial<FetchResponse_Message>(this, message, value);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch_Message): Fetch_Message {
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FetchResponse_Message): FetchResponse_Message {
|
||||||
let message = target ?? this.create(), end = reader.pos + length;
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
while (reader.pos < end) {
|
while (reader.pos < end) {
|
||||||
let [fieldNo, wireType] = reader.tag();
|
let [fieldNo, wireType] = reader.tag();
|
||||||
@ -277,15 +319,14 @@ class Fetch_Message$Type extends MessageType<Fetch_Message> {
|
|||||||
case /* string content */ 2:
|
case /* string content */ 2:
|
||||||
message.content = reader.string();
|
message.content = reader.string();
|
||||||
break;
|
break;
|
||||||
case /* optional uint64 ref */ 3:
|
case /* repeated string files_hash */ 3:
|
||||||
message.ref = reader.uint64().toBigInt();
|
message.filesHash.push(reader.string());
|
||||||
break;
|
break;
|
||||||
case /* repeated uint64 files_id */ 4:
|
case /* optional string igid */ 4:
|
||||||
if (wireType === WireType.LengthDelimited)
|
message.igid = reader.string();
|
||||||
for (let e = reader.int32() + reader.pos; reader.pos < e;)
|
break;
|
||||||
message.filesId.push(reader.uint64().toBigInt());
|
case /* repeated string comments_hash */ 5:
|
||||||
else
|
message.commentsHash.push(reader.string());
|
||||||
message.filesId.push(reader.uint64().toBigInt());
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
let u = options.readUnknownField;
|
let u = options.readUnknownField;
|
||||||
@ -298,23 +339,22 @@ class Fetch_Message$Type extends MessageType<Fetch_Message> {
|
|||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
internalBinaryWrite(message: Fetch_Message, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
internalBinaryWrite(message: FetchResponse_Message, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
/* uint64 id = 1; */
|
/* uint64 id = 1; */
|
||||||
if (message.id !== 0n)
|
if (message.id !== 0n)
|
||||||
writer.tag(1, WireType.Varint).uint64(message.id);
|
writer.tag(1, WireType.Varint).uint64(message.id);
|
||||||
/* string content = 2; */
|
/* string content = 2; */
|
||||||
if (message.content !== "")
|
if (message.content !== "")
|
||||||
writer.tag(2, WireType.LengthDelimited).string(message.content);
|
writer.tag(2, WireType.LengthDelimited).string(message.content);
|
||||||
/* optional uint64 ref = 3; */
|
/* repeated string files_hash = 3; */
|
||||||
if (message.ref !== undefined)
|
for (let i = 0; i < message.filesHash.length; i++)
|
||||||
writer.tag(3, WireType.Varint).uint64(message.ref);
|
writer.tag(3, WireType.LengthDelimited).string(message.filesHash[i]);
|
||||||
/* repeated uint64 files_id = 4; */
|
/* optional string igid = 4; */
|
||||||
if (message.filesId.length) {
|
if (message.igid !== undefined)
|
||||||
writer.tag(4, WireType.LengthDelimited).fork();
|
writer.tag(4, WireType.LengthDelimited).string(message.igid);
|
||||||
for (let i = 0; i < message.filesId.length; i++)
|
/* repeated string comments_hash = 5; */
|
||||||
writer.uint64(message.filesId[i]);
|
for (let i = 0; i < message.commentsHash.length; i++)
|
||||||
writer.join();
|
writer.tag(5, WireType.LengthDelimited).string(message.commentsHash[i]);
|
||||||
}
|
|
||||||
let u = options.writeUnknownFields;
|
let u = options.writeUnknownFields;
|
||||||
if (u !== false)
|
if (u !== false)
|
||||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
@ -322,6 +362,6 @@ class Fetch_Message$Type extends MessageType<Fetch_Message> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated MessageType for protobuf message Fetch.Message
|
* @generated MessageType for protobuf message FetchResponse.Message
|
||||||
*/
|
*/
|
||||||
export const Fetch_Message = new Fetch_Message$Type();
|
export const FetchResponse_Message = new FetchResponse_Message$Type();
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import { Marked } from "marked";
|
|
||||||
import { markedHighlight } from "marked-highlight";
|
|
||||||
import hljs from "highlight.js";
|
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
|
||||||
|
|
||||||
|
|
||||||
export default class TextMark {
|
|
||||||
marked: Marked
|
|
||||||
constructor() {
|
|
||||||
this.marked = new Marked(
|
|
||||||
markedHighlight({
|
|
||||||
emptyLangClass: 'hljs',
|
|
||||||
langPrefix: 'hljs language-',
|
|
||||||
highlight(code, lang, _) {
|
|
||||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
||||||
return hljs.highlight(code, { language }).value;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.marked = this.marked.use({
|
|
||||||
async: true,
|
|
||||||
gfm: true,
|
|
||||||
pedantic: false,
|
|
||||||
hooks: {
|
|
||||||
postprocess(e) {
|
|
||||||
return DOMPurify.sanitize(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async parse(pure_mark: boolean, content: string): Promise<string> {
|
|
||||||
if (!pure_mark) {
|
|
||||||
let processed = "";
|
|
||||||
content.split("\n").forEach((e) => {
|
|
||||||
processed += e + " \n"
|
|
||||||
})
|
|
||||||
content = processed;
|
|
||||||
|
|
||||||
}
|
|
||||||
return this.marked.parse(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
|||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sticky top-0 z-10 grid h-16 w-full grid-cols-4 text-nowrap bg-background">
|
<div class="sticky top-0 z-50 grid h-16 w-full grid-cols-4 text-nowrap bg-background">
|
||||||
<div class="col-span-3 grid grid-cols-4 text-4xl">
|
<div class="col-span-3 grid grid-cols-4 text-4xl">
|
||||||
<a class="m-auto" href="/">中工匿名</a>
|
<a class="m-auto" href="/">中工匿名</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MessageCard from './message_card.svelte';
|
import MessageCard from './message_card.svelte';
|
||||||
import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill';
|
import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill';
|
||||||
import Overlay, { toggle_overlay, get_overlay } from './overlay.svelte';
|
|
||||||
import Marker, { get_content, files, clear_content } from './marker.svelte';
|
import Marker, { get_content, files, clear_content } from './marker.svelte';
|
||||||
import api_inst, { ResultEnum, type Result } from '$lib/api';
|
import api_inst, { type Result } from '$lib/api';
|
||||||
|
import { FetchResponse_Message, type PostResponse } from '$lib/protobuf/niming';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
|
import { Dialog } from 'bits-ui';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
let timer = false;
|
let timer = false;
|
||||||
|
let open = $state(false);
|
||||||
|
|
||||||
const send = () => {
|
const send = () => {
|
||||||
if (!timer) {
|
if (!timer) {
|
||||||
timer = true;
|
timer = true;
|
||||||
api_inst.send_post(get_content(), undefined, files).then((result: Result<null>) => {
|
api_inst.send_post(get_content(), undefined, files).then((result: Result<PostResponse>) => {
|
||||||
alert(result.message);
|
alert(result.message);
|
||||||
if (result.kind == ResultEnum.Ok) {
|
if (result.ok()) {
|
||||||
clear_content();
|
clear_content();
|
||||||
toggle_overlay();
|
open = false;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
timer = false;
|
timer = false;
|
||||||
@ -22,40 +27,64 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let posts = new SvelteSet<FetchResponse_Message>();
|
||||||
|
onMount(() => {
|
||||||
|
api_inst.page = 0;
|
||||||
|
let observe = new IntersectionObserver(
|
||||||
|
(_) => {
|
||||||
|
console.log('Intersecting');
|
||||||
|
api_inst.fetch().then((e) => {
|
||||||
|
console.log(api_inst.page);
|
||||||
|
if (e.ok() && e.enclose != undefined) {
|
||||||
|
e.enclose?.posts.forEach((post) => {
|
||||||
|
posts.add(post);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
rootMargin: '0px',
|
||||||
|
threshold: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const element = document.querySelector('#loader');
|
||||||
|
if (element != null) {
|
||||||
|
observe.observe(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$inspect(posts);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="width:100dvw;" class="grid h-dvh px-5 py-3">
|
<div
|
||||||
<!-- <p class="justify-self-center m-auto">空空如也...</p> -->
|
class="relative grid h-fit min-h-dvh w-dvw grid-cols-1 justify-items-center gap-10 py-3 lg:grid-cols-2 lg:px-[10dvw]"
|
||||||
<MessageCard
|
>
|
||||||
content="
|
{#each posts as p}
|
||||||
# H1
|
<MessageCard post={p}></MessageCard>
|
||||||
## H2
|
{/each}
|
||||||
### H3
|
<div class="absolute bottom-0 left-0 h-10 w-full text-center" id="loader">Loading...</div>
|
||||||
[url](https://google.com)
|
|
||||||
![image](http://localhost:5173/favicon.png)
|
|
||||||
Test
|
|
||||||
NormalText
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="add" onclick={toggle_overlay} class="text-3xl"> <MingcuteAddCircleFill /> </button>
|
<Dialog.Root bind:open>
|
||||||
<Overlay>
|
<Dialog.Trigger class="fixed bottom-[15dvh] right-[10dvw] text-4xl">
|
||||||
<div
|
<MingcuteAddCircleFill />
|
||||||
class="flex flex-col rounded-lg bg-primary px-5 py-3 md:w-4/6 lg:w-1/2"
|
</Dialog.Trigger>
|
||||||
style="height:60dvh;"
|
<Dialog.Portal>
|
||||||
>
|
<Dialog.Overlay
|
||||||
<div class="relative h-6 w-full">
|
transition={fade}
|
||||||
<button class="absolute left-0" onclick={toggle_overlay}>取消</button>
|
transitionConfig={{ duration: 150 }}
|
||||||
<button class="absolute right-0" onclickcapture={send}>發送</button>
|
class="fixed inset-0 z-50 bg-black/80"
|
||||||
</div>
|
/>
|
||||||
<Marker />
|
<Dialog.Content
|
||||||
</div>
|
class="fixed left-[50%] top-[50%] z-50 h-[40dvh] w-[50dvw] translate-x-[-50%] translate-y-[-50%]
|
||||||
</Overlay>
|
rounded-lg border bg-background p-5 shadow-popover outline-none sm:w-[80dvw] md:w-[60dvw]"
|
||||||
|
>
|
||||||
<style>
|
<div class="grid grid-cols-3">
|
||||||
#add {
|
<Dialog.Close class="text-left">離開</Dialog.Close>
|
||||||
position: fixed;
|
<Dialog.Title class="text-center">新增文章</Dialog.Title>
|
||||||
bottom: calc(15dvh);
|
<button class="pr-1 text-right" onclickcapture={send}>Send</button>
|
||||||
right: calc(10dvw);
|
</div>
|
||||||
}
|
<Marker></Marker>
|
||||||
</style>
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
3
src/routes/links/+page.svelte
Normal file
3
src/routes/links/+page.svelte
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<p>IG: <a href="">IG 帳號</a></p>
|
||||||
|
</div>
|
@ -5,13 +5,6 @@
|
|||||||
|
|
||||||
let files: Array<File> = [];
|
let files: Array<File> = [];
|
||||||
const get_content = () => {
|
const get_content = () => {
|
||||||
let length = dyn_files == undefined ? 0 : dyn_files.length;
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
let item = dyn_files?.item(i);
|
|
||||||
if (item != null) {
|
|
||||||
files.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
const clear_content = () => {
|
const clear_content = () => {
|
||||||
@ -24,9 +17,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import type { KeyboardEventHandler } from 'svelte/elements';
|
import type { KeyboardEventHandler } from 'svelte/elements';
|
||||||
import MingcutePicFill from '~icons/mingcute/pic-fill';
|
import MingcutePicFill from '~icons/mingcute/pic-fill';
|
||||||
|
import MingcuteCloseCircleFill from '~icons/mingcute/close-circle-fill';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
// It makes some of ARIA commands into inputs (like tab into \t)
|
||||||
const key_scrap: KeyboardEventHandler<HTMLTextAreaElement> = (event: KeyboardEvent) => {
|
const key_scrap: KeyboardEventHandler<HTMLTextAreaElement> = (event: KeyboardEvent) => {
|
||||||
let ct: HTMLTextAreaElement = event.currentTarget as HTMLTextAreaElement;
|
let ct: HTMLTextAreaElement = event.currentTarget as HTMLTextAreaElement;
|
||||||
if (event.key == 'Tab') {
|
if (event.key == 'Tab') {
|
||||||
@ -37,6 +34,26 @@
|
|||||||
ct.selectionStart = ct.selectionEnd = start + 1;
|
ct.selectionStart = ct.selectionEnd = start + 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// it use svelte's map in order to achieve reactivity(like $state)
|
||||||
|
// https://svelte.dev/docs/svelte/svelte-reactivity
|
||||||
|
let dyn_image_links = new SvelteMap<string, string>();
|
||||||
|
|
||||||
|
// it create a map of name and local files path to show images
|
||||||
|
const update_image = () => {
|
||||||
|
if (dyn_files != undefined) {
|
||||||
|
dyn_image_links.clear();
|
||||||
|
for (let i = 0; i < dyn_files.length; i++) {
|
||||||
|
let item = dyn_files.item(i);
|
||||||
|
if (item != null) {
|
||||||
|
dyn_image_links.set(item.name, URL.createObjectURL(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const delete_image = (key: string) => {
|
||||||
|
dyn_image_links.delete(key);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-col justify-items-center gap-3 pt-[3%]">
|
<div class="flex h-full w-full flex-col justify-items-center gap-3 pt-[3%]">
|
||||||
@ -48,16 +65,37 @@
|
|||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<input
|
<Tooltip.Root>
|
||||||
id="enclosed"
|
<Tooltip.Trigger>
|
||||||
type="file"
|
<input
|
||||||
accept=".gif,.png,.jpeg,.jpg,.webm,.heic,.heif,.mp4,.webp"
|
id="enclosed"
|
||||||
multiple
|
type="file"
|
||||||
class="hidden"
|
accept=".gif,.png,.jpeg,.jpg,.webm,.heic,.heif,.mp4,.webp"
|
||||||
bind:files={dyn_files}
|
multiple
|
||||||
/>
|
class="hidden"
|
||||||
<label for="enclosed" class="mx-auto w-fit"><MingcutePicFill class="text-xl" /> </label>
|
bind:files={dyn_files}
|
||||||
|
onchange={update_image}
|
||||||
|
/>
|
||||||
|
<label for="enclosed" class="mx-auto w-fit"><MingcutePicFill class="text-xl" /> </label>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>
|
||||||
|
<p>Add a picture</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- selected images preview -->
|
||||||
|
{#if dyn_image_links.size > 0}
|
||||||
|
<div class="flex h-[75px] w-full max-w-full flex-row gap-3 overflow-y-hidden">
|
||||||
|
{#each dyn_image_links as entries}
|
||||||
|
<div class="relative h-[70px] w-[70px]">
|
||||||
|
<img src={entries[1]} alt="selected files" class="object-scale-down" />
|
||||||
|
<button class="absolute right-0 top-0" onclick={() => delete_image(entries[0])}>
|
||||||
|
<MingcuteCloseCircleFill />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,53 +1,73 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { content }: { content: string } = $props();
|
let { post }: { post: FetchResponse_Message } = $props();
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import { Marked } from 'marked';
|
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
|
||||||
|
import type { FetchResponse_Message } from '$lib/protobuf/niming';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
import { markedHighlight } from 'marked-highlight';
|
|
||||||
import hljs from 'highlight.js';
|
//Icons
|
||||||
//TODO Need sanitizer
|
import MingcuteForwardFill from '~icons/mingcute/forward-fill';
|
||||||
const marked = new Marked(
|
import MingcuteInsFill from '~icons/mingcute/ins-fill';
|
||||||
markedHighlight({
|
import MingcuteHeartFill from '~icons/mingcute/heart-fill';
|
||||||
emptyLangClass: 'hljs',
|
|
||||||
langPrefix: 'hljs language-',
|
const url = /(https|http):\/\/[\S]*/g;
|
||||||
highlight(code, lang, info) {
|
const parser = (text: string) => {
|
||||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
text.replaceAll(url, (sub) => {
|
||||||
return hljs.highlight(code, { language }).value;
|
return `<a href=${sub}>${sub}</a>`;
|
||||||
}
|
});
|
||||||
})
|
return text;
|
||||||
);
|
};
|
||||||
marked.use({
|
let item = DOMPurify.sanitize(parser(post.content));
|
||||||
hooks: {
|
|
||||||
postprocess: (text) => {
|
|
||||||
return DOMPurify.sanitize(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let parsed = marked.parse(content, {
|
|
||||||
async: true,
|
|
||||||
gfm: true,
|
|
||||||
pedantic: false
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root>
|
<Dialog.Root>
|
||||||
<Dialog.Trigger
|
<div class="flex h-fit w-fit flex-row-reverse">
|
||||||
class="max-h-80 max-w-64 overflow-y-hidden bg-amber-400 text-slate-900 drop-shadow-[5px_10px_#f38f0f]"
|
<div class="z-10 flex w-10 flex-col-reverse gap-3 text-2xl">
|
||||||
>
|
<!-- Interactions -->
|
||||||
<div class="inner_card">
|
<button class=" text-center" aria-label="like"><MingcuteHeartFill /></button>
|
||||||
{#await parsed}
|
<button
|
||||||
loading
|
class=" text-center"
|
||||||
{:then item}
|
aria-label="ig"
|
||||||
{@html item}
|
onclick={() => {
|
||||||
{/await}
|
window.open('', '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MingcuteInsFill />
|
||||||
|
</button>
|
||||||
|
<button class=" text-center" aria-label="forward"><MingcuteForwardFill /></button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Trigger>
|
<Dialog.Trigger
|
||||||
|
class="h-fit max-h-80 max-w-[70dvw] overflow-y-hidden bg-amber-400 text-slate-900 drop-shadow-[5px_10px_#f38f0f] lg:max-w-96"
|
||||||
|
>
|
||||||
|
<div class="inner_card">
|
||||||
|
{@html item}
|
||||||
|
{#if post.filesHash.length > 0}
|
||||||
|
<ScrollArea class="mt-10 w-full" orientation="horizontal">
|
||||||
|
{#each post.filesHash as hash}
|
||||||
|
<img src={'/article/file/' + hash} alt="pictures" />
|
||||||
|
{/each}
|
||||||
|
</ScrollArea>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
</div>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
{#await parsed}
|
{@html item}
|
||||||
loading
|
{#if post.filesHash.length > 1}
|
||||||
{:then item}
|
<ScrollArea class="w-full whitespace-nowrap rounded-md border" orientation="horizontal">
|
||||||
{@html item}
|
<div class="flex h-[35dvh] w-fit space-x-4 p-4">
|
||||||
{/await}
|
{#each post.filesHash as hash}
|
||||||
|
<img src={'/article/file/' + hash} alt="pictures" class="aspect-auto object-cover" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
{:else if post.filesHash.length == 1}
|
||||||
|
<img
|
||||||
|
src={'/article/file/' + post.filesHash[0]}
|
||||||
|
alt="pictures"
|
||||||
|
class="aspect-auto object-cover"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
@ -59,9 +79,9 @@
|
|||||||
code {
|
code {
|
||||||
@apply bg-amber-600;
|
@apply bg-amber-600;
|
||||||
}
|
}
|
||||||
mask-image: linear-gradient(0deg, transparent, white 50%);
|
mask-image: linear-gradient(0deg, transparent, white 30%);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0px 5%;
|
padding: 5px 5% 40px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
<script module lang="ts">
|
|
||||||
let overlay = $state(false);
|
|
||||||
const toggle_overlay = () => {
|
|
||||||
overlay = !overlay;
|
|
||||||
if (overlay) {
|
|
||||||
document.body.classList.add('overflow-y-hidden');
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('overflow-y-hidden');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const get_overlay = () => {
|
|
||||||
return overlay;
|
|
||||||
};
|
|
||||||
export { toggle_overlay, get_overlay };
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
let { children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={'fixed left-0 top-0 z-20 h-dvh w-dvw ' +
|
|
||||||
(overlay ? 'overlay_blur grid place-items-center' : 'hidden')}
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.overlay_blur {
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,6 +1,4 @@
|
|||||||
<div class="flex justify-center">
|
<div class="w-dvw text-center text-xl font-bold lg:text-3xl">
|
||||||
<div class="w-1/2 break-keep text-center text-2xl">
|
<p>“中工匿名”不記錄個人姓名。</p>
|
||||||
<p>“中工匿名”不記錄個人網路位置、名字。</p>
|
<p>如訊息侵害他人權利,違反大眾道德觀,皆不保證留存。</p>
|
||||||
<p>如訊息侵害他人權利,違反大眾道德觀,皆不保證留存。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,13 +3,13 @@ import { defineConfig } from 'vite';
|
|||||||
import Icons from 'unplugin-icons/vite'
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit(), Icons({ compiler: 'svelte' })],
|
plugins: [sveltekit(), Icons({ compiler: 'svelte' })],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/article': {
|
'/article': {
|
||||||
target: "http://10.16.20.17:5000",
|
target: "http://10.16.20.17:5000",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user