@godxjp/ui 0.2.0 → 2.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.
@@ -0,0 +1,768 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { ChevronDown, PanelLeftOpen, PanelLeftClose, Search, SlidersHorizontal, X, Check, Clock } from 'lucide-react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { clsx } from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import * as Dialog from '@radix-ui/react-dialog';
7
+ import { useState, useRef, useEffect, useCallback } from 'react';
8
+ import i18next from 'i18next';
9
+ import 'i18next-browser-languagedetector';
10
+ import * as Popover from '@radix-ui/react-popover';
11
+ import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
12
+ import { Command } from 'cmdk';
13
+
14
+ function AppShell({ sidebar, topbar, children, sidebarCollapsed = false }) {
15
+ return /* @__PURE__ */ jsxs("div", { className: "app-root", "data-collapsed": sidebarCollapsed, children: [
16
+ /* @__PURE__ */ jsx("aside", { className: "app-sidebar", children: sidebar }),
17
+ /* @__PURE__ */ jsx("header", { className: "app-topbar", children: topbar }),
18
+ /* @__PURE__ */ jsx("main", { className: "app-main", children })
19
+ ] });
20
+ }
21
+ function cn(...inputs) {
22
+ return twMerge(clsx(inputs));
23
+ }
24
+ function Sidebar({
25
+ activeId,
26
+ onSelect,
27
+ sections,
28
+ product,
29
+ onProductClick,
30
+ collapsed = false,
31
+ footer
32
+ }) {
33
+ const { t } = useTranslation();
34
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
35
+ /* @__PURE__ */ jsxs(
36
+ "button",
37
+ {
38
+ type: "button",
39
+ className: "sb-product",
40
+ onClick: onProductClick,
41
+ "aria-label": product.name,
42
+ children: [
43
+ /* @__PURE__ */ jsx(
44
+ "span",
45
+ {
46
+ className: "sb-logo-mark",
47
+ style: { background: product.color },
48
+ children: product.name[0]?.toUpperCase() ?? "?"
49
+ }
50
+ ),
51
+ !collapsed && /* @__PURE__ */ jsxs(
52
+ "span",
53
+ {
54
+ className: "sb-product-meta col flex-1 min-w-0",
55
+ style: { display: "flex" },
56
+ children: [
57
+ /* @__PURE__ */ jsx("span", { className: "sb-product-name", children: product.name }),
58
+ /* @__PURE__ */ jsx("span", { className: "sb-product-tenant", children: product.role })
59
+ ]
60
+ }
61
+ ),
62
+ !collapsed && /* @__PURE__ */ jsx("span", { className: "sb-product-tenant shrink-0", children: /* @__PURE__ */ jsx(ChevronDown, { size: 14 }) })
63
+ ]
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: sections.map((section, i) => /* @__PURE__ */ jsxs("div", { className: "sb-section", children: [
67
+ section.label && !collapsed && /* @__PURE__ */ jsx("div", { className: "sb-section-label", children: section.label }),
68
+ /* @__PURE__ */ jsx("div", { className: "sb-nav", role: "navigation", children: section.items.map((item) => {
69
+ const Icon = item.icon;
70
+ const isActive = item.id === activeId;
71
+ return /* @__PURE__ */ jsxs(
72
+ "button",
73
+ {
74
+ type: "button",
75
+ className: cn("sb-nav-item"),
76
+ "data-active": isActive,
77
+ "aria-current": isActive ? "page" : void 0,
78
+ "aria-disabled": item.disabled,
79
+ onClick: () => !item.disabled && onSelect(item.id),
80
+ children: [
81
+ /* @__PURE__ */ jsx("span", { className: "sb-icon", children: /* @__PURE__ */ jsx(Icon, { size: 16 }) }),
82
+ /* @__PURE__ */ jsx("span", { className: "sb-label", children: item.label }),
83
+ item.badge !== void 0 && item.badge !== "" && /* @__PURE__ */ jsx("span", { className: "sb-badge", children: item.badge })
84
+ ]
85
+ },
86
+ item.id
87
+ );
88
+ }) })
89
+ ] }, section.label ?? i)) }),
90
+ footer && /* @__PURE__ */ jsx("div", { className: "sb-footer", children: footer })
91
+ ] });
92
+ }
93
+ function Topbar({
94
+ product,
95
+ project,
96
+ onProductOpen,
97
+ onProjectOpen,
98
+ onSearchOpen,
99
+ onTweaksOpen,
100
+ collapsed = false,
101
+ onToggleCollapsed,
102
+ rightSlot
103
+ }) {
104
+ const { t } = useTranslation();
105
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
106
+ onToggleCollapsed && /* @__PURE__ */ jsx(
107
+ "button",
108
+ {
109
+ type: "button",
110
+ className: "tb-icon-btn",
111
+ "aria-label": t("shell.sidebarCollapse"),
112
+ "aria-pressed": collapsed,
113
+ onClick: onToggleCollapsed,
114
+ children: collapsed ? /* @__PURE__ */ jsx(PanelLeftOpen, { size: 16 }) : /* @__PURE__ */ jsx(PanelLeftClose, { size: 16 })
115
+ }
116
+ ),
117
+ /* @__PURE__ */ jsxs("div", { className: "tb-switcher", children: [
118
+ /* @__PURE__ */ jsxs(
119
+ "button",
120
+ {
121
+ type: "button",
122
+ className: "tb-chip",
123
+ "aria-label": product.name,
124
+ onClick: onProductOpen,
125
+ children: [
126
+ /* @__PURE__ */ jsx(
127
+ "span",
128
+ {
129
+ className: "tb-chip-icon",
130
+ style: { background: product.color },
131
+ children: product.name[0]?.toUpperCase() ?? "?"
132
+ }
133
+ ),
134
+ /* @__PURE__ */ jsx("span", { className: "tb-chip-label", children: product.name }),
135
+ /* @__PURE__ */ jsx("span", { className: "tb-chip-caret", children: /* @__PURE__ */ jsx(ChevronDown, { size: 12 }) })
136
+ ]
137
+ }
138
+ ),
139
+ /* @__PURE__ */ jsx("span", { className: "tb-chip-sep", children: "/" }),
140
+ /* @__PURE__ */ jsxs(
141
+ "button",
142
+ {
143
+ type: "button",
144
+ className: cn("tb-chip", !project && "empty"),
145
+ "aria-label": project ? project.name : t("shell.pickProject"),
146
+ onClick: onProjectOpen,
147
+ children: [
148
+ /* @__PURE__ */ jsx("span", { className: "tb-chip-label", children: project ? project.name : t("shell.pickProject") }),
149
+ /* @__PURE__ */ jsx("span", { className: "tb-chip-caret", children: /* @__PURE__ */ jsx(ChevronDown, { size: 12 }) })
150
+ ]
151
+ }
152
+ )
153
+ ] }),
154
+ /* @__PURE__ */ jsxs(
155
+ "button",
156
+ {
157
+ type: "button",
158
+ className: "tb-search ml-auto",
159
+ onClick: onSearchOpen,
160
+ children: [
161
+ /* @__PURE__ */ jsx(Search, { size: 14 }),
162
+ /* @__PURE__ */ jsxs("span", { children: [
163
+ t("common.search"),
164
+ "\u2026"
165
+ ] }),
166
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "\u2318K" })
167
+ ]
168
+ }
169
+ ),
170
+ rightSlot,
171
+ onTweaksOpen && /* @__PURE__ */ jsx(
172
+ "button",
173
+ {
174
+ type: "button",
175
+ className: "tb-icon-btn",
176
+ "aria-label": t("tweaks.title"),
177
+ onClick: onTweaksOpen,
178
+ children: /* @__PURE__ */ jsx(SlidersHorizontal, { size: 16 })
179
+ }
180
+ )
181
+ ] });
182
+ }
183
+
184
+ // src/data/products.ts
185
+ var PRODUCTS = [
186
+ {
187
+ id: "restaurant",
188
+ name: "godx-restaurant",
189
+ tenant: "restaurant",
190
+ role: "\u30EC\u30B9\u30C8\u30E9\u30F3\u7BA1\u7406",
191
+ desc: "\u5E97\u8217\u5411\u3051\u7D71\u5408\u7BA1\u7406\u30D7\u30E9\u30C3\u30C8\u30D5\u30A9\u30FC\u30E0",
192
+ color: "oklch(58% 0.18 25)",
193
+ owner: "Satoshi F",
194
+ devs: 6,
195
+ projects: [
196
+ { id: "api", name: "restaurant-api", stack: "NestJS \xB7 PostgreSQL", kind: "service", devs: 3, status: "active", branch: "main", lastCommit: "12\u5206\u524D", openIssues: 8, prs: 2, sandbox: true },
197
+ { id: "admin", name: "restaurant-admin", stack: "Next.js 14 \xB7 React", kind: "web", devs: 2, status: "active", branch: "main", lastCommit: "1\u6642\u9593\u524D", openIssues: 5, prs: 1, sandbox: true },
198
+ { id: "pos", name: "restaurant-pos", stack: "Tauri \xB7 Vue 3", kind: "desktop", devs: 1, status: "active", branch: "feature/print", lastCommit: "30\u5206\u524D", openIssues: 3, prs: 1, sandbox: true },
199
+ { id: "kds", name: "restaurant-kds", stack: "React \xB7 Electron", kind: "workstation", devs: 1, status: "review", branch: "main", lastCommit: "\u6628\u65E5", openIssues: 2, prs: 1, sandbox: true },
200
+ { id: "kintai", name: "restaurant-kintai", stack: "Vue 3 \xB7 Laravel", kind: "service", devs: 2, status: "active", branch: "main", lastCommit: "5\u5206\u524D", openIssues: 4, prs: 0, sandbox: true },
201
+ { id: "mobile", name: "restaurant-mobile", stack: "React Native", kind: "mobile", devs: 1, status: "planning", branch: "develop", lastCommit: "3\u65E5\u524D", openIssues: 1, prs: 0, sandbox: false }
202
+ ]
203
+ },
204
+ {
205
+ id: "godx",
206
+ name: "godx-admin",
207
+ tenant: "godx",
208
+ role: "Platform admin",
209
+ desc: "GoDX Forge developer workspace",
210
+ color: "oklch(60% 0.137 163)",
211
+ owner: "Satoshi F",
212
+ devs: 4,
213
+ projects: [
214
+ { id: "frontend", name: "godx-admin-frontend", stack: "React \xB7 Vite", kind: "web", devs: 2, status: "active", branch: "master", lastCommit: "2\u6642\u9593\u524D", openIssues: 6, prs: 2, sandbox: true },
215
+ { id: "api", name: "godx-admin-api", stack: "Go \xB7 Gin", kind: "service", devs: 1, status: "active", branch: "master", lastCommit: "4\u6642\u9593\u524D", openIssues: 3, prs: 1, sandbox: true },
216
+ { id: "ui", name: "@godxjp/ui", stack: "TypeScript \xB7 React", kind: "library", devs: 2, status: "active", branch: "master", lastCommit: "1\u6642\u9593\u524D", openIssues: 2, prs: 0, sandbox: false }
217
+ ]
218
+ },
219
+ {
220
+ id: "kintai",
221
+ name: "dxs-kintai",
222
+ tenant: "kintai",
223
+ role: "HR / Attendance",
224
+ desc: "\u52E4\u6020\u7BA1\u7406\u30D7\u30E9\u30C3\u30C8\u30D5\u30A9\u30FC\u30E0",
225
+ color: "oklch(56% 0.15 240)",
226
+ owner: "Naoki N",
227
+ devs: 3,
228
+ projects: [
229
+ { id: "frontend", name: "kintai-web", stack: "Vue 3 \xB7 Vite", kind: "web", devs: 2, status: "active", branch: "main", lastCommit: "20\u5206\u524D", openIssues: 7, prs: 1, sandbox: true },
230
+ { id: "backend", name: "kintai-api", stack: "Laravel 11", kind: "service", devs: 1, status: "active", branch: "main", lastCommit: "1\u65E5\u524D", openIssues: 4, prs: 0, sandbox: true }
231
+ ]
232
+ },
233
+ {
234
+ id: "tempo",
235
+ name: "dxs-tempo",
236
+ tenant: "tempo",
237
+ role: "Shop / Inventory",
238
+ desc: "\u5E97\u8217\u30FB\u5728\u5EAB\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9",
239
+ color: "oklch(48% 0.16 285)",
240
+ owner: "Naoki N",
241
+ devs: 2,
242
+ projects: [
243
+ { id: "api", name: "tempo-api", stack: "Go \xB7 Echo", kind: "service", devs: 2, status: "active", branch: "main", lastCommit: "5\u6642\u9593\u524D", openIssues: 9, prs: 1, sandbox: true },
244
+ { id: "ops", name: "tempo-ops", stack: "Terraform", kind: "infra", devs: 1, status: "planning", branch: "main", lastCommit: "1\u9031\u9593\u524D", openIssues: 1, prs: 0, sandbox: false }
245
+ ]
246
+ },
247
+ {
248
+ id: "betoya",
249
+ name: "betoya",
250
+ tenant: "betoya",
251
+ role: "Vietnamese restaurant",
252
+ desc: "\u30D9\u30C8\u5C4B Tenant",
253
+ color: "oklch(58% 0.159 150)",
254
+ owner: "Anh K",
255
+ devs: 1,
256
+ projects: [
257
+ { id: "site", name: "betoya-site", stack: "Astro", kind: "web", devs: 1, status: "active", branch: "main", lastCommit: "\u6628\u65E5", openIssues: 2, prs: 0, sandbox: false }
258
+ ]
259
+ }
260
+ ];
261
+ var SUPPORTED_LOCALES = ["ja", "en", "vi"];
262
+ var FORGE_LOCALE_STORAGE_KEY = "forge.locale";
263
+ var i18n_default = i18next;
264
+
265
+ // src/hooks/useTweaks.ts
266
+ var STORAGE_KEY = "forge.tweaks";
267
+ var DEFAULTS = {
268
+ density: "default",
269
+ theme: "light",
270
+ tenant: "godx",
271
+ locale: "ja",
272
+ sidebarCollapsed: false
273
+ };
274
+ function loadInitial() {
275
+ if (typeof window === "undefined") return DEFAULTS;
276
+ try {
277
+ const raw = window.localStorage.getItem(STORAGE_KEY);
278
+ const stored = raw ? JSON.parse(raw) : {};
279
+ const detected = i18n_default.language?.slice(0, 2) || "ja";
280
+ return {
281
+ ...DEFAULTS,
282
+ ...stored,
283
+ locale: stored.locale ?? detected
284
+ };
285
+ } catch {
286
+ return DEFAULTS;
287
+ }
288
+ }
289
+ function useTweaks() {
290
+ const [tweaks, setTweaks] = useState(loadInitial);
291
+ useEffect(() => {
292
+ try {
293
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(tweaks));
294
+ } catch {
295
+ }
296
+ }, [tweaks]);
297
+ useEffect(() => {
298
+ const html = document.documentElement;
299
+ html.dataset.theme = tweaks.theme;
300
+ html.dataset.density = tweaks.density;
301
+ html.dataset.tenant = tweaks.tenant;
302
+ html.lang = tweaks.locale;
303
+ }, [tweaks.theme, tweaks.density, tweaks.tenant, tweaks.locale]);
304
+ useEffect(() => {
305
+ if (i18n_default.language?.slice(0, 2) !== tweaks.locale) {
306
+ void i18n_default.changeLanguage(tweaks.locale);
307
+ try {
308
+ window.localStorage.setItem(FORGE_LOCALE_STORAGE_KEY, tweaks.locale);
309
+ } catch {
310
+ }
311
+ }
312
+ }, [tweaks.locale]);
313
+ const setTweak = useCallback((key, value) => {
314
+ setTweaks((prev) => ({ ...prev, [key]: value }));
315
+ }, []);
316
+ return { tweaks, setTweak, setTweaks };
317
+ }
318
+ PRODUCTS.map((p) => ({ value: p.tenant, label: p.name }));
319
+ function TweaksPanel({ open, onOpenChange }) {
320
+ const { t } = useTranslation();
321
+ const { tweaks, setTweak } = useTweaks();
322
+ return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
323
+ /* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-black/30 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" }),
324
+ /* @__PURE__ */ jsxs(Dialog.Content, { className: "fixed right-0 top-0 z-50 h-full w-80 bg-popover text-popover-foreground border-l border-border shadow-2xl data-[state=open]:animate-in data-[state=open]:slide-in-from-right", children: [
325
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-border px-4 h-12", children: [
326
+ /* @__PURE__ */ jsx(Dialog.Title, { className: "font-medium text-sm", children: t("tweaks.title") }),
327
+ /* @__PURE__ */ jsx(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx("button", { className: "tb-icon-btn", "aria-label": "Close", children: /* @__PURE__ */ jsx(X, { size: 14 }) }) })
328
+ ] }),
329
+ /* @__PURE__ */ jsxs("div", { className: "overflow-y-auto h-[calc(100%-3rem)] p-4 flex flex-col gap-6", children: [
330
+ /* @__PURE__ */ jsxs(Section, { label: t("tweaks.display"), children: [
331
+ /* @__PURE__ */ jsx(
332
+ Radio,
333
+ {
334
+ label: t("tweaks.density"),
335
+ value: tweaks.density,
336
+ onChange: (v) => setTweak("density", v),
337
+ options: [
338
+ { value: "compact", label: t("tweaks.densityCompact") },
339
+ { value: "default", label: t("tweaks.densityDefault") },
340
+ { value: "comfortable", label: t("tweaks.densityComfortable") }
341
+ ]
342
+ }
343
+ ),
344
+ /* @__PURE__ */ jsx(
345
+ Radio,
346
+ {
347
+ label: t("tweaks.theme"),
348
+ value: tweaks.theme,
349
+ onChange: (v) => setTweak("theme", v),
350
+ options: [
351
+ { value: "light", label: t("tweaks.themeLight") },
352
+ { value: "dark", label: t("tweaks.themeDark") }
353
+ ]
354
+ }
355
+ ),
356
+ /* @__PURE__ */ jsx(
357
+ Toggle,
358
+ {
359
+ label: t("shell.sidebarCollapse"),
360
+ value: tweaks.sidebarCollapsed,
361
+ onChange: (v) => setTweak("sidebarCollapsed", v)
362
+ }
363
+ )
364
+ ] }),
365
+ /* @__PURE__ */ jsx(Section, { label: t("tweaks.product"), children: /* @__PURE__ */ jsx(
366
+ Select,
367
+ {
368
+ label: t("tweaks.product"),
369
+ value: tweaks.tenant,
370
+ onChange: (v) => setTweak("tenant", v),
371
+ options: PRODUCTS.map((p) => ({ value: p.tenant, label: p.name }))
372
+ }
373
+ ) }),
374
+ /* @__PURE__ */ jsx(Section, { label: t("tweaks.locale"), children: /* @__PURE__ */ jsx(
375
+ Radio,
376
+ {
377
+ label: t("tweaks.language"),
378
+ value: tweaks.locale,
379
+ onChange: (v) => setTweak("locale", v),
380
+ options: SUPPORTED_LOCALES.map((code) => ({
381
+ value: code,
382
+ label: { ja: "\u65E5\u672C\u8A9E", en: "English", vi: "Ti\u1EBFng Vi\u1EC7t" }[code]
383
+ }))
384
+ }
385
+ ) })
386
+ ] })
387
+ ] })
388
+ ] }) });
389
+ }
390
+ function Section({ label, children }) {
391
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
392
+ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground", children: label }),
393
+ children
394
+ ] });
395
+ }
396
+ function Radio({
397
+ label,
398
+ value,
399
+ onChange,
400
+ options
401
+ }) {
402
+ return /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1", children: [
403
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-foreground", children: label }),
404
+ /* @__PURE__ */ jsx("div", { className: "grid auto-cols-fr grid-flow-col rounded-md border border-border overflow-hidden", children: options.map((opt) => /* @__PURE__ */ jsx(
405
+ "button",
406
+ {
407
+ type: "button",
408
+ "data-active": opt.value === value,
409
+ onClick: () => onChange(opt.value),
410
+ className: "px-2 py-1 text-xs hover:bg-accent/40 data-[active=true]:bg-primary data-[active=true]:text-primary-foreground",
411
+ children: opt.label
412
+ },
413
+ opt.value
414
+ )) })
415
+ ] });
416
+ }
417
+ function Toggle({
418
+ label,
419
+ value,
420
+ onChange
421
+ }) {
422
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2 text-xs", children: [
423
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
424
+ /* @__PURE__ */ jsx(
425
+ "button",
426
+ {
427
+ type: "button",
428
+ role: "switch",
429
+ "aria-checked": value,
430
+ onClick: () => onChange(!value),
431
+ className: "relative inline-flex h-5 w-9 items-center rounded-full bg-input data-[on=true]:bg-primary transition-colors",
432
+ "data-on": value,
433
+ children: /* @__PURE__ */ jsx(
434
+ "span",
435
+ {
436
+ className: "inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform translate-x-0.5 data-[on=true]:translate-x-4",
437
+ "data-on": value
438
+ }
439
+ )
440
+ }
441
+ )
442
+ ] });
443
+ }
444
+ function Select({
445
+ label,
446
+ value,
447
+ onChange,
448
+ options
449
+ }) {
450
+ return /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1", children: [
451
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-foreground", children: label }),
452
+ /* @__PURE__ */ jsx(
453
+ "select",
454
+ {
455
+ className: "input",
456
+ value,
457
+ onChange: (e) => onChange(e.target.value),
458
+ children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
459
+ }
460
+ )
461
+ ] });
462
+ }
463
+ function ProductSwitcher({
464
+ trigger,
465
+ activeId,
466
+ products = PRODUCTS,
467
+ onSelect,
468
+ open,
469
+ onOpenChange
470
+ }) {
471
+ const { t } = useTranslation();
472
+ const [query, setQuery] = useState("");
473
+ const q = query.trim().toLowerCase();
474
+ const filtered = q ? products.filter(
475
+ (p) => p.name.toLowerCase().includes(q) || p.role.toLowerCase().includes(q)
476
+ ) : products;
477
+ return /* @__PURE__ */ jsxs(Popover.Root, { open, onOpenChange, children: [
478
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: trigger }),
479
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(
480
+ Popover.Content,
481
+ {
482
+ align: "start",
483
+ sideOffset: 4,
484
+ className: "sw-pop",
485
+ onOpenAutoFocus: (e) => e.preventDefault(),
486
+ children: [
487
+ /* @__PURE__ */ jsxs("div", { className: "sw-pop-search", children: [
488
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "text-muted-foreground" }),
489
+ /* @__PURE__ */ jsx(
490
+ "input",
491
+ {
492
+ autoFocus: true,
493
+ value: query,
494
+ onChange: (e) => setQuery(e.target.value),
495
+ placeholder: t("shell.searchProducts"),
496
+ className: "flex-1 bg-transparent outline-none"
497
+ }
498
+ ),
499
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "esc" })
500
+ ] }),
501
+ /* @__PURE__ */ jsxs("div", { className: "sw-pop-list", children: [
502
+ /* @__PURE__ */ jsx("div", { className: "sw-pop-section", children: /* @__PURE__ */ jsxs("span", { children: [
503
+ t("nav.products"),
504
+ " \xB7 ",
505
+ filtered.length
506
+ ] }) }),
507
+ filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "sw-pop-empty", children: "\u2014" }) : filtered.map((p) => {
508
+ const isActive = p.id === activeId;
509
+ return /* @__PURE__ */ jsxs(
510
+ "button",
511
+ {
512
+ type: "button",
513
+ className: cn("sw-pop-item w-full text-left"),
514
+ "data-active": isActive,
515
+ onClick: () => {
516
+ onSelect(p);
517
+ setQuery("");
518
+ },
519
+ children: [
520
+ /* @__PURE__ */ jsx(
521
+ "span",
522
+ {
523
+ className: "sb-logo-mark",
524
+ style: { background: p.color },
525
+ children: p.name[0]?.toUpperCase() ?? "?"
526
+ }
527
+ ),
528
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-w-0 flex-1", children: [
529
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: p.name }),
530
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground truncate", children: p.role })
531
+ ] }),
532
+ /* @__PURE__ */ jsxs("span", { className: "sw-pop-item-meta", children: [
533
+ p.projects.length,
534
+ " \xB7 ",
535
+ p.devs
536
+ ] }),
537
+ isActive && /* @__PURE__ */ jsx(Check, { size: 12, className: "text-primary" })
538
+ ]
539
+ },
540
+ p.id
541
+ );
542
+ })
543
+ ] })
544
+ ]
545
+ }
546
+ ) })
547
+ ] });
548
+ }
549
+ function ProjectSwitcher({
550
+ trigger,
551
+ activeProductId,
552
+ activeProjectId,
553
+ recent = [],
554
+ products = PRODUCTS,
555
+ onSelect,
556
+ open,
557
+ onOpenChange
558
+ }) {
559
+ const { t } = useTranslation();
560
+ const [query, setQuery] = useState("");
561
+ const productById = new Map(products.map((p) => [p.id, p]));
562
+ const recentResolved = recent.map((r) => {
563
+ const product = productById.get(r.productId);
564
+ const project = product?.projects.find((p) => p.id === r.projectId);
565
+ return product && project ? { product, project } : null;
566
+ }).filter((x) => !!x).slice(0, 3);
567
+ const q = query.trim().toLowerCase();
568
+ const filteredProducts = products.map((product) => ({
569
+ product,
570
+ projects: q ? product.projects.filter(
571
+ (p) => p.name.toLowerCase().includes(q) || p.stack.toLowerCase().includes(q)
572
+ ) : product.projects
573
+ })).filter((group) => group.projects.length > 0);
574
+ return /* @__PURE__ */ jsxs(Popover.Root, { open, onOpenChange, children: [
575
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: trigger }),
576
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(
577
+ Popover.Content,
578
+ {
579
+ align: "start",
580
+ sideOffset: 4,
581
+ className: "sw-pop",
582
+ style: { width: 420 },
583
+ onOpenAutoFocus: (e) => e.preventDefault(),
584
+ children: [
585
+ /* @__PURE__ */ jsxs("div", { className: "sw-pop-search", children: [
586
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "text-muted-foreground" }),
587
+ /* @__PURE__ */ jsx(
588
+ "input",
589
+ {
590
+ autoFocus: true,
591
+ value: query,
592
+ onChange: (e) => setQuery(e.target.value),
593
+ placeholder: t("shell.searchProjects"),
594
+ className: "flex-1 bg-transparent outline-none"
595
+ }
596
+ ),
597
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "esc" })
598
+ ] }),
599
+ /* @__PURE__ */ jsxs("div", { className: "sw-pop-list", children: [
600
+ !q && recentResolved.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
601
+ /* @__PURE__ */ jsx("div", { className: "sw-pop-section", children: /* @__PURE__ */ jsx("span", { children: t("shell.recent") }) }),
602
+ recentResolved.map(({ product, project }) => /* @__PURE__ */ jsxs(
603
+ "button",
604
+ {
605
+ type: "button",
606
+ className: "sw-pop-item w-full text-left",
607
+ onClick: () => onSelect(project, product),
608
+ children: [
609
+ /* @__PURE__ */ jsx(Clock, { size: 12, className: "text-muted-foreground" }),
610
+ /* @__PURE__ */ jsx("span", { className: "flex-1 min-w-0 truncate font-mono text-xs", children: project.name }),
611
+ /* @__PURE__ */ jsx("span", { className: "sw-pop-item-meta truncate", children: product.name })
612
+ ]
613
+ },
614
+ `${product.id}:${project.id}`
615
+ ))
616
+ ] }),
617
+ filteredProducts.length === 0 && /* @__PURE__ */ jsx("div", { className: "sw-pop-empty", children: "\u2014" }),
618
+ filteredProducts.map(({ product, projects }) => /* @__PURE__ */ jsxs("div", { children: [
619
+ /* @__PURE__ */ jsxs("div", { className: "sw-pop-section", children: [
620
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
621
+ /* @__PURE__ */ jsx(
622
+ "span",
623
+ {
624
+ className: "sb-logo-mark",
625
+ style: { background: product.color, width: 12, height: 12, fontSize: 8 },
626
+ children: product.name[0]?.toUpperCase() ?? "?"
627
+ }
628
+ ),
629
+ product.name
630
+ ] }),
631
+ /* @__PURE__ */ jsx("span", { children: projects.length })
632
+ ] }),
633
+ projects.map((project) => {
634
+ const isActive = product.id === activeProductId && project.id === activeProjectId;
635
+ return /* @__PURE__ */ jsxs(
636
+ "button",
637
+ {
638
+ type: "button",
639
+ className: cn("sw-pop-item w-full text-left"),
640
+ "data-active": isActive,
641
+ onClick: () => onSelect(project, product),
642
+ children: [
643
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs truncate flex-1 min-w-0", children: project.name }),
644
+ /* @__PURE__ */ jsx("span", { className: "sw-kind-chip", children: project.kind }),
645
+ /* @__PURE__ */ jsxs("span", { className: "sw-pop-item-meta", children: [
646
+ project.stack,
647
+ " \xB7 ",
648
+ project.devs,
649
+ "d \xB7 ",
650
+ project.openIssues,
651
+ "\u2197 \xB7",
652
+ " ",
653
+ project.prs,
654
+ "pr"
655
+ ] }),
656
+ isActive && /* @__PURE__ */ jsx(Check, { size: 12, className: "text-primary" })
657
+ ]
658
+ },
659
+ project.id
660
+ );
661
+ })
662
+ ] }, product.id))
663
+ ] }),
664
+ /* @__PURE__ */ jsxs("div", { className: "sw-pop-foot", children: [
665
+ /* @__PURE__ */ jsx("span", { children: t("shell.browseAllProducts") }),
666
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto", children: [
667
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "\u2191\u2193" }),
668
+ " ",
669
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "\u23CE" })
670
+ ] })
671
+ ] })
672
+ ]
673
+ }
674
+ ) })
675
+ ] });
676
+ }
677
+ function CommandPalette({ open, onOpenChange, commands }) {
678
+ const { t } = useTranslation();
679
+ const inputRef = useRef(null);
680
+ useEffect(() => {
681
+ const handler = (e) => {
682
+ const isModifier = e.metaKey || e.ctrlKey;
683
+ if (isModifier && e.key.toLowerCase() === "k") {
684
+ e.preventDefault();
685
+ onOpenChange(!open);
686
+ } else if (e.key === "Escape" && open) {
687
+ onOpenChange(false);
688
+ }
689
+ };
690
+ window.addEventListener("keydown", handler);
691
+ return () => window.removeEventListener("keydown", handler);
692
+ }, [open, onOpenChange]);
693
+ const groups = /* @__PURE__ */ new Map();
694
+ for (const cmd of commands) {
695
+ const key = cmd.group ?? "";
696
+ const arr = groups.get(key) ?? [];
697
+ arr.push(cmd);
698
+ groups.set(key, arr);
699
+ }
700
+ return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
701
+ /* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-50 bg-black/30 backdrop-blur-sm" }),
702
+ /* @__PURE__ */ jsxs(
703
+ Dialog.Content,
704
+ {
705
+ className: "fixed left-1/2 top-[20%] z-50 w-[min(560px,calc(100vw-2rem))] -translate-x-1/2 rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none",
706
+ "aria-describedby": void 0,
707
+ children: [
708
+ /* @__PURE__ */ jsx(VisuallyHidden.Root, { children: /* @__PURE__ */ jsx(Dialog.Title, { children: t("common.search") }) }),
709
+ /* @__PURE__ */ jsxs(Command, { label: "Command palette", className: "flex flex-col", children: [
710
+ /* @__PURE__ */ jsx(
711
+ Command.Input,
712
+ {
713
+ ref: inputRef,
714
+ autoFocus: true,
715
+ placeholder: `${t("common.search")}\u2026`,
716
+ className: "w-full border-b border-border bg-transparent px-4 py-3 text-sm outline-none placeholder:text-muted-foreground"
717
+ }
718
+ ),
719
+ /* @__PURE__ */ jsxs(Command.List, { className: "max-h-80 overflow-y-auto p-1", children: [
720
+ /* @__PURE__ */ jsx(Command.Empty, { className: "px-3 py-6 text-center text-xs text-muted-foreground", children: "\u2014" }),
721
+ [...groups.entries()].map(([groupLabel, items]) => /* @__PURE__ */ jsx(
722
+ Command.Group,
723
+ {
724
+ heading: groupLabel ? /* @__PURE__ */ jsx("span", { className: "px-2 py-1 text-[10px] uppercase tracking-wider text-muted-foreground", children: groupLabel }) : void 0,
725
+ children: items.map((cmd) => /* @__PURE__ */ jsxs(
726
+ Command.Item,
727
+ {
728
+ value: cmd.label,
729
+ onSelect: () => {
730
+ cmd.onSelect();
731
+ onOpenChange(false);
732
+ },
733
+ className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm cursor-pointer data-[selected=true]:bg-accent",
734
+ children: [
735
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: cmd.label }),
736
+ cmd.hint && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: cmd.hint })
737
+ ]
738
+ },
739
+ cmd.id
740
+ ))
741
+ },
742
+ groupLabel || "default"
743
+ ))
744
+ ] }),
745
+ /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-3 py-2 text-[10px] text-muted-foreground flex items-center gap-3", children: [
746
+ /* @__PURE__ */ jsxs("span", { children: [
747
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "\u2191\u2193" }),
748
+ " navigate"
749
+ ] }),
750
+ /* @__PURE__ */ jsxs("span", { children: [
751
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "\u23CE" }),
752
+ " select"
753
+ ] }),
754
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto", children: [
755
+ /* @__PURE__ */ jsx("kbd", { className: "kbd", children: "esc" }),
756
+ " close"
757
+ ] })
758
+ ] })
759
+ ] })
760
+ ]
761
+ }
762
+ )
763
+ ] }) });
764
+ }
765
+
766
+ export { AppShell, CommandPalette, ProductSwitcher, ProjectSwitcher, Sidebar, Topbar, TweaksPanel };
767
+ //# sourceMappingURL=shell.js.map
768
+ //# sourceMappingURL=shell.js.map