@open-slide/core 0.0.8 → 0.0.9
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/dist/{build-CXY2DSzy.js → build-pqF4W1Yi.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-BYTf0qVz.js → config-CtwxMYv9.js} +365 -44
- package/dist/{dev-BxCKugi3.js → dev-CJX97uiy.js} +1 -1
- package/dist/{preview-C1F-rHfx.js → preview-IuLPcL5y.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +3 -1
- package/src/app/App.tsx +2 -0
- package/src/app/components/PdfProgressToast.tsx +23 -0
- package/src/app/components/inspector/CommentWidget.tsx +1 -1
- package/src/app/components/inspector/InspectOverlay.tsx +81 -41
- package/src/app/components/inspector/InspectorPanel.tsx +805 -0
- package/src/app/components/inspector/InspectorProvider.tsx +199 -13
- package/src/app/components/inspector/SaveBar.tsx +77 -0
- package/src/app/components/ui/input.tsx +21 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/select.tsx +190 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +38 -0
- package/src/app/components/ui/textarea.tsx +18 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +55 -0
- package/src/app/lib/export-pdf.ts +197 -0
- package/src/app/lib/inspector/fiber.ts +40 -5
- package/src/app/lib/inspector/useEditor.ts +61 -0
- package/src/app/lib/print-ready.ts +58 -0
- package/src/app/routes/Slide.tsx +47 -3
- package/src/app/components/inspector/CommentPopover.tsx +0 -94
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { Crosshair } from 'lucide-react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react';
|
|
3
12
|
import { Button } from '@/components/ui/button';
|
|
4
13
|
import { type SlideComment, useComments } from '@/lib/inspector/useComments';
|
|
14
|
+
import { type Edit, type EditOp, useEditor } from '@/lib/inspector/useEditor';
|
|
5
15
|
|
|
6
|
-
export type
|
|
16
|
+
export type SelectedTarget = {
|
|
7
17
|
line: number;
|
|
8
18
|
column: number;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
anchor: HTMLElement;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Bucket = {
|
|
23
|
+
line: number;
|
|
24
|
+
column: number;
|
|
25
|
+
styleOps: Map<string, string | null>;
|
|
26
|
+
textOp: { value: string } | null;
|
|
27
|
+
// Pre-edit snapshot of the DOM, captured the first time we touch
|
|
28
|
+
// each style key / text. Used by `cancelEdits` to revert.
|
|
29
|
+
origStyle: Map<string, string>;
|
|
30
|
+
origText: { value: string } | null;
|
|
12
31
|
};
|
|
13
32
|
|
|
14
33
|
type InspectorCtx = {
|
|
@@ -21,8 +40,18 @@ type InspectorCtx = {
|
|
|
21
40
|
refetch: () => Promise<void>;
|
|
22
41
|
add: (line: number, column: number, text: string) => Promise<void>;
|
|
23
42
|
remove: (id: string) => Promise<void>;
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
selected: SelectedTarget | null;
|
|
44
|
+
setSelected: (s: SelectedTarget | null) => void;
|
|
45
|
+
applyEdit: (line: number, column: number, ops: EditOp[]) => Promise<void>;
|
|
46
|
+
applyEdits: (edits: Edit[]) => Promise<void>;
|
|
47
|
+
// Mutate the DOM optimistically, snapshot the pre-edit values, and
|
|
48
|
+
// remember the ops. `commitEdits` (manual Save or auto-flush on
|
|
49
|
+
// close) is what actually writes to disk; `cancelEdits` reverts.
|
|
50
|
+
bufferOps: (line: number, column: number, anchor: HTMLElement, ops: EditOp[]) => void;
|
|
51
|
+
pendingCount: number;
|
|
52
|
+
commitEdits: () => Promise<void>;
|
|
53
|
+
cancelEdits: () => void;
|
|
54
|
+
committing: boolean;
|
|
26
55
|
};
|
|
27
56
|
|
|
28
57
|
const Ctx = createContext<InspectorCtx | null>(null);
|
|
@@ -35,19 +64,151 @@ export function useInspector(): InspectorCtx {
|
|
|
35
64
|
|
|
36
65
|
export function InspectorProvider({ slideId, children }: { slideId: string; children: ReactNode }) {
|
|
37
66
|
const [active, setActive] = useState(false);
|
|
38
|
-
const [
|
|
67
|
+
const [selected, setSelected] = useState<SelectedTarget | null>(null);
|
|
39
68
|
const { comments, error, refetch, add, remove } = useComments(slideId);
|
|
69
|
+
const { applyEdit, applyEdits } = useEditor(slideId);
|
|
70
|
+
|
|
71
|
+
const pendingRef = useRef<Map<string, Bucket>>(new Map());
|
|
72
|
+
const [pendingCount, setPendingCount] = useState(0);
|
|
73
|
+
const [committing, setCommitting] = useState(false);
|
|
74
|
+
|
|
75
|
+
const refreshCount = useCallback(() => {
|
|
76
|
+
let n = 0;
|
|
77
|
+
for (const b of pendingRef.current.values()) {
|
|
78
|
+
if (b.styleOps.size > 0 || b.textOp !== null) n++;
|
|
79
|
+
}
|
|
80
|
+
setPendingCount(n);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const bufferOps = useCallback(
|
|
84
|
+
(line: number, column: number, anchor: HTMLElement, ops: EditOp[]) => {
|
|
85
|
+
const key = `${line}:${column}`;
|
|
86
|
+
let bucket = pendingRef.current.get(key);
|
|
87
|
+
if (!bucket) {
|
|
88
|
+
bucket = {
|
|
89
|
+
line,
|
|
90
|
+
column,
|
|
91
|
+
styleOps: new Map(),
|
|
92
|
+
textOp: null,
|
|
93
|
+
origStyle: new Map(),
|
|
94
|
+
origText: null,
|
|
95
|
+
};
|
|
96
|
+
pendingRef.current.set(key, bucket);
|
|
97
|
+
}
|
|
98
|
+
const style = anchor.style as unknown as Record<string, string>;
|
|
99
|
+
for (const op of ops) {
|
|
100
|
+
if (op.kind === 'set-style') {
|
|
101
|
+
if (!bucket.origStyle.has(op.key)) {
|
|
102
|
+
bucket.origStyle.set(op.key, style[op.key] ?? '');
|
|
103
|
+
}
|
|
104
|
+
bucket.styleOps.set(op.key, op.value);
|
|
105
|
+
if (anchor.isConnected) style[op.key] = op.value ?? '';
|
|
106
|
+
} else if (op.kind === 'set-text') {
|
|
107
|
+
if (bucket.origText === null) {
|
|
108
|
+
bucket.origText = { value: anchor.textContent ?? '' };
|
|
109
|
+
}
|
|
110
|
+
bucket.textOp = { value: op.value };
|
|
111
|
+
if (anchor.isConnected) anchor.textContent = op.value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
refreshCount();
|
|
115
|
+
},
|
|
116
|
+
[refreshCount],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const commitEdits = useCallback(async () => {
|
|
120
|
+
const buckets = pendingRef.current;
|
|
121
|
+
if (buckets.size === 0) return;
|
|
122
|
+
const edits: Edit[] = [];
|
|
123
|
+
for (const { line, column, styleOps, textOp } of buckets.values()) {
|
|
124
|
+
const list: EditOp[] = [];
|
|
125
|
+
for (const [k, v] of styleOps) list.push({ kind: 'set-style', key: k, value: v });
|
|
126
|
+
if (textOp !== null) list.push({ kind: 'set-text', value: textOp.value });
|
|
127
|
+
if (list.length > 0) edits.push({ line, column, ops: list });
|
|
128
|
+
}
|
|
129
|
+
pendingRef.current = new Map();
|
|
130
|
+
setPendingCount(0);
|
|
131
|
+
if (edits.length === 0) return;
|
|
132
|
+
setCommitting(true);
|
|
133
|
+
try {
|
|
134
|
+
await applyEdits(edits);
|
|
135
|
+
} finally {
|
|
136
|
+
setCommitting(false);
|
|
137
|
+
}
|
|
138
|
+
}, [applyEdits]);
|
|
139
|
+
|
|
140
|
+
const cancelEdits = useCallback(() => {
|
|
141
|
+
if (pendingRef.current.size === 0) return;
|
|
142
|
+
const root = document.querySelector<HTMLElement>('[data-inspector-root]');
|
|
143
|
+
for (const b of pendingRef.current.values()) {
|
|
144
|
+
const el = root?.querySelector<HTMLElement>(`[data-slide-loc="${b.line}:${b.column}"]`);
|
|
145
|
+
if (!el) continue;
|
|
146
|
+
const style = el.style as unknown as Record<string, string>;
|
|
147
|
+
for (const [k, v] of b.origStyle) style[k] = v;
|
|
148
|
+
if (b.origText !== null) el.textContent = b.origText.value;
|
|
149
|
+
}
|
|
150
|
+
pendingRef.current = new Map();
|
|
151
|
+
setPendingCount(0);
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
// Auto-flush on inspector close and on route unmount so toggling
|
|
155
|
+
// off or navigating away doesn't drop buffered edits.
|
|
156
|
+
const commitRef = useRef(commitEdits);
|
|
157
|
+
commitRef.current = commitEdits;
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!active) commitRef.current().catch(() => {});
|
|
160
|
+
}, [active]);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
return () => {
|
|
163
|
+
commitRef.current().catch(() => {});
|
|
164
|
+
};
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
// Re-apply buffered ops onto any `[data-slide-loc]` element that gets
|
|
168
|
+
// (re)mounted in the slide canvas. Without this, navigating to a
|
|
169
|
+
// different page and back drops the optimistic styles, since the
|
|
170
|
+
// page's DOM nodes are torn down on unmount even though the buffer
|
|
171
|
+
// (keyed by source line:col) survives.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const root = document.querySelector<HTMLElement>('[data-inspector-root]');
|
|
174
|
+
if (!root) return;
|
|
175
|
+
|
|
176
|
+
const applyBuffered = (el: HTMLElement) => {
|
|
177
|
+
const loc = el.dataset.slideLoc;
|
|
178
|
+
if (!loc) return;
|
|
179
|
+
const bucket = pendingRef.current.get(loc);
|
|
180
|
+
if (!bucket) return;
|
|
181
|
+
const style = el.style as unknown as Record<string, string>;
|
|
182
|
+
for (const [key, value] of bucket.styleOps) {
|
|
183
|
+
const v = value ?? '';
|
|
184
|
+
if (style[key] !== v) style[key] = v;
|
|
185
|
+
}
|
|
186
|
+
if (bucket.textOp !== null && el.textContent !== bucket.textOp.value) {
|
|
187
|
+
el.textContent = bucket.textOp.value;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const replayAll = () => {
|
|
192
|
+
if (pendingRef.current.size === 0) return;
|
|
193
|
+
root.querySelectorAll<HTMLElement>('[data-slide-loc]').forEach(applyBuffered);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
replayAll();
|
|
197
|
+
const observer = new MutationObserver(replayAll);
|
|
198
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
199
|
+
return () => observer.disconnect();
|
|
200
|
+
}, []);
|
|
40
201
|
|
|
41
202
|
const toggle = useCallback(() => {
|
|
42
203
|
setActive((a) => {
|
|
43
|
-
if (a)
|
|
204
|
+
if (a) setSelected(null);
|
|
44
205
|
return !a;
|
|
45
206
|
});
|
|
46
207
|
}, []);
|
|
47
208
|
|
|
48
209
|
const cancel = useCallback(() => {
|
|
49
210
|
setActive(false);
|
|
50
|
-
|
|
211
|
+
setSelected(null);
|
|
51
212
|
}, []);
|
|
52
213
|
|
|
53
214
|
const value = useMemo<InspectorCtx>(
|
|
@@ -61,10 +222,35 @@ export function InspectorProvider({ slideId, children }: { slideId: string; chil
|
|
|
61
222
|
refetch,
|
|
62
223
|
add,
|
|
63
224
|
remove,
|
|
64
|
-
|
|
65
|
-
|
|
225
|
+
selected,
|
|
226
|
+
setSelected,
|
|
227
|
+
applyEdit,
|
|
228
|
+
applyEdits,
|
|
229
|
+
bufferOps,
|
|
230
|
+
pendingCount,
|
|
231
|
+
commitEdits,
|
|
232
|
+
cancelEdits,
|
|
233
|
+
committing,
|
|
66
234
|
}),
|
|
67
|
-
[
|
|
235
|
+
[
|
|
236
|
+
slideId,
|
|
237
|
+
active,
|
|
238
|
+
toggle,
|
|
239
|
+
cancel,
|
|
240
|
+
comments,
|
|
241
|
+
error,
|
|
242
|
+
refetch,
|
|
243
|
+
add,
|
|
244
|
+
remove,
|
|
245
|
+
selected,
|
|
246
|
+
applyEdit,
|
|
247
|
+
applyEdits,
|
|
248
|
+
bufferOps,
|
|
249
|
+
pendingCount,
|
|
250
|
+
commitEdits,
|
|
251
|
+
cancelEdits,
|
|
252
|
+
committing,
|
|
253
|
+
],
|
|
68
254
|
);
|
|
69
255
|
|
|
70
256
|
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Check, Loader2, Save, Undo2 } from 'lucide-react';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { useInspector } from './InspectorProvider';
|
|
5
|
+
|
|
6
|
+
// Optimistic DOM updates make the canvas *look* saved, so without
|
|
7
|
+
// this affordance a user could close the tab thinking their tweaks
|
|
8
|
+
// hit disk when they're still buffered in memory.
|
|
9
|
+
export function SaveBar() {
|
|
10
|
+
const { pendingCount, commitEdits, cancelEdits, committing } = useInspector();
|
|
11
|
+
const [justSaved, setJustSaved] = useState(false);
|
|
12
|
+
|
|
13
|
+
// Brief "Saved" hold so the bar's disappearance feels intentional.
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!justSaved) return;
|
|
16
|
+
const t = setTimeout(() => setJustSaved(false), 1200);
|
|
17
|
+
return () => clearTimeout(t);
|
|
18
|
+
}, [justSaved]);
|
|
19
|
+
|
|
20
|
+
const visible = pendingCount > 0 || committing || justSaved;
|
|
21
|
+
if (!visible) return null;
|
|
22
|
+
|
|
23
|
+
const onSave = async () => {
|
|
24
|
+
await commitEdits();
|
|
25
|
+
setJustSaved(true);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
data-inspector-ui
|
|
31
|
+
className="pointer-events-none absolute bottom-6 left-1/2 z-30 -translate-x-1/2 animate-in fade-in slide-in-from-bottom-2 duration-200"
|
|
32
|
+
>
|
|
33
|
+
<div className="pointer-events-auto flex items-center gap-2 rounded-full border bg-card/95 py-1 pr-1 pl-3 shadow-lg backdrop-blur">
|
|
34
|
+
{justSaved ? (
|
|
35
|
+
<span className="flex items-center gap-1.5 text-xs font-medium text-foreground">
|
|
36
|
+
<Check className="size-3.5 text-emerald-600" />
|
|
37
|
+
Saved
|
|
38
|
+
</span>
|
|
39
|
+
) : (
|
|
40
|
+
<span className="text-xs font-medium text-foreground">
|
|
41
|
+
{pendingCount} unsaved {pendingCount === 1 ? 'change' : 'changes'}
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
{!justSaved && (
|
|
45
|
+
<Button
|
|
46
|
+
size="sm"
|
|
47
|
+
variant="ghost"
|
|
48
|
+
className="h-7 rounded-full px-2.5 text-[11px] text-muted-foreground hover:text-foreground"
|
|
49
|
+
onClick={cancelEdits}
|
|
50
|
+
disabled={committing || pendingCount === 0}
|
|
51
|
+
>
|
|
52
|
+
<Undo2 className="size-3.5" />
|
|
53
|
+
Discard
|
|
54
|
+
</Button>
|
|
55
|
+
)}
|
|
56
|
+
<Button
|
|
57
|
+
size="sm"
|
|
58
|
+
className="h-7 rounded-full px-3 text-[11px]"
|
|
59
|
+
onClick={onSave}
|
|
60
|
+
disabled={committing || pendingCount === 0}
|
|
61
|
+
>
|
|
62
|
+
{committing ? (
|
|
63
|
+
<>
|
|
64
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
65
|
+
Saving
|
|
66
|
+
</>
|
|
67
|
+
) : (
|
|
68
|
+
<>
|
|
69
|
+
<Save className="size-3.5" />
|
|
70
|
+
Save
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
|
12
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
13
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Label as LabelPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Label({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<LabelPrimitive.Root
|
|
14
|
+
data-slot="label"
|
|
15
|
+
className={cn(
|
|
16
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { Label }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Progress as ProgressPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Progress({
|
|
9
|
+
className,
|
|
10
|
+
value,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<ProgressPrimitive.Root
|
|
15
|
+
data-slot="progress"
|
|
16
|
+
className={cn(
|
|
17
|
+
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<ProgressPrimitive.Indicator
|
|
23
|
+
data-slot="progress-indicator"
|
|
24
|
+
className="h-full w-full flex-1 bg-primary transition-all"
|
|
25
|
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
26
|
+
/>
|
|
27
|
+
</ProgressPrimitive.Root>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { Progress }
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
5
|
+
import { Select as SelectPrimitive } from "radix-ui"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
function Select({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
12
|
+
return <SelectPrimitive.Root data-slot="select" {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function SelectGroup({
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
18
|
+
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function SelectValue({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
24
|
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SelectTrigger({
|
|
28
|
+
className,
|
|
29
|
+
size = "default",
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
33
|
+
size?: "sm" | "default"
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<SelectPrimitive.Trigger
|
|
37
|
+
data-slot="select-trigger"
|
|
38
|
+
data-size={size}
|
|
39
|
+
className={cn(
|
|
40
|
+
"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
<SelectPrimitive.Icon asChild>
|
|
47
|
+
<ChevronDownIcon className="size-4 opacity-50" />
|
|
48
|
+
</SelectPrimitive.Icon>
|
|
49
|
+
</SelectPrimitive.Trigger>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SelectContent({
|
|
54
|
+
className,
|
|
55
|
+
children,
|
|
56
|
+
position = "item-aligned",
|
|
57
|
+
align = "center",
|
|
58
|
+
...props
|
|
59
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
60
|
+
return (
|
|
61
|
+
<SelectPrimitive.Portal>
|
|
62
|
+
<SelectPrimitive.Content
|
|
63
|
+
data-slot="select-content"
|
|
64
|
+
className={cn(
|
|
65
|
+
"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
66
|
+
position === "popper" &&
|
|
67
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
position={position}
|
|
71
|
+
align={align}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<SelectScrollUpButton />
|
|
75
|
+
<SelectPrimitive.Viewport
|
|
76
|
+
className={cn(
|
|
77
|
+
"p-1",
|
|
78
|
+
position === "popper" &&
|
|
79
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</SelectPrimitive.Viewport>
|
|
84
|
+
<SelectScrollDownButton />
|
|
85
|
+
</SelectPrimitive.Content>
|
|
86
|
+
</SelectPrimitive.Portal>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function SelectLabel({
|
|
91
|
+
className,
|
|
92
|
+
...props
|
|
93
|
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
94
|
+
return (
|
|
95
|
+
<SelectPrimitive.Label
|
|
96
|
+
data-slot="select-label"
|
|
97
|
+
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function SelectItem({
|
|
104
|
+
className,
|
|
105
|
+
children,
|
|
106
|
+
...props
|
|
107
|
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
108
|
+
return (
|
|
109
|
+
<SelectPrimitive.Item
|
|
110
|
+
data-slot="select-item"
|
|
111
|
+
className={cn(
|
|
112
|
+
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
113
|
+
className
|
|
114
|
+
)}
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
<span
|
|
118
|
+
data-slot="select-item-indicator"
|
|
119
|
+
className="absolute right-2 flex size-3.5 items-center justify-center"
|
|
120
|
+
>
|
|
121
|
+
<SelectPrimitive.ItemIndicator>
|
|
122
|
+
<CheckIcon className="size-4" />
|
|
123
|
+
</SelectPrimitive.ItemIndicator>
|
|
124
|
+
</span>
|
|
125
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
126
|
+
</SelectPrimitive.Item>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SelectSeparator({
|
|
131
|
+
className,
|
|
132
|
+
...props
|
|
133
|
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
134
|
+
return (
|
|
135
|
+
<SelectPrimitive.Separator
|
|
136
|
+
data-slot="select-separator"
|
|
137
|
+
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
|
138
|
+
{...props}
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function SelectScrollUpButton({
|
|
144
|
+
className,
|
|
145
|
+
...props
|
|
146
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
147
|
+
return (
|
|
148
|
+
<SelectPrimitive.ScrollUpButton
|
|
149
|
+
data-slot="select-scroll-up-button"
|
|
150
|
+
className={cn(
|
|
151
|
+
"flex cursor-default items-center justify-center py-1",
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
{...props}
|
|
155
|
+
>
|
|
156
|
+
<ChevronUpIcon className="size-4" />
|
|
157
|
+
</SelectPrimitive.ScrollUpButton>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function SelectScrollDownButton({
|
|
162
|
+
className,
|
|
163
|
+
...props
|
|
164
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
165
|
+
return (
|
|
166
|
+
<SelectPrimitive.ScrollDownButton
|
|
167
|
+
data-slot="select-scroll-down-button"
|
|
168
|
+
className={cn(
|
|
169
|
+
"flex cursor-default items-center justify-center py-1",
|
|
170
|
+
className
|
|
171
|
+
)}
|
|
172
|
+
{...props}
|
|
173
|
+
>
|
|
174
|
+
<ChevronDownIcon className="size-4" />
|
|
175
|
+
</SelectPrimitive.ScrollDownButton>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export {
|
|
180
|
+
Select,
|
|
181
|
+
SelectContent,
|
|
182
|
+
SelectGroup,
|
|
183
|
+
SelectItem,
|
|
184
|
+
SelectLabel,
|
|
185
|
+
SelectScrollDownButton,
|
|
186
|
+
SelectScrollUpButton,
|
|
187
|
+
SelectSeparator,
|
|
188
|
+
SelectTrigger,
|
|
189
|
+
SelectValue,
|
|
190
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slider as SliderPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Slider({
|
|
7
|
+
className,
|
|
8
|
+
defaultValue,
|
|
9
|
+
value,
|
|
10
|
+
min = 0,
|
|
11
|
+
max = 100,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
|
14
|
+
const _values = React.useMemo(
|
|
15
|
+
() =>
|
|
16
|
+
Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: Array.isArray(defaultValue)
|
|
19
|
+
? defaultValue
|
|
20
|
+
: [min, max],
|
|
21
|
+
[value, defaultValue, min, max]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<SliderPrimitive.Root
|
|
26
|
+
data-slot="slider"
|
|
27
|
+
defaultValue={defaultValue}
|
|
28
|
+
value={value}
|
|
29
|
+
min={min}
|
|
30
|
+
max={max}
|
|
31
|
+
className={cn(
|
|
32
|
+
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
<SliderPrimitive.Track
|
|
38
|
+
data-slot="slider-track"
|
|
39
|
+
className={cn(
|
|
40
|
+
"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<SliderPrimitive.Range
|
|
44
|
+
data-slot="slider-range"
|
|
45
|
+
className={cn(
|
|
46
|
+
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
</SliderPrimitive.Track>
|
|
50
|
+
{Array.from({ length: _values.length }, (_, index) => (
|
|
51
|
+
<SliderPrimitive.Thumb
|
|
52
|
+
data-slot="slider-thumb"
|
|
53
|
+
key={index}
|
|
54
|
+
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</SliderPrimitive.Root>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { Slider }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircleCheckIcon,
|
|
3
|
+
InfoIcon,
|
|
4
|
+
Loader2Icon,
|
|
5
|
+
OctagonXIcon,
|
|
6
|
+
TriangleAlertIcon,
|
|
7
|
+
} from "lucide-react"
|
|
8
|
+
import { useTheme } from "next-themes"
|
|
9
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
|
10
|
+
|
|
11
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
12
|
+
const { theme = "system" } = useTheme()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Sonner
|
|
16
|
+
theme={theme as ToasterProps["theme"]}
|
|
17
|
+
className="toaster group"
|
|
18
|
+
icons={{
|
|
19
|
+
success: <CircleCheckIcon className="size-4" />,
|
|
20
|
+
info: <InfoIcon className="size-4" />,
|
|
21
|
+
warning: <TriangleAlertIcon className="size-4" />,
|
|
22
|
+
error: <OctagonXIcon className="size-4" />,
|
|
23
|
+
loading: <Loader2Icon className="size-4 animate-spin" />,
|
|
24
|
+
}}
|
|
25
|
+
style={
|
|
26
|
+
{
|
|
27
|
+
"--normal-bg": "var(--popover)",
|
|
28
|
+
"--normal-text": "var(--popover-foreground)",
|
|
29
|
+
"--normal-border": "var(--border)",
|
|
30
|
+
"--border-radius": "var(--radius)",
|
|
31
|
+
} as React.CSSProperties
|
|
32
|
+
}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { Toaster }
|