@nextclaw/ui 0.14.2 → 0.14.3

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 (129) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{api-a14tR_z1.js → api-BP_8IaxB.js} +3 -3
  3. package/dist/assets/{channels-list-page-CFGkm4DT.js → channels-list-page-DEh5Jjqx.js} +1 -1
  4. package/dist/assets/chat-page-BRB0N5WW.js +105 -0
  5. package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
  6. package/dist/assets/{confirm-dialog-ClH_Ec7n.js → confirm-dialog-DsivFJ4r.js} +2 -2
  7. package/dist/assets/{desktop-update-config-DPNDnVpk.js → desktop-update-config-B0w9p2ep.js} +1 -1
  8. package/dist/assets/{dist-BtCS2Ucq.js → dist-CikkhOlq.js} +1 -1
  9. package/dist/assets/{dist-CLGGLm8b.js → dist-PVA_xL1D.js} +1 -1
  10. package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
  11. package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
  12. package/dist/assets/{doc-browser-context-D4JwMMxK.js → doc-browser-context-MTw4EvKd.js} +1 -1
  13. package/dist/assets/{ellipsis-CtJXjIol.js → ellipsis-y6vDtq4M.js} +1 -1
  14. package/dist/assets/{external-link-CdJiG13p.js → external-link-DVJIQf_G.js} +1 -1
  15. package/dist/assets/index-BiE-K8-q.css +1 -0
  16. package/dist/assets/index-D9BAE_L_.js +105 -0
  17. package/dist/assets/{mcp-marketplace-page-BU0kIjhR.js → mcp-marketplace-page-B1SZNY5m.js} +1 -1
  18. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
  19. package/dist/assets/{model-config-page-D6hYXROd.js → model-config-page-DotaTUHb.js} +1 -1
  20. package/dist/assets/plus-BbZqquJe.js +1 -0
  21. package/dist/assets/{provider-scoped-model-input-DAX2HgmR.js → provider-scoped-model-input-DsFRuWJH.js} +1 -1
  22. package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
  23. package/dist/assets/{react-C6cZgP44.js → react-VNqT5Zp7.js} +1 -1
  24. package/dist/assets/remote-B8TiUfwW.js +1 -0
  25. package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
  26. package/dist/assets/{runtime-config-page-C5xKaV3i.js → runtime-config-page-S6ErRHR8.js} +1 -1
  27. package/dist/assets/{save-DKA0x4zk.js → save-BOJ477H-.js} +1 -1
  28. package/dist/assets/{search-BNlcoj5c.js → search-BEti5beA.js} +1 -1
  29. package/dist/assets/{search-config-page-Cq9uJ-N8.js → search-config-page-DGj5am03.js} +1 -1
  30. package/dist/assets/{secrets-config-page-CxSbZLBo.js → secrets-config-page--IzrIOid.js} +1 -1
  31. package/dist/assets/{select-CvTlDZTQ.js → select-hoj8dK4K.js} +2 -2
  32. package/dist/assets/{tag-chip-8ffF2y2C.js → tag-chip-T8-h-die.js} +1 -1
  33. package/dist/assets/{use-config-DmT0o1gv.js → use-config-CjRV2hwL.js} +1 -1
  34. package/dist/assets/x-D9PXs_xP.js +1 -0
  35. package/dist/index.html +69 -19
  36. package/index.html +55 -5
  37. package/package.json +7 -7
  38. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +3 -3
  39. package/src/app/components/layout/header.tsx +3 -3
  40. package/src/app/components/layout/page-layout.tsx +2 -2
  41. package/src/app/components/layout/runtime-status-entry.tsx +7 -7
  42. package/src/app/components/layout/sidebar-items.tsx +10 -10
  43. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  44. package/src/app/components/layout/sidebar.tsx +6 -6
  45. package/src/app/styles/design-system.css +388 -26
  46. package/src/app/test/vitest-setup.ts +31 -0
  47. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
  48. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
  49. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -1
  50. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
  51. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  52. package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
  53. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +2 -2
  54. package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
  55. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +3 -3
  56. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +14 -14
  57. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +15 -15
  58. package/src/features/chat/components/layout/chat-sidebar.tsx +3 -3
  59. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
  60. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
  61. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +28 -9
  62. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +18 -6
  63. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +3 -13
  64. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-query.ts +19 -3
  65. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
  66. package/src/features/chat/features/message/components/chat-message-list.container.tsx +6 -6
  67. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -5
  68. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +4 -4
  69. package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
  70. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
  71. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
  72. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  73. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  74. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  75. package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
  76. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
  77. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +0 -7
  78. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
  79. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
  80. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
  81. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +3 -3
  82. package/src/features/chat/features/welcome/components/chat-welcome.tsx +4 -4
  83. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
  84. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
  85. package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
  86. package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
  87. package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
  88. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
  89. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
  90. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
  91. package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
  92. package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
  93. package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
  94. package/src/features/settings/components/config/provider-form.tsx +10 -10
  95. package/src/features/settings/components/config/provider-models-section.tsx +16 -16
  96. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  97. package/src/features/settings/pages/providers-config-page.tsx +17 -17
  98. package/src/shared/components/config-split-page.tsx +11 -11
  99. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
  100. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
  101. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
  102. package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
  103. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
  104. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  105. package/src/shared/components/ui/button.tsx +4 -4
  106. package/src/shared/components/ui/card.tsx +4 -4
  107. package/src/shared/components/ui/dialog.tsx +5 -5
  108. package/src/shared/components/ui/input.tsx +1 -1
  109. package/src/shared/components/ui/label.tsx +1 -1
  110. package/src/shared/components/ui/popover.tsx +1 -1
  111. package/src/shared/components/ui/select.tsx +3 -3
  112. package/src/shared/components/ui/switch.tsx +3 -3
  113. package/src/shared/components/ui/tabs.tsx +4 -4
  114. package/src/shared/components/ui/textarea.tsx +1 -1
  115. package/src/shared/lib/i18n/locales/en-US/core.json +7 -2
  116. package/src/shared/lib/i18n/locales/zh-CN/core.json +7 -2
  117. package/src/shared/lib/theme/index.ts +94 -52
  118. package/dist/assets/chat-page-CEdavJXc.js +0 -105
  119. package/dist/assets/config-split-page-B5b9B0uE.js +0 -1
  120. package/dist/assets/doc-browser-5nMWYKBD.js +0 -1
  121. package/dist/assets/doc-browser-BJRLhztv.js +0 -1
  122. package/dist/assets/index-BDN1zzMQ.js +0 -105
  123. package/dist/assets/index-g3uSZXoc.css +0 -1
  124. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +0 -1
  125. package/dist/assets/plus-DPjb0piO.js +0 -1
  126. package/dist/assets/providers-config-page-BzAeaACA.js +0 -1
  127. package/dist/assets/remote-B0G80uPr.js +0 -1
  128. package/dist/assets/rotate-cw-DnEB1Rju.js +0 -1
  129. package/dist/assets/x-BiSSpgqB.js +0 -1
