@rsktash/beads-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.github/workflows/publish.yml +28 -0
  2. package/app/protocol.js +216 -0
  3. package/bin/bdui +19 -0
  4. package/client/index.html +12 -0
  5. package/client/postcss.config.js +11 -0
  6. package/client/src/App.tsx +35 -0
  7. package/client/src/components/IssueCard.tsx +73 -0
  8. package/client/src/components/Layout.tsx +175 -0
  9. package/client/src/components/Markdown.tsx +77 -0
  10. package/client/src/components/PriorityBadge.tsx +26 -0
  11. package/client/src/components/SearchDialog.tsx +137 -0
  12. package/client/src/components/SectionEditor.tsx +212 -0
  13. package/client/src/components/StatusBadge.tsx +64 -0
  14. package/client/src/components/TypeBadge.tsx +26 -0
  15. package/client/src/hooks/use-mutation.ts +55 -0
  16. package/client/src/hooks/use-search.ts +19 -0
  17. package/client/src/hooks/use-subscription.ts +187 -0
  18. package/client/src/index.css +133 -0
  19. package/client/src/lib/avatar.ts +17 -0
  20. package/client/src/lib/types.ts +115 -0
  21. package/client/src/lib/ws-client.ts +214 -0
  22. package/client/src/lib/ws-context.tsx +28 -0
  23. package/client/src/main.tsx +10 -0
  24. package/client/src/views/Board.tsx +200 -0
  25. package/client/src/views/Detail.tsx +398 -0
  26. package/client/src/views/List.tsx +461 -0
  27. package/client/tailwind.config.ts +68 -0
  28. package/client/tsconfig.json +16 -0
  29. package/client/vite.config.ts +20 -0
  30. package/package.json +43 -0
  31. package/server/app.js +120 -0
  32. package/server/app.test.js +30 -0
  33. package/server/bd.js +227 -0
  34. package/server/bd.test.js +194 -0
  35. package/server/cli/cli.test.js +207 -0
  36. package/server/cli/commands.integration.test.js +148 -0
  37. package/server/cli/commands.js +285 -0
  38. package/server/cli/commands.unit.test.js +408 -0
  39. package/server/cli/daemon.js +340 -0
  40. package/server/cli/daemon.test.js +31 -0
  41. package/server/cli/index.js +135 -0
  42. package/server/cli/open.js +178 -0
  43. package/server/cli/open.test.js +26 -0
  44. package/server/cli/usage.js +27 -0
  45. package/server/config.js +36 -0
  46. package/server/db.js +154 -0
  47. package/server/db.test.js +169 -0
  48. package/server/dolt-pool.js +257 -0
  49. package/server/dolt-queries.js +646 -0
  50. package/server/index.js +97 -0
  51. package/server/list-adapters.js +395 -0
  52. package/server/list-adapters.test.js +208 -0
  53. package/server/logging.js +23 -0
  54. package/server/registry-watcher.js +200 -0
  55. package/server/subscriptions.js +299 -0
  56. package/server/subscriptions.test.js +128 -0
  57. package/server/validators.js +124 -0
  58. package/server/watcher.js +139 -0
  59. package/server/watcher.test.js +120 -0
  60. package/server/ws.comments.test.js +262 -0
  61. package/server/ws.delete.test.js +119 -0
  62. package/server/ws.js +1309 -0
  63. package/server/ws.labels.test.js +95 -0
  64. package/server/ws.list-refresh.coalesce.test.js +95 -0
  65. package/server/ws.list-subscriptions.test.js +403 -0
  66. package/server/ws.mutation-window.test.js +147 -0
  67. package/server/ws.mutations.test.js +389 -0
  68. package/server/ws.test.js +52 -0
