@cryptiklemur/lattice 0.0.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.
Files changed (162) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release.yml +44 -0
  3. package/.impeccable.md +66 -0
  4. package/.releaserc.json +32 -0
  5. package/.serena/project.yml +138 -0
  6. package/CLAUDE.md +35 -0
  7. package/CONTRIBUTING.md +93 -0
  8. package/LICENSE +21 -0
  9. package/README.md +83 -0
  10. package/bun.lock +1459 -0
  11. package/bunfig.toml +2 -0
  12. package/client/index.html +32 -0
  13. package/client/package.json +37 -0
  14. package/client/public/icons/icon-192.svg +11 -0
  15. package/client/public/icons/icon-512.svg +11 -0
  16. package/client/public/manifest.json +24 -0
  17. package/client/public/sw.js +61 -0
  18. package/client/src/App.tsx +28 -0
  19. package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
  20. package/client/src/components/chat/ChatInput.tsx +241 -0
  21. package/client/src/components/chat/ChatView.tsx +727 -0
  22. package/client/src/components/chat/Message.tsx +362 -0
  23. package/client/src/components/chat/ModelSelector.tsx +87 -0
  24. package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
  25. package/client/src/components/chat/StatusBar.tsx +50 -0
  26. package/client/src/components/chat/ToolGroup.tsx +129 -0
  27. package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
  28. package/client/src/components/chat/toolSummary.ts +41 -0
  29. package/client/src/components/dashboard/DashboardView.tsx +219 -0
  30. package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
  31. package/client/src/components/mesh/NodeBadge.tsx +24 -0
  32. package/client/src/components/mesh/PairingDialog.tsx +281 -0
  33. package/client/src/components/panels/FileBrowser.tsx +241 -0
  34. package/client/src/components/panels/StickyNotes.tsx +187 -0
  35. package/client/src/components/panels/Terminal.tsx +128 -0
  36. package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
  37. package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
  38. package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
  39. package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
  40. package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
  41. package/client/src/components/project-settings/ProjectRules.tsx +277 -0
  42. package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
  43. package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
  44. package/client/src/components/settings/Appearance.tsx +151 -0
  45. package/client/src/components/settings/ClaudeSettings.tsx +151 -0
  46. package/client/src/components/settings/Environment.tsx +185 -0
  47. package/client/src/components/settings/GlobalMcp.tsx +207 -0
  48. package/client/src/components/settings/GlobalSkills.tsx +125 -0
  49. package/client/src/components/settings/MeshStatus.tsx +145 -0
  50. package/client/src/components/settings/SettingsView.tsx +57 -0
  51. package/client/src/components/settings/SkillMarketplace.tsx +175 -0
  52. package/client/src/components/settings/mcp-shared.tsx +194 -0
  53. package/client/src/components/settings/skill-shared.tsx +177 -0
  54. package/client/src/components/setup/SetupWizard.tsx +750 -0
  55. package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
  56. package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
  57. package/client/src/components/sidebar/ProjectRail.tsx +291 -0
  58. package/client/src/components/sidebar/SearchFilter.tsx +52 -0
  59. package/client/src/components/sidebar/SessionList.tsx +384 -0
  60. package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
  61. package/client/src/components/sidebar/Sidebar.tsx +209 -0
  62. package/client/src/components/sidebar/UserIsland.tsx +59 -0
  63. package/client/src/components/sidebar/UserMenu.tsx +101 -0
  64. package/client/src/components/ui/CommandPalette.tsx +321 -0
  65. package/client/src/components/ui/ErrorBoundary.tsx +56 -0
  66. package/client/src/components/ui/IconPicker.tsx +209 -0
  67. package/client/src/components/ui/LatticeLogomark.tsx +19 -0
  68. package/client/src/components/ui/PopupMenu.tsx +98 -0
  69. package/client/src/components/ui/SaveFooter.tsx +38 -0
  70. package/client/src/components/ui/Toast.tsx +112 -0
  71. package/client/src/hooks/useMesh.ts +89 -0
  72. package/client/src/hooks/useProjectSettings.ts +56 -0
  73. package/client/src/hooks/useProjects.ts +66 -0
  74. package/client/src/hooks/useSaveState.ts +59 -0
  75. package/client/src/hooks/useSession.ts +317 -0
  76. package/client/src/hooks/useSidebar.ts +74 -0
  77. package/client/src/hooks/useSkills.ts +30 -0
  78. package/client/src/hooks/useTheme.ts +114 -0
  79. package/client/src/hooks/useWebSocket.ts +26 -0
  80. package/client/src/main.tsx +10 -0
  81. package/client/src/providers/WebSocketProvider.tsx +146 -0
  82. package/client/src/router.tsx +391 -0
  83. package/client/src/stores/mesh.ts +78 -0
  84. package/client/src/stores/session.ts +322 -0
  85. package/client/src/stores/sidebar.ts +336 -0
  86. package/client/src/stores/theme.ts +44 -0
  87. package/client/src/styles/global.css +167 -0
  88. package/client/src/styles/theme-vars.css +18 -0
  89. package/client/src/themes/index.ts +79 -0
  90. package/client/src/utils/findDuplicateKeys.ts +12 -0
  91. package/client/tsconfig.json +14 -0
  92. package/client/vite.config.ts +20 -0
  93. package/package.json +46 -0
  94. package/server/package.json +22 -0
  95. package/server/src/auth/passphrase.ts +48 -0
  96. package/server/src/config.ts +55 -0
  97. package/server/src/daemon.ts +338 -0
  98. package/server/src/features/ralph-loop.ts +173 -0
  99. package/server/src/features/scheduler.ts +281 -0
  100. package/server/src/features/sticky-notes.ts +102 -0
  101. package/server/src/handlers/chat.ts +194 -0
  102. package/server/src/handlers/fs.ts +84 -0
  103. package/server/src/handlers/loop.ts +37 -0
  104. package/server/src/handlers/mesh.ts +125 -0
  105. package/server/src/handlers/notes.ts +45 -0
  106. package/server/src/handlers/project-settings.ts +174 -0
  107. package/server/src/handlers/scheduler.ts +47 -0
  108. package/server/src/handlers/session.ts +159 -0
  109. package/server/src/handlers/settings.ts +109 -0
  110. package/server/src/handlers/skills.ts +380 -0
  111. package/server/src/handlers/terminal.ts +70 -0
  112. package/server/src/identity.ts +26 -0
  113. package/server/src/index.ts +190 -0
  114. package/server/src/mesh/connector.ts +209 -0
  115. package/server/src/mesh/discovery.ts +123 -0
  116. package/server/src/mesh/pairing.ts +94 -0
  117. package/server/src/mesh/peers.ts +52 -0
  118. package/server/src/mesh/proxy.ts +103 -0
  119. package/server/src/mesh/session-sync.ts +107 -0
  120. package/server/src/project/context-breakdown.ts +289 -0
  121. package/server/src/project/file-browser.ts +106 -0
  122. package/server/src/project/project-files.ts +267 -0
  123. package/server/src/project/registry.ts +57 -0
  124. package/server/src/project/sdk-bridge.ts +566 -0
  125. package/server/src/project/session.ts +432 -0
  126. package/server/src/project/terminal.ts +69 -0
  127. package/server/src/tls.ts +51 -0
  128. package/server/src/ws/broadcast.ts +31 -0
  129. package/server/src/ws/router.ts +104 -0
  130. package/server/src/ws/server.ts +2 -0
  131. package/server/tsconfig.json +16 -0
  132. package/shared/package.json +11 -0
  133. package/shared/src/constants.ts +7 -0
  134. package/shared/src/index.ts +4 -0
  135. package/shared/src/messages.ts +638 -0
  136. package/shared/src/models.ts +136 -0
  137. package/shared/src/project-settings.ts +45 -0
  138. package/shared/tsconfig.json +11 -0
  139. package/themes/amoled.json +20 -0
  140. package/themes/ayu-light.json +9 -0
  141. package/themes/catppuccin-latte.json +9 -0
  142. package/themes/catppuccin-mocha.json +9 -0
  143. package/themes/clay-light.json +10 -0
  144. package/themes/clay.json +10 -0
  145. package/themes/dracula.json +9 -0
  146. package/themes/everforest-light.json +9 -0
  147. package/themes/everforest.json +9 -0
  148. package/themes/github-light.json +9 -0
  149. package/themes/gruvbox-dark.json +9 -0
  150. package/themes/gruvbox-light.json +9 -0
  151. package/themes/monokai.json +9 -0
  152. package/themes/nord-light.json +9 -0
  153. package/themes/nord.json +9 -0
  154. package/themes/one-dark.json +9 -0
  155. package/themes/one-light.json +9 -0
  156. package/themes/rose-pine-dawn.json +9 -0
  157. package/themes/rose-pine.json +9 -0
  158. package/themes/solarized-dark.json +9 -0
  159. package/themes/solarized-light.json +9 -0
  160. package/themes/tokyo-night-light.json +9 -0
  161. package/themes/tokyo-night.json +9 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,277 @@
