@nextclaw/ui 0.24.0-beta.0 → 0.24.0-beta.1

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 (80) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/assets/api-BqVIq0lF.js +13 -0
  3. package/dist/assets/{appearance-settings-page-CPoFZLw8.js → appearance-settings-page-CTWn1eAT.js} +1 -1
  4. package/dist/assets/{book-open-jcUt0rr9.js → book-open-o2x7OLiO.js} +1 -1
  5. package/dist/assets/{channels-list-page-anMJAkfK.js → channels-list-page-Bzn-pzyJ.js} +1 -1
  6. package/dist/assets/{config-split-page-AcuFnbIC.js → config-split-page-CnRbXXlh.js} +1 -1
  7. package/dist/assets/{confirm-dialog-B-atCfC5.js → confirm-dialog-B_Hbcra2.js} +1 -1
  8. package/dist/assets/{createLucideIcon-Blz0FzrV.js → createLucideIcon-rwwpqyks.js} +1 -1
  9. package/dist/assets/{desktop-capabilities-page-8lKF_Ob-.js → desktop-capabilities-page-C2ayYQTr.js} +1 -1
  10. package/dist/assets/{desktop-update-config-DABXDpbj.js → desktop-update-config-C13czpyL.js} +1 -1
  11. package/dist/assets/{dist-eH-_j8yU.js → dist-BsF3hfOi.js} +1 -1
  12. package/dist/assets/{dist-D8kcEpTf.js → dist-aNN3iQxV.js} +1 -1
  13. package/dist/assets/{doc-browser-DzXRehlW.js → doc-browser-D8dIOo9E.js} +1 -1
  14. package/dist/assets/doc-browser-DcuOOtpP.js +1 -0
  15. package/dist/assets/{doc-browser-context-BlbUcg2J.js → doc-browser-context-Dhsit2Ma.js} +1 -1
  16. package/dist/assets/{extensions-mquwAJ2Z.js → extensions-CD6xvKF_.js} +1 -1
  17. package/dist/assets/{form-actions-CaZDGEmB.js → form-actions-CC4Wz_lL.js} +1 -1
  18. package/dist/assets/index-BTsbdtv6.js +109 -0
  19. package/dist/assets/index-Bcfy2kTh.css +1 -0
  20. package/dist/assets/loader-circle-B6aFbB-8.js +1 -0
  21. package/dist/assets/mcp-marketplace-page-BtUAypA7.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-gFtgyueL.js → mcp-marketplace-page-r2MmD9xO.js} +1 -1
  23. package/dist/assets/{model-config-page-hFBpeRAO.js → model-config-page-DboxoT-I.js} +1 -1
  24. package/dist/assets/{navigation-link-Hb5Z0UxZ.js → navigation-link-C_6oj3zz.js} +1 -1
  25. package/dist/assets/plus-DRsq9Kvs.js +1 -0
  26. package/dist/assets/{privacy-settings-page-C18zo3ox.js → privacy-settings-page-C2sDBqhb.js} +1 -1
  27. package/dist/assets/projects-BcPcx-uD.js +1 -0
  28. package/dist/assets/{providers-config-page-YU0-AOXJ.js → providers-config-page-CmUFl9RF.js} +1 -1
  29. package/dist/assets/react-BjO9I4WZ.js +8 -0
  30. package/dist/assets/{refresh-cw-ZHKfE3yx.js → refresh-cw-DsT2kvXi.js} +1 -1
  31. package/dist/assets/remote-Ci6x91ES.js +1 -0
  32. package/dist/assets/{rotate-cw-CLhOXGwE.js → rotate-cw-BCAs6c3R.js} +1 -1
  33. package/dist/assets/{runtime-config-page-Dmy1A_0Y.js → runtime-config-page-cQKNeAjL.js} +1 -1
  34. package/dist/assets/{save-CWuuZs77.js → save-CadRMTMQ.js} +1 -1
  35. package/dist/assets/{search-DcmsKAn2.js → search-B6tB4fzB.js} +1 -1
  36. package/dist/assets/{search-config-page-CxBacx6W.js → search-config-page-C8lOIkxn.js} +1 -1
  37. package/dist/assets/{secrets-config-page-B4py8-KR.js → secrets-config-page-8clzAag2.js} +1 -1
  38. package/dist/assets/security-config-DPbM6IVT.js +1 -0
  39. package/dist/assets/{select-BEr0Iy_f.js → select-CO9S_C2C.js} +1 -1
  40. package/dist/assets/skeleton-IHILNhIW.js +1 -0
  41. package/dist/assets/{tag-chip-ByqDkn6g.js → tag-chip-BZKk0UkK.js} +1 -1
  42. package/dist/assets/x-BuvHIb1d.js +1 -0
  43. package/dist/index.html +21 -21
  44. package/package.json +5 -5
  45. package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +362 -241
  46. package/src/features/apps/components/app-document-access-section.tsx +187 -0
  47. package/src/features/apps/components/app-package-card.tsx +150 -275
  48. package/src/features/apps/hooks/use-app-packages.ts +91 -35
  49. package/src/features/chat/index.ts +5 -1
  50. package/src/features/projects/AGENTS.md +3 -2
  51. package/src/features/projects/components/project-overview.tsx +70 -139
  52. package/src/features/projects/components/work/project-work-item-drawer.test.tsx +65 -0
  53. package/src/features/projects/components/work/project-work-item-drawer.tsx +367 -0
  54. package/src/features/projects/components/work/project-work-items.test.tsx +98 -0
  55. package/src/features/projects/components/work/project-work-items.tsx +199 -0
  56. package/src/features/projects/components/work/project-work-state-settings.tsx +285 -0
  57. package/src/features/projects/hooks/use-project-observation.ts +56 -26
  58. package/src/features/projects/hooks/use-project-work.test.tsx +58 -0
  59. package/src/features/projects/hooks/use-project-work.ts +215 -0
  60. package/src/features/projects/pages/project-home-page.tsx +119 -138
  61. package/src/features/projects/pages/projects-page.test.tsx +79 -308
  62. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +16 -1
  63. package/src/shared/lib/i18n/locales/en-US/projects.json +60 -1
  64. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +16 -1
  65. package/src/shared/lib/i18n/locales/zh-CN/projects.json +60 -1
  66. package/dist/assets/api-CoqPRKmF.js +0 -13
  67. package/dist/assets/doc-browser-DJEMr7Zk.js +0 -1
  68. package/dist/assets/index-DyR5iR0O.css +0 -1
  69. package/dist/assets/index-XxXj8WDS.js +0 -109
  70. package/dist/assets/loader-circle-0qPOOYQ5.js +0 -1
  71. package/dist/assets/mcp-marketplace-page-uanamJ6L.js +0 -1
  72. package/dist/assets/plus-CYOvNvcV.js +0 -1
  73. package/dist/assets/projects-C8Gm9RS8.js +0 -1
  74. package/dist/assets/react-DF-kP7iN.js +0 -8
  75. package/dist/assets/remote-B_utkBra.js +0 -1
  76. package/dist/assets/security-config-CTP4IQWw.js +0 -1
  77. package/dist/assets/skeleton-B2NHluWf.js +0 -1
  78. package/dist/assets/x-CENkFyK5.js +0 -1
  79. package/src/features/projects/components/project-work-items.test.tsx +0 -78
  80. package/src/features/projects/components/project-work-items.tsx +0 -272
