@md-meta-view/web 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.json +25 -0
- package/index.html +12 -0
- package/package.json +54 -0
- package/src/components/column-filter.tsx +70 -0
- package/src/components/data-table.tsx +390 -0
- package/src/components/markdown-view.tsx +104 -0
- package/src/components/theme-toggle.tsx +21 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +88 -0
- package/src/components/ui/resizable.tsx +48 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/hooks/use-md-data.ts +71 -0
- package/src/hooks/use-theme.ts +38 -0
- package/src/index.css +131 -0
- package/src/lib/router.ts +32 -0
- package/src/lib/search-params.test.ts +163 -0
- package/src/lib/search-params.ts +72 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +11 -0
- package/src/routes/__root.tsx +27 -0
- package/src/routes/index.tsx +131 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
13
|
+
secondary:
|
|
14
|
+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
15
|
+
destructive:
|
|
16
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
17
|
+
outline:
|
|
18
|
+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function Badge({
|
|
31
|
+
className,
|
|
32
|
+
variant = "default",
|
|
33
|
+
render,
|
|
34
|
+
...props
|
|
35
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
36
|
+
return useRender({
|
|
37
|
+
defaultTagName: "span",
|
|
38
|
+
props: mergeProps<"span">(
|
|
39
|
+
{
|
|
40
|
+
className: cn(badgeVariants({ variant }), className),
|
|
41
|
+
},
|
|
42
|
+
props,
|
|
43
|
+
),
|
|
44
|
+
render,
|
|
45
|
+
state: {
|
|
46
|
+
slot: "badge",
|
|
47
|
+
variant,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
12
|
+
outline:
|
|
13
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
14
|
+
secondary:
|
|
15
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
16
|
+
ghost:
|
|
17
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
18
|
+
destructive:
|
|
19
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default:
|
|
24
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
25
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
27
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
28
|
+
icon: "size-8",
|
|
29
|
+
"icon-xs":
|
|
30
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
31
|
+
"icon-sm":
|
|
32
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
33
|
+
"icon-lg": "size-9",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: "default",
|
|
38
|
+
size: "default",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function Button({
|
|
44
|
+
className,
|
|
45
|
+
variant = "default",
|
|
46
|
+
size = "default",
|
|
47
|
+
...props
|
|
48
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
49
|
+
return (
|
|
50
|
+
<ButtonPrimitive
|
|
51
|
+
data-slot="button"
|
|
52
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Input as InputPrimitive } from "@base-ui/react/input";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
7
|
+
return (
|
|
8
|
+
<InputPrimitive
|
|
9
|
+
type={type}
|
|
10
|
+
data-slot="input"
|
|
11
|
+
className={cn(
|
|
12
|
+
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Input };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
|
7
|
+
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
|
11
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function PopoverContent({
|
|
15
|
+
className,
|
|
16
|
+
align = "center",
|
|
17
|
+
alignOffset = 0,
|
|
18
|
+
side = "bottom",
|
|
19
|
+
sideOffset = 4,
|
|
20
|
+
...props
|
|
21
|
+
}: PopoverPrimitive.Popup.Props &
|
|
22
|
+
Pick<
|
|
23
|
+
PopoverPrimitive.Positioner.Props,
|
|
24
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
25
|
+
>) {
|
|
26
|
+
return (
|
|
27
|
+
<PopoverPrimitive.Portal>
|
|
28
|
+
<PopoverPrimitive.Positioner
|
|
29
|
+
align={align}
|
|
30
|
+
alignOffset={alignOffset}
|
|
31
|
+
side={side}
|
|
32
|
+
sideOffset={sideOffset}
|
|
33
|
+
className="isolate z-50"
|
|
34
|
+
>
|
|
35
|
+
<PopoverPrimitive.Popup
|
|
36
|
+
data-slot="popover-content"
|
|
37
|
+
className={cn(
|
|
38
|
+
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
</PopoverPrimitive.Positioner>
|
|
44
|
+
</PopoverPrimitive.Portal>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="popover-header"
|
|
52
|
+
className={cn("flex flex-col gap-0.5 text-sm", className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
|
|
59
|
+
return (
|
|
60
|
+
<PopoverPrimitive.Title
|
|
61
|
+
data-slot="popover-title"
|
|
62
|
+
className={cn("font-heading font-medium", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function PopoverDescription({
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: PopoverPrimitive.Description.Props) {
|
|
72
|
+
return (
|
|
73
|
+
<PopoverPrimitive.Description
|
|
74
|
+
data-slot="popover-description"
|
|
75
|
+
className={cn("text-muted-foreground", className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
Popover,
|
|
83
|
+
PopoverContent,
|
|
84
|
+
PopoverDescription,
|
|
85
|
+
PopoverHeader,
|
|
86
|
+
PopoverTitle,
|
|
87
|
+
PopoverTrigger,
|
|
88
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as ResizablePrimitive from "react-resizable-panels";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function ResizablePanelGroup({
|
|
6
|
+
className,
|
|
7
|
+
...props
|
|
8
|
+
}: ResizablePrimitive.GroupProps) {
|
|
9
|
+
return (
|
|
10
|
+
<ResizablePrimitive.Group
|
|
11
|
+
data-slot="resizable-panel-group"
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex h-full w-full aria-[orientation=vertical]:flex-col",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
|
|
22
|
+
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ResizableHandle({
|
|
26
|
+
withHandle,
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: ResizablePrimitive.SeparatorProps & {
|
|
30
|
+
withHandle?: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<ResizablePrimitive.Separator
|
|
34
|
+
data-slot="resizable-handle"
|
|
35
|
+
className={cn(
|
|
36
|
+
"relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{withHandle && (
|
|
42
|
+
<div className="z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border" />
|
|
43
|
+
)}
|
|
44
|
+
</ResizablePrimitive.Separator>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="table-container"
|
|
9
|
+
className="relative w-full overflow-x-auto"
|
|
10
|
+
>
|
|
11
|
+
<table
|
|
12
|
+
data-slot="table"
|
|
13
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
21
|
+
return (
|
|
22
|
+
<thead
|
|
23
|
+
data-slot="table-header"
|
|
24
|
+
className={cn("[&_tr]:border-b", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
31
|
+
return (
|
|
32
|
+
<tbody
|
|
33
|
+
data-slot="table-body"
|
|
34
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
41
|
+
return (
|
|
42
|
+
<tfoot
|
|
43
|
+
data-slot="table-footer"
|
|
44
|
+
className={cn(
|
|
45
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
54
|
+
return (
|
|
55
|
+
<tr
|
|
56
|
+
data-slot="table-row"
|
|
57
|
+
className={cn(
|
|
58
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
67
|
+
return (
|
|
68
|
+
<th
|
|
69
|
+
data-slot="table-head"
|
|
70
|
+
className={cn(
|
|
71
|
+
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground border-r last:border-r-0 [&:has([role=checkbox])]:pr-0",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
80
|
+
return (
|
|
81
|
+
<td
|
|
82
|
+
data-slot="table-cell"
|
|
83
|
+
className={cn(
|
|
84
|
+
"p-2 align-middle whitespace-nowrap border-r last:border-r-0 [&:has([role=checkbox])]:pr-0",
|
|
85
|
+
className,
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function TableCaption({
|
|
93
|
+
className,
|
|
94
|
+
...props
|
|
95
|
+
}: React.ComponentProps<"caption">) {
|
|
96
|
+
return (
|
|
97
|
+
<caption
|
|
98
|
+
data-slot="table-caption"
|
|
99
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
Table,
|
|
107
|
+
TableBody,
|
|
108
|
+
TableCaption,
|
|
109
|
+
TableCell,
|
|
110
|
+
TableFooter,
|
|
111
|
+
TableHead,
|
|
112
|
+
TableHeader,
|
|
113
|
+
TableRow,
|
|
114
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { MdData } from "@md-meta-view/core";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export function useMdData() {
|
|
5
|
+
const [data, setData] = useState<MdData | null>(null);
|
|
6
|
+
const [loading, setLoading] = useState(true);
|
|
7
|
+
|
|
8
|
+
const fetchData = useCallback(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch("/api/entries");
|
|
11
|
+
if (res.ok) {
|
|
12
|
+
const json = await res.json();
|
|
13
|
+
setData(json);
|
|
14
|
+
} else {
|
|
15
|
+
const staticRes = await fetch("/data.json");
|
|
16
|
+
if (staticRes.ok) {
|
|
17
|
+
const json = await staticRes.json();
|
|
18
|
+
setData(json);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
const staticRes = await fetch("/data.json");
|
|
24
|
+
if (staticRes.ok) {
|
|
25
|
+
const json = await staticRes.json();
|
|
26
|
+
setData(json);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
console.error("Failed to load data");
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetchData();
|
|
38
|
+
}, [fetchData]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let ws: WebSocket | null = null;
|
|
42
|
+
|
|
43
|
+
const connectWs = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch("/api/ws-port");
|
|
46
|
+
if (!res.ok) return;
|
|
47
|
+
const { port } = await res.json();
|
|
48
|
+
ws = new WebSocket(`ws://localhost:${port}`);
|
|
49
|
+
ws.onmessage = (event) => {
|
|
50
|
+
const message = JSON.parse(event.data);
|
|
51
|
+
if (message.type === "update") {
|
|
52
|
+
setData(message.data);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
ws.onclose = () => {
|
|
56
|
+
setTimeout(connectWs, 2000);
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
// Static build mode, no WebSocket
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
connectWs();
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
ws?.close();
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return { data, loading };
|
|
71
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
type Theme = "light" | "dark" | "system";
|
|
4
|
+
|
|
5
|
+
function getSystemTheme(): "light" | "dark" {
|
|
6
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
7
|
+
? "dark"
|
|
8
|
+
: "light";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function applyTheme(theme: Theme) {
|
|
12
|
+
const resolved = theme === "system" ? getSystemTheme() : theme;
|
|
13
|
+
document.documentElement.classList.toggle("dark", resolved === "dark");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useTheme() {
|
|
17
|
+
const [theme, setThemeState] = useState<Theme>(() => {
|
|
18
|
+
const saved = localStorage.getItem("md-meta-view-theme") as Theme | null;
|
|
19
|
+
return saved ?? "system";
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
applyTheme(theme);
|
|
24
|
+
localStorage.setItem("md-meta-view-theme", theme);
|
|
25
|
+
}, [theme]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (theme !== "system") return;
|
|
29
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
30
|
+
const handler = () => applyTheme("system");
|
|
31
|
+
mq.addEventListener("change", handler);
|
|
32
|
+
return () => mq.removeEventListener("change", handler);
|
|
33
|
+
}, [theme]);
|
|
34
|
+
|
|
35
|
+
const setTheme = (t: Theme) => setThemeState(t);
|
|
36
|
+
|
|
37
|
+
return { theme, setTheme };
|
|
38
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@plugin "@tailwindcss/typography";
|
|
3
|
+
@import "tw-animate-css";
|
|
4
|
+
@import "shadcn/tailwind.css";
|
|
5
|
+
@import "@fontsource-variable/geist";
|
|
6
|
+
|
|
7
|
+
@custom-variant dark (&:is(.dark *));
|
|
8
|
+
|
|
9
|
+
@theme inline {
|
|
10
|
+
--font-heading: var(--font-sans);
|
|
11
|
+
--font-sans: 'Geist Variable', sans-serif;
|
|
12
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
13
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
14
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
15
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
16
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
17
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
18
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
19
|
+
--color-sidebar: var(--sidebar);
|
|
20
|
+
--color-chart-5: var(--chart-5);
|
|
21
|
+
--color-chart-4: var(--chart-4);
|
|
22
|
+
--color-chart-3: var(--chart-3);
|
|
23
|
+
--color-chart-2: var(--chart-2);
|
|
24
|
+
--color-chart-1: var(--chart-1);
|
|
25
|
+
--color-ring: var(--ring);
|
|
26
|
+
--color-input: var(--input);
|
|
27
|
+
--color-border: var(--border);
|
|
28
|
+
--color-destructive: var(--destructive);
|
|
29
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
30
|
+
--color-accent: var(--accent);
|
|
31
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
32
|
+
--color-muted: var(--muted);
|
|
33
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
34
|
+
--color-secondary: var(--secondary);
|
|
35
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
36
|
+
--color-primary: var(--primary);
|
|
37
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
38
|
+
--color-popover: var(--popover);
|
|
39
|
+
--color-card-foreground: var(--card-foreground);
|
|
40
|
+
--color-card: var(--card);
|
|
41
|
+
--color-foreground: var(--foreground);
|
|
42
|
+
--color-background: var(--background);
|
|
43
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
44
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
45
|
+
--radius-lg: var(--radius);
|
|
46
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
47
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
48
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
49
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:root {
|
|
53
|
+
--background: oklch(1 0 0);
|
|
54
|
+
--foreground: oklch(0.145 0 0);
|
|
55
|
+
--card: oklch(1 0 0);
|
|
56
|
+
--card-foreground: oklch(0.145 0 0);
|
|
57
|
+
--popover: oklch(1 0 0);
|
|
58
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
59
|
+
--primary: oklch(0.205 0 0);
|
|
60
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
61
|
+
--secondary: oklch(0.97 0 0);
|
|
62
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
63
|
+
--muted: oklch(0.97 0 0);
|
|
64
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
65
|
+
--accent: oklch(0.97 0 0);
|
|
66
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
67
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
68
|
+
--border: oklch(0.922 0 0);
|
|
69
|
+
--input: oklch(0.922 0 0);
|
|
70
|
+
--ring: oklch(0.708 0 0);
|
|
71
|
+
--chart-1: oklch(0.87 0 0);
|
|
72
|
+
--chart-2: oklch(0.556 0 0);
|
|
73
|
+
--chart-3: oklch(0.439 0 0);
|
|
74
|
+
--chart-4: oklch(0.371 0 0);
|
|
75
|
+
--chart-5: oklch(0.269 0 0);
|
|
76
|
+
--radius: 0.625rem;
|
|
77
|
+
--sidebar: oklch(0.985 0 0);
|
|
78
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
79
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
80
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
81
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
82
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
83
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
84
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dark {
|
|
88
|
+
--background: oklch(0.205 0 0);
|
|
89
|
+
--foreground: oklch(0.96 0 0);
|
|
90
|
+
--card: oklch(0.27 0 0);
|
|
91
|
+
--card-foreground: oklch(0.96 0 0);
|
|
92
|
+
--popover: oklch(0.27 0 0);
|
|
93
|
+
--popover-foreground: oklch(0.96 0 0);
|
|
94
|
+
--primary: oklch(0.9 0 0);
|
|
95
|
+
--primary-foreground: oklch(0.22 0 0);
|
|
96
|
+
--secondary: oklch(0.32 0 0);
|
|
97
|
+
--secondary-foreground: oklch(0.96 0 0);
|
|
98
|
+
--muted: oklch(0.32 0 0);
|
|
99
|
+
--muted-foreground: oklch(0.72 0 0);
|
|
100
|
+
--accent: oklch(0.32 0 0);
|
|
101
|
+
--accent-foreground: oklch(0.96 0 0);
|
|
102
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
103
|
+
--border: oklch(1 0 0 / 14%);
|
|
104
|
+
--input: oklch(1 0 0 / 18%);
|
|
105
|
+
--ring: oklch(0.6 0 0);
|
|
106
|
+
--chart-1: oklch(0.87 0 0);
|
|
107
|
+
--chart-2: oklch(0.6 0 0);
|
|
108
|
+
--chart-3: oklch(0.48 0 0);
|
|
109
|
+
--chart-4: oklch(0.4 0 0);
|
|
110
|
+
--chart-5: oklch(0.32 0 0);
|
|
111
|
+
--sidebar: oklch(0.24 0 0);
|
|
112
|
+
--sidebar-foreground: oklch(0.96 0 0);
|
|
113
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
114
|
+
--sidebar-primary-foreground: oklch(0.96 0 0);
|
|
115
|
+
--sidebar-accent: oklch(0.32 0 0);
|
|
116
|
+
--sidebar-accent-foreground: oklch(0.96 0 0);
|
|
117
|
+
--sidebar-border: oklch(1 0 0 / 14%);
|
|
118
|
+
--sidebar-ring: oklch(0.6 0 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@layer base {
|
|
122
|
+
* {
|
|
123
|
+
@apply border-border outline-ring/50;
|
|
124
|
+
}
|
|
125
|
+
body {
|
|
126
|
+
@apply bg-background text-foreground;
|
|
127
|
+
}
|
|
128
|
+
html {
|
|
129
|
+
@apply font-sans;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRootRoute, createRoute, createRouter } from "@tanstack/react-router";
|
|
2
|
+
import { RootLayout } from "@/routes/__root";
|
|
3
|
+
import { IndexPage } from "@/routes/index";
|
|
4
|
+
import type { SearchParams } from "@/lib/search-params";
|
|
5
|
+
|
|
6
|
+
const validateSearch = (search: Record<string, unknown>): SearchParams => ({
|
|
7
|
+
sort: (search.sort as string) || undefined,
|
|
8
|
+
filter: (search.filter as string) || undefined,
|
|
9
|
+
q: (search.q as string) || undefined,
|
|
10
|
+
file: (search.file as string) || undefined,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const rootRoute = createRootRoute({
|
|
14
|
+
component: RootLayout,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const indexRoute = createRoute({
|
|
18
|
+
getParentRoute: () => rootRoute,
|
|
19
|
+
path: "/",
|
|
20
|
+
validateSearch,
|
|
21
|
+
component: IndexPage,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const routeTree = rootRoute.addChildren([indexRoute]);
|
|
25
|
+
|
|
26
|
+
export const router = createRouter({ routeTree });
|
|
27
|
+
|
|
28
|
+
declare module "@tanstack/react-router" {
|
|
29
|
+
interface Register {
|
|
30
|
+
router: typeof router;
|
|
31
|
+
}
|
|
32
|
+
}
|