1
+ import { useState, useEffect } from "react";
2
+ import { X, Plus, ChevronDown, ChevronRight } from "lucide-react";
3
+ import { SaveFooter } from "../ui/SaveFooter";
4
+ import { useSaveState } from "../../hooks/useSaveState";
5
+ import type { ProjectSettings } from "@lattice/shared";
6
+
7
+ interface RuleEntry {
8
+ filename: string;
9
+ content: string;
10
+ }
11
+
12
+ export function ProjectRules({
13
+ settings,
14
+ updateSection,
15
+ }: {
16
+ settings: ProjectSettings;
17
+ updateSection: (section: string, data: Record<string, unknown>) => void;
18
+ }) {
19
+ var globalRules = settings.global.rules ?? [];
20
+
21
+ var [rules, setRules] = useState<RuleEntry[]>(function () {
22
+ return (settings.rules ?? []).map(function (r) {
23
+ return { filename: r.filename, content: r.content };
24
+ });
25
+ });
26
+ var [expandedGlobal, setExpandedGlobal] = useState<Set<number>>(new Set());
27
+ var [expandedProject, setExpandedProject] = useState<Set<number>>(new Set());
28
+ var [adding, setAdding] = useState(false);
29
+ var [newFilename, setNewFilename] = useState("");
30
+ var [newContent, setNewContent] = useState("");
31
+ var save = useSaveState();
32
+
33
+ useEffect(function () {
34
+ if (save.saving) {
35
+ save.confirmSave();
36
+ } else {
37
+ setRules((settings.rules ?? []).map(function (r) {
38
+ return { filename: r.filename, content: r.content };
39
+ }));
40
+ save.resetFromServer();
41
+ }
42
+ }, [settings]);
43
+
44
+ function toggleGlobal(idx: number) {
45
+ setExpandedGlobal(function (prev) {
46
+ var next = new Set(prev);
47
+ if (next.has(idx)) {
48
+ next.delete(idx);
49
+ } else {
50
+ next.add(idx);
51
+ }
52
+ return next;
53
+ });
54
+ }
55
+
56
+ function toggleProject(idx: number) {
57
+ setExpandedProject(function (prev) {
58
+ var next = new Set(prev);
59
+ if (next.has(idx)) {
60
+ next.delete(idx);
61
+ } else {
62
+ next.add(idx);
63
+ }
64
+ return next;
65
+ });
66
+ }
67
+
68
+ function handleContentChange(idx: number, content: string) {
69
+ setRules(function (prev) {
70
+ return prev.map(function (r, i) {
71
+ return i === idx ? { ...r, content } : r;
72
+ });
73
+ });
74
+ save.markDirty();
75
+ }
76
+
77
+ function handleDelete(idx: number) {
78
+ setRules(function (prev) {
79
+ return prev.filter(function (_, i) { return i !== idx; });
80
+ });
81
+ setExpandedProject(new Set());
82
+ save.markDirty();
83
+ }
84
+
85
+ function handleAdd() {
86
+ var fn = newFilename.trim();
87
+ if (!fn || !fn.endsWith(".md")) return;
88
+ setRules(function (prev) {
89
+ return [...prev, { filename: fn, content: newContent }];
90
+ });
91
+ setNewFilename("");
92
+ setNewContent("");
93
+ setAdding(false);
94
+ save.markDirty();
95
+ }
96
+
97
+ function handleCancelAdd() {
98
+ setAdding(false);
99
+ setNewFilename("");
100
+ setNewContent("");
101
+ }
102
+
103
+ function handleSave() {
104
+ save.startSave();
105
+ updateSection("rules", { rules });
106
+ }
107
+
108
+ function preview(content: string): string {
109
+ var trimmed = content.trim();
110
+ if (trimmed.length <= 80) return trimmed;
111
+ return trimmed.slice(0, 80) + "...";
112
+ }
113
+
114
+ var textareaClass = "w-full px-3 py-2 bg-base-300 border border-base-content/15 rounded-xl text-base-content font-mono text-[12px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms] resize-y min-h-[120px]";
115
+ var inputClass = "w-full h-9 sm:h-7 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content font-mono text-[12px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]";
116
+
117
+ return (
118
+ <div className="py-2">
119
+ <div className="mb-6">
120
+ <h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
121
+ Global Rules
122
+ </h2>
123
+ {globalRules.length === 0 && (
124
+ <div className="py-4 text-center text-[13px] text-base-content/30">
125
+ No global rules.
126
+ </div>
127
+ )}
128
+ {globalRules.length > 0 && (
129
+ <div className="flex flex-col gap-1.5">
130
+ {globalRules.map(function (rule, idx) {
131
+ var isExpanded = expandedGlobal.has(idx);
132
+ return (
133
+ <div key={rule.filename + "-" + idx} className="border border-base-content/10 rounded-xl overflow-hidden">
134
+ <button
135
+ onClick={function () { toggleGlobal(idx); }}
136
+ className="w-full flex items-center gap-2 px-3 py-2 bg-base-300/50 hover:bg-base-300/70 transition-colors duration-[120ms] cursor-pointer text-left"
137
+ >
138
+ {isExpanded
139
+ ? <ChevronDown size={12} className="text-base-content/40 flex-shrink-0" />
140
+ : <ChevronRight size={12} className="text-base-content/40 flex-shrink-0" />
141
+ }
142
+ <span className="font-mono text-[12px] text-base-content/40 flex-1 truncate">
143
+ {rule.filename}
144
+ </span>
145
+ <span className="text-[10px] uppercase tracking-wider text-base-content/30">
146
+ global
147
+ </span>
148
+ </button>
149
+ {!isExpanded && (
150
+ <div className="px-3 py-1.5 text-[11px] text-base-content/30 font-mono truncate">
151
+ {preview(rule.content)}
152
+ </div>
153
+ )}
154
+ {isExpanded && (
155
+ <pre className="px-3 py-2 text-[12px] text-base-content/40 font-mono whitespace-pre-wrap break-words bg-base-300/30 max-h-[300px] overflow-auto">
156
+ {rule.content}
157
+ </pre>
158
+ )}
159
+ </div>
160
+ );
161
+ })}
162
+ </div>
163
+ )}
164
+ </div>
165
+
166
+ <div>
167
+ <h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
168
+ Project Rules
169
+ </h2>
170
+ {rules.length === 0 && !adding && (
171
+ <div className="py-4 text-center text-[13px] text-base-content/30 mb-3">
172
+ No project rules.
173
+ </div>
174
+ )}
175
+ {rules.length > 0 && (
176
+ <div className="flex flex-col gap-1.5 mb-3">
177
+ {rules.map(function (rule, idx) {
178
+ var isExpanded = expandedProject.has(idx);
179
+ return (
180
+ <div key={rule.filename + "-" + idx} className="border border-base-content/15 rounded-xl overflow-hidden">
181
+ <div className="w-full flex items-center gap-2 px-3 py-2 bg-base-300 text-left">
182
+ <button
183
+ onClick={function () { toggleProject(idx); }}
184
+ className="flex items-center gap-2 flex-1 min-w-0 cursor-pointer hover:text-base-content/80 transition-colors"
185
+ >
186
+ {isExpanded
187
+ ? <ChevronDown size={12} className="text-base-content/60 flex-shrink-0" />
188
+ : <ChevronRight size={12} className="text-base-content/60 flex-shrink-0" />
189
+ }
190
+ <span className="font-mono text-[12px] text-base-content flex-1 truncate text-left">
191
+ {rule.filename}
192
+ </span>
193
+ </button>
194
+ <button
195
+ onClick={function () { handleDelete(idx); }}
196
+ className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error flex-shrink-0 focus-visible:ring-2 focus-visible:ring-primary"
197
+ aria-label={"Delete " + rule.filename}
198
+ >
199
+ <X size={14} />
200
+ </button>
201
+ </div>
202
+ {!isExpanded && (
203
+ <div className="px-3 py-1.5 text-[11px] text-base-content/50 font-mono truncate">
204
+ {preview(rule.content)}
205
+ </div>
206
+ )}
207
+ {isExpanded && (
208
+ <div className="px-3 py-2 bg-base-300/30">
209
+ <textarea
210
+ value={rule.content}
211
+ onChange={function (e) { handleContentChange(idx, e.target.value); }}
212
+ className={textareaClass}
213
+ rows={8}
214
+ aria-label={"Content for " + rule.filename}
215
+ />
216
+ </div>
217
+ )}
218
+ </div>
219
+ );
220
+ })}
221
+ </div>
222
+ )}
223
+
224
+ {adding && (
225
+ <div className="border border-dashed border-base-content/20 rounded-xl p-3 mb-3 flex flex-col gap-2">
226
+ <input
227
+ type="text"
228
+ value={newFilename}
229
+ onChange={function (e) { setNewFilename(e.target.value); }}
230
+ placeholder="filename.md"
231
+ aria-label="New rule filename"
232
+ className={inputClass}
233
+ />
234
+ <textarea
235
+ value={newContent}
236
+ onChange={function (e) { setNewContent(e.target.value); }}
237
+ placeholder="Rule content..."
238
+ aria-label="New rule content"
239
+ className={textareaClass}
240
+ rows={5}
241
+ />
242
+ <div className="flex gap-2 justify-end">
243
+ <button
244
+ onClick={handleCancelAdd}
245
+ className="btn btn-ghost btn-sm text-[12px]"
246
+ >
247
+ Cancel
248
+ </button>
249
+ <button
250
+ onClick={handleAdd}
251
+ disabled={!newFilename.trim() || !newFilename.trim().endsWith(".md")}
252
+ className={
253
+ "btn btn-sm btn-primary text-[12px]" +
254
+ (!newFilename.trim() || !newFilename.trim().endsWith(".md") ? " opacity-50 cursor-not-allowed" : "")
255
+ }
256
+ >
257
+ Add
258
+ </button>
259
+ </div>
260
+ </div>
261
+ )}
262
+
263
+ {!adding && (
264
+ <button
265
+ onClick={function () { setAdding(true); }}
266
+ className="flex items-center gap-1.5 px-3 py-2.5 sm:py-1.5 rounded-xl border border-dashed border-base-content/20 bg-transparent text-base-content/40 text-[12px] hover:text-base-content/60 hover:border-base-content/30 transition-colors duration-[120ms] mb-5 cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100"
267
+ >
268
+ <Plus size={12} />
269
+ Add Rule
270
+ </button>
271
+ )}
272
+
273
+ <SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
274
+ </div>
275
+ </div>
276
+ );
277
+ }
@@ -0,0 +1,99 @@
1
+ import { useSidebar } from "../../hooks/useSidebar";
2
+ import { useProjectSettings } from "../../hooks/useProjectSettings";
3
+ import { Menu } from "lucide-react";
4
+ import type { ProjectSettingsSection } from "../../stores/sidebar";
5
+ import type { ProjectSettings } from "@lattice/shared";
6
+ import { ProjectGeneral } from "./ProjectGeneral";
7
+ import { ProjectClaude } from "./ProjectClaude";
8
+ import { ProjectEnvironment } from "./ProjectEnvironment";
9
+ import { ProjectPermissions } from "./ProjectPermissions";
10
+ import { ProjectSkills } from "./ProjectSkills";
11
+ import { ProjectRules } from "./ProjectRules";
12
+ import { ProjectMcp } from "./ProjectMcp";
13
+
14
+ var SECTION_CONFIG: Record<string, { title: string }> = {
15
+ general: { title: "General" },
16
+ claude: { title: "Claude" },
17
+ environment: { title: "Environment" },
18
+ mcp: { title: "MCP Servers" },
19
+ skills: { title: "Skills" },
20
+ rules: { title: "Rules" },
21
+ permissions: { title: "Permissions" },
22
+ };
23
+
24
+ function renderSection(
25
+ section: ProjectSettingsSection,
26
+ settings: ProjectSettings,
27
+ updateSection: (section: string, data: Record<string, unknown>) => void,
28
+ projectSlug?: string,
29
+ ) {
30
+ if (section === "general") {
31
+ return <ProjectGeneral settings={settings} updateSection={updateSection} />;
32
+ }
33
+
34
+ if (section === "claude") {
35
+ return <ProjectClaude settings={settings} updateSection={updateSection} />;
36
+ }
37
+
38
+ if (section === "environment") {
39
+ return <ProjectEnvironment settings={settings} updateSection={updateSection} />;
40
+ }
41
+
42
+ if (section === "permissions") {
43
+ return <ProjectPermissions settings={settings} updateSection={updateSection} />;
44
+ }
45
+
46
+ if (section === "skills") {
47
+ return <ProjectSkills settings={settings} projectSlug={projectSlug} />;
48
+ }
49
+
50
+ if (section === "rules") {
51
+ return <ProjectRules settings={settings} updateSection={updateSection} />;
52
+ }
53
+
54
+ if (section === "mcp") {
55
+ return <ProjectMcp settings={settings} updateSection={updateSection} />;
56
+ }
57
+
58
+ return (
59
+ <div className="py-2 text-[13px] text-base-content/40">
60
+ {section} section coming soon.
61
+ </div>
62
+ );
63
+ }
64
+
65
+ export function ProjectSettingsView() {
66
+ var { activeView, activeProjectSlug, toggleDrawer } = useSidebar();
67
+
68
+ if (activeView.type !== "project-settings") {
69
+ return null;
70
+ }
71
+
72
+ var section = activeView.section;
73
+ var config = SECTION_CONFIG[section];
74
+ var { settings, loading, error, updateSection } = useProjectSettings(activeProjectSlug);
75
+
76
+ return (
77
+ <div className="flex-1 overflow-auto px-4 sm:px-8 py-4 sm:py-6 max-w-3xl">
78
+ {config && (
79
+ <div className="mb-6 flex items-center gap-3">
80
+ <button
81
+ className="btn btn-ghost btn-sm btn-square lg:hidden"
82
+ aria-label="Toggle sidebar"
83
+ onClick={toggleDrawer}
84
+ >
85
+ <Menu size={18} />
86
+ </button>
87
+ <h1 className="text-lg font-mono font-bold text-base-content">{config.title}</h1>
88
+ </div>
89
+ )}
90
+ {loading && (
91
+ <div className="text-[13px] text-base-content/40 py-4">Loading...</div>
92
+ )}
93
+ {error && (
94
+ <div className="text-[13px] text-error py-4">{error}</div>
95
+ )}
96
+ {!loading && !error && settings && renderSection(section, settings, updateSection, activeProjectSlug ?? undefined)}
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,91 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useWebSocket } from "../../hooks/useWebSocket";
3
+ import { SkillMarketplace } from "../settings/SkillMarketplace";
4
+ import { SkillItem, SkillViewModal } from "../settings/skill-shared";
5
+ import type { ProjectSettings, ServerMessage } from "@lattice/shared";
6
+
7
+ interface ProjectSkillsProps {
8
+ settings: ProjectSettings;
9
+ projectSlug?: string;
10
+ }
11
+
12
+ export function ProjectSkills({ settings, projectSlug }: ProjectSkillsProps) {
13
+ var { send, subscribe, unsubscribe } = useWebSocket();
14
+ var globalSkills = settings.global.skills;
15
+ var projectSkills = settings.skills;
16
+ var hasAny = globalSkills.length > 0 || projectSkills.length > 0;
17
+ var [viewContent, setViewContent] = useState<{ path: string; content: string } | null>(null);
18
+
19
+ useEffect(function () {
20
+ function handleViewResult(msg: ServerMessage) {
21
+ if (msg.type !== "skills:view_result") return;
22
+ var data = msg as { type: "skills:view_result"; path: string; content: string };
23
+ setViewContent({ path: data.path, content: data.content });
24
+ }
25
+
26
+ subscribe("skills:view_result", handleViewResult);
27
+
28
+ return function () {
29
+ unsubscribe("skills:view_result", handleViewResult);
30
+ };
31
+ }, []);
32
+
33
+ function handleView(path: string) {
34
+ send({ type: "skills:view", path: path } as any);
35
+ }
36
+
37
+ return (
38
+ <div className="py-2 space-y-6">
39
+ {!hasAny && (
40
+ <div className="py-12 text-center text-[13px] text-base-content/40">
41
+ No skills found.
42
+ </div>
43
+ )}
44
+
45
+ {globalSkills.length > 0 && (
46
+ <div>
47
+ <div className="text-[12px] font-semibold text-base-content/40 mb-2">Global Skills</div>
48
+ <div className="space-y-2">
49
+ {globalSkills.map(function (skill) {
50
+ return (
51
+ <SkillItem
52
+ key={skill.path}
53
+ skill={skill}
54
+ badge="global"
55
+ onClick={function () { handleView(skill.path); }}
56
+ />
57
+ );
58
+ })}
59
+ </div>
60
+ </div>
61
+ )}
62
+
63
+ {projectSkills.length > 0 && (
64
+ <div>
65
+ <div className="text-[12px] font-semibold text-base-content/40 mb-2">Project Skills</div>
66
+ <div className="space-y-2">
67
+ {projectSkills.map(function (skill) {
68
+ return (
69
+ <SkillItem
70
+ key={skill.path}
71
+ skill={skill}
72
+ onClick={function () { handleView(skill.path); }}
73
+ />
74
+ );
75
+ })}
76
+ </div>
77
+ </div>
78
+ )}
79
+
80
+ <SkillMarketplace defaultScope="project" defaultProjectSlug={projectSlug} />
81
+
82
+ {viewContent && (
83
+ <SkillViewModal
84
+ path={viewContent.path}
85
+ content={viewContent.content}
86
+ onClose={function () { setViewContent(null); }}
87
+ />
88
+ )}
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +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
+ }