@@ -0,0 +1,461 @@
1
+ import { useState, useMemo, useEffect, useRef } from "react";
2
+ import { useSubscription } from "../hooks/use-subscription";
3
+ import { StatusBadge } from "../components/StatusBadge";
4
+ import { PriorityBadge } from "../components/PriorityBadge";
5
+ import { TypeBadge } from "../components/TypeBadge";
6
+ import { getInitials, getAvatarColor } from "../lib/avatar";
7
+ import type { Issue } from "../lib/types";
8
+
9
+ const PAGE_SIZE = 20;
10
+
11
+ const SearchIcon = () => (
12
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
13
+ <circle cx="7" cy="7" r="5" />
14
+ <line x1="11" y1="11" x2="14" y2="14" />
15
+ </svg>
16
+ );
17
+
18
+ function SkeletonRow() {
19
+ return (
20
+ <div
21
+ className="flex items-center gap-3 px-4 py-3"
22
+ style={{ borderBottom: "1px solid var(--border-subtle)" }}
23
+ >
24
+ <div className="w-36 h-3 rounded skeleton-shimmer" />
25
+ <div className="w-20 h-5 rounded-full skeleton-shimmer" />
26
+ <div className="w-8 h-5 rounded skeleton-shimmer" />
27
+ <div className="flex-1 h-3 rounded skeleton-shimmer" />
28
+ <div className="w-14 h-5 rounded skeleton-shimmer" />
29
+ <div className="w-24 h-5 rounded skeleton-shimmer" />
30
+ <div className="w-16 h-3 rounded skeleton-shimmer" />
31
+ </div>
32
+ );
33
+ }
34
+
35
+ function Spinner() {
36
+ return (
37
+ <svg
38
+ width="20"
39
+ height="20"
40
+ viewBox="0 0 20 20"
41
+ fill="none"
42
+ style={{ animation: "spin 0.8s linear infinite" }}
43
+ >
44
+ <circle cx="10" cy="10" r="8" stroke="var(--border-default)" strokeWidth="2" />
45
+ <path
46
+ d="M10 2a8 8 0 0 1 8 8"
47
+ stroke="var(--accent)"
48
+ strokeWidth="2"
49
+ strokeLinecap="round"
50
+ />
51
+ </svg>
52
+ );
53
+ }
54
+
55
+ function Toolbar({
56
+ search,
57
+ setSearch,
58
+ statusFilter,
59
+ setStatusFilter,
60
+ typeFilter,
61
+ setTypeFilter,
62
+ }: {
63
+ search: string;
64
+ setSearch: (v: string) => void;
65
+ statusFilter: string;
66
+ setStatusFilter: (v: string) => void;
67
+ typeFilter: string;
68
+ setTypeFilter: (v: string) => void;
69
+ }) {
70
+ return (
71
+ <div
72
+ className="flex items-center gap-3 mb-4 px-4 py-3 rounded-lg"
73
+ style={{
74
+ background: "var(--bg-elevated)",
75
+ border: "1px solid var(--border-subtle)",
76
+ boxShadow: "var(--shadow-sm)",
77
+ }}
78
+ >
79
+ <div className="relative flex-1 max-w-xs">
80
+ <span className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: "var(--text-tertiary)" }}>
81
+ <SearchIcon />
82
+ </span>
83
+ <input
84
+ type="text"
85
+ value={search}
86
+ onChange={(e) => setSearch(e.target.value)}
87
+ placeholder="Search issues..."
88
+ className="w-full pl-9 pr-3 py-1.5 text-sm rounded-md outline-none"
89
+ style={{
90
+ border: "1px solid var(--border-default)",
91
+ background: "var(--bg-base)",
92
+ color: "var(--text-primary)",
93
+ }}
94
+ onFocus={(e) => { e.currentTarget.style.borderColor = "var(--accent)"; }}
95
+ onBlur={(e) => { e.currentTarget.style.borderColor = "var(--border-default)"; }}
96
+ />
97
+ </div>
98
+ <select
99
+ value={statusFilter}
100
+ onChange={(e) => setStatusFilter(e.target.value)}
101
+ className="px-3 py-1.5 text-sm rounded-md"
102
+ style={{
103
+ border: "1px solid var(--border-default)",
104
+ background: "var(--bg-base)",
105
+ color: "var(--text-primary)",
106
+ }}
107
+ >
108
+ <option value="all">All statuses</option>
109
+ <option value="open">Open</option>
110
+ <option value="in_progress">In Progress</option>
111
+ <option value="blocked">Blocked</option>
112
+ <option value="closed">Closed</option>
113
+ </select>
114
+ <select
115
+ value={typeFilter}
116
+ onChange={(e) => setTypeFilter(e.target.value)}
117
+ className="px-3 py-1.5 text-sm rounded-md"
118
+ style={{
119
+ border: "1px solid var(--border-default)",
120
+ background: "var(--bg-base)",
121
+ color: "var(--text-primary)",
122
+ }}
123
+ >
124
+ <option value="all">All types</option>
125
+ <option value="epic">Epic</option>
126
+ <option value="feature">Feature</option>
127
+ <option value="task">Task</option>
128
+ <option value="bug">Bug</option>
129
+ <option value="chore">Chore</option>
130
+ </select>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ function Pagination({
136
+ page,
137
+ totalPages,
138
+ total,
139
+ pageSize,
140
+ onPageChange,
141
+ }: {
142
+ page: number;
143
+ totalPages: number;
144
+ total: number;
145
+ pageSize: number;
146
+ onPageChange: (p: number) => void;
147
+ }) {
148
+ const from = page * pageSize + 1;
149
+ const to = Math.min((page + 1) * pageSize, total);
150
+
151
+ return (
152
+ <div className="flex items-center justify-between px-1 py-2 text-xs" style={{ color: "var(--text-tertiary)" }}>
153
+ <span>
154
+ {total > 0 ? `${from}\u2013${to} of ${total}` : "0 issues"}
155
+ </span>
156
+ <div className="flex items-center gap-1">
157
+ <button
158
+ disabled={page === 0}
159
+ onClick={() => onPageChange(0)}
160
+ className="px-2 py-1 rounded disabled:opacity-30"
161
+ style={{ border: "1px solid var(--border-default)", background: "var(--bg-elevated)" }}
162
+ >
163
+ &laquo;&laquo;
164
+ </button>
165
+ <button
166
+ disabled={page === 0}
167
+ onClick={() => onPageChange(page - 1)}
168
+ className="px-2 py-1 rounded disabled:opacity-30"
169
+ style={{ border: "1px solid var(--border-default)", background: "var(--bg-elevated)" }}
170
+ >
171
+ &laquo;
172
+ </button>
173
+ <span className="px-2">
174
+ {page + 1} / {totalPages || 1}
175
+ </span>
176
+ <button
177
+ disabled={page >= totalPages - 1}
178
+ onClick={() => onPageChange(page + 1)}
179
+ className="px-2 py-1 rounded disabled:opacity-30"
180
+ style={{ border: "1px solid var(--border-default)", background: "var(--bg-elevated)" }}
181
+ >
182
+ &raquo;
183
+ </button>
184
+ <button
185
+ disabled={page >= totalPages - 1}
186
+ onClick={() => onPageChange(totalPages - 1)}
187
+ className="px-2 py-1 rounded disabled:opacity-30"
188
+ style={{ border: "1px solid var(--border-default)", background: "var(--bg-elevated)" }}
189
+ >
190
+ &raquo;&raquo;
191
+ </button>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
196
+
197
+ function IssueRow({
198
+ issue,
199
+ selected,
200
+ onClick,
201
+ }: {
202
+ issue: Issue;
203
+ selected: boolean;
204
+ onClick: () => void;
205
+ }) {
206
+ const ts = new Date(issue.updated_at).toLocaleDateString();
207
+
208
+ return (
209
+ <button
210
+ onClick={onClick}
211
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors"
212
+ style={{
213
+ borderBottom: "1px solid var(--border-subtle)",
214
+ background: selected ? "var(--bg-hover)" : "transparent",
215
+ }}
216
+ onMouseEnter={(e) => {
217
+ if (!selected) e.currentTarget.style.background = "var(--bg-hover)";
218
+ }}
219
+ onMouseLeave={(e) => {
220
+ if (!selected) e.currentTarget.style.background = "transparent";
221
+ }}
222
+ >
223
+ <span className="font-mono text-xs w-36 shrink-0 text-left" style={{ color: "var(--text-tertiary)" }}>
224
+ {issue.id}
225
+ </span>
226
+ <span className="w-28 shrink-0">
227
+ <StatusBadge status={issue.status} />
228
+ </span>
229
+ <span className="w-12 shrink-0">
230
+ <PriorityBadge priority={issue.priority} />
231
+ </span>
232
+ <span className="flex-1 text-left truncate" style={{ color: "var(--text-primary)" }}>
233
+ {issue.title}
234
+ {issue.issue_type === "epic" && issue.total_children != null && (
235
+ <span
236
+ className="ml-2 text-xs px-1.5 py-0.5 rounded"
237
+ style={{ background: "rgba(0,0,0,0.04)", color: "var(--text-tertiary)" }}
238
+ >
239
+ {issue.total_children} sub
240
+ </span>
241
+ )}
242
+ </span>
243
+ <span className="w-16 shrink-0">
244
+ <TypeBadge type={issue.issue_type} />
245
+ </span>
246
+ <span className="w-28 shrink-0 flex items-center gap-1.5 justify-end">
247
+ {issue.assignee ? (
248
+ <>
249
+ <div
250
+ className="flex items-center justify-center rounded-full text-[9px] font-bold shrink-0"
251
+ style={{
252
+ width: "20px",
253
+ height: "20px",
254
+ backgroundColor: getAvatarColor(issue.assignee),
255
+ color: "var(--text-inverse)",
256
+ }}
257
+ >
258
+ {getInitials(issue.assignee)}
259
+ </div>
260
+ <span className="text-xs truncate" style={{ color: "var(--text-secondary)" }}>
261
+ {issue.assignee}
262
+ </span>
263
+ </>
264
+ ) : (
265
+ <span className="text-xs" style={{ color: "var(--text-tertiary)" }}>&mdash;</span>
266
+ )}
267
+ </span>
268
+ <span className="text-xs w-20 text-right shrink-0" style={{ color: "var(--text-tertiary)" }}>
269
+ {ts}
270
+ </span>
271
+ </button>
272
+ );
273
+ }
274
+
275
+ export function List() {
276
+ const [search, setSearch] = useState("");
277
+ const [debouncedSearch, setDebouncedSearch] = useState("");
278
+ const [statusFilter, setStatusFilter] = useState("all");
279
+ const [typeFilter, setTypeFilter] = useState("all");
280
+ const [page, setPage] = useState(0);
281
+ const [selectedIndex, setSelectedIndex] = useState(-1);
282
+ const listRef = useRef<HTMLDivElement>(null);
283
+
284
+ // Debounce search input
285
+ useEffect(() => {
286
+ const timer = setTimeout(() => setDebouncedSearch(search), 300);
287
+ return () => clearTimeout(timer);
288
+ }, [search]);
289
+
290
+ // Build server-side subscription params
291
+ const subParams = useMemo(() => {
292
+ const params: Record<string, string | number | boolean> = {};
293
+ if (debouncedSearch.trim()) params.q = debouncedSearch.trim();
294
+ if (statusFilter !== "all") params.status = statusFilter;
295
+ if (typeFilter !== "all") params.type = typeFilter;
296
+ return params;
297
+ }, [debouncedSearch, statusFilter, typeFilter]);
298
+
299
+ // Server-side search+filter via search-issues subscription
300
+ const { issues: allFiltered, loading, refreshing, total } = useSubscription(
301
+ "search-issues",
302
+ subParams,
303
+ );
304
+
305
+ const totalPages = Math.ceil(total / PAGE_SIZE);
306
+
307
+ // Client-side pagination of server-filtered results
308
+ const paginatedIssues = useMemo(() => {
309
+ const start = page * PAGE_SIZE;
310
+ return allFiltered.slice(start, start + PAGE_SIZE);
311
+ }, [allFiltered, page]);
312
+
313
+ // Reset page when filters change
314
+ useEffect(() => {
315
+ setPage(0);
316
+ setSelectedIndex(-1);
317
+ }, [statusFilter, typeFilter, search]);
318
+
319
+ // Reset selection on page change
320
+ useEffect(() => {
321
+ setSelectedIndex(-1);
322
+ }, [page]);
323
+
324
+ // Keyboard navigation
325
+ useEffect(() => {
326
+ const handler = (e: KeyboardEvent) => {
327
+ if (
328
+ e.target instanceof HTMLInputElement ||
329
+ e.target instanceof HTMLTextAreaElement ||
330
+ e.target instanceof HTMLSelectElement
331
+ )
332
+ return;
333
+ if (e.key === "j")
334
+ setSelectedIndex((i) => Math.min(i + 1, paginatedIssues.length - 1));
335
+ if (e.key === "k") setSelectedIndex((i) => Math.max(i - 1, -1));
336
+ if (e.key === "Enter" && paginatedIssues[selectedIndex]) {
337
+ window.location.hash = `#/detail/${paginatedIssues[selectedIndex].id}`;
338
+ }
339
+ };
340
+ window.addEventListener("keydown", handler);
341
+ return () => window.removeEventListener("keydown", handler);
342
+ }, [paginatedIssues, selectedIndex]);
343
+
344
+ return (
345
+ <div className="p-6 h-full flex flex-col" style={{ background: "var(--bg-base)" }}>
346
+ <div className="mb-4">
347
+ <h1
348
+ className="text-xl font-bold"
349
+ style={{ color: "var(--text-primary)" }}
350
+ >
351
+ Issues
352
+ </h1>
353
+ <p className="text-sm mt-0.5" style={{ color: "var(--text-tertiary)" }}>
354
+ {loading ? "\u00A0" : `${total} issues`}
355
+ </p>
356
+ </div>
357
+
358
+ <Toolbar
359
+ search={search}
360
+ setSearch={setSearch}
361
+ statusFilter={statusFilter}
362
+ setStatusFilter={setStatusFilter}
363
+ typeFilter={typeFilter}
364
+ setTypeFilter={setTypeFilter}
365
+ />
366
+
367
+ {/* Table header */}
368
+ <div
369
+ className="rounded-t-lg overflow-hidden"
370
+ style={{
371
+ background: "var(--bg-elevated)",
372
+ border: "1px solid var(--border-subtle)",
373
+ borderBottom: "none",
374
+ boxShadow: "var(--shadow-card)",
375
+ }}
376
+ >
377
+ <div
378
+ className="flex items-center gap-3 px-4 py-2 text-xs font-semibold uppercase tracking-wider"
379
+ style={{
380
+ color: "var(--text-tertiary)",
381
+ borderBottom: "1px solid var(--border-subtle)",
382
+ fontSize: "11px",
383
+ }}
384
+ >
385
+ <span className="w-36 shrink-0">ID</span>
386
+ <span className="w-28 shrink-0">Status</span>
387
+ <span className="w-12 shrink-0">Priority</span>
388
+ <span className="flex-1">Title</span>
389
+ <span className="w-16 shrink-0">Type</span>
390
+ <span className="w-28 shrink-0 text-right">Assignee</span>
391
+ <span className="w-20 shrink-0 text-right">Date</span>
392
+ </div>
393
+ </div>
394
+
395
+ {/* Table body */}
396
+ <div
397
+ ref={listRef}
398
+ className="flex-1 overflow-y-auto rounded-b-lg relative"
399
+ style={{
400
+ background: "var(--bg-elevated)",
401
+ border: "1px solid var(--border-subtle)",
402
+ borderTop: "none",
403
+ boxShadow: "var(--shadow-card)",
404
+ }}
405
+ >
406
+ {/* Initial load: skeleton shimmer */}
407
+ {loading && (
408
+ <>
409
+ {Array.from({ length: 8 }).map((_, i) => (
410
+ <SkeletonRow key={i} />
411
+ ))}
412
+ </>
413
+ )}
414
+
415
+ {/* Refresh overlay: dim existing list + centered spinner */}
416
+ {refreshing && (
417
+ <div
418
+ className="absolute inset-0 z-10 flex items-center justify-center"
419
+ style={{
420
+ background: "rgba(253,251,247,0.6)",
421
+ backdropFilter: "blur(1px)",
422
+ }}
423
+ >
424
+ <Spinner />
425
+ </div>
426
+ )}
427
+
428
+ {/* Data rows */}
429
+ {!loading && (
430
+ paginatedIssues.length === 0 ? (
431
+ <p className="text-sm p-8 text-center" style={{ color: "var(--text-tertiary)" }}>
432
+ No issues match your filters
433
+ </p>
434
+ ) : (
435
+ paginatedIssues.map((issue, index) => (
436
+ <IssueRow
437
+ key={issue.id}
438
+ issue={issue}
439
+ selected={index === selectedIndex}
440
+ onClick={() => {
441
+ setSelectedIndex(index);
442
+ window.location.hash = `#/detail/${issue.id}`;
443
+ }}
444
+ />
445
+ ))
446
+ )
447
+ )}
448
+ </div>
449
+
450
+ {!loading && totalPages > 1 && (
451
+ <Pagination
452
+ page={page}
453
+ totalPages={totalPages}
454
+ total={total}
455
+ pageSize={PAGE_SIZE}
456
+ onPageChange={setPage}
457
+ />
458
+ )}
459
+ </div>
460
+ );
461
+ }
@@ -0,0 +1,68 @@
1
+ import type { Config } from "tailwindcss";
2
+ import path from "path";
3
+ import typography from "@tailwindcss/typography";
4
+
5
+ const clientDir = __dirname;
6
+
7
+ export default {
8
+ content: [
9
+ path.join(clientDir, "src/**/*.{ts,tsx}"),
10
+ path.join(clientDir, "index.html"),
11
+ ],
12
+ theme: {
13
+ extend: {
14
+ fontFamily: {
15
+ sans: ["'DM Sans'", "system-ui", "-apple-system", "sans-serif"],
16
+ mono: ["'JetBrains Mono'", "ui-monospace", "monospace"],
17
+ },
18
+ colors: {
19
+ status: {
20
+ open: "var(--status-open)",
21
+ "in-progress": "var(--status-in-progress)",
22
+ blocked: "var(--status-blocked)",
23
+ closed: "var(--status-closed)",
24
+ },
25
+ priority: {
26
+ 0: "var(--priority-0)",
27
+ 1: "var(--priority-1)",
28
+ 2: "var(--priority-2)",
29
+ 3: "var(--priority-3)",
30
+ 4: "var(--priority-4)",
31
+ },
32
+ surface: {
33
+ base: "var(--bg-base)",
34
+ card: "var(--bg-surface)",
35
+ elevated: "var(--bg-elevated)",
36
+ hover: "var(--bg-hover)",
37
+ },
38
+ ink: {
39
+ primary: "var(--text-primary)",
40
+ secondary: "var(--text-secondary)",
41
+ tertiary: "var(--text-tertiary)",
42
+ },
43
+ accent: {
44
+ DEFAULT: "var(--accent)",
45
+ soft: "var(--accent-soft)",
46
+ },
47
+ type: {
48
+ task: "var(--type-task)",
49
+ bug: "var(--type-bug)",
50
+ feature: "var(--type-feature)",
51
+ epic: "var(--type-epic)",
52
+ chore: "var(--type-chore)",
53
+ },
54
+ },
55
+ boxShadow: {
56
+ sm: "var(--shadow-sm)",
57
+ card: "var(--shadow-card)",
58
+ md: "var(--shadow-md)",
59
+ },
60
+ borderRadius: {
61
+ sm: "var(--radius-sm)",
62
+ md: "var(--radius-md)",
63
+ lg: "var(--radius-lg)",
64
+ },
65
+ },
66
+ },
67
+ plugins: [typography],
68
+ } satisfies Config;
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "baseUrl": ".",
13
+ "paths": { "@/*": ["src/*"] }
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import path from "path";
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ root: path.resolve(__dirname),
8
+ build: {
9
+ outDir: path.resolve(__dirname, "../dist"),
10
+ emptyOutDir: true,
11
+ },
12
+ server: {
13
+ proxy: {
14
+ "/ws": {
15
+ target: "ws://localhost:3333",
16
+ ws: true,
17
+ },
18
+ },
19
+ },
20
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rsktash/beads-ui",
3
+ "version": "0.1.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/rsktash/beads-ui.git"
7
+ },
8
+ "type": "module",
9
+ "bin": {
10
+ "bdui": "bin/bdui"
11
+ },
12
+ "scripts": {
13
+ "dev": "concurrently \"vite --config client/vite.config.ts\" \"node server/index.js\"",
14
+ "build": "vite build --config client/vite.config.ts",
15
+ "start": "node server/index.js"
16
+ },
17
+ "dependencies": {
18
+ "debug": "^4.3.7",
19
+ "dompurify": "^3.2.0",
20
+ "express": "^4.21.0",
21
+ "marked": "^15.0.0",
22
+ "mysql2": "^3.20.0",
23
+ "react": "^19.0.0",
24
+ "react-dom": "^19.0.0",
25
+ "react-window": "^1.8.10",
26
+ "shiki": "^1.24.0",
27
+ "ws": "^8.18.0"
28
+ },
29
+ "devDependencies": {
30
+ "@tailwindcss/typography": "^0.5.15",
31
+ "@types/dompurify": "^3.0.5",
32
+ "@types/react": "^19.0.0",
33
+ "@types/react-dom": "^19.0.0",
34
+ "@types/react-window": "^1.8.8",
35
+ "@vitejs/plugin-react": "^4.3.0",
36
+ "autoprefixer": "^10.4.20",
37
+ "concurrently": "^9.1.0",
38
+ "postcss": "^8.4.49",
39
+ "tailwindcss": "^3.4.0",
40
+ "typescript": "^5.7.0",
41
+ "vite": "^6.0.0"
42
+ }
43
+ }