@cryptiklemur/lattice 0.0.0 → 1.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/.github/workflows/release.yml +4 -4
- package/.releaserc.json +2 -1
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -70
- package/client/src/components/mesh/NodeBadge.tsx +24 -24
- package/client/src/components/mesh/PairingDialog.tsx +281 -281
- package/client/src/components/panels/FileBrowser.tsx +241 -241
- package/client/src/components/panels/StickyNotes.tsx +187 -187
- package/client/src/components/settings/Appearance.tsx +151 -151
- package/client/src/components/settings/MeshStatus.tsx +145 -145
- package/client/src/components/settings/SettingsView.tsx +57 -57
- package/client/src/components/setup/SetupWizard.tsx +750 -750
- package/client/src/components/ui/ErrorBoundary.tsx +56 -56
- package/client/src/router.tsx +391 -391
- package/client/vite.config.ts +20 -20
- package/package.json +1 -1
- package/server/src/handlers/chat.ts +194 -194
- package/server/src/handlers/settings.ts +109 -109
- package/themes/amoled.json +20 -20
- package/themes/ayu-light.json +9 -9
- package/themes/catppuccin-latte.json +9 -9
- package/themes/catppuccin-mocha.json +9 -9
- package/themes/clay-light.json +10 -10
- package/themes/clay.json +10 -10
- package/themes/dracula.json +9 -9
- package/themes/everforest-light.json +9 -9
- package/themes/everforest.json +9 -9
- package/themes/github-light.json +9 -9
- package/themes/gruvbox-dark.json +9 -9
- package/themes/gruvbox-light.json +9 -9
- package/themes/monokai.json +9 -9
- package/themes/nord-light.json +9 -9
- package/themes/nord.json +9 -9
- package/themes/one-dark.json +9 -9
- package/themes/one-light.json +9 -9
- package/themes/rose-pine-dawn.json +9 -9
- package/themes/rose-pine.json +9 -9
- package/themes/solarized-dark.json +9 -9
- package/themes/solarized-light.json +9 -9
- package/themes/tokyo-night-light.json +9 -9
- package/themes/tokyo-night.json +9 -9
- package/.serena/project.yml +0 -138
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import type { StickyNote, ServerMessage } from "@lattice/shared";
|
|
3
|
-
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
-
|
|
5
|
-
interface NoteCardProps {
|
|
6
|
-
note: StickyNote;
|
|
7
|
-
onEdit: (id: string) => void;
|
|
8
|
-
onDelete: (id: string) => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function NoteCard(props: NoteCardProps) {
|
|
12
|
-
var { note, onEdit, onDelete } = props;
|
|
13
|
-
return (
|
|
14
|
-
<div className="card bg-base-200 border border-base-300">
|
|
15
|
-
<div className="card-body p-3">
|
|
16
|
-
<div className="text-[13px] text-base-content whitespace-pre-wrap break-words leading-relaxed min-h-12">
|
|
17
|
-
{note.content}
|
|
18
|
-
</div>
|
|
19
|
-
<div className="flex gap-1.5 justify-end mt-2">
|
|
20
|
-
<button
|
|
21
|
-
onClick={function () { onEdit(note.id); }}
|
|
22
|
-
className="btn btn-ghost btn-xs border border-base-300"
|
|
23
|
-
>
|
|
24
|
-
Edit
|
|
25
|
-
</button>
|
|
26
|
-
<button
|
|
27
|
-
onClick={function () { onDelete(note.id); }}
|
|
28
|
-
className="btn btn-ghost btn-xs border border-base-300 text-base-content/60"
|
|
29
|
-
>
|
|
30
|
-
Delete
|
|
31
|
-
</button>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface EditModalProps {
|
|
39
|
-
initial: string;
|
|
40
|
-
onSave: (content: string) => void;
|
|
41
|
-
onCancel: () => void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function EditModal(props: EditModalProps) {
|
|
45
|
-
var { initial, onSave, onCancel } = props;
|
|
46
|
-
var [content, setContent] = useState(initial);
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<div className="fixed inset-0 bg-black/50 z-[1000] flex items-center justify-center" role="dialog" aria-modal="true" aria-label="Edit note">
|
|
50
|
-
<div className="card bg-base-200 border border-base-300 w-[400px] max-w-[90vw] shadow-2xl">
|
|
51
|
-
<div className="card-body p-5">
|
|
52
|
-
<div className="text-[13px] font-semibold text-base-content mb-3">Edit Note</div>
|
|
53
|
-
<textarea
|
|
54
|
-
autoFocus
|
|
55
|
-
value={content}
|
|
56
|
-
onChange={function (e) { setContent(e.target.value); }}
|
|
57
|
-
className="textarea textarea-bordered w-full min-h-[120px] bg-base-300 text-base-content text-[13px] resize-y"
|
|
58
|
-
/>
|
|
59
|
-
<div className="flex gap-2 justify-end mt-3">
|
|
60
|
-
<button
|
|
61
|
-
onClick={onCancel}
|
|
62
|
-
className="btn btn-ghost btn-sm"
|
|
63
|
-
>
|
|
64
|
-
Cancel
|
|
65
|
-
</button>
|
|
66
|
-
<button
|
|
67
|
-
onClick={function () { onSave(content); }}
|
|
68
|
-
className="btn btn-primary btn-sm"
|
|
69
|
-
>
|
|
70
|
-
Save
|
|
71
|
-
</button>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function StickyNotes() {
|
|
80
|
-
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
81
|
-
var [notes, setNotes] = useState<StickyNote[]>([]);
|
|
82
|
-
var [editingId, setEditingId] = useState<string | null>(null);
|
|
83
|
-
var [creating, setCreating] = useState(false);
|
|
84
|
-
|
|
85
|
-
var editingNote = editingId ? notes.find(function (n) { return n.id === editingId; }) : null;
|
|
86
|
-
|
|
87
|
-
var handleMessage = useCallback(function (msg: ServerMessage) {
|
|
88
|
-
if (msg.type === "notes:list_result") {
|
|
89
|
-
setNotes(msg.notes);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (msg.type === "notes:created") {
|
|
93
|
-
setNotes(function (prev) { return [...prev, msg.note]; });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (msg.type === "notes:updated") {
|
|
97
|
-
setNotes(function (prev) {
|
|
98
|
-
return prev.map(function (n) { return n.id === msg.note.id ? msg.note : n; });
|
|
99
|
-
});
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (msg.type === "notes:deleted") {
|
|
103
|
-
setNotes(function (prev) { return prev.filter(function (n) { return n.id !== msg.id; }); });
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
}, []);
|
|
107
|
-
|
|
108
|
-
useEffect(function () {
|
|
109
|
-
subscribe("notes:list_result", handleMessage);
|
|
110
|
-
subscribe("notes:created", handleMessage);
|
|
111
|
-
subscribe("notes:updated", handleMessage);
|
|
112
|
-
subscribe("notes:deleted", handleMessage);
|
|
113
|
-
send({ type: "notes:list" });
|
|
114
|
-
return function () {
|
|
115
|
-
unsubscribe("notes:list_result", handleMessage);
|
|
116
|
-
unsubscribe("notes:created", handleMessage);
|
|
117
|
-
unsubscribe("notes:updated", handleMessage);
|
|
118
|
-
unsubscribe("notes:deleted", handleMessage);
|
|
119
|
-
};
|
|
120
|
-
}, [send, subscribe, unsubscribe, handleMessage]);
|
|
121
|
-
|
|
122
|
-
function handleCreate(content: string) {
|
|
123
|
-
if (content.trim()) {
|
|
124
|
-
send({ type: "notes:create", content: content.trim() });
|
|
125
|
-
}
|
|
126
|
-
setCreating(false);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function handleEdit(content: string) {
|
|
130
|
-
if (editingId && content.trim()) {
|
|
131
|
-
send({ type: "notes:update", id: editingId, content: content.trim() });
|
|
132
|
-
}
|
|
133
|
-
setEditingId(null);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function handleDelete(id: string) {
|
|
137
|
-
send({ type: "notes:delete", id });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<div className="flex flex-col h-full bg-base-100">
|
|
142
|
-
<div className="flex items-center justify-between px-4 py-3 border-b border-base-300">
|
|
143
|
-
<span className="text-[13px] font-semibold text-base-content">Sticky Notes</span>
|
|
144
|
-
<button
|
|
145
|
-
onClick={function () { setCreating(true); }}
|
|
146
|
-
className="btn btn-primary btn-xs"
|
|
147
|
-
>
|
|
148
|
-
New Note
|
|
149
|
-
</button>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
<div className="flex-1 overflow-auto p-3 flex flex-col gap-2.5">
|
|
153
|
-
{notes.length === 0 && (
|
|
154
|
-
<div className="text-base-content/50 text-[13px] text-center mt-10">
|
|
155
|
-
No notes yet. Create one to get started.
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
{notes.map(function (note) {
|
|
159
|
-
return (
|
|
160
|
-
<NoteCard
|
|
161
|
-
key={note.id}
|
|
162
|
-
note={note}
|
|
163
|
-
onEdit={setEditingId}
|
|
164
|
-
onDelete={handleDelete}
|
|
165
|
-
/>
|
|
166
|
-
);
|
|
167
|
-
})}
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
{creating && (
|
|
171
|
-
<EditModal
|
|
172
|
-
initial=""
|
|
173
|
-
onSave={handleCreate}
|
|
174
|
-
onCancel={function () { setCreating(false); }}
|
|
175
|
-
/>
|
|
176
|
-
)}
|
|
177
|
-
|
|
178
|
-
{editingNote && (
|
|
179
|
-
<EditModal
|
|
180
|
-
initial={editingNote.content}
|
|
181
|
-
onSave={handleEdit}
|
|
182
|
-
onCancel={function () { setEditingId(null); }}
|
|
183
|
-
/>
|
|
184
|
-
)}
|
|
185
|
-
</div>
|
|
186
|
-
);
|
|
187
|
-
}
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import type { StickyNote, ServerMessage } from "@lattice/shared";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
|
|
5
|
+
interface NoteCardProps {
|
|
6
|
+
note: StickyNote;
|
|
7
|
+
onEdit: (id: string) => void;
|
|
8
|
+
onDelete: (id: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function NoteCard(props: NoteCardProps) {
|
|
12
|
+
var { note, onEdit, onDelete } = props;
|
|
13
|
+
return (
|
|
14
|
+
<div className="card bg-base-200 border border-base-300">
|
|
15
|
+
<div className="card-body p-3">
|
|
16
|
+
<div className="text-[13px] text-base-content whitespace-pre-wrap break-words leading-relaxed min-h-12">
|
|
17
|
+
{note.content}
|
|
18
|
+
</div>
|
|
19
|
+
<div className="flex gap-1.5 justify-end mt-2">
|
|
20
|
+
<button
|
|
21
|
+
onClick={function () { onEdit(note.id); }}
|
|
22
|
+
className="btn btn-ghost btn-xs border border-base-300"
|
|
23
|
+
>
|
|
24
|
+
Edit
|
|
25
|
+
</button>
|
|
26
|
+
<button
|
|
27
|
+
onClick={function () { onDelete(note.id); }}
|
|
28
|
+
className="btn btn-ghost btn-xs border border-base-300 text-base-content/60"
|
|
29
|
+
>
|
|
30
|
+
Delete
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface EditModalProps {
|
|
39
|
+
initial: string;
|
|
40
|
+
onSave: (content: string) => void;
|
|
41
|
+
onCancel: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function EditModal(props: EditModalProps) {
|
|
45
|
+
var { initial, onSave, onCancel } = props;
|
|
46
|
+
var [content, setContent] = useState(initial);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="fixed inset-0 bg-black/50 z-[1000] flex items-center justify-center" role="dialog" aria-modal="true" aria-label="Edit note">
|
|
50
|
+
<div className="card bg-base-200 border border-base-300 w-[400px] max-w-[90vw] shadow-2xl">
|
|
51
|
+
<div className="card-body p-5">
|
|
52
|
+
<div className="text-[13px] font-semibold text-base-content mb-3">Edit Note</div>
|
|
53
|
+
<textarea
|
|
54
|
+
autoFocus
|
|
55
|
+
value={content}
|
|
56
|
+
onChange={function (e) { setContent(e.target.value); }}
|
|
57
|
+
className="textarea textarea-bordered w-full min-h-[120px] bg-base-300 text-base-content text-[13px] resize-y"
|
|
58
|
+
/>
|
|
59
|
+
<div className="flex gap-2 justify-end mt-3">
|
|
60
|
+
<button
|
|
61
|
+
onClick={onCancel}
|
|
62
|
+
className="btn btn-ghost btn-sm"
|
|
63
|
+
>
|
|
64
|
+
Cancel
|
|
65
|
+
</button>
|
|
66
|
+
<button
|
|
67
|
+
onClick={function () { onSave(content); }}
|
|
68
|
+
className="btn btn-primary btn-sm"
|
|
69
|
+
>
|
|
70
|
+
Save
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function StickyNotes() {
|
|
80
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
81
|
+
var [notes, setNotes] = useState<StickyNote[]>([]);
|
|
82
|
+
var [editingId, setEditingId] = useState<string | null>(null);
|
|
83
|
+
var [creating, setCreating] = useState(false);
|
|
84
|
+
|
|
85
|
+
var editingNote = editingId ? notes.find(function (n) { return n.id === editingId; }) : null;
|
|
86
|
+
|
|
87
|
+
var handleMessage = useCallback(function (msg: ServerMessage) {
|
|
88
|
+
if (msg.type === "notes:list_result") {
|
|
89
|
+
setNotes(msg.notes);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (msg.type === "notes:created") {
|
|
93
|
+
setNotes(function (prev) { return [...prev, msg.note]; });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (msg.type === "notes:updated") {
|
|
97
|
+
setNotes(function (prev) {
|
|
98
|
+
return prev.map(function (n) { return n.id === msg.note.id ? msg.note : n; });
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (msg.type === "notes:deleted") {
|
|
103
|
+
setNotes(function (prev) { return prev.filter(function (n) { return n.id !== msg.id; }); });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
useEffect(function () {
|
|
109
|
+
subscribe("notes:list_result", handleMessage);
|
|
110
|
+
subscribe("notes:created", handleMessage);
|
|
111
|
+
subscribe("notes:updated", handleMessage);
|
|
112
|
+
subscribe("notes:deleted", handleMessage);
|
|
113
|
+
send({ type: "notes:list" });
|
|
114
|
+
return function () {
|
|
115
|
+
unsubscribe("notes:list_result", handleMessage);
|
|
116
|
+
unsubscribe("notes:created", handleMessage);
|
|
117
|
+
unsubscribe("notes:updated", handleMessage);
|
|
118
|
+
unsubscribe("notes:deleted", handleMessage);
|
|
119
|
+
};
|
|
120
|
+
}, [send, subscribe, unsubscribe, handleMessage]);
|
|
121
|
+
|
|
122
|
+
function handleCreate(content: string) {
|
|
123
|
+
if (content.trim()) {
|
|
124
|
+
send({ type: "notes:create", content: content.trim() });
|
|
125
|
+
}
|
|
126
|
+
setCreating(false);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function handleEdit(content: string) {
|
|
130
|
+
if (editingId && content.trim()) {
|
|
131
|
+
send({ type: "notes:update", id: editingId, content: content.trim() });
|
|
132
|
+
}
|
|
133
|
+
setEditingId(null);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handleDelete(id: string) {
|
|
137
|
+
send({ type: "notes:delete", id });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex flex-col h-full bg-base-100">
|
|
142
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-base-300">
|
|
143
|
+
<span className="text-[13px] font-semibold text-base-content">Sticky Notes</span>
|
|
144
|
+
<button
|
|
145
|
+
onClick={function () { setCreating(true); }}
|
|
146
|
+
className="btn btn-primary btn-xs"
|
|
147
|
+
>
|
|
148
|
+
New Note
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="flex-1 overflow-auto p-3 flex flex-col gap-2.5">
|
|
153
|
+
{notes.length === 0 && (
|
|
154
|
+
<div className="text-base-content/50 text-[13px] text-center mt-10">
|
|
155
|
+
No notes yet. Create one to get started.
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{notes.map(function (note) {
|
|
159
|
+
return (
|
|
160
|
+
<NoteCard
|
|
161
|
+
key={note.id}
|
|
162
|
+
note={note}
|
|
163
|
+
onEdit={setEditingId}
|
|
164
|
+
onDelete={handleDelete}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{creating && (
|
|
171
|
+
<EditModal
|
|
172
|
+
initial=""
|
|
173
|
+
onSave={handleCreate}
|
|
174
|
+
onCancel={function () { setCreating(false); }}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{editingNote && (
|
|
179
|
+
<EditModal
|
|
180
|
+
initial={editingNote.content}
|
|
181
|
+
onSave={handleEdit}
|
|
182
|
+
onCancel={function () { setEditingId(null); }}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import { memo, useMemo, useCallback } from "react";
|
|
2
|
-
import { useTheme } from "../../hooks/useTheme";
|
|
3
|
-
import { Sun, Moon, Check } from "lucide-react";
|
|
4
|
-
import type { ThemeEntry } from "../../themes/index";
|
|
5
|
-
|
|
6
|
-
var SWATCH_KEYS = [
|
|
7
|
-
"base00", "base01", "base02", "base03",
|
|
8
|
-
"base04", "base05", "base06", "base07",
|
|
9
|
-
"base08", "base09", "base0A", "base0B",
|
|
10
|
-
"base0C", "base0D", "base0E", "base0F",
|
|
11
|
-
] as const;
|
|
12
|
-
|
|
13
|
-
var ThemeCard = memo(function ThemeCard({
|
|
14
|
-
entry,
|
|
15
|
-
active,
|
|
16
|
-
onSelect,
|
|
17
|
-
}: {
|
|
18
|
-
entry: ThemeEntry;
|
|
19
|
-
active: boolean;
|
|
20
|
-
onSelect: (id: string) => void;
|
|
21
|
-
}) {
|
|
22
|
-
var t = entry.theme;
|
|
23
|
-
|
|
24
|
-
function handleClick() {
|
|
25
|
-
onSelect(entry.id);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<button
|
|
30
|
-
onClick={handleClick}
|
|
31
|
-
className={
|
|
32
|
-
"flex flex-col gap-2 p-3 sm:p-2.5 px-3 rounded-lg border cursor-pointer text-left transition-colors duration-[120ms] relative focus-visible:ring-2 focus-visible:ring-primary " +
|
|
33
|
-
(active
|
|
34
|
-
? "border-primary bg-base-300 shadow-sm"
|
|
35
|
-
: "border-base-content/15 bg-base-300 hover:border-base-content/30")
|
|
36
|
-
}
|
|
37
|
-
>
|
|
38
|
-
{active && (
|
|
39
|
-
<div className="absolute top-1.5 right-1.5 w-3.5 h-3.5 rounded-full bg-primary flex items-center justify-center">
|
|
40
|
-
<Check size={8} className="text-primary-content" strokeWidth={1.8} />
|
|
41
|
-
</div>
|
|
42
|
-
)}
|
|
43
|
-
|
|
44
|
-
<div className="flex gap-[3px] flex-wrap w-[80px]">
|
|
45
|
-
{SWATCH_KEYS.map(function (key) {
|
|
46
|
-
return (
|
|
47
|
-
<div
|
|
48
|
-
key={key}
|
|
49
|
-
className="w-[10px] h-[10px] rounded-sm flex-shrink-0 ring-1 ring-base-content/10"
|
|
50
|
-
style={{ background: "#" + t[key] }}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
})}
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<div className="text-[12px] font-medium text-base-content">
|
|
57
|
-
{t.name}
|
|
58
|
-
</div>
|
|
59
|
-
</button>
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
function ThemeGroup({
|
|
64
|
-
label,
|
|
65
|
-
entries,
|
|
66
|
-
currentThemeId,
|
|
67
|
-
onSelect,
|
|
68
|
-
}: {
|
|
69
|
-
label: string;
|
|
70
|
-
entries: ThemeEntry[];
|
|
71
|
-
currentThemeId: string;
|
|
72
|
-
onSelect: (id: string) => void;
|
|
73
|
-
}) {
|
|
74
|
-
if (entries.length === 0) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div className="mb-6">
|
|
80
|
-
<div className="text-[11px] font-mono font-bold tracking-[0.1em] uppercase text-base-content/40 mb-3">
|
|
81
|
-
{label}
|
|
82
|
-
</div>
|
|
83
|
-
<div className="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-2">
|
|
84
|
-
{entries.map(function (entry) {
|
|
85
|
-
return (
|
|
86
|
-
<ThemeCard
|
|
87
|
-
key={entry.id}
|
|
88
|
-
entry={entry}
|
|
89
|
-
active={entry.id === currentThemeId}
|
|
90
|
-
onSelect={onSelect}
|
|
91
|
-
/>
|
|
92
|
-
);
|
|
93
|
-
})}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function Appearance() {
|
|
100
|
-
var { mode, currentThemeId, toggleMode, setTheme, themes } = useTheme();
|
|
101
|
-
|
|
102
|
-
var darkThemes = useMemo(function () {
|
|
103
|
-
return themes.filter(function (e) { return e.theme.variant === "dark"; });
|
|
104
|
-
}, [themes]);
|
|
105
|
-
|
|
106
|
-
var lightThemes = useMemo(function () {
|
|
107
|
-
return themes.filter(function (e) { return e.theme.variant === "light"; });
|
|
108
|
-
}, [themes]);
|
|
109
|
-
|
|
110
|
-
var handleThemeSelect = useCallback(function (id: string) {
|
|
111
|
-
setTheme(id);
|
|
112
|
-
}, [setTheme]);
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div className="py-2">
|
|
116
|
-
<div className="flex items-center justify-between mb-6">
|
|
117
|
-
<div className="text-[12px] font-semibold text-base-content/40">Color Mode</div>
|
|
118
|
-
<button
|
|
119
|
-
onClick={toggleMode}
|
|
120
|
-
className="btn btn-ghost btn-sm border border-base-content/20"
|
|
121
|
-
>
|
|
122
|
-
{mode === "dark" ? (
|
|
123
|
-
<>
|
|
124
|
-
<Sun size={12} />
|
|
125
|
-
Switch to Light
|
|
126
|
-
</>
|
|
127
|
-
) : (
|
|
128
|
-
<>
|
|
129
|
-
<Moon size={12} />
|
|
130
|
-
Switch to Dark
|
|
131
|
-
</>
|
|
132
|
-
)}
|
|
133
|
-
</button>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<ThemeGroup
|
|
137
|
-
label="Dark Themes"
|
|
138
|
-
entries={darkThemes}
|
|
139
|
-
currentThemeId={currentThemeId}
|
|
140
|
-
onSelect={handleThemeSelect}
|
|
141
|
-
/>
|
|
142
|
-
|
|
143
|
-
<ThemeGroup
|
|
144
|
-
label="Light Themes"
|
|
145
|
-
entries={lightThemes}
|
|
146
|
-
currentThemeId={currentThemeId}
|
|
147
|
-
onSelect={handleThemeSelect}
|
|
148
|
-
/>
|
|
149
|
-
</div>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
1
|
+
import { memo, useMemo, useCallback } from "react";
|
|
2
|
+
import { useTheme } from "../../hooks/useTheme";
|
|
3
|
+
import { Sun, Moon, Check } from "lucide-react";
|
|
4
|
+
import type { ThemeEntry } from "../../themes/index";
|
|
5
|
+
|
|
6
|
+
var SWATCH_KEYS = [
|
|
7
|
+
"base00", "base01", "base02", "base03",
|
|
8
|
+
"base04", "base05", "base06", "base07",
|
|
9
|
+
"base08", "base09", "base0A", "base0B",
|
|
10
|
+
"base0C", "base0D", "base0E", "base0F",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
var ThemeCard = memo(function ThemeCard({
|
|
14
|
+
entry,
|
|
15
|
+
active,
|
|
16
|
+
onSelect,
|
|
17
|
+
}: {
|
|
18
|
+
entry: ThemeEntry;
|
|
19
|
+
active: boolean;
|
|
20
|
+
onSelect: (id: string) => void;
|
|
21
|
+
}) {
|
|
22
|
+
var t = entry.theme;
|
|
23
|
+
|
|
24
|
+
function handleClick() {
|
|
25
|
+
onSelect(entry.id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
onClick={handleClick}
|
|
31
|
+
className={
|
|
32
|
+
"flex flex-col gap-2 p-3 sm:p-2.5 px-3 rounded-lg border cursor-pointer text-left transition-colors duration-[120ms] relative focus-visible:ring-2 focus-visible:ring-primary " +
|
|
33
|
+
(active
|
|
34
|
+
? "border-primary bg-base-300 shadow-sm"
|
|
35
|
+
: "border-base-content/15 bg-base-300 hover:border-base-content/30")
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{active && (
|
|
39
|
+
<div className="absolute top-1.5 right-1.5 w-3.5 h-3.5 rounded-full bg-primary flex items-center justify-center">
|
|
40
|
+
<Check size={8} className="text-primary-content" strokeWidth={1.8} />
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
<div className="flex gap-[3px] flex-wrap w-[80px]">
|
|
45
|
+
{SWATCH_KEYS.map(function (key) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
key={key}
|
|
49
|
+
className="w-[10px] h-[10px] rounded-sm flex-shrink-0 ring-1 ring-base-content/10"
|
|
50
|
+
style={{ background: "#" + t[key] }}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="text-[12px] font-medium text-base-content">
|
|
57
|
+
{t.name}
|
|
58
|
+
</div>
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function ThemeGroup({
|
|
64
|
+
label,
|
|
65
|
+
entries,
|
|
66
|
+
currentThemeId,
|
|
67
|
+
onSelect,
|
|
68
|
+
}: {
|
|
69
|
+
label: string;
|
|
70
|
+
entries: ThemeEntry[];
|
|
71
|
+
currentThemeId: string;
|
|
72
|
+
onSelect: (id: string) => void;
|
|
73
|
+
}) {
|
|
74
|
+
if (entries.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="mb-6">
|
|
80
|
+
<div className="text-[11px] font-mono font-bold tracking-[0.1em] uppercase text-base-content/40 mb-3">
|
|
81
|
+
{label}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-2">
|
|
84
|
+
{entries.map(function (entry) {
|
|
85
|
+
return (
|
|
86
|
+
<ThemeCard
|
|
87
|
+
key={entry.id}
|
|
88
|
+
entry={entry}
|
|
89
|
+
active={entry.id === currentThemeId}
|
|
90
|
+
onSelect={onSelect}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function Appearance() {
|
|
100
|
+
var { mode, currentThemeId, toggleMode, setTheme, themes } = useTheme();
|
|
101
|
+
|
|
102
|
+
var darkThemes = useMemo(function () {
|
|
103
|
+
return themes.filter(function (e) { return e.theme.variant === "dark"; });
|
|
104
|
+
}, [themes]);
|
|
105
|
+
|
|
106
|
+
var lightThemes = useMemo(function () {
|
|
107
|
+
return themes.filter(function (e) { return e.theme.variant === "light"; });
|
|
108
|
+
}, [themes]);
|
|
109
|
+
|
|
110
|
+
var handleThemeSelect = useCallback(function (id: string) {
|
|
111
|
+
setTheme(id);
|
|
112
|
+
}, [setTheme]);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="py-2">
|
|
116
|
+
<div className="flex items-center justify-between mb-6">
|
|
117
|
+
<div className="text-[12px] font-semibold text-base-content/40">Color Mode</div>
|
|
118
|
+
<button
|
|
119
|
+
onClick={toggleMode}
|
|
120
|
+
className="btn btn-ghost btn-sm border border-base-content/20"
|
|
121
|
+
>
|
|
122
|
+
{mode === "dark" ? (
|
|
123
|
+
<>
|
|
124
|
+
<Sun size={12} />
|
|
125
|
+
Switch to Light
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
128
|
+
<>
|
|
129
|
+
<Moon size={12} />
|
|
130
|
+
Switch to Dark
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<ThemeGroup
|
|
137
|
+
label="Dark Themes"
|
|
138
|
+
entries={darkThemes}
|
|
139
|
+
currentThemeId={currentThemeId}
|
|
140
|
+
onSelect={handleThemeSelect}
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
<ThemeGroup
|
|
144
|
+
label="Light Themes"
|
|
145
|
+
entries={lightThemes}
|
|
146
|
+
currentThemeId={currentThemeId}
|
|
147
|
+
onSelect={handleThemeSelect}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|