@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,232 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Plus, Pencil, Trash2 } from "lucide-react";
3
+ import { useSaveState } from "../../hooks/useSaveState";
4
+ import { SaveFooter } from "../ui/SaveFooter";
5
+ import type { ProjectSettings, McpServerConfig } from "@lattice/shared";
6
+ import {
7
+ type FormState,
8
+ emptyForm,
9
+ formFromConfig,
10
+ formToConfig,
11
+ typeBadge,
12
+ configSummary,
13
+ ServerForm,
14
+ } from "../settings/mcp-shared";
15
+
16
+ export function ProjectMcp({
17
+ settings,
18
+ updateSection,
19
+ }: {
20
+ settings: ProjectSettings;
21
+ updateSection: (section: string, data: Record<string, unknown>) => void;
22
+ }) {
23
+ var globalServers = settings.global.mcpServers ?? {};
24
+ var globalEntries = Object.entries(globalServers);
25
+
26
+ var [servers, setServers] = useState<Record<string, McpServerConfig>>(function () {
27
+ return { ...(settings.mcpServers ?? {}) };
28
+ });
29
+ var save = useSaveState();
30
+
31
+ var [adding, setAdding] = useState(false);
32
+ var [addForm, setAddForm] = useState<FormState>(emptyForm);
33
+ var [editingName, setEditingName] = useState<string | null>(null);
34
+ var [editForm, setEditForm] = useState<FormState>(emptyForm);
35
+ var [confirmDelete, setConfirmDelete] = useState<string | null>(null);
36
+
37
+ useEffect(function () {
38
+ if (save.saving) {
39
+ save.confirmSave();
40
+ } else {
41
+ setServers({ ...(settings.mcpServers ?? {}) });
42
+ save.resetFromServer();
43
+ }
44
+ }, [settings]);
45
+
46
+ var projectEntries = Object.entries(servers);
47
+
48
+ function handleAddSave() {
49
+ var name = addForm.name.trim();
50
+ if (!name) return;
51
+ var next = { ...servers, [name]: formToConfig(addForm) };
52
+ setServers(next);
53
+ setAdding(false);
54
+ setAddForm(emptyForm());
55
+ save.markDirty();
56
+ }
57
+
58
+ function handleEditStart(name: string) {
59
+ setEditingName(name);
60
+ setEditForm(formFromConfig(name, servers[name]));
61
+ setAdding(false);
62
+ setConfirmDelete(null);
63
+ }
64
+
65
+ function handleEditSave() {
66
+ if (!editingName) return;
67
+ var newName = editForm.name.trim();
68
+ if (!newName) return;
69
+ var next = { ...servers };
70
+ if (newName !== editingName) {
71
+ delete next[editingName];
72
+ }
73
+ next[newName] = formToConfig(editForm);
74
+ setServers(next);
75
+ setEditingName(null);
76
+ save.markDirty();
77
+ }
78
+
79
+ function handleDelete(name: string) {
80
+ if (confirmDelete !== name) {
81
+ setConfirmDelete(name);
82
+ return;
83
+ }
84
+ var next = { ...servers };
85
+ delete next[name];
86
+ setServers(next);
87
+ if (editingName === name) setEditingName(null);
88
+ setConfirmDelete(null);
89
+ save.markDirty();
90
+ }
91
+
92
+ function handleSave() {
93
+ save.startSave();
94
+ updateSection("mcp", { mcpServers: servers });
95
+ }
96
+
97
+ var existingNamesForAdd = new Set(Object.keys(servers));
98
+ var existingNamesForEdit = new Set(
99
+ Object.keys(servers).filter(function (n) { return n !== editingName; })
100
+ );
101
+
102
+ return (
103
+ <div className="py-2">
104
+ <div className="mb-6">
105
+ <h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
106
+ Global MCP Servers
107
+ </h2>
108
+ {globalEntries.length === 0 && (
109
+ <div className="py-4 text-center text-[13px] text-base-content/30">
110
+ No global MCP servers.
111
+ </div>
112
+ )}
113
+ {globalEntries.length > 0 && (
114
+ <div className="flex flex-col gap-2">
115
+ {globalEntries.map(function ([name, config]) {
116
+ return (
117
+ <div
118
+ key={name}
119
+ className="flex items-center gap-3 px-3 py-2 rounded-xl bg-base-300/50 border border-base-content/10"
120
+ >
121
+ <span className="font-mono text-[12px] text-base-content/40 font-semibold flex-shrink-0">{name}</span>
122
+ {typeBadge(config)}
123
+ <div className="flex-1 min-w-0">{configSummary(config)}</div>
124
+ <span className="text-[10px] uppercase tracking-wider text-base-content/30 flex-shrink-0">global</span>
125
+ </div>
126
+ );
127
+ })}
128
+ </div>
129
+ )}
130
+ </div>
131
+
132
+ <div>
133
+ <h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
134
+ Project MCP Servers
135
+ </h2>
136
+
137
+ {projectEntries.length === 0 && !adding && (
138
+ <div className="py-4 text-center text-[13px] text-base-content/30 mb-3">
139
+ No project MCP servers.
140
+ </div>
141
+ )}
142
+
143
+ <div className="flex flex-col gap-2 mb-3">
144
+ {projectEntries.map(function ([name, config]) {
145
+ if (editingName === name) {
146
+ return (
147
+ <ServerForm
148
+ key={name}
149
+ form={editForm}
150
+ setForm={setEditForm}
151
+ onSave={handleEditSave}
152
+ onCancel={function () { setEditingName(null); }}
153
+ existingNames={existingNamesForEdit}
154
+ idPrefix="mcp-edit"
155
+ />
156
+ );
157
+ }
158
+ var isConfirming = confirmDelete === name;
159
+ return (
160
+ <div
161
+ key={name}
162
+ className="flex items-center gap-3 px-3 py-2 rounded-xl bg-base-300 border border-base-content/15"
163
+ >
164
+ <span className="font-mono text-[12px] text-base-content font-semibold flex-shrink-0">{name}</span>
165
+ {typeBadge(config)}
166
+ <div className="flex-1 min-w-0">{configSummary(config)}</div>
167
+ {isConfirming ? (
168
+ <div className="flex gap-1.5 flex-shrink-0">
169
+ <button
170
+ onClick={function () { handleDelete(name); }}
171
+ className="btn btn-error btn-xs"
172
+ >
173
+ Confirm
174
+ </button>
175
+ <button
176
+ onClick={function () { setConfirmDelete(null); }}
177
+ className="btn btn-ghost btn-xs"
178
+ >
179
+ Cancel
180
+ </button>
181
+ </div>
182
+ ) : (
183
+ <div className="flex gap-1 flex-shrink-0">
184
+ <button
185
+ onClick={function () { handleEditStart(name); }}
186
+ aria-label={"Edit " + name}
187
+ className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-primary focus-visible:ring-2 focus-visible:ring-primary"
188
+ >
189
+ <Pencil size={12} />
190
+ </button>
191
+ <button
192
+ onClick={function () { handleDelete(name); }}
193
+ aria-label={"Delete " + name}
194
+ className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error focus-visible:ring-2 focus-visible:ring-primary"
195
+ >
196
+ <Trash2 size={12} />
197
+ </button>
198
+ </div>
199
+ )}
200
+ </div>
201
+ );
202
+ })}
203
+ </div>
204
+
205
+ {adding && (
206
+ <div className="mb-3">
207
+ <ServerForm
208
+ form={addForm}
209
+ setForm={setAddForm}
210
+ onSave={handleAddSave}
211
+ onCancel={function () { setAdding(false); setAddForm(emptyForm()); }}
212
+ existingNames={existingNamesForAdd}
213
+ idPrefix="mcp-add"
214
+ />
215
+ </div>
216
+ )}
217
+
218
+ {!adding && (
219
+ <button
220
+ onClick={function () { setAdding(true); setEditingName(null); setConfirmDelete(null); }}
221
+ 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"
222
+ >
223
+ <Plus size={12} />
224
+ Add Server
225
+ </button>
226
+ )}
227
+
228
+ <SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
229
+ </div>
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,209 @@
1
+ import { useState, useEffect } from "react";
2
+ import { X, Plus } from "lucide-react";
3
+ import { SaveFooter } from "../ui/SaveFooter";
4
+ import { useSaveState } from "../../hooks/useSaveState";
5
+ import type { ProjectSettings } from "@lattice/shared";
6
+
7
+ function RuleList({
8
+ rules,
9
+ onDelete,
10
+ readOnly,
11
+ labelPrefix,
12
+ }: {
13
+ rules: string[];
14
+ onDelete?: (index: number) => void;
15
+ readOnly?: boolean;
16
+ labelPrefix?: string;
17
+ }) {
18
+ if (rules.length === 0) {
19
+ return (
20
+ <div className="py-3 text-center text-[13px] text-base-content/30">
21
+ No rules defined.
22
+ </div>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <div className="flex flex-col gap-1.5">
28
+ {rules.map(function (rule, idx) {
29
+ return (
30
+ <div
31
+ key={rule + "-" + idx}
32
+ className={
33
+ "flex items-center gap-2 h-9 sm:h-7 px-3 rounded-xl border font-mono text-[12px] " +
34
+ (readOnly
35
+ ? "bg-base-300/50 border-base-content/10 text-base-content/40"
36
+ : "bg-base-300 border-base-content/15 text-base-content")
37
+ }
38
+ >
39
+ <span className="flex-1 truncate">{rule}</span>
40
+ {readOnly && (
41
+ <span className="text-[10px] uppercase tracking-wider text-base-content/30 flex-shrink-0">
42
+ global
43
+ </span>
44
+ )}
45
+ {!readOnly && onDelete && (
46
+ <button
47
+ onClick={function () { onDelete(idx); }}
48
+ aria-label={"Delete " + (labelPrefix || "") + " rule: " + rule}
49
+ title="Delete"
50
+ 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"
51
+ >
52
+ <X size={12} />
53
+ </button>
54
+ )}
55
+ </div>
56
+ );
57
+ })}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function AddRuleRow({ onAdd, label }: { onAdd: (rule: string) => void; label: string }) {
63
+ var [value, setValue] = useState("");
64
+
65
+ function handleAdd() {
66
+ var trimmed = value.trim();
67
+ if (!trimmed) return;
68
+ onAdd(trimmed);
69
+ setValue("");
70
+ }
71
+
72
+ function handleKeyDown(e: React.KeyboardEvent) {
73
+ if (e.key === "Enter") {
74
+ e.preventDefault();
75
+ handleAdd();
76
+ }
77
+ }
78
+
79
+ return (
80
+ <div className="flex items-center gap-1.5 mt-1.5">
81
+ <input
82
+ type="text"
83
+ value={value}
84
+ onChange={function (e) { setValue(e.target.value); }}
85
+ onKeyDown={handleKeyDown}
86
+ placeholder="e.g. Bash(curl:*)"
87
+ aria-label={label}
88
+ className="flex-1 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]"
89
+ />
90
+ <button
91
+ onClick={handleAdd}
92
+ disabled={!value.trim()}
93
+ className={
94
+ "flex items-center gap-1.5 px-3 h-9 sm:h-7 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] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary" +
95
+ (!value.trim() ? " opacity-50 cursor-not-allowed" : "")
96
+ }
97
+ >
98
+ <Plus size={12} />
99
+ Add
100
+ </button>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ export function ProjectPermissions({
106
+ settings,
107
+ updateSection,
108
+ }: {
109
+ settings: ProjectSettings;
110
+ updateSection: (section: string, data: Record<string, unknown>) => void;
111
+ }) {
112
+ var [allow, setAllow] = useState<string[]>(function () {
113
+ return [...(settings.permissions.allow ?? [])];
114
+ });
115
+ var [deny, setDeny] = useState<string[]>(function () {
116
+ return [...(settings.permissions.deny ?? [])];
117
+ });
118
+ var save = useSaveState();
119
+
120
+ useEffect(function () {
121
+ if (save.saving) {
122
+ save.confirmSave();
123
+ } else {
124
+ setAllow([...(settings.permissions.allow ?? [])]);
125
+ setDeny([...(settings.permissions.deny ?? [])]);
126
+ save.resetFromServer();
127
+ }
128
+ }, [settings]);
129
+
130
+ function handleDeleteAllow(idx: number) {
131
+ setAllow(function (prev) {
132
+ var next = [...prev];
133
+ next.splice(idx, 1);
134
+ return next;
135
+ });
136
+ save.markDirty();
137
+ }
138
+
139
+ function handleDeleteDeny(idx: number) {
140
+ setDeny(function (prev) {
141
+ var next = [...prev];
142
+ next.splice(idx, 1);
143
+ return next;
144
+ });
145
+ save.markDirty();
146
+ }
147
+
148
+ function handleAddAllow(rule: string) {
149
+ setAllow(function (prev) { return [...prev, rule]; });
150
+ save.markDirty();
151
+ }
152
+
153
+ function handleAddDeny(rule: string) {
154
+ setDeny(function (prev) { return [...prev, rule]; });
155
+ save.markDirty();
156
+ }
157
+
158
+ function handleSave() {
159
+ save.startSave();
160
+ updateSection("permissions", { allow, deny });
161
+ }
162
+
163
+ var globalAllow = settings.global.permissions?.allow ?? [];
164
+ var globalDeny = settings.global.permissions?.deny ?? [];
165
+
166
+ return (
167
+ <div className="py-2">
168
+ <div className="mb-6">
169
+ <h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
170
+ Allow Rules
171
+ </h2>
172
+ <RuleList rules={allow} onDelete={handleDeleteAllow} labelPrefix="allow" />
173
+ <AddRuleRow onAdd={handleAddAllow} label="New allow rule" />
174
+ </div>
175
+
176
+ <div className="mb-6">
177
+ <h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
178
+ Deny Rules
179
+ </h2>
180
+ <RuleList rules={deny} onDelete={handleDeleteDeny} labelPrefix="deny" />
181
+ <AddRuleRow onAdd={handleAddDeny} label="New deny rule" />
182
+ </div>
183
+
184
+ <div className="mb-8">
185
+ <SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
186
+ </div>
187
+
188
+ {(globalAllow.length > 0 || globalDeny.length > 0) && (
189
+ <div>
190
+ <h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
191
+ Global Permissions
192
+ </h2>
193
+ {globalAllow.length > 0 && (
194
+ <div className="mb-4">
195
+ <div className="text-[11px] text-base-content/40 uppercase tracking-wider mb-1.5">Allow</div>
196
+ <RuleList rules={globalAllow} readOnly />
197
+ </div>
198
+ )}
199
+ {globalDeny.length > 0 && (
200
+ <div className="mb-4">
201
+ <div className="text-[11px] text-base-content/40 uppercase tracking-wider mb-1.5">Deny</div>
202
+ <RuleList rules={globalDeny} readOnly />
203
+ </div>
204
+ )}
205
+ </div>
206
+ )}
207
+ </div>
208
+ );
209
+ }