@@ -1,9 +1,5 @@
1
- import { useRef, useState } from 'react';
2
- import type {
3
- AppDataEntry,
4
- AppPackageOperationView,
5
- AppPackageView,
6
- } from '@nextclaw/client-sdk';
1
+ import { useRef, useState } from "react";
2
+ import type { AppDataEntry, AppPackageOperationView, AppPackageView } from "@nextclaw/client-sdk";
7
3
  import {
8
4
  AlertCircle,
9
5
  AppWindow,
@@ -22,29 +18,19 @@ import {
22
18
  ShieldAlert,
23
19
  ShieldCheck,
24
20
  type LucideIcon,
25
- } from 'lucide-react';
26
- import { AppArtwork } from '@/features/apps/components/app-artwork';
27
- import {
28
- AppDataRemovalChoice,
29
- AppStorageUsageDetails,
30
- formatBytes,
31
- } from '@/features/app-data';
32
- import { isAppPackageOperationActive } from '@/features/apps/hooks/use-app-packages';
33
- import { readAppPackageAvailability } from '@/features/apps/utils/app-package-readiness.utils';
34
- import type { PanelAppEntryView } from '@/shared/lib/api';
35
- import { Button } from '@/shared/components/ui/button';
36
- import { Card } from '@/shared/components/ui/card';
37
- import {
38
- Dialog,
39
- DialogContent,
40
- DialogDescription,
41
- DialogFooter,
42
- DialogHeader,
43
- DialogTitle,
44
- } from '@/shared/components/ui/dialog';
45
- import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
46
- import { getLanguage, t } from '@/shared/lib/i18n';
47
- import { cn } from '@/shared/lib/utils';
21
+ } from "lucide-react";
22
+ import { AppArtwork } from "@/features/apps/components/app-artwork";
23
+ import { AppDocumentAccessSection } from "@/features/apps/components/app-document-access-section";
24
+ import { AppDataRemovalChoice, AppStorageUsageDetails, formatBytes } from "@/features/app-data";
25
+ import { isAppPackageOperationActive } from "@/features/apps/hooks/use-app-packages";
26
+ import { readAppPackageAvailability } from "@/features/apps/utils/app-package-readiness.utils";
27
+ import type { PanelAppEntryView } from "@/shared/lib/api";
28
+ import { Button } from "@/shared/components/ui/button";
29
+ import { Card } from "@/shared/components/ui/card";
30
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog";
31
+ import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
32
+ import { getLanguage, t } from "@/shared/lib/i18n";
33
+ import { cn } from "@/shared/lib/utils";
48
34
 