@@ -39,7 +39,7 @@ export function ChatWelcomeSessionTypePicker({
39
39
  <PopoverTrigger asChild>
40
40
  <button
41
41
  type="button"
42
- className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
42
+ className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
43
43
  aria-label={t('chatWelcomeSessionTypePickerLabel')}
44
44
  >
45
45
  {selectedOption.icon?.src ? (
@@ -56,12 +56,12 @@ export function ChatWelcomeSessionTypePicker({
56
56
  <Bot className="h-4 w-4 shrink-0" />
57
57
  )}
58
58
  <span className="truncate">{selectedOption.label}</span>
59
- <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
59
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
60
60
  </button>
61
61
  </PopoverTrigger>
62
62
  <PopoverContent
63
63
  align="start"
64
- className="flex w-[min(16rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
64
+ className="flex w-[min(16rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
65
65
  style={{ maxHeight: SESSION_TYPE_PICKER_MAX_HEIGHT }}
66
66
  >
67
67
  <div className="min-h-0 flex-1 overflow-y-auto">
@@ -64,22 +64,22 @@ export function ChatWelcome({
64
64
  return (
65
65
  <div className="flex min-h-full items-center justify-center px-4 py-8 sm:p-8">
66
66
  <div className="min-w-0 w-full max-w-[min(760px,100%)]">
67
- <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10">
67
+ <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl border border-border bg-accent shadow-card">
68
68
  <Bot className="h-8 w-8 text-primary" />
69
69
  </div>
70
70
 
71
71
  <div className="text-center">
72
- <h2 className="text-2xl font-semibold text-gray-950 sm:text-3xl">
72
+ <h2 className="text-2xl font-semibold text-foreground sm:text-3xl">
73
73
  {t('chatWelcomeTitle')}
74
74
  </h2>
75
- <p className="mt-3 text-sm font-medium text-gray-500 sm:text-base">
75
+ <p className="mt-3 text-sm font-medium text-muted-foreground sm:text-base">
76
76
  {t('chatWelcomeSubtitle')}
77
77
  </p>
78
78
  </div>
79
79
 
80
80
  {inputSlot ? <div className="mt-8">{inputSlot}</div> : null}
81
81
 
82
- <div className="mt-3 flex flex-wrap items-center gap-2 px-1 text-sm text-gray-500">
82
+ <div className="mt-3 flex flex-wrap items-center gap-2 px-1 text-sm text-muted-foreground">
83
83
  {onSelectProjectRoot ? (
84
84
  <ChatWelcomeProjectPicker
85
85
  defaultProjectRoot={defaultProjectRoot}
@@ -18,10 +18,15 @@ function createSummary(
18
18
  }
19
19
 
20
20
  describe("buildChatWelcomeProjectOptions", () => {
21
- it("builds recent project options from session summaries", () => {
21
+ it("builds recent project options without treating the default workspace as a project", () => {
22
22
  const options = buildChatWelcomeProjectOptions({
23
23
  defaultProjectRoot: "/Users/demo/.nextclaw/workspace",
24
24
  sessionSummaries: [
25
+ createSummary({
26
+ sessionId: "session-default",
27
+ lastMessageAt: "2026-06-12T10:00:00.000Z",
28
+ metadata: { project_root: "/Users/demo/.nextclaw/workspace" },
29
+ }),
25
30
  createSummary({
26
31
  sessionId: "session-1",
27
32
  lastMessageAt: "2026-06-10T10:00:00.000Z",
@@ -36,17 +41,10 @@ describe("buildChatWelcomeProjectOptions", () => {
36
41
  });
37
42
 
38
43
  expect(options).toEqual([
39
- {
40
- projectRoot: "/Users/demo/.nextclaw/workspace",
41
- projectName: "workspace",
42
- sessionCount: 0,
43
- isDefault: true,
44
- },
45
44
  {
46
45
  projectRoot: "/tmp/project-alpha",
47
46
  projectName: "project-alpha",
48
47
  sessionCount: 2,
49
- isDefault: false,
50
48
  },
51
49
  ]);
52
50
  });
@@ -6,7 +6,6 @@ export type ChatWelcomeProjectOption = {
6
6
  projectRoot: string;
7
7
  projectName: string;
8
8
  sessionCount: number;
9
- isDefault: boolean;
10
9
  };
11
10
 
12
11
  function readSessionActivityAt(session: {
@@ -28,7 +27,7 @@ export function buildChatWelcomeProjectOptions(params: {
28
27
 
29
28
  for (const session of adaptNcpSessionSummaries([...sessionSummaries])) {
30
29
  const projectRoot = session.projectRoot?.trim();
31
- if (!projectRoot) {
30
+ if (!projectRoot || projectRoot === defaultProjectRoot) {
32
31
  continue;
33
32
  }
34
33
  const existing = groups.get(projectRoot);
@@ -45,26 +44,12 @@ export function buildChatWelcomeProjectOptions(params: {
45
44
  getSessionProjectName(projectRoot) ||
46
45
  projectRoot,
47
46
  sessionCount: 1,
48
- isDefault: projectRoot === defaultProjectRoot,
49
47
  latestUpdatedAt,
50
48
  });
51
49
  }
52
50
 
53
- if (defaultProjectRoot && !groups.has(defaultProjectRoot)) {
54
- groups.set(defaultProjectRoot, {
55
- projectRoot: defaultProjectRoot,
56
- projectName: getSessionProjectName(defaultProjectRoot) ?? defaultProjectRoot,
57
- sessionCount: 0,
58
- isDefault: true,
59
- latestUpdatedAt: Number.POSITIVE_INFINITY,
60
- });
61
- }
62
-
63
51
  return [...groups.values()]
64
52
  .sort((left, right) => {
65
- if (left.isDefault !== right.isDefault) {
66
- return left.isDefault ? -1 : 1;
67
- }
68
53
  return right.latestUpdatedAt - left.latestUpdatedAt;
69
54
  })
70
55
  .map(({ latestUpdatedAt: _latestUpdatedAt, ...option }) => option);
@@ -38,26 +38,26 @@ export function PanelAppListItem({
38
38
  };
39
39
 
40
40
  return (
41
- <div className="group w-full min-w-0 rounded-lg border border-gray-200 bg-white px-2.5 py-2.5 transition-colors hover:border-amber-200 hover:bg-amber-50/50">
41
+ <div className="group w-full min-w-0 rounded-lg border border-border bg-card px-2.5 py-2.5 transition-colors hover:border-primary/35 hover:bg-accent/60">
42
42
  <div className="flex min-w-0 items-start gap-2">
43
43
  <button type="button" onClick={onOpen} className="min-w-0 flex-1 text-left">
44
44
  <span className="flex min-w-0 items-center gap-2">
45
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-amber-50 text-base text-amber-700">
45
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent text-base text-accent-foreground">
46
46
  <PanelAppIcon icon={entry.icon} title={entry.title} />
47
47
  </span>
48
- <span className="block min-w-0 flex-1 truncate text-sm font-medium text-gray-900">{entry.title}</span>
48
+ <span className="block min-w-0 flex-1 truncate text-sm font-medium text-foreground">{entry.title}</span>
49
49
  </span>
50
50
  {entry.description ? (
51
- <span className="mt-1.5 block truncate text-xs leading-5 text-gray-600">{entry.description}</span>
51
+ <span className="mt-1.5 block truncate text-xs leading-5 text-muted-foreground">{entry.description}</span>
52
52
  ) : null}
53
- <span className="mt-0.5 block truncate text-[11px] leading-4 text-gray-400">{secondary}</span>
53
+ <span className="mt-0.5 block truncate text-[11px] leading-4 text-muted-foreground/70">{secondary}</span>
54
54
  </button>
55
55
  <div className="flex shrink-0 items-center gap-1">
56
56
  <button
57
57
  type="button"
58
58
  onClick={handleFavorite}
59
59
  disabled={favoritePending}
60
- className="rounded-md p-1.5 text-gray-400 transition-colors hover:bg-white hover:text-amber-500 disabled:opacity-50"
60
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-amber-500 disabled:opacity-50"
61
61
  title={favoriteLabel}
62
62
  aria-label={favoriteLabel}
63
63
  >
@@ -67,7 +67,7 @@ export function PanelAppListItem({
67
67
  <PopoverTrigger asChild>
68
68
  <button
69
69
  type="button"
70
- className="rounded-md p-1.5 text-gray-400 transition-colors hover:bg-white hover:text-gray-700"
70
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground"
71
71
  aria-label={t('panelAppsMoreActions')}
72
72
  disabled={deletePending}
73
73
  >
@@ -118,7 +118,7 @@ function PanelAppMenuItem({
118
118
  type="button"
119
119
  className={cn(
120
120
  'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
121
- destructive ? 'text-destructive hover:bg-destructive/10' : 'text-gray-700 hover:bg-gray-100',
121
+ destructive ? 'text-destructive hover:bg-destructive/10' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
122
122
  )}
123
123
  disabled={disabled}
124
124
  onClick={onClick}
@@ -13,18 +13,18 @@ export function PanelAppToolbar({
13
13
  onRefresh,
14
14
  }: PanelAppToolbarProps) {
15
15
  return (
16
- <div className="flex items-center justify-between gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
16
+ <div className="flex items-center justify-between gap-2 border-b border-border/70 bg-card px-3.5 py-2 shrink-0">
17
17
  <button
18
18
  type="button"
19
19
  onClick={onOpenApps}
20
- className="inline-flex shrink-0 items-center rounded-md p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
20
+ className="inline-flex shrink-0 items-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
21
21
  title={t('appsTitle')}
22
22
  aria-label={t('appsTitle')}
23
23
  >
24
24
  <ArrowLeft className="h-3.5 w-3.5" />
25
25
  </button>
26
26
  <span
27
- className="min-w-0 flex-1 truncate text-xs font-medium text-gray-600"
27
+ className="min-w-0 flex-1 truncate text-xs font-medium text-muted-foreground"
28
28
  title={appTitle}
29
29
  >
30
30
  {appTitle}
@@ -32,7 +32,7 @@ export function PanelAppToolbar({
32
32
  <button
33
33
  type="button"
34
34
  onClick={onRefresh}
35
- className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800"
35
+ className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
36
36
  title={t('panelAppsRefreshCurrent')}
37
37
  aria-label={t('panelAppsRefreshCurrent')}
38
38
  >
@@ -83,7 +83,7 @@ export function PanelAppsList({
83
83
 
84
84
  if (panelApps.isLoading) {
85
85
  return (
86
- <div className="flex h-full items-center justify-center text-sm text-gray-500">{t('panelAppsLoading')}</div>
86
+ <div className="flex h-full items-center justify-center text-sm text-muted-foreground">{t('panelAppsLoading')}</div>
87
87
  );
88
88
  }
89
89
 
@@ -94,17 +94,17 @@ export function PanelAppsList({
94
94
  }
95
95
 
96
96
  return (
97
- <div className="flex h-full min-h-0 flex-col bg-white">
98
- <div className="flex items-center justify-between gap-2 border-b border-gray-100 px-4 py-3">
97
+ <div className="flex h-full min-h-0 flex-col bg-card text-card-foreground">
98
+ <div className="flex items-center justify-between gap-2 border-b border-border/70 px-4 py-3">
99
99
  <div className="flex min-w-0 items-center gap-1.5">
100
100
  {headerContent ?? (
101
- <div className="truncate text-sm font-semibold text-gray-900">{t('panelAppsTitle')}</div>
101
+ <div className="truncate text-sm font-semibold text-foreground">{t('panelAppsTitle')}</div>
102
102
  )}
103
103
  {panelApps.data?.panelsPath ? (
104
104
  <TooltipProvider delayDuration={250}>
105
105
  <Tooltip>
106
106
  <TooltipTrigger asChild>
107
- <button type="button" className="rounded p-0.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-700" aria-label={t('panelAppsTitle')}><HelpCircle className="h-3.5 w-3.5" /></button>
107
+ <button type="button" className="rounded p-0.5 text-muted-foreground/70 transition-colors hover:bg-accent hover:text-accent-foreground" aria-label={t('panelAppsTitle')}><HelpCircle className="h-3.5 w-3.5" /></button>
108
108
  </TooltipTrigger>
109
109
  <TooltipContent side="bottom" className="max-w-[320px] break-all font-mono text-xs">{panelApps.data.panelsPath}</TooltipContent>
110
110
  </Tooltip>
@@ -114,16 +114,16 @@ export function PanelAppsList({
114
114
  <button
115
115
  type="button"
116
116
  onClick={() => void panelApps.refetch()}
117
- className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800"
117
+ className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
118
118
  title={t('panelAppsRefresh')}
119
119
  aria-label={t('panelAppsRefresh')}
120
120
  >
121
121
  <RefreshCw className="h-3.5 w-3.5" />
122
122
  </button>
123
123
  </div>
124
- <div className="border-b border-gray-100 px-3 py-2">
124
+ <div className="border-b border-border/70 px-3 py-2">
125
125
  <Tabs value={viewMode} onValueChange={(value) => setViewMode(value as PanelAppViewMode)}>
126
- <TabsList className="grid h-auto w-full grid-cols-5 rounded-lg bg-gray-100/70 p-0.5">
126
+ <TabsList className="grid h-auto w-full grid-cols-5 rounded-lg bg-muted/70 p-0.5">
127
127
  <TabsTrigger value="smart" className="px-2 py-1 text-xs">{t('panelAppsSortSmart')}</TabsTrigger>
128
128
  <TabsTrigger value="favorites" className="px-2 py-1 text-xs">{t('panelAppsFavorites')}</TabsTrigger>
129
129
  <TabsTrigger value="recent-open" className="px-2 py-1 text-xs">{t('panelAppsSortRecentOpen')}</TabsTrigger>
@@ -139,7 +139,7 @@ export function PanelAppsList({
139
139
  onRefresh={() => void panelApps.refetch()}
140
140
  />
141
141
  ) : entries.length === 0 ? (
142
- <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-gray-500">{t('panelAppsEmptyFiltered')}</div>
142
+ <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-muted-foreground">{t('panelAppsEmptyFiltered')}</div>
143
143
  ) : (
144
144
  <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-3">
145
145
  <div className="space-y-1.5">
@@ -176,8 +176,8 @@ function PanelAppsEmptyGuide({
176
176
  <div className="mx-auto flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
177
177
  <AppWindow className="h-5 w-5" />
178
178
  </div>
179
- <h2 className="mt-3 text-sm font-semibold text-gray-900">{t('panelAppsEmptyTitle')}</h2>
180
- <p className="mt-1 text-xs leading-5 text-gray-500">{t('panelAppsEmptyDescription')}</p>
179
+ <h2 className="mt-3 text-sm font-semibold text-foreground">{t('panelAppsEmptyTitle')}</h2>
180
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">{t('panelAppsEmptyDescription')}</p>
181
181
 
182
182
  <div className="mt-4 space-y-2 text-left">
183
183
  <PanelAppsEmptyGuideStep
@@ -195,16 +195,16 @@ function PanelAppsEmptyGuide({
195
195
  </div>
196
196
 
197
197
  {panelsPath ? (
198
- <div className="mt-4 rounded-md bg-gray-50 px-3 py-2 text-left">
199
- <div className="text-[11px] font-medium uppercase text-gray-400">{t('panelAppsPanelsPath')}</div>
200
- <code className="mt-1 block break-all text-xs text-gray-600">{panelsPath}</code>
198
+ <div className="mt-4 rounded-md bg-muted/60 px-3 py-2 text-left">
199
+ <div className="text-[11px] font-medium uppercase text-muted-foreground/70">{t('panelAppsPanelsPath')}</div>
200
+ <code className="mt-1 block break-all text-xs text-muted-foreground">{panelsPath}</code>
201
201
  </div>
202
202
  ) : null}
203
203
 
204
204
  <button
205
205
  type="button"
206
206
  onClick={onRefresh}
207
- className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-gray-200 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
207
+ className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
208
208
  >
209
209
  <RefreshCw className="h-3.5 w-3.5" />
210
210
  {t('panelAppsRefresh')}
@@ -229,10 +229,10 @@ function PanelAppsEmptyGuideStep({
229
229
  }) {
230
230
  const content = (
231
231
  <>
232
- <div className="mt-0.5 text-gray-500">{icon}</div>
232
+ <div className="mt-0.5 text-muted-foreground">{icon}</div>
233
233
  <div className="min-w-0">
234
- <div className="text-xs font-medium text-gray-800">{title}</div>
235
- <div className="mt-0.5 text-xs leading-5 text-gray-500">{description}</div>
234
+ <div className="text-xs font-medium text-foreground">{title}</div>
235
+ <div className="mt-0.5 text-xs leading-5 text-muted-foreground">{description}</div>
236
236
  {actionLabel ? (
237
237
  <div className="mt-2 text-xs font-medium text-primary">{actionLabel}</div>
238
238
  ) : null}
@@ -245,7 +245,7 @@ function PanelAppsEmptyGuideStep({
245
245
  <button
246
246
  type="button"
247
247
  onClick={onAction}
248
- className="flex w-full gap-2 rounded-md bg-gray-50 px-3 py-2.5 text-left transition-colors hover:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
248
+ className="flex w-full gap-2 rounded-md bg-muted/60 px-3 py-2.5 text-left transition-colors hover:bg-accent/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
249
249
  >
250
250
  {content}
251
251
  </button>
@@ -253,7 +253,7 @@ function PanelAppsEmptyGuideStep({
253
253
  }
254
254
 
255
255
  return (
256
- <div className="flex gap-2 rounded-md bg-gray-50 px-3 py-2.5">
256
+ <div className="flex gap-2 rounded-md bg-muted/60 px-3 py-2.5">
257
257
  {content}
258
258
  </div>
259
259
  );
@@ -1,21 +1,45 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest';
2
- import { pwaShellThemeManager } from '@/features/pwa';
2
+ import { pwaShellThemeManager } from '@/features/pwa/managers/pwa-shell-theme.manager';
3
3
 
4
4
  describe('PwaShellThemeManager', () => {
5
5
  beforeEach(() => {
6
- document.head.innerHTML = '<meta name="theme-color" content="#F9F8F5" />';
6
+ document.head.innerHTML = '<meta name="theme-color" content="#FFFFFF" />';
7
7
  document.body.innerHTML = '<div id="root"></div>';
8
8
  document.documentElement.removeAttribute('data-theme');
9
9
  document.documentElement.style.backgroundColor = '';
10
10
  document.body.style.backgroundColor = '';
11
11
  });
12
12
 
13
+ it('applies natural shell colors by default', () => {
14
+ pwaShellThemeManager.syncCurrentTheme();
15
+
16
+ const meta = document.querySelector('meta[name="theme-color"]');
17
+ expect(meta?.getAttribute('content')).toBe('#FAF9F7');
18
+ expect(document.body.style.backgroundColor).toBe('rgb(250, 249, 247)');
19
+ });
20
+
21
+ it('applies natural shell colors explicitly', () => {
22
+ pwaShellThemeManager.syncTheme('natural');
23
+
24
+ const meta = document.querySelector('meta[name="theme-color"]');
25
+ expect(meta?.getAttribute('content')).toBe('#FAF9F7');
26
+ expect(document.body.style.backgroundColor).toBe('rgb(250, 249, 247)');
27
+ });
28
+
29
+ it('applies minimal shell colors explicitly', () => {
30
+ pwaShellThemeManager.syncTheme('minimal');
31
+
32
+ const meta = document.querySelector('meta[name="theme-color"]');
33
+ expect(meta?.getAttribute('content')).toBe('#FFFFFF');
34
+ expect(document.body.style.backgroundColor).toBe('rgb(255, 255, 255)');
35
+ });
36
+
13
37
  it('applies warm shell colors', () => {
14
38
  pwaShellThemeManager.syncTheme('warm');
15
39
 
16
40
  const meta = document.querySelector('meta[name="theme-color"]');
17
- expect(meta?.getAttribute('content')).toBe('#F9F8F5');
18
- expect(document.body.style.backgroundColor).toBe('rgb(249, 248, 245)');
41
+ expect(meta?.getAttribute('content')).toBe('#FAF8F4');
42
+ expect(document.body.style.backgroundColor).toBe('rgb(250, 248, 244)');
19
43
  });
20
44
 
21
45
  it('applies cool shell colors from current theme attribute', () => {
@@ -24,7 +48,47 @@ describe('PwaShellThemeManager', () => {
24
48
  pwaShellThemeManager.syncCurrentTheme();
25
49
 
26
50
  const meta = document.querySelector('meta[name="theme-color"]');
27
- expect(meta?.getAttribute('content')).toBe('#F8FAFB');
28
- expect(document.body.style.backgroundColor).toBe('rgb(248, 250, 251)');
51
+ expect(meta?.getAttribute('content')).toBe('#F8FAFC');
52
+ expect(document.body.style.backgroundColor).toBe('rgb(248, 250, 252)');
53
+ });
54
+
55
+ it('applies dawn shell colors from current theme attribute', () => {
56
+ document.documentElement.setAttribute('data-theme', 'dawn');
57
+
58
+ pwaShellThemeManager.syncCurrentTheme();
59
+
60
+ const meta = document.querySelector('meta[name="theme-color"]');
61
+ expect(meta?.getAttribute('content')).toBe('#FAF7F4');
62
+ expect(document.body.style.backgroundColor).toBe('rgb(250, 247, 244)');
63
+ });
64
+
65
+ it('applies graphite shell colors from current theme attribute', () => {
66
+ document.documentElement.setAttribute('data-theme', 'graphite');
67
+
68
+ pwaShellThemeManager.syncCurrentTheme();
69
+
70
+ const meta = document.querySelector('meta[name="theme-color"]');
71
+ expect(meta?.getAttribute('content')).toBe('#F8F8F7');
72
+ expect(document.body.style.backgroundColor).toBe('rgb(248, 248, 247)');
73
+ });
74
+
75
+ it('maps the legacy leaf theme to warm shell colors', () => {
76
+ document.documentElement.setAttribute('data-theme', 'leaf');
77
+
78
+ pwaShellThemeManager.syncCurrentTheme();
79
+
80
+ const meta = document.querySelector('meta[name="theme-color"]');
81
+ expect(meta?.getAttribute('content')).toBe('#FAF8F4');
82
+ expect(document.body.style.backgroundColor).toBe('rgb(250, 248, 244)');
83
+ });
84
+
85
+ it('applies probe shell colors from current theme attribute', () => {
86
+ document.documentElement.setAttribute('data-theme', 'probe');
87
+
88
+ pwaShellThemeManager.syncCurrentTheme();
89
+
90
+ const meta = document.querySelector('meta[name="theme-color"]');
91
+ expect(meta?.getAttribute('content')).toBe('#FEFCF6');
92
+ expect(document.body.style.backgroundColor).toBe('rgb(254, 252, 246)');
29
93
  });
30
94
  });
@@ -1,8 +1,13 @@
1
- import type { UiTheme } from '@/shared/lib/theme';
1
+ import { normalizeTheme, type UiTheme } from '@/shared/lib/theme';
2
2
 
3
3
  const PWA_SHELL_THEME_COLORS: Record<UiTheme, string> = {
4
- warm: '#F9F8F5',
5
- cool: '#F8FAFB'
4
+ natural: '#FAF9F7',
5
+ minimal: '#FFFFFF',
6
+ warm: '#FAF8F4',
7
+ cool: '#F8FAFC',
8
+ dawn: '#FAF7F4',
9
+ graphite: '#F8F8F7',
10
+ probe: '#FEFCF6'
6
11
  };
7
12
 
8
13
  export class PwaShellThemeManager {
@@ -22,8 +27,8 @@ export class PwaShellThemeManager {
22
27
  return;
23
28
  }
24
29
 
25
- const currentTheme = document.documentElement.getAttribute('data-theme') === 'cool' ? 'cool' : 'warm';
26
- this.syncTheme(currentTheme);
30
+ const currentTheme = document.documentElement.getAttribute('data-theme');
31
+ this.syncTheme(normalizeTheme(currentTheme) ?? 'natural');
27
32
  };
28
33
 
29
34
  private updateThemeMeta = (themeColor: string) => {
@@ -37,10 +37,10 @@ export function ServiceActionAuthorizationDialog() {
37
37
  </DialogDescription>
38
38
  </DialogHeader>
39
39
  {pending ? (
40
- <div className="space-y-3 rounded-md border border-gray-100 bg-gray-50 p-3 text-sm">
40
+ <div className="space-y-3 rounded-md border border-border bg-muted/60 p-3 text-sm">
41
41
  <AuthorizationField label={t('serviceActionAuthorizationSource')} value={pending.panelAppId} />
42
42
  <div className="space-y-2">
43
- <div className="text-xs font-medium text-gray-500">
43
+ <div className="text-xs font-medium text-muted-foreground">
44
44
  {t('serviceActionAuthorizationActions')}
45
45
  </div>
46
46
  <div className="max-h-64 space-y-2 overflow-auto">
@@ -78,18 +78,18 @@ function AuthorizationActionItem({
78
78
  };
79
79
  }) {
80
80
  return (
81
- <div className="space-y-1 rounded border border-gray-200 bg-white px-3 py-2">
81
+ <div className="space-y-1 rounded border border-border bg-card px-3 py-2">
82
82
  <div className="flex items-start justify-between gap-3">
83
- <div className="min-w-0 break-words text-xs font-medium text-gray-900">
83
+ <div className="min-w-0 break-words text-xs font-medium text-foreground">
84
84
  {action.actionTitle ?? action.actionId}
85
85
  </div>
86
86
  <div className="shrink-0 rounded bg-amber-50 px-1.5 py-0.5 text-[11px] font-medium text-amber-700">
87
87
  {action.risk ?? 'dangerous'}
88
88
  </div>
89
89
  </div>
90
- <div className="break-words text-[11px] text-gray-500">{action.actionId}</div>
90
+ <div className="break-words text-[11px] text-muted-foreground">{action.actionId}</div>
91
91
  {action.actionDescription ? (
92
- <div className="break-words text-xs text-gray-600">{action.actionDescription}</div>
92
+ <div className="break-words text-xs text-muted-foreground">{action.actionDescription}</div>
93
93
  ) : null}
94
94
  </div>
95
95
  );
@@ -104,8 +104,8 @@ function AuthorizationField({
104
104
  }) {
105
105
  return (
106
106
  <div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
107
- <div className="text-xs font-medium text-gray-500">{label}</div>
108
- <div className="min-w-0 break-words text-xs text-gray-900">{value}</div>
107
+ <div className="text-xs font-medium text-muted-foreground">{label}</div>
108
+ <div className="min-w-0 break-words text-xs text-foreground">{value}</div>
109
109
  </div>
110
110
  );
111
111
  }