@nextclaw/ui 0.26.1 → 0.26.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.
- package/CHANGELOG.md +27 -0
- package/dist/.vite/manifest.json +366 -365
- package/dist/assets/{account-panel-ZM2qzKF4.js → account-panel-DIdHjKG-.js} +1 -1
- package/dist/assets/{api-Cjq-o1VN.js → api-DkLJrSCd.js} +2 -2
- package/dist/assets/app-presenter-provider-Co8uKOyW.js +124 -0
- package/dist/assets/{appearance-settings-page-B4zouKzg.js → appearance-settings-page-DI8aDY2G.js} +1 -1
- package/dist/assets/{book-open-AteA89HX.js → book-open-BPMkKSci.js} +1 -1
- package/dist/assets/{card-BLXvBstA.js → card-Bqu8zWLq.js} +1 -1
- package/dist/assets/{channels-list-page-fj8kMo-T.js → channels-list-page-CCIaTQu7.js} +1 -1
- package/dist/assets/{circle-dashed-LSYZTzM7.js → circle-dashed-B48a0t6p.js} +1 -1
- package/dist/assets/{client-DudCRJw4.js → client-imrVphQb.js} +1 -1
- package/dist/assets/{config-split-page-G7Kt2BnV.js → config-split-page-Bd-bGKsw.js} +1 -1
- package/dist/assets/{confirm-dialog-DShT64F6.js → confirm-dialog-10Qb3dpZ.js} +1 -1
- package/dist/assets/context-menu-CRqH2SEk.js +1 -0
- package/dist/assets/{createLucideIcon-D7hvIHV-.js → createLucideIcon-B4xj2I2g.js} +1 -1
- package/dist/assets/{desktop-capabilities-page-CzqzFZUY.js → desktop-capabilities-page-CFcT2g8-.js} +1 -1
- package/dist/assets/{desktop-update-config-CgeQ0Ffo.js → desktop-update-config-CRJd_An1.js} +1 -1
- package/dist/assets/{dialog-C7glmAQJ.js → dialog-BgL3vKJQ.js} +2 -2
- package/dist/assets/doc-browser-52t2_rSF.js +1 -0
- package/dist/assets/{doc-browser-context-BlhBAOLH.js → doc-browser-context-DH4aWvkS.js} +1 -1
- package/dist/assets/{download-BYgECE0n.js → download-Cr337h4W.js} +1 -1
- package/dist/assets/{es2015-CsjAAQ6X.js → es2015-D-CRf3_Z.js} +1 -1
- package/dist/assets/{extensions-qYL-NpL3.js → extensions-CsmJPz7g.js} +1 -1
- package/dist/assets/{eye-BBuTzJ53.js → eye-DrQRSAfJ.js} +1 -1
- package/dist/assets/{form-actions-CLGWO52v.js → form-actions-BEeWTGUZ.js} +1 -1
- package/dist/assets/{i18n-qBGEbcW6.js → i18n-OGmF0Ws_.js} +2 -2
- package/dist/assets/{keyboard-shortcuts-settings-page-B-wK_6zG.js → keyboard-shortcuts-settings-page-DQU56SrW.js} +1 -1
- package/dist/assets/loader-circle-DbOQWT9C.js +1 -0
- package/dist/assets/login-page-DW1AZXqX.js +1 -0
- package/dist/assets/{login-page-Cd84lpVV.js → login-page-sgivRYG1.js} +1 -1
- package/dist/assets/main-B-oxUq-o.js +2 -0
- package/dist/assets/main-BpL6OjwM.css +1 -0
- package/dist/assets/main-i18n-catalog.config-Bpu5GcwF.js +1 -0
- package/dist/assets/mcp-marketplace-page-DSLRpJEB.js +1 -0
- package/dist/assets/{model-config-page-D0DQlSDa.js → model-config-page-CDQBhmVr.js} +1 -1
- package/dist/assets/{navigation-link-32iM1-e6.js → navigation-link-D3pzNHeA.js} +1 -1
- package/dist/assets/panel-app-BYfQu_TS.js +1 -0
- package/dist/assets/panel-app-i18n-catalog.config-D44qmtfk.js +1 -0
- package/dist/assets/{panelStandalone-C9EKaApN.js → panelStandalone-CGkYeMK5.js} +2 -2
- package/dist/assets/panelStandalone-D-mhAGNa.css +1 -0
- package/dist/assets/{privacy-settings-page-Bxxx12OO.js → privacy-settings-page-CjoAkoDt.js} +1 -1
- package/dist/assets/projects-C7Zxpcoi.js +1 -0
- package/dist/assets/{provider-models-C3frdXeW.js → provider-models-YW7rIeOD.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CYetrPYZ.js → provider-scoped-model-input-axJ-pTEu.js} +1 -1
- package/dist/assets/{providers-config-page-DESB1h8g.js → providers-config-page-_QhBXQ4M.js} +1 -1
- package/dist/assets/{puzzle-CWHfFyYN.js → puzzle--RLCeYML.js} +1 -1
- package/dist/assets/{pwa-CiGUjpcd.js → pwa-DcizGowz.js} +1 -1
- package/dist/assets/{refresh-cw-y3L6PyLt.js → refresh-cw-syCVj_nw.js} +1 -1
- package/dist/assets/remote-BLQU7aY5.js +1 -0
- package/dist/assets/{rotate-cw-CNE9qSHi.js → rotate-cw-BlH9El3x.js} +1 -1
- package/dist/assets/{runtime-config-page-5o98T-TD.js → runtime-config-page-Cco9dS73.js} +1 -1
- package/dist/assets/{save-B0p5a0ET.js → save-BWynuRQ2.js} +1 -1
- package/dist/assets/{search-config-page-CpGcnIzi.js → search-config-page-Cc74_SAU.js} +1 -1
- package/dist/assets/{secrets-config-page-C4VTiW3r.js → secrets-config-page-raYDzlFC.js} +1 -1
- package/dist/assets/security-config-aE_jOwy-.js +1 -0
- package/dist/assets/{select-CctTN205.js → select-6Uq4chZW.js} +1 -1
- package/dist/assets/{service-apps-panel-CKE5HKNY.js → service-apps-panel-jBWAa4qZ.js} +1 -1
- package/dist/assets/{settings-2-EPflrNk6.js → settings-2-COvVf1XX.js} +1 -1
- package/dist/assets/{status-dot-CensgJZC.js → status-dot-DZlxa5zW.js} +1 -1
- package/dist/assets/{tag-chip-Bjzb4-qW.js → tag-chip-DXHtZ8AS.js} +1 -1
- package/dist/assets/{theme-provider-hlle7Skk.js → theme-provider-C5PCDBPB.js} +1 -1
- package/dist/assets/{trash-2-C5KH5Rw9.js → trash-2-DazgBvmJ.js} +1 -1
- package/dist/assets/{use-config-CPl_F79t.js → use-config-DRRGF9Zb.js} +1 -1
- package/dist/assets/{use-desktop-capabilities-CEg9-DrQ.js → use-desktop-capabilities-CdfOUjdE.js} +1 -1
- package/dist/assets/{use-viewport-layout-CPZUmTPi.js → use-viewport-layout-DdxVnCYc.js} +1 -1
- package/dist/assets/x-CQIp_nJT.js +1 -0
- package/dist/index.html +34 -34
- package/dist/panel-standalone.html +11 -11
- package/package.json +8 -8
- package/src/app/components/layout/page-layout.tsx +5 -2
- package/src/app/configs/__tests__/app-navigation.config.test.ts +10 -0
- package/src/app/configs/app-navigation.config.ts +6 -2
- package/src/app/styles/charcoal-theme.css +1 -1
- package/src/app/styles/design-system.css +8 -2
- package/src/app/styles/island-theme.css +1 -1
- package/src/features/agents/components/agent-details-dialog.tsx +1 -1
- package/src/features/agents/components/agents-page.tsx +5 -3
- package/src/features/apps/components/app-package-card.tsx +7 -6
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -3
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +4 -4
- package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +12 -5
- package/src/features/chat/features/session/components/chat-mobile-session-row.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +3 -5
- package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +4 -3
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
- package/src/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar.tsx +3 -1
- package/src/features/cron/components/cron-config.tsx +1 -0
- package/src/features/cron/components/cron-job-row.tsx +4 -4
- package/src/features/inbox/components/inbox-delivery-content.tsx +1 -1
- package/src/features/inbox/pages/inbox-page.tsx +24 -28
- package/src/features/marketplace/components/marketplace-page-parts.tsx +4 -3
- package/src/features/marketplace/components/marketplace-page.tsx +2 -2
- package/src/features/panel-apps/components/panel-app-open-standalone-menu-item.tsx +3 -1
- package/src/features/projects/components/project-overview.tsx +1 -1
- package/src/features/projects/pages/project-home-page.tsx +11 -2
- package/src/features/projects/pages/projects-page.test.tsx +4 -2
- package/src/platforms/mobile/components/mobile-app-shell.tsx +1 -1
- package/src/platforms/mobile/components/mobile-bottom-nav.tsx +8 -7
- package/src/platforms/mobile/components/mobile-settings-shell.tsx +24 -23
- package/src/platforms/mobile/components/mobile-topbar.tsx +10 -11
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -2
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +36 -5
- package/src/shared/components/doc-browser/doc-browser.tsx +5 -1
- package/src/shared/components/settings/settings-page.tsx +1 -1
- package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +5 -5
- package/src/shared/components/ui/actions/action-feedback.ts +8 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +13 -12
- package/src/shared/components/ui/button.tsx +1 -1
- package/src/shared/components/ui/context-menu/context-menu-items.tsx +4 -3
- package/src/shared/components/ui/dialog.tsx +4 -3
- package/src/shared/components/ui/input.tsx +1 -0
- package/src/shared/components/ui/select.tsx +2 -1
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +5 -3
- package/src/shared/components/ui/tabs.tsx +2 -2
- package/src/shared/components/ui/textarea.tsx +1 -0
- package/src/shared/components/workbench/workbench-surface-toolbar.tsx +3 -4
- package/src/shared/components/workbench/workbench-surface.tsx +2 -2
- package/src/shared/lib/i18n/locales/en-US/agents.json +2 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +1 -1
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +1 -1
- package/dist/assets/app-presenter-provider-C02YPoMr.js +0 -124
- package/dist/assets/context-menu-rd7b_kVD.js +0 -1
- package/dist/assets/doc-browser-dBiR9Qi0.js +0 -1
- package/dist/assets/loader-circle-BA7EMkih.js +0 -1
- package/dist/assets/login-page-VdMyNkFf.js +0 -1
- package/dist/assets/main-CR0SrTBf.js +0 -2
- package/dist/assets/main-DXH6EdXI.css +0 -1
- package/dist/assets/main-i18n-catalog.config-Btp_OowF.js +0 -1
- package/dist/assets/mcp-marketplace-page-jacRwiI-.js +0 -1
- package/dist/assets/panel-app-CC0bVSu4.js +0 -1
- package/dist/assets/panel-app-i18n-catalog.config-LUSBp5Ce.js +0 -1
- package/dist/assets/panelStandalone-Y91OJkgV.css +0 -1
- package/dist/assets/projects-D56yAPNT.js +0 -1
- package/dist/assets/remote-DiGFV8nP.js +0 -1
- package/dist/assets/security-config-C5-RQVpS.js +0 -1
- package/dist/assets/x--xxUvHKW.js +0 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SelectValue,
|
|
8
8
|
} from "@/shared/components/ui/select";
|
|
9
9
|
import { Skeleton } from "@/shared/components/ui/skeleton";
|
|
10
|
+
import { Input } from "@/shared/components/ui/input";
|
|
10
11
|
import { t } from "@/shared/lib/i18n";
|
|
11
12
|
import { Loader2, PackageSearch } from "lucide-react";
|
|
12
13
|
import type { Ref } from "react";
|
|
@@ -30,10 +31,10 @@ export function FilterPanel({
|
|
|
30
31
|
}) {
|
|
31
32
|
return (
|
|
32
33
|
<div className="mb-4">
|
|
33
|
-
<div className="flex items-center gap-3">
|
|
34
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
34
35
|
<div className="relative min-w-0 flex-1">
|
|
35
36
|
<PackageSearch className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
36
|
-
<
|
|
37
|
+
<Input
|
|
37
38
|
value={searchText}
|
|
38
39
|
onChange={(event) => onSearchTextChange(event.target.value)}
|
|
39
40
|
placeholder={searchPlaceholder}
|
|
@@ -52,7 +53,7 @@ export function FilterPanel({
|
|
|
52
53
|
value={sort}
|
|
53
54
|
onValueChange={(value) => onSortChange(value as MarketplaceSort)}
|
|
54
55
|
>
|
|
55
|
-
<SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
|
|
56
|
+
<SelectTrigger className="h-9 w-[110px] sm:w-[150px] shrink-0 rounded-lg">
|
|
56
57
|
<SelectValue />
|
|
57
58
|
</SelectTrigger>
|
|
58
59
|
<SelectContent>
|
|
@@ -142,12 +142,12 @@ export function MarketplacePage({
|
|
|
142
142
|
<PageLayout className="flex h-full min-h-0 flex-col pb-0">
|
|
143
143
|
{!curatedSceneRoute.isSceneRoute && (
|
|
144
144
|
<>
|
|
145
|
-
<div className="mb-3 flex items-start justify-between gap-3 border-b border-
|
|
145
|
+
<div className="mb-3 flex flex-wrap items-start justify-between gap-3 border-b border-border/60">
|
|
146
146
|
<Tabs
|
|
147
147
|
value={scope}
|
|
148
148
|
onValueChange={(value) => setScope(value as ScopeType)}
|
|
149
149
|
>
|
|
150
|
-
<TabsList className="mb-0 h-auto flex-1 justify-start gap-6 rounded-none border-b-0 bg-transparent p-0 text-
|
|
150
|
+
<TabsList className="mb-0 h-auto flex-1 justify-start gap-4 sm:gap-6 rounded-none border-b-0 bg-transparent p-0 text-muted-foreground">
|
|
151
151
|
<TabsTrigger
|
|
152
152
|
value="all"
|
|
153
153
|
className="rounded-none border-b-2 border-transparent bg-transparent px-0 pb-3 pt-0 shadow-none hover:bg-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:text-gray-950 data-[state=active]:shadow-none"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ExternalLink } from "lucide-react";
|
|
2
|
+
import { ACTION_FEEDBACK, ACTION_MENU_ITEM_CLASS } from '@/shared/components/ui/actions/action-feedback';
|
|
3
|
+
import { cn } from '@/shared/lib/utils';
|
|
2
4
|
import type { PanelAppEntryView } from "@/shared/lib/api";
|
|
3
5
|
import { t } from "@/shared/lib/i18n";
|
|
4
6
|
|
|
@@ -22,7 +24,7 @@ export function PanelAppOpenStandaloneMenuItem({
|
|
|
22
24
|
// `opener` keeps this trusted same-origin target auxiliary, so the current browser owns the new tab.
|
|
23
25
|
rel="opener"
|
|
24
26
|
referrerPolicy="no-referrer"
|
|
25
|
-
className=
|
|
27
|
+
className={cn(ACTION_MENU_ITEM_CLASS, ACTION_FEEDBACK.item)}
|
|
26
28
|
onClick={onSelect}
|
|
27
29
|
>
|
|
28
30
|
<ExternalLink className="h-4 w-4 shrink-0" />
|
|
@@ -32,7 +32,7 @@ export function ProjectOverview({
|
|
|
32
32
|
return (
|
|
33
33
|
<div className="space-y-5">
|
|
34
34
|
<section
|
|
35
|
-
className="grid
|
|
35
|
+
className="grid grid-cols-2 gap-3 lg:grid-cols-4"
|
|
36
36
|
aria-label={t("projectsWorkSummary")}
|
|
37
37
|
>
|
|
38
38
|
{summary.data ? (
|
|
@@ -81,8 +81,17 @@ export function ProjectsPage() {
|
|
|
81
81
|
);
|
|
82
82
|
if (!selectedProject || !tab)
|
|
83
83
|
return (
|
|
84
|
-
<main className="p-
|
|
85
|
-
{t("projectsChoose")}
|
|
84
|
+
<main className="space-y-3 p-4 text-sm text-muted-foreground sm:p-6">
|
|
85
|
+
<label htmlFor="project-home-selection" className="block font-medium text-foreground">{t("projectsChoose")}</label>
|
|
86
|
+
<select
|
|
87
|
+
id="project-home-selection"
|
|
88
|
+
value=""
|
|
89
|
+
onChange={(event) => navigate(`/projects/${encodeURIComponent(event.target.value)}/overview`)}
|
|
90
|
+
className="min-h-11 w-full rounded-xl border border-border bg-card px-3 text-base text-foreground"
|
|
91
|
+
>
|
|
92
|
+
<option value="" disabled>{t("projectsChoose")}</option>
|
|
93
|
+
{registered.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
|
|
94
|
+
</select>
|
|
86
95
|
</main>
|
|
87
96
|
);
|
|
88
97
|
|
|
@@ -135,11 +135,13 @@ describe("ProjectsPage", () => {
|
|
|
135
135
|
).toBeTruthy();
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
it("
|
|
138
|
+
it("lets users enter a project without a desktop sidebar", () => {
|
|
139
139
|
renderPage("/projects");
|
|
140
140
|
expect(mocks.useProjectAgreement).toHaveBeenCalledWith(null);
|
|
141
141
|
expect(mocks.useProjectSkills).toHaveBeenCalledWith(null);
|
|
142
|
-
|
|
142
|
+
const picker = screen.getByRole("combobox", { name: /Choose a project|选择项目/ });
|
|
143
|
+
fireEvent.change(picker, { target: { value: "project-1" } });
|
|
144
|
+
expect(screen.getByRole("tab", { name: /Overview|概览/ }).getAttribute("aria-selected")).toBe("true");
|
|
143
145
|
});
|
|
144
146
|
|
|
145
147
|
it("loads each fixed project material only on its own tab", () => {
|
|
@@ -42,7 +42,7 @@ export function MobileAppShell({
|
|
|
42
42
|
{isMainRoute ? (
|
|
43
43
|
<div className="h-full min-h-0 overflow-hidden">{children}</div>
|
|
44
44
|
) : (
|
|
45
|
-
<main className="h-full overflow-auto px-4 py-4 custom-scrollbar">
|
|
45
|
+
<main className="h-full overflow-auto overscroll-contain px-4 py-4 custom-scrollbar">
|
|
46
46
|
<div className="mx-auto max-w-3xl animate-fade-in">{children}</div>
|
|
47
47
|
</main>
|
|
48
48
|
)}
|
|
@@ -19,13 +19,13 @@ export function MobileBottomNav() {
|
|
|
19
19
|
return (
|
|
20
20
|
<nav
|
|
21
21
|
data-testid="mobile-bottom-nav"
|
|
22
|
-
aria-label={t("
|
|
22
|
+
aria-label={t("mobileNavigation")}
|
|
23
23
|
className="shrink-0 border-t border-border/60 bg-background text-foreground"
|
|
24
24
|
style={{
|
|
25
25
|
paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 0.25rem)",
|
|
26
26
|
}}
|
|
27
27
|
>
|
|
28
|
-
<ul className="grid grid-cols-6 gap-
|
|
28
|
+
<ul className="grid grid-cols-6 gap-0.5 px-1 pt-1">
|
|
29
29
|
{getMobileBottomNavItems(t).map((item) => {
|
|
30
30
|
const active = item.target === "/settings"
|
|
31
31
|
? isSettingsRoute(pathname)
|
|
@@ -34,10 +34,11 @@ export function MobileBottomNav() {
|
|
|
34
34
|
<li key={item.target}>
|
|
35
35
|
<Link
|
|
36
36
|
to={item.target}
|
|
37
|
+
aria-label={item.label}
|
|
37
38
|
aria-current={active ? "page" : undefined}
|
|
38
39
|
data-testid={active ? "mobile-nav-active-indicator" : undefined}
|
|
39
40
|
className={cn(
|
|
40
|
-
"group flex min-h-
|
|
41
|
+
"group flex min-h-12 flex-col items-center justify-center gap-1 rounded-xl px-0.5 py-1 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
|
|
41
42
|
active
|
|
42
43
|
? "bg-muted text-foreground"
|
|
43
44
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
@@ -46,7 +47,7 @@ export function MobileBottomNav() {
|
|
|
46
47
|
<span className="relative">
|
|
47
48
|
<item.icon
|
|
48
49
|
className={cn(
|
|
49
|
-
"h-
|
|
50
|
+
"h-5 w-5",
|
|
50
51
|
active ? "text-foreground" : "text-muted-foreground",
|
|
51
52
|
)}
|
|
52
53
|
/>
|
|
@@ -54,7 +55,7 @@ export function MobileBottomNav() {
|
|
|
54
55
|
<span className="absolute -right-1 -top-1 h-1.5 w-1.5 rounded-full bg-primary ring-1 ring-background" />
|
|
55
56
|
) : null}
|
|
56
57
|
</span>
|
|
57
|
-
<span className="max-w-full truncate">{item.label}</span>
|
|
58
|
+
<span className="max-w-full truncate">{item.target === "/agents" ? t("mobileAgents") : item.label}</span>
|
|
58
59
|
</Link>
|
|
59
60
|
</li>
|
|
60
61
|
);
|
|
@@ -63,9 +64,9 @@ export function MobileBottomNav() {
|
|
|
63
64
|
<button
|
|
64
65
|
type="button"
|
|
65
66
|
onClick={() => openApps(docBrowser)}
|
|
66
|
-
className="group flex min-h-
|
|
67
|
+
className="group flex min-h-12 w-full flex-col items-center justify-center gap-1 rounded-xl px-0.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
67
68
|
>
|
|
68
|
-
<Boxes className="h-
|
|
69
|
+
<Boxes className="h-5 w-5 text-muted-foreground" />
|
|
69
70
|
<span className="max-w-full truncate">{t("appsTitle")}</span>
|
|
70
71
|
</button>
|
|
71
72
|
</li>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NavLink } from "react-router-dom";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
2
3
|
import { getSettingsNavSections } from "@/app/configs/app-navigation.config";
|
|
3
4
|
import { t } from "@/shared/lib/i18n";
|
|
4
5
|
import { cn } from "@/shared/lib/utils";
|
|
5
6
|
import { useDesktopCapabilityAvailability } from "@/features/desktop-capabilities";
|
|
7
|
+
import { SettingsGroup } from "@/shared/components/settings/setting-row";
|
|
6
8
|
|
|
7
9
|
export function MobileSettingsShell() {
|
|
8
10
|
const desktopCapabilityAvailable = useDesktopCapabilityAvailability();
|
|
@@ -19,29 +21,28 @@ export function MobileSettingsShell() {
|
|
|
19
21
|
{settingsNavSections.map((section) => (
|
|
20
22
|
<section key={section.label} className="space-y-2">
|
|
21
23
|
<h2 className="px-1 pt-2 text-xs font-medium text-muted-foreground">{section.label}</h2>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
))}
|
|
24
|
+
<SettingsGroup>
|
|
25
|
+
{section.items.map((item) => (
|
|
26
|
+
<NavLink
|
|
27
|
+
key={item.target}
|
|
28
|
+
to={item.target}
|
|
29
|
+
className={({ isActive }) => cn(
|
|
30
|
+
"flex min-h-14 items-center gap-3 px-4 py-3 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-border",
|
|
31
|
+
isActive
|
|
32
|
+
? "bg-[var(--interaction-selection)]"
|
|
33
|
+
: "hover:bg-[var(--interaction-hover)] active:bg-[var(--interaction-hover)]",
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center text-muted-foreground">
|
|
37
|
+
<item.icon className="h-5 w-5" />
|
|
38
|
+
</div>
|
|
39
|
+
<div className="min-w-0 flex-1">
|
|
40
|
+
<p className="text-[15px] font-medium text-foreground">{item.label}</p>
|
|
41
|
+
</div>
|
|
42
|
+
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground/60" aria-hidden="true" />
|
|
43
|
+
</NavLink>
|
|
44
|
+
))}
|
|
45
|
+
</SettingsGroup>
|
|
45
46
|
</section>
|
|
46
47
|
))}
|
|
47
48
|
</div>
|
|
@@ -2,6 +2,7 @@ import { ArrowLeft } from "lucide-react";
|
|
|
2
2
|
import { useNavigate, useLocation } from "react-router-dom";
|
|
3
3
|
import { resolveMobileRouteMeta } from "@/app/configs/app-navigation.config";
|
|
4
4
|
import { t } from "@/shared/lib/i18n";
|
|
5
|
+
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
5
6
|
|
|
6
7
|
export function MobileTopbar({ leadingInset }: { leadingInset?: string }) {
|
|
7
8
|
const navigate = useNavigate();
|
|
@@ -11,31 +12,29 @@ export function MobileTopbar({ leadingInset }: { leadingInset?: string }) {
|
|
|
11
12
|
return (
|
|
12
13
|
<header
|
|
13
14
|
data-testid="mobile-topbar"
|
|
14
|
-
className="shrink-0 border-b border-border/60 bg-
|
|
15
|
+
className="shrink-0 border-b border-border/60 bg-background text-foreground"
|
|
15
16
|
style={{
|
|
16
|
-
paddingTop: "
|
|
17
|
+
paddingTop: "env(safe-area-inset-top, 0px)",
|
|
17
18
|
}}
|
|
18
19
|
>
|
|
19
20
|
<div
|
|
20
|
-
className="flex
|
|
21
|
+
className="flex h-[var(--mobile-header-height)] items-center gap-2 pl-3 pr-3"
|
|
21
22
|
style={{ paddingLeft: leadingInset ? `calc(${leadingInset} + 0.75rem)` : undefined }}
|
|
22
23
|
>
|
|
23
24
|
{routeMeta.backTarget ? (
|
|
24
|
-
<
|
|
25
|
-
|
|
25
|
+
<IconActionButton
|
|
26
|
+
size="lg"
|
|
26
27
|
onClick={() => navigate(routeMeta.backTarget as string)}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<ArrowLeft className="h-4 w-4" />
|
|
31
|
-
</button>
|
|
28
|
+
label={routeMeta.backLabel ?? t("backToMain")}
|
|
29
|
+
icon={<ArrowLeft className="h-4 w-4" />}
|
|
30
|
+
/>
|
|
32
31
|
) : null}
|
|
33
32
|
<div className="min-w-0 flex-1">
|
|
34
33
|
<h1 className="truncate text-center text-[17px] font-semibold text-foreground">
|
|
35
34
|
{routeMeta.title}
|
|
36
35
|
</h1>
|
|
37
36
|
</div>
|
|
38
|
-
{routeMeta.backTarget ? <div className="
|
|
37
|
+
{routeMeta.backTarget ? <div className="w-[var(--icon-control-size)] shrink-0" aria-hidden="true" /> : null}
|
|
39
38
|
</div>
|
|
40
39
|
</header>
|
|
41
40
|
);
|
|
@@ -545,7 +545,8 @@ describe("DocBrowser panel app navigation", () => {
|
|
|
545
545
|
|
|
546
546
|
expect(screen.queryByRole("button", { name: "Pin to left sidebar" })).toBeNull();
|
|
547
547
|
await user.click(screen.getByRole("button", { name: "More panel app actions" }));
|
|
548
|
-
await user.click(screen.getByRole("
|
|
548
|
+
await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
|
|
549
|
+
await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
|
|
549
550
|
expect(panelAppHooks.mutate).toHaveBeenCalledWith({
|
|
550
551
|
id: "piano",
|
|
551
552
|
preferences: { mainSidebar: true },
|
|
@@ -571,7 +572,8 @@ describe("DocBrowser panel app navigation", () => {
|
|
|
571
572
|
render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
|
|
572
573
|
|
|
573
574
|
await user.click(screen.getByRole("button", { name: "More panel app actions" }));
|
|
574
|
-
await user.click(screen.getByRole("
|
|
575
|
+
await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
|
|
576
|
+
await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
|
|
575
577
|
expect(panelAppHooks.mutate).toHaveBeenCalledWith({
|
|
576
578
|
id: "piano",
|
|
577
579
|
preferences: { mainSidebar: true },
|
|
@@ -651,6 +653,36 @@ describe("DocBrowser floating interactions", () => {
|
|
|
651
653
|
installDocBrowserPointerTestEnvironment();
|
|
652
654
|
});
|
|
653
655
|
|
|
656
|
+
it("keeps compact navigation, tab selection, resource and app actions reachable", () => {
|
|
657
|
+
const onBack = vi.fn();
|
|
658
|
+
const onSelect = vi.fn();
|
|
659
|
+
const onClose = vi.fn();
|
|
660
|
+
const onAdd = vi.fn();
|
|
661
|
+
const onRefresh = vi.fn();
|
|
662
|
+
render(<DocBrowserTabStrip compact tabs={docBrowserState.tabs} activeTabId="docs"
|
|
663
|
+
canGoBack canGoForward={false} onGoBack={onBack} onGoForward={vi.fn()}
|
|
664
|
+
onOpenNewTab={vi.fn()} onSetActiveTab={onSelect} onCloseTab={onClose}
|
|
665
|
+
mobileToolbar={<button onClick={onRefresh}>Refresh app</button>}
|
|
666
|
+
getTabMenuGroups={() => [{ key: 'chat', items: [{ key: 'add', label: 'Add to Chat', onSelect: onAdd }] }]}
|
|
667
|
+
/>);
|
|
668
|
+
const open = () => fireEvent.click(screen.getByRole('button', { name: 'More tab actions' }));
|
|
669
|
+
expect(screen.queryByRole('button', { name: 'Back' })).toBeNull();
|
|
670
|
+
open();
|
|
671
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Back' }));
|
|
672
|
+
expect(onBack).toHaveBeenCalledOnce();
|
|
673
|
+
open();
|
|
674
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Docs' }));
|
|
675
|
+
expect(onSelect).toHaveBeenCalledWith('docs');
|
|
676
|
+
open();
|
|
677
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Add to Chat' }));
|
|
678
|
+
expect(onAdd).toHaveBeenCalledOnce();
|
|
679
|
+
open();
|
|
680
|
+
fireEvent.click(screen.getByRole('button', { name: 'Refresh app' }));
|
|
681
|
+
expect(onRefresh).toHaveBeenCalledOnce();
|
|
682
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Close tab' }));
|
|
683
|
+
expect(onClose).toHaveBeenCalledWith('docs');
|
|
684
|
+
});
|
|
685
|
+
|
|
654
686
|
it("drags the shared header without stealing tab actions", () => {
|
|
655
687
|
new WorkbenchSurfaceManager().place('global-resources', 'floating');
|
|
656
688
|
render(<DocBrowser />);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { PageResourceIcon } from '@/features/right-panel-resources';
|
|
2
|
+
import { useState, type ReactNode } from 'react';
|
|
3
|
+
import { ChevronDown } from 'lucide-react';
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
5
|
+
import { ContextMenuItems } from '@/shared/components/ui/context-menu/context-menu';
|
|
2
6
|
import {
|
|
3
7
|
ArrowLeft,
|
|
4
8
|
ArrowRight,
|
|
@@ -16,6 +20,8 @@ import {
|
|
|
16
20
|
import { t } from "@/shared/lib/i18n";
|
|
17
21
|
|
|
18
22
|
type DocBrowserTabStripProps = {
|
|
23
|
+
compact?: boolean;
|
|
24
|
+
mobileToolbar?: ReactNode;
|
|
19
25
|
tabs: DocBrowserTab[];
|
|
20
26
|
activeTabId: string;
|
|
21
27
|
canGoBack: boolean;
|
|
@@ -47,7 +53,10 @@ export function DocBrowserTabStrip({
|
|
|
47
53
|
onSetActiveTab,
|
|
48
54
|
onCloseTab,
|
|
49
55
|
getTabMenuGroups,
|
|
56
|
+
mobileToolbar,
|
|
57
|
+
compact = false,
|
|
50
58
|
}: DocBrowserTabStripProps) {
|
|
59
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
51
60
|
const backLabel = t("docBrowserBack");
|
|
52
61
|
const closeTabLabel = t("workbenchCloseTab");
|
|
53
62
|
const dockLabel = dockState?.isDocked
|
|
@@ -87,18 +96,40 @@ export function DocBrowserTabStrip({
|
|
|
87
96
|
: []),
|
|
88
97
|
];
|
|
89
98
|
|
|
99
|
+
const current = tabs.find(tab => tab.id === activeTabId);
|
|
100
|
+
const mobileGroups: ContextMenuGroup[] = [
|
|
101
|
+
{ key: 'tabs', items: compactTabs.map(tab => ({ key: tab.key, label: tab.label, icon: tab.leadingIcon, pressed: tab.active, onSelect: tab.onSelect })) },
|
|
102
|
+
{ key: 'navigation', items: actions.map(action => ({ key: action.key, label: action.label, icon: action.icon, disabled: action.disabled, onSelect: action.onClick })) },
|
|
103
|
+
...(current ? getTabMenuGroups?.(current) ?? [] : []),
|
|
104
|
+
...(current ? [{ key: 'close-tab', items: [{ key: 'close-tab', label: closeTabLabel, onSelect: () => onCloseTab(current.id) }] }] : []),
|
|
105
|
+
];
|
|
106
|
+
if (compact) return (
|
|
107
|
+
<div className="flex h-12 min-w-0 flex-1 items-center">
|
|
108
|
+
<Popover open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
|
109
|
+
<PopoverTrigger asChild>
|
|
110
|
+
<button type="button" className="flex h-8 min-w-0 items-center gap-2 rounded-lg px-2 text-sm font-medium hover:bg-[var(--interaction-hover)]" aria-label={t('docBrowserTabMoreActions')}>
|
|
111
|
+
{current ? <DocBrowserTabIcon tab={current} /> : null}
|
|
112
|
+
<span className="truncate">{current?.title || t('docBrowserTabUntitled')}</span><ChevronDown className="h-4 w-4 shrink-0" />
|
|
113
|
+
</button>
|
|
114
|
+
</PopoverTrigger>
|
|
115
|
+
<PopoverContent align="start" className="max-h-[70dvh] w-72 overflow-y-auto p-1">
|
|
116
|
+
<ContextMenuItems groups={mobileGroups} onClose={() => setMobileMenuOpen(false)} />
|
|
117
|
+
{mobileToolbar}
|
|
118
|
+
</PopoverContent>
|
|
119
|
+
</Popover>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
90
122
|
return (
|
|
91
123
|
<CompactTabStrip
|
|
92
124
|
testId="doc-browser-tab-strip"
|
|
93
125
|
actionsTestId="doc-browser-tab-actions"
|
|
94
126
|
tabs={compactTabs}
|
|
95
127
|
actions={actions}
|
|
96
|
-
className="h-10 min-w-0 flex-1 gap-1 border-0 bg-transparent px-0 select-none"
|
|
97
|
-
scrollClassName="doc-browser-tab-scrollbar flex h-full items-center gap-0.5"
|
|
128
|
+
className="flex h-10 min-w-0 flex-1 gap-1 border-0 bg-transparent px-0 select-none"
|
|
129
|
+
scrollClassName="doc-browser-tab-scrollbar flex h-full max-md:h-11 max-md:basis-full items-center gap-0.5"
|
|
98
130
|
tabsClassName="items-center gap-0.5"
|
|
99
|
-
actionsClassName="h-full items-center gap-0.5"
|
|
100
|
-
|
|
101
|
-
tabBaseClassName="inline-flex cursor-pointer items-center gap-0.5 h-7 px-2 rounded-md text-xs max-w-[220px] shrink-0 transition-colors"
|
|
131
|
+
actionsClassName="h-full max-md:h-11 max-md:w-full items-center gap-0.5"
|
|
132
|
+
tabBaseClassName="inline-flex cursor-pointer items-center gap-0.5 h-7 max-md:min-h-11 px-2 rounded-md text-xs max-w-[220px] shrink-0 transition-colors"
|
|
102
133
|
activeTabClassName="bg-muted/80 text-foreground"
|
|
103
134
|
inactiveTabClassName="text-muted-foreground hover:bg-muted/45 hover:text-foreground"
|
|
104
135
|
labelClassName="px-0.5 text-xs font-normal"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
|
|
2
3
|
import {
|
|
3
4
|
DOC_BROWSER_HOME_TAB_KIND,
|
|
4
5
|
getDefaultDocsUrl,
|
|
@@ -258,6 +259,7 @@ export function DocBrowser({
|
|
|
258
259
|
|
|
259
260
|
|
|
260
261
|
const isFullscreen = displayMode === 'fullscreen';
|
|
262
|
+
const { isMobile } = useViewportLayout();
|
|
261
263
|
const customRenderParams = currentTab ? {
|
|
262
264
|
currentUrl,
|
|
263
265
|
open,
|
|
@@ -284,7 +286,7 @@ export function DocBrowser({
|
|
|
284
286
|
urlInput={urlInput}
|
|
285
287
|
/>
|
|
286
288
|
|
|
287
|
-
{customToolbar}
|
|
289
|
+
{!isMobile ? customToolbar : null}
|
|
288
290
|
|
|
289
291
|
<DocBrowserFrameContent
|
|
290
292
|
currentTab={currentTab}
|
|
@@ -310,6 +312,8 @@ export function DocBrowser({
|
|
|
310
312
|
onWidthCommit={setDockedWidth} onClose={close} closeLabel={t('workbenchHideGroup')}
|
|
311
313
|
hidden={!isOpen} fullscreen={isFullscreen} navigation={
|
|
312
314
|
<DocBrowserTabStrip
|
|
315
|
+
mobileToolbar={customToolbar}
|
|
316
|
+
compact={isMobile}
|
|
313
317
|
tabs={tabs}
|
|
314
318
|
activeTabId={activeTabId}
|
|
315
319
|
canGoBack={canGoBack}
|
|
@@ -27,7 +27,7 @@ export function SettingsPage({
|
|
|
27
27
|
className
|
|
28
28
|
)}
|
|
29
29
|
>
|
|
30
|
-
<PageHeader title={title} description={description} actions={actions} />
|
|
30
|
+
<PageHeader title={title} description={description} actions={actions} mobileTitleInShell />
|
|
31
31
|
{children}
|
|
32
32
|
</PageLayout>
|
|
33
33
|
);
|
|
@@ -17,7 +17,7 @@ describe('IconActionButton', () => {
|
|
|
17
17
|
expect(button.className).toContain('h-6');
|
|
18
18
|
expect(button.className).toContain('w-6');
|
|
19
19
|
expect(button.className).toContain('p-1');
|
|
20
|
-
expect(button.className).toContain('hover:bg-
|
|
20
|
+
expect(button.className).toContain('hover:before:bg-[var(--interaction-hover)]');
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it('defaults to medium size and soft accent hover', () => {
|
|
@@ -31,7 +31,7 @@ describe('IconActionButton', () => {
|
|
|
31
31
|
const button = screen.getByRole('button', { name: 'More' });
|
|
32
32
|
expect(button.className).toContain('h-7');
|
|
33
33
|
expect(button.className).toContain('w-7');
|
|
34
|
-
expect(button.className).toContain('hover:bg-[var(--interaction-hover)]');
|
|
34
|
+
expect(button.className).toContain('hover:before:bg-[var(--interaction-hover)]');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it('uses surface-relative feedback without introducing a new surface', () => {
|
|
@@ -44,9 +44,9 @@ describe('IconActionButton', () => {
|
|
|
44
44
|
);
|
|
45
45
|
|
|
46
46
|
const button = screen.getByRole('button', { name: 'Add project' });
|
|
47
|
-
expect(button.className).toContain('hover:bg-
|
|
48
|
-
expect(button.className).toContain('hover:text-
|
|
49
|
-
expect(button.className).toContain('active:bg-
|
|
47
|
+
expect(button.className).toContain('hover:before:bg-[var(--interaction-hover)]');
|
|
48
|
+
expect(button.className).toContain('hover:text-accent-foreground');
|
|
49
|
+
expect(button.className).toContain('active:before:bg-[var(--interaction-hover)]');
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('ignores restored pointer focus while preserving keyboard tooltip focus', () => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Neutral actions share one theme color across icon and menu surfaces. */
|
|
2
|
+
export const ACTION_MENU_ITEM_CLASS = 'flex min-h-8 w-full items-center gap-2 rounded-lg px-2.5 py-1 text-left text-[13px] leading-5 font-medium outline-none transition-colors disabled:pointer-events-none disabled:opacity-45';
|
|
3
|
+
|
|
4
|
+
export const ACTION_FEEDBACK = {
|
|
5
|
+
icon: 'hover:before:bg-[var(--interaction-hover)] active:before:bg-[var(--interaction-hover)] disabled:hover:before:bg-transparent disabled:active:before:bg-transparent',
|
|
6
|
+
item: 'hover:bg-[var(--interaction-hover)] focus-visible:bg-[var(--interaction-hover)] active:bg-[var(--interaction-hover)] disabled:hover:bg-transparent disabled:active:bg-transparent',
|
|
7
|
+
destructive: 'text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10 disabled:hover:bg-transparent',
|
|
8
|
+
} as const;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
TooltipTrigger,
|
|
7
7
|
} from '@/shared/components/ui/tooltip';
|
|
8
8
|
import { cn } from '@/shared/lib/utils';
|
|
9
|
+
import { ACTION_FEEDBACK } from './action-feedback';
|
|
9
10
|
|
|
10
11
|
type IconActionButtonSize = 'sm' | 'md' | 'lg';
|
|
11
12
|
type IconActionButtonTone = 'default' | 'surface' | 'strong';
|
|
@@ -17,7 +18,7 @@ type IconActionButtonProps = Omit<
|
|
|
17
18
|
icon: React.ReactNode;
|
|
18
19
|
label: string;
|
|
19
20
|
size?: IconActionButtonSize;
|
|
20
|
-
/**
|
|
21
|
+
/** @deprecated All neutral icon actions now share the theme feedback color. */
|
|
21
22
|
tone?: IconActionButtonTone;
|
|
22
23
|
tooltip?: string | false | null;
|
|
23
24
|
tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
@@ -29,14 +30,9 @@ const SIZE_CLASS: Record<IconActionButtonSize, string> = {
|
|
|
29
30
|
lg: 'h-8 w-8 rounded-lg p-1.5',
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
surface:
|
|
36
|
-
'text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900 active:bg-gray-200/80 disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
|
|
37
|
-
strong:
|
|
38
|
-
'text-muted-foreground hover:bg-black/10 hover:text-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
|
|
39
|
-
};
|
|
33
|
+
export function IconActionGroup({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
34
|
+
return <div {...props} className={cn('flex shrink-0 items-center gap-1 max-md:gap-0', className)}>{children}</div>;
|
|
35
|
+
}
|
|
40
36
|
|
|
41
37
|
const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonProps>(
|
|
42
38
|
(
|
|
@@ -60,14 +56,19 @@ const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonPro
|
|
|
60
56
|
type="button"
|
|
61
57
|
disabled={disabled}
|
|
62
58
|
aria-label={label}
|
|
59
|
+
data-tone={tone}
|
|
63
60
|
className={cn(
|
|
64
|
-
'inline-flex shrink-0 items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed',
|
|
61
|
+
'relative inline-flex shrink-0 items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed',
|
|
65
62
|
SIZE_CLASS[size],
|
|
66
|
-
|
|
63
|
+
'text-muted-foreground hover:text-accent-foreground disabled:text-muted-foreground/45 disabled:hover:text-muted-foreground/45',
|
|
64
|
+
ACTION_FEEDBACK.icon,
|
|
65
|
+
size === 'sm' ? 'max-md:h-8 max-md:w-8' : 'max-md:h-[var(--icon-control-size)] max-md:w-[var(--icon-control-size)]',
|
|
66
|
+
"before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:transition-colors before:content-[''] max-md:before:inset-auto max-md:before:h-[var(--icon-feedback-size)] max-md:before:w-[var(--icon-feedback-size)]",
|
|
67
|
+
size === 'sm' && 'max-md:before:h-7 max-md:before:w-7',
|
|
67
68
|
className
|
|
68
69
|
)}
|
|
69
70
|
>
|
|
70
|
-
{icon}
|
|
71
|
+
<span className="relative inline-flex items-center justify-center">{icon}</span>
|
|
71
72
|
</button>
|
|
72
73
|
);
|
|
73
74
|
const content = tooltip === false ? null : tooltip ?? label;
|
|
@@ -42,7 +42,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
42
42
|
({ className, variant, size, ...props }, ref) => {
|
|
43
43
|
return (
|
|
44
44
|
<button
|
|
45
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
45
|
+
className={cn(buttonVariants({ variant, size, className }), size !== 'icon' && 'max-md:min-h-[var(--control-touch-size)]')}
|
|
46
46
|
ref={ref}
|
|
47
47
|
{...props}
|
|
48
48
|
/>
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"
|
|
|
2
2
|
import * as Popover from "@radix-ui/react-popover";
|
|
3
3
|
import { ChevronRight } from "lucide-react";
|
|
4
4
|
import { cn } from "@/shared/lib/utils";
|
|
5
|
+
import { ACTION_FEEDBACK, ACTION_MENU_ITEM_CLASS } from '../actions/action-feedback';
|
|
5
6
|
import type { ContextMenuGroup, ContextMenuItem } from "./context-menu";
|
|
6
7
|
|
|
7
8
|
export const CONTEXT_MENU_SURFACE_CLASS =
|
|
@@ -59,7 +60,7 @@ function ContextSubmenu({ item, open, onOpenChange, onClose, className, children
|
|
|
59
60
|
aria-expanded={open}
|
|
60
61
|
disabled={item.disabled}
|
|
61
62
|
data-menu-key={item.key}
|
|
62
|
-
className={cn(className, open && "bg-
|
|
63
|
+
className={cn(className, open && "bg-[var(--interaction-hover)]")}
|
|
63
64
|
onPointerEnter={(event) => {
|
|
64
65
|
if (event.pointerType === "touch") return;
|
|
65
66
|
keyboardOpen.current = false;
|
|
@@ -154,8 +155,8 @@ export function ContextMenuItems({ groups, onClose }: {
|
|
|
154
155
|
{index > 0 ? <div className="my-1 h-px bg-border" /> : null}
|
|
155
156
|
{group.items.map((item) => {
|
|
156
157
|
const className = cn(
|
|
157
|
-
|
|
158
|
-
item.destructive ?
|
|
158
|
+
ACTION_MENU_ITEM_CLASS,
|
|
159
|
+
item.destructive ? ACTION_FEEDBACK.destructive : `text-foreground ${ACTION_FEEDBACK.item}`,
|
|
159
160
|
);
|
|
160
161
|
const content = <MenuItemContent item={item} />;
|
|
161
162
|
if (item.children) return <ContextSubmenu key={item.key} item={item} open={openKey === item.key}
|