49
35
  export function AppPackageCard({
50
36
  appPackage,
@@ -72,7 +58,7 @@ export function AppPackageCard({
72
58
  onUninstall: (purgeData: boolean) => void;
73
59
  onUpdate: () => void;
74
60
  panelApps: PanelAppEntryView[];
75
- storageUsage?: AppDataEntry['usage'];
61
+ storageUsage?: AppDataEntry["usage"];
76
62
  storageUsageLoading: boolean;
77
63
  storageUsageUnavailable: boolean;
78
64
  unavailableMessage?: string;
@@ -82,24 +68,15 @@ export function AppPackageCard({
82
68
  const [purgeData, setPurgeData] = useState(false);
83
69
  const skipMenuFocusRestoreRef = useRef(false);
84
70
  const uninstallCancelRef = useRef<HTMLButtonElement>(null);
85
- const panelComponents = appPackage.components.filter((component) => component.kind === 'panel');
71
+ const panelComponents = appPackage.components.filter((component) => component.kind === "panel");
86
72
  const serviceCount = appPackage.components.length - panelComponents.length;
87
- const rollbackVersions = appPackage.installedVersions.filter(
88
- (version) => version !== appPackage.activeVersion,
89
- );
73
+ const rollbackVersions = appPackage.installedVersions.filter((version) => version !== appPackage.activeVersion);
90
74
  const displayName = readLocalizedText(appPackage.name, appPackage.nameI18n) ?? appPackage.id;
91
- const displayDescription = readLocalizedText(
92
- appPackage.description,
93
- appPackage.descriptionI18n,
94
- );
75
+ const displayDescription = readLocalizedText(appPackage.description, appPackage.descriptionI18n);
95
76
  const operationActive = operation ? isAppPackageOperationActive(operation.status) : false;
96
77
  const pending = isPending || operationActive;
97
78
  const availability = readAppPackageAvailability(appPackage, unavailableMessage);
98
- const storageUsageLabel = resolveStorageUsageLabel(
99
- storageUsage,
100
- storageUsageLoading,
101
- storageUsageUnavailable,
102
- );
79
+ const storageUsageLabel = resolveStorageUsageLabel(storageUsage, storageUsageLoading, storageUsageUnavailable);
103
80
 
104
81
  return (
105
82
  <Card surface="flat" hover={false} className="overflow-hidden border-border/60 shadow-[0_1px_2px_rgba(0,0,0,0.025)]">
@@ -108,75 +85,51 @@ export function AppPackageCard({
108
85
  <div className="min-w-0 flex-1">
109
86
  <div className="flex min-w-0 items-center gap-2">
110
87
  <h2 className="truncate text-sm font-semibold text-foreground">{displayName}</h2>
111
- <span className={cn(
112
- 'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium',
113
- availability.className,
114
- )}>
115
- {availability.label}
116
- </span>
88
+ <span className={cn("shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium", availability.className)}>{availability.label}</span>
117
89
  </div>
118
90
  {availability.message ? (
119
- <p role="alert" className={cn('mt-2 text-xs leading-5', availability.messageClassName)}>
91
+ <p role="alert" className={cn("mt-2 text-xs leading-5", availability.messageClassName)}>
120
92
  {availability.message}
121
93
  </p>
122
94
  ) : null}
123
- {!appPackage.enabled && appPackage.readiness.status !== 'ready' ? (
95
+ {!appPackage.enabled && appPackage.readiness.status !== "ready" ? (
124
96
  <ul className="mt-1.5 space-y-1 text-xs leading-5 text-muted-foreground">
125
97
  {appPackage.readiness.requirements.map((requirement) => (
126
98
  <li key={`${requirement.componentId}:${requirement.kind}:${requirement.id}`}>
127
99
  {requirement.title}
128
- {requirement.description ? ` — ${requirement.description}` : ''}
100
+ {requirement.description ? ` — ${requirement.description}` : ""}
129
101
  </li>
130
102
  ))}
131
103
  </ul>
132
104
  ) : null}
133
- {displayDescription ? (
134
- <p className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
135
- {displayDescription}
136
- </p>
137
- ) : null}
105
+ {displayDescription ? <p className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">{displayDescription}</p> : null}
138
106
  <div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground/75">
139
107
  <span>v{appPackage.activeVersion}</span>
140
- {appPackage.builtIn ? <span>{t('appPackagesBuiltIn')}</span> : null}
108
+ {appPackage.builtIn ? <span>{t("appPackagesBuiltIn")}</span> : null}
141
109
  {serviceCount > 0 ? (
142
110
  <span className="inline-flex items-center gap-1">
143
111
  <Server className="h-3 w-3" />
144
- {t('appPackagesLocalService')}
112
+ {t("appPackagesLocalService")}
145
113
  </span>
146
114
  ) : null}
147
- <span className={cn(
148
- 'inline-flex items-center gap-1',
149
- appPackage.isolation === 'full-user' && 'text-amber-700 dark:text-amber-300',
150
- )}>
151
- {appPackage.isolation === 'full-user'
152
- ? <ShieldAlert className="h-3 w-3" />
153
- : <ShieldCheck className="h-3 w-3" />}
154
- {appPackage.isolation === 'full-user'
155
- ? t('appPackagesIsolationFullUser')
156
- : appPackage.isolation === 'host-mediated'
157
- ? t('appPackagesIsolationMediated')
158
- : t('appPackagesIsolationPanel')}
115
+ <span className={cn("inline-flex items-center gap-1", appPackage.isolation === "full-user" && "text-amber-700 dark:text-amber-300")}>
116
+ {appPackage.isolation === "full-user" ? <ShieldAlert className="h-3 w-3" /> : <ShieldCheck className="h-3 w-3" />}
117
+ {appPackage.isolation === "full-user" ? t("appPackagesIsolationFullUser") : appPackage.isolation === "host-mediated" ? t("appPackagesIsolationMediated") : t("appPackagesIsolationPanel")}
159
118
  </span>
160
119
  </div>
161
- <div
162
- className="mt-2 flex min-w-0 items-center gap-1.5 rounded-md bg-muted/35 px-2 py-1 text-[10px] text-muted-foreground"
163
- title={appPackage.storage.dataDirectory}
164
- >
120
+ <div className="mt-2 flex min-w-0 items-center gap-1.5 rounded-md bg-muted/35 px-2 py-1 text-[10px] text-muted-foreground" title={appPackage.storage.dataDirectory}>
165
121
  <HardDrive className="h-3 w-3 shrink-0" />
166
122
  <span className="shrink-0">
167
- {t('appPackagesDataUsage')}{' '}
168
- {storageUsageLabel}
123
+ {t("appPackagesDataUsage")} {storageUsageLabel}
169
124
  </span>
170
- <code className="min-w-0 truncate font-mono text-[10px]">
171
- {appPackage.storage.dataDirectory}
172
- </code>
125
+ <code className="min-w-0 truncate font-mono text-[10px]">{appPackage.storage.dataDirectory}</code>
173
126
  </div>
174
127
  </div>
175
128
  <div className="flex shrink-0 items-center gap-1">
176
129
  <Button
177
130
  type="button"
178
131
  size="sm"
179
- variant={appPackage.enabled ? 'outline' : 'default'}
132
+ variant={appPackage.enabled ? "outline" : "default"}
180
133
  disabled={pending || !availability.interactive}
181
134
  title={availability.message}
182
135
  onClick={appPackage.enabled ? onDisable : onEnable}
@@ -189,65 +142,65 @@ export function AppPackageCard({
189
142
  })}
190
143
  </Button>
191
144
  <Popover open={menuOpen} onOpenChange={setMenuOpen}>
192
- <PopoverTrigger asChild>
193
- <button
194
- type="button"
195
- aria-label={t('appPackagesMoreActions')}
196
- title={t('appPackagesMoreActions')}
197
- disabled={pending}
198
- className="rounded-full p-2 text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:opacity-50"
199
- >
200
- <MoreHorizontal className="h-4 w-4" />
201
- </button>
202
- </PopoverTrigger>
203
- <PopoverContent
204
- align="end"
205
- className="w-52 rounded-xl p-1.5"
206
- onCloseAutoFocus={(event) => {
207
- if (skipMenuFocusRestoreRef.current) {
208
- event.preventDefault();
209
- skipMenuFocusRestoreRef.current = false;
210
- }
211
- }}
145
+ <PopoverTrigger asChild>
146
+ <button
147
+ type="button"
148
+ aria-label={t("appPackagesMoreActions")}
149
+ title={t("appPackagesMoreActions")}
150
+ disabled={pending}
151
+ className="rounded-full p-2 text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:opacity-50"
212
152
  >
153
+ <MoreHorizontal className="h-4 w-4" />
154
+ </button>
155
+ </PopoverTrigger>
156
+ <PopoverContent
157
+ align="end"
158
+ className="w-52 rounded-xl p-1.5"
159
+ onCloseAutoFocus={(event) => {
160
+ if (skipMenuFocusRestoreRef.current) {
161
+ event.preventDefault();
162
+ skipMenuFocusRestoreRef.current = false;
163
+ }
164
+ }}
165
+ >
166
+ <AppPackageMenuItem
167
+ icon={RefreshCw}
168
+ label={t("appPackagesCheckUpdate")}
169
+ onClick={() => {
170
+ setMenuOpen(false);
171
+ onUpdate();
172
+ }}
173
+ />
174
+ {rollbackVersions.map((version) => (
213
175
  <AppPackageMenuItem
214
- icon={RefreshCw}
215
- label={t('appPackagesCheckUpdate')}
216
- onClick={() => {
217
- setMenuOpen(false);
218
- onUpdate();
219
- }}
220
- />
221
- {rollbackVersions.map((version) => (
222
- <AppPackageMenuItem
223
- key={version}
224
- icon={RotateCcw}
225
- label={`${t('appPackagesRollback')} v${version}`}
226
- onClick={() => {
227
- setMenuOpen(false);
228
- onRollback(version);
229
- }}
230
- />
231
- ))}
232
- <AppPackageMenuItem
233
- destructive
234
- icon={Trash2}
235
- label={t('appPackagesUninstall')}
176
+ key={version}
177
+ icon={RotateCcw}
178
+ label={`${t("appPackagesRollback")} v${version}`}
236
179
  onClick={() => {
237
- skipMenuFocusRestoreRef.current = true;
238
180
  setMenuOpen(false);
239
- setPurgeData(false);
240
- setUninstallOpen(true);
181
+ onRollback(version);
241
182
  }}
242
183
  />
243
- </PopoverContent>
244
- </Popover>
184
+ ))}
185
+ <AppPackageMenuItem
186
+ destructive
187
+ icon={Trash2}
188
+ label={t("appPackagesUninstall")}
189
+ onClick={() => {
190
+ skipMenuFocusRestoreRef.current = true;
191
+ setMenuOpen(false);
192
+ setPurgeData(false);
193
+ setUninstallOpen(true);
194
+ }}
195
+ />
196
+ </PopoverContent>
197
+ </Popover>
245
198
  </div>
246
199
  </div>
247
200
 
248
- {operation && (operationActive || operation.status === 'failed' || operation.status === 'interrupted') ? (
249
- <PackageOperationStatus operation={operation} />
250
- ) : null}
201
+ {operation && (operationActive || operation.status === "failed" || operation.status === "interrupted") ? <PackageOperationStatus operation={operation} /> : null}
202
+
203
+ <AppDocumentAccessSection appId={appPackage.id} appName={displayName} disabled={pending || !availability.interactive} scopes={appPackage.documentAccess ?? []} />
251
204
 
252
205
  {panelComponents.length > 0 ? (
253
206
  <div className="border-t border-border/60 bg-muted/20 p-2">
@@ -255,33 +208,28 @@ export function AppPackageCard({
255
208
  {panelComponents.map((component) => {
256
209
  const panelApp = panelApps.find((entry) => entry.appId === component.id);
257
210
  const Icon = resolveComponentIcon(component.id);
258
- const componentTitle = readLocalizedText(component.title, component.titleI18n)
259
- ?? component.id;
211
+ const componentTitle = readLocalizedText(component.title, component.titleI18n) ?? component.id;
260
212
  return (
261
213
  <button
262
214
  key={component.id}
263
215
  type="button"
264
216
  disabled={!appPackage.enabled || !panelApp || pending || !availability.interactive}
265
- onClick={() => panelApp && onOpenPanelApp({
266
- ...panelApp,
267
- title: componentTitle,
268
- description: readLocalizedText(
269
- component.description,
270
- component.descriptionI18n,
271
- ),
272
- })}
217
+ onClick={() =>
218
+ panelApp &&
219
+ onOpenPanelApp({
220
+ ...panelApp,
221
+ title: componentTitle,
222
+ description: readLocalizedText(component.description, component.descriptionI18n),
223
+ })
224
+ }
273
225
  className="group flex min-w-0 items-center gap-2 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-[var(--interaction-hover)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-default disabled:opacity-55"
274
- title={availability.message ?? (!appPackage.enabled ? t('appPackagesEnableToOpen') : componentTitle)}
226
+ title={availability.message ?? (!appPackage.enabled ? t("appPackagesEnableToOpen") : componentTitle)}
275
227
  >
276
228
  <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-card text-muted-foreground shadow-sm ring-1 ring-border/60">
277
229
  <ComponentIcon icon={component.icon} fallback={Icon} />
278
230
  </span>
279
- <span className="min-w-0 flex-1 truncate text-xs font-medium text-foreground">
280
- {componentTitle}
281
- </span>
282
- {appPackage.enabled ? (
283
- <ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50 transition-transform group-hover:translate-x-0.5" />
284
- ) : null}
231
+ <span className="min-w-0 flex-1 truncate text-xs font-medium text-foreground">{componentTitle}</span>
232
+ {appPackage.enabled ? <ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50 transition-transform group-hover:translate-x-0.5" /> : null}
285
233
  </button>
286
234
  );
287
235
  })}
@@ -298,41 +246,19 @@ export function AppPackageCard({
298
246
  }}
299
247
  >
300
248
  <DialogHeader>
301
- <DialogTitle>{t('appPackagesUninstallTitle')}</DialogTitle>
302
- <DialogDescription>{t('appPackagesUninstallDescription')}</DialogDescription>
249
+ <DialogTitle>{t("appPackagesUninstallTitle")}</DialogTitle>
250
+ <DialogDescription>{t("appPackagesUninstallDescription")}</DialogDescription>
303
251
  </DialogHeader>
304
252
  <div className="mt-4 grid gap-2">
305
- <AppDataRemovalChoice
306
- checked={!purgeData}
307
- description={t('appPackagesKeepDataDescription')}
308
- label={t('appPackagesKeepData')}
309
- onClick={() => setPurgeData(false)}
310
- />
311
- <AppDataRemovalChoice
312
- checked={purgeData}
313
- destructive
314
- description={t('appPackagesDeleteDataDescription')}
315
- label={t('appPackagesDeleteData')}
316
- onClick={() => setPurgeData(true)}
317
- />
253
+ <AppDataRemovalChoice checked={!purgeData} description={t("appPackagesKeepDataDescription")} label={t("appPackagesKeepData")} onClick={() => setPurgeData(false)} />
254
+ <AppDataRemovalChoice checked={purgeData} destructive description={t("appPackagesDeleteDataDescription")} label={t("appPackagesDeleteData")} onClick={() => setPurgeData(true)} />
318
255
  </div>
319
256
  <div className="mt-3">
320
- <AppPackageStorageUsage
321
- loading={storageUsageLoading}
322
- storage={appPackage.storage}
323
- usage={storageUsage}
324
- unavailable={storageUsageUnavailable}
325
- />
257
+ <AppPackageStorageUsage loading={storageUsageLoading} storage={appPackage.storage} usage={storageUsage} unavailable={storageUsageUnavailable} />
326
258
  </div>
327
259
  <DialogFooter className="mt-5 gap-2 sm:gap-0">
328
- <Button
329
- ref={uninstallCancelRef}
330
- autoFocus
331
- type="button"
332
- variant="outline"
333
- onClick={() => setUninstallOpen(false)}
334
- >
335
- {t('cancel')}
260
+ <Button ref={uninstallCancelRef} autoFocus type="button" variant="outline" onClick={() => setUninstallOpen(false)}>
261
+ {t("cancel")}
336
262
  </Button>
337
263
  <Button
338
264
  type="button"
@@ -342,7 +268,7 @@ export function AppPackageCard({
342
268
  setUninstallOpen(false);
343
269
  }}
344
270
  >
345
- {t('appPackagesUninstall')}
271
+ {t("appPackagesUninstall")}
346
272
  </Button>
347
273
  </DialogFooter>
348
274
  </DialogContent>
@@ -351,85 +277,45 @@ export function AppPackageCard({
351
277
  );
352
278
  }
353
279
 
354
- function AppPackageStorageUsage({
355
- loading,
356
- storage,
357
- unavailable,
358
- usage,
359
- }: {
360
- loading: boolean;
361
- storage: AppPackageView['storage'];
362
- unavailable: boolean;
363
- usage?: AppDataEntry['usage'];
364
- }) {
280
+ function AppPackageStorageUsage({ loading, storage, unavailable, usage }: { loading: boolean; storage: AppPackageView["storage"]; unavailable: boolean; usage?: AppDataEntry["usage"] }) {
365
281
  if (usage) {
366
282
  return <AppStorageUsageDetails storage={storage} usage={usage} />;
367
283
  }
368
284
  return (
369
285
  <div className="rounded-xl bg-muted/45 px-3 py-3 text-xs text-muted-foreground">
370
- {loading ? (
371
- <LoaderCircle className="mr-1.5 inline h-3.5 w-3.5 animate-spin" />
372
- ) : null}
373
- {t(loading
374
- ? 'appPackagesLoading'
375
- : unavailable ? 'appPackagesDataUsageUnavailable' : 'appPackagesNoDataYet')}
286
+ {loading ? <LoaderCircle className="mr-1.5 inline h-3.5 w-3.5 animate-spin" /> : null}
287
+ {t(loading ? "appPackagesLoading" : unavailable ? "appPackagesDataUsageUnavailable" : "appPackagesNoDataYet")}
376
288
  </div>
377
289
  );
378
290
  }
379
291
 
380
- function resolveStorageUsageLabel(
381
- usage: AppDataEntry['usage'] | undefined,
382
- loading: boolean,
383
- unavailable: boolean,
384
- ): string {
292
+ function resolveStorageUsageLabel(usage: AppDataEntry["usage"] | undefined, loading: boolean, unavailable: boolean): string {
385
293
  if (usage) return formatBytes(usage.totalBytes);
386
- if (loading) return t('appPackagesLoading');
387
- return t(unavailable ? 'appPackagesDataUsageUnavailable' : 'appPackagesNoDataYet');
294
+ if (loading) return t("appPackagesLoading");
295
+ return t(unavailable ? "appPackagesDataUsageUnavailable" : "appPackagesNoDataYet");
388
296
  }
389
297
 
390
- function renderPrimaryActionLabel({
391
- enabled,
392
- isPending,
393
- operation,
394
- operationActive,
395
- }: {
396
- enabled: boolean;
397
- isPending: boolean;
398
- operation?: AppPackageOperationView;
399
- operationActive: boolean;
400
- }) {
298
+ function renderPrimaryActionLabel({ enabled, isPending, operation, operationActive }: { enabled: boolean; isPending: boolean; operation?: AppPackageOperationView; operationActive: boolean }) {
401
299
  if (operationActive) {
402
300
  return (
403
301
  <>
404
302
  <LoaderCircle className="mr-1.5 h-3.5 w-3.5 animate-spin" />
405
- {operation ? operationLabel(operation) : t('appPackagesWorking')}
303
+ {operation ? operationLabel(operation) : t("appPackagesWorking")}
406
304
  </>
407
305
  );
408
306
  }
409
- if (isPending) return t('appPackagesWorking');
410
- return enabled ? t('appPackagesDisable') : t('appPackagesEnable');
307
+ if (isPending) return t("appPackagesWorking");
308
+ return enabled ? t("appPackagesDisable") : t("appPackagesEnable");
411
309
  }
412
310
 
413
- function AppPackageMenuItem({
414
- destructive = false,
415
- icon: Icon,
416
- label,
417
- onClick,
418
- }: {
419
- destructive?: boolean;
420
- icon: LucideIcon;
421
- label: string;
422
- onClick: () => void;
423
- }) {
311
+ function AppPackageMenuItem({ destructive = false, icon: Icon, label, onClick }: { destructive?: boolean; icon: LucideIcon; label: string; onClick: () => void }) {
424
312
  return (
425
313
  <button
426
314
  type="button"
427
315
  onClick={onClick}
428
316
  className={cn(
429
- 'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors',
430
- destructive
431
- ? 'text-destructive hover:bg-destructive/10'
432
- : 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
317
+ "flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors",
318
+ destructive ? "text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground",
433
319
  )}
434
320
  >
435
321
  <Icon className="h-4 w-4 shrink-0" />
@@ -438,19 +324,8 @@ function AppPackageMenuItem({
438
324
  );
439
325
  }
440
326
 
441
- function ComponentIcon({
442
- fallback: Fallback,
443
- icon,
444
- }: {
445
- fallback: LucideIcon;
446
- icon?: string;
447
- }) {
448
- if (icon && (
449
- icon.startsWith('data:') ||
450
- icon.startsWith('/') ||
451
- icon.startsWith('http://') ||
452
- icon.startsWith('https://')
453
- )) {
327
+ function ComponentIcon({ fallback: Fallback, icon }: { fallback: LucideIcon; icon?: string }) {
328
+ if (icon && (icon.startsWith("data:") || icon.startsWith("/") || icon.startsWith("http://") || icon.startsWith("https://"))) {
454
329
  return <img src={icon} alt="" className="h-4 w-4 rounded object-cover" />;
455
330
  }
456
331
  if (icon) {
@@ -460,21 +335,16 @@ function ComponentIcon({
460
335
  }
461
336
 
462
337
  function PackageOperationStatus({ operation }: { operation: AppPackageOperationView }) {
463
- const failed = operation.status === 'failed' || operation.status === 'interrupted';
338
+ const failed = operation.status === "failed" || operation.status === "interrupted";
464
339
  const progress = Math.max(4, Math.round((operation.completedSteps / operation.totalSteps) * 100));
465
340
  return (
466
- <div className="border-t border-border/50 px-4 py-2.5" role={failed ? 'alert' : 'status'} aria-live="polite">
341
+ <div className="border-t border-border/50 px-4 py-2.5" role={failed ? "alert" : "status"} aria-live="polite">
467
342
  <div className="flex items-center gap-2 text-[11px]">
468
343
  {failed ? <AlertCircle className="h-3.5 w-3.5 text-destructive" /> : <LoaderCircle className="h-3.5 w-3.5 animate-spin text-primary" />}
469
- <span className={failed ? 'text-destructive' : 'text-muted-foreground'}>
470
- {failed ? operation.error ?? t('appPackagesActionFailed') : operationLabel(operation)}
471
- </span>
344
+ <span className={failed ? "text-destructive" : "text-muted-foreground"}>{failed ? (operation.error ?? t("appPackagesActionFailed")) : operationLabel(operation)}</span>
472
345
  </div>
473
346
  <div className="mt-2 h-1 overflow-hidden rounded-full bg-muted">
474
- <div
475
- className={cn('h-full rounded-full transition-[width] duration-300', failed ? 'bg-destructive' : 'bg-primary')}
476
- style={{ width: failed ? '100%' : `${progress}%` }}
477
- />
347
+ <div className={cn("h-full rounded-full transition-[width] duration-300", failed ? "bg-destructive" : "bg-primary")} style={{ width: failed ? "100%" : `${progress}%` }} />
478
348
  </div>
479
349
  </div>
480
350
  );
@@ -482,34 +352,39 @@ function PackageOperationStatus({ operation }: { operation: AppPackageOperationV
482
352
 
483
353
  function operationLabel(operation: AppPackageOperationView): string {
484
354
  switch (operation.status) {
485
- case 'queued': return t('appPackagesPreparing');
486
- case 'resolving': return t('appPackagesResolving');
487
- case 'downloading': return t('appPackagesDownloading');
488
- case 'verifying': return t('appPackagesVerifying');
489
- case 'installing': return operation.action === 'rollback'
490
- ? `${t('appPackagesSwitchingVersion')} ${operation.targetVersion ? `v${operation.targetVersion}` : ''}`.trim()
491
- : operation.action === 'uninstall'
492
- ? t('appPackagesUninstalling')
493
- : t('appPackagesInstalling');
494
- case 'finalizing': return t('appPackagesFinalizing');
495
- case 'succeeded': return t('appPackagesCompleted');
496
- case 'failed':
497
- case 'interrupted': return t('appPackagesActionFailed');
355
+ case "queued":
356
+ return t("appPackagesPreparing");
357
+ case "resolving":
358
+ return t("appPackagesResolving");
359
+ case "downloading":
360
+ return t("appPackagesDownloading");
361
+ case "verifying":
362
+ return t("appPackagesVerifying");
363
+ case "installing":
364
+ return operation.action === "rollback"
365
+ ? `${t("appPackagesSwitchingVersion")} ${operation.targetVersion ? `v${operation.targetVersion}` : ""}`.trim()
366
+ : operation.action === "uninstall"
367
+ ? t("appPackagesUninstalling")
368
+ : t("appPackagesInstalling");
369
+ case "finalizing":
370
+ return t("appPackagesFinalizing");
371
+ case "succeeded":
372
+ return t("appPackagesCompleted");
373
+ case "failed":
374
+ case "interrupted":
375
+ return t("appPackagesActionFailed");
498
376
  }
499
377
  }
500
378
 
501
379
  function resolveComponentIcon(componentId: string): LucideIcon {
502
- if (componentId.endsWith('-todos')) return CheckSquare2;
503
- if (componentId.endsWith('-notes')) return NotebookText;
504
- if (componentId.endsWith('-favorites')) return Bookmark;
505
- if (componentId.endsWith('-calendar')) return CalendarDays;
380
+ if (componentId.endsWith("-todos")) return CheckSquare2;
381
+ if (componentId.endsWith("-notes")) return NotebookText;
382
+ if (componentId.endsWith("-favorites")) return Bookmark;
383
+ if (componentId.endsWith("-calendar")) return CalendarDays;
506
384
  return AppWindow;
507
385
  }
508
386
 
509
- function readLocalizedText(
510
- fallback: string | undefined,
511
- localized: Record<string, string> | undefined,
512
- ): string | undefined {
513
- const languageTag = getLanguage() === 'zh' ? 'zh-CN' : 'en-US';
387
+ function readLocalizedText(fallback: string | undefined, localized: Record<string, string> | undefined): string | undefined {
388
+ const languageTag = getLanguage() === "zh" ? "zh-CN" : "en-US";
514
389
  return localized?.[languageTag] ?? fallback;
515
390
  }