Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 122 additions & 21 deletions design-system/apps/design-lab/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type LabRoute =
| { page: "tokens" }
| { componentName: string; page: "component" };

type ComponentNavGroup = "components" | "flow-chat";
type ComponentNavGroupState = Record<ComponentNavGroup, boolean>;

interface SearchDestination {
detail: string;
icon: LucideIcon | IconName;
Expand All @@ -82,9 +85,32 @@ const flowChatComponents = componentRegistry.filter(
(component) => component.category === "flow-chat",
);
const standardComponents = componentRegistry.filter(
(component) => component.category !== "flow-chat",
(component) => component.category !== "flow-chat" && component.name !== "Icon",
);

function getComponentNavGroup(route: LabRoute): ComponentNavGroup | undefined {
if (route.page === "components" || route.page === "flow-chat") {
return route.page;
}
if (route.page !== "component" || route.componentName === "Icon") {
return undefined;
}
const component = componentRegistry.find(
(candidate) => candidate.name === route.componentName,
);
return component?.category === "flow-chat" ? "flow-chat" : "components";
}

function revealComponentNavGroup(
current: ComponentNavGroupState,
route: LabRoute,
): ComponentNavGroupState {
const group = getComponentNavGroup(route);
return group && !current[group]
? { ...current, [group]: true }
: current;
}

function getThemeDataName(
colorScheme: ColorScheme,
contrast: ContrastMode,
Expand Down Expand Up @@ -145,6 +171,13 @@ export function App() {
const [contrast, setContrast] = useState<ContrastMode>("standard");
const [density, setDensity] = useState<DensityMode>("comfortable");
const [route, setRoute] = useState<LabRoute>(() => parseRoute(window.location.hash));
const [expandedComponentGroups, setExpandedComponentGroups] = useState<ComponentNavGroupState>(() => {
const initialGroup = getComponentNavGroup(route);
return {
components: initialGroup === "components",
"flow-chat": initialGroup === "flow-chat",
};
});
const [componentScope, setComponentScope] = useState("all");
const [drafts, setDrafts] = useState<TokenDrafts>(loadTokenDrafts);
const [searchQuery, setSearchQuery] = useState("");
Expand Down Expand Up @@ -253,7 +286,9 @@ export function App() {

useEffect(() => {
function syncRoute() {
setRoute(parseRoute(window.location.hash));
const nextRoute = parseRoute(window.location.hash);
setRoute(nextRoute);
setExpandedComponentGroups((current) => revealComponentNavGroup(current, nextRoute));
setSidebarOpen(false);
window.scrollTo({ top: 0 });
}
Expand Down Expand Up @@ -292,8 +327,11 @@ export function App() {
return () => window.removeEventListener("pointerdown", handlePointerDown);
}, [settingsOpen]);

function navigate(nextRoute: LabRoute) {
function navigate(nextRoute: LabRoute, { revealGroup = true } = {}) {
const nextHash = routeHash(nextRoute);
if (revealGroup) {
setExpandedComponentGroups((current) => revealComponentNavGroup(current, nextRoute));
}
setSearchOpen(false);
setSearchQuery("");
setSidebarOpen(false);
Expand Down Expand Up @@ -359,8 +397,9 @@ export function App() {
: undefined;
const isFlowChatRoute = route.page === "flow-chat"
|| activeComponent?.category === "flow-chat";
const isIconRoute = activeComponent?.name === "Icon";
const isStandardComponentRoute = route.page === "components"
|| Boolean(activeComponent && activeComponent.category !== "flow-chat");
|| Boolean(activeComponent && activeComponent.category !== "flow-chat" && !isIconRoute);

return (
<ThemeRoot
Expand Down Expand Up @@ -440,29 +479,44 @@ export function App() {

<span className="lab-nav-label">{t("nav.library")}</span>
<a
aria-controls="lab-standard-component-links"
aria-current={isStandardComponentRoute ? "page" : undefined}
data-expanded={isStandardComponentRoute || undefined}
aria-expanded={expandedComponentGroups.components}
data-expanded={expandedComponentGroups.components || undefined}
href="#components"
id="lab-standard-components-trigger"
onClick={(event) => {
event.preventDefault();
navigate({ page: "components" });
const nextExpanded = !expandedComponentGroups.components;
setExpandedComponentGroups((current) => ({
...current,
components: nextExpanded,
}));
if (nextExpanded && !isStandardComponentRoute) {
navigate({ page: "components" }, { revealGroup: false });
}
}}
>
<Blocks aria-hidden="true" size={17} />
<span>{t("nav.components")}</span>
<span className="lab-nav-group-meta">
<small>{standardComponents.length}</small>
<CatalogIcon
aria-hidden="true"
className="lab-nav-group-chevron"
data-expanded={expandedComponentGroups.components || undefined}
name="chevron-right"
size="sm"
/>
</span>
</a>
<a
aria-current={route.page === "patterns" ? "page" : undefined}
href="#patterns"
onClick={(event) => {
event.preventDefault();
navigate({ page: "patterns" });
}}
<div
aria-labelledby="lab-standard-components-trigger"
className="lab-component-links"
hidden={!expandedComponentGroups.components}
id="lab-standard-component-links"
role="group"
>
<PanelsTopLeft aria-hidden="true" size={17} />
<span>{t("nav.patterns")}</span>
</a>
<div className="lab-component-links">
{standardComponents.map((component) => {
const Icon = componentIcons[component.name] ?? Blocks;
const active = route.page === "component" && route.componentName === component.name;
Expand All @@ -483,19 +537,66 @@ export function App() {
})}
</div>
<a
aria-current={isIconRoute ? "page" : undefined}
href="#component/icon"
onClick={(event) => {
event.preventDefault();
navigate({ componentName: "Icon", page: "component" });
}}
>
<CatalogIcon aria-hidden="true" name="spark" size="md" style={{ width: 17, height: 17 }} />
<span>Icon</span>
</a>
<a
aria-current={route.page === "patterns" ? "page" : undefined}
href="#patterns"
onClick={(event) => {
event.preventDefault();
navigate({ page: "patterns" });
}}
>
<PanelsTopLeft aria-hidden="true" size={17} />
<span>{t("nav.patterns")}</span>
</a>
<a
aria-controls="lab-flow-chat-component-links"
aria-current={isFlowChatRoute ? "page" : undefined}
data-expanded={isFlowChatRoute || undefined}
aria-expanded={expandedComponentGroups["flow-chat"]}
data-expanded={expandedComponentGroups["flow-chat"] || undefined}
href="#flow-chat"
id="lab-flow-chat-trigger"
onClick={(event) => {
event.preventDefault();
navigate({ page: "flow-chat" });
const nextExpanded = !expandedComponentGroups["flow-chat"];
setExpandedComponentGroups((current) => ({
...current,
"flow-chat": nextExpanded,
}));
if (nextExpanded && !isFlowChatRoute) {
navigate({ page: "flow-chat" }, { revealGroup: false });
}
}}
>
<SquareTerminal aria-hidden="true" size={17} />
<span>{t("nav.flowChat")}</span>
<small>{flowChatComponents.length}</small>
<span className="lab-nav-group-meta">
<small>{flowChatComponents.length}</small>
<CatalogIcon
aria-hidden="true"
className="lab-nav-group-chevron"
data-expanded={expandedComponentGroups["flow-chat"] || undefined}
name="chevron-right"
size="sm"
/>
</span>
</a>
<div className="lab-component-links">
<div
aria-labelledby="lab-flow-chat-trigger"
className="lab-component-links"
hidden={!expandedComponentGroups["flow-chat"]}
id="lab-flow-chat-component-links"
role="group"
>
{flowChatComponents.map((component) => {
const Icon = componentIcons[component.name] ?? SquareTerminal;
const active = route.page === "component" && route.componentName === component.name;
Expand Down
21 changes: 20 additions & 1 deletion design-system/apps/design-lab/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,30 @@ body,
text-transform: capitalize;
}

.lab-nav-group-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.lab-nav-group-chevron {
transition: transform 150ms ease;
}

.lab-nav-group-chevron[data-expanded] {
transform: rotate(90deg);
}

.lab-component-links {
display: grid;
gap: 2px;
padding: 2px 0 2px 19px;
}

.lab-component-links[hidden] {
display: none;
}

.lab-component-links a {
min-height: 34px;
padding-block: 5px;
Expand Down Expand Up @@ -2401,7 +2419,8 @@ input.lab-force-focus {

.lab-sidebar,
.lab-sidebar-backdrop,
.component-card {
.component-card,
.lab-nav-group-chevron {
transition: none;
}
}
Expand Down
17 changes: 17 additions & 0 deletions design-system/apps/design-lab/vite/icon-consumers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@ test("Lab navigation, theme resources and export actions consume the shared icon
assert.match(workbench, /<Icon name="arrow-down" size="sm"/);
assert.doesNotMatch(workbench, /\bDownload\b/);
});

test("Icon is a standalone navigation item and component groups are collapsible", async () => {
const source = relative => readFile(new URL(`../src/${relative}`, import.meta.url), "utf8");
const [app, styles] = await Promise.all([
source("App.tsx"),
source("styles.css"),
]);

assert.match(app, /component\.category !== "flow-chat" && component\.name !== "Icon"/);
assert.match(app, /href="#component\/icon"/);
assert.match(app, /aria-controls="lab-standard-component-links"/);
assert.match(app, /aria-controls="lab-flow-chat-component-links"/);
assert.match(app, /hidden=\{!expandedComponentGroups\.components\}/);
assert.match(app, /hidden=\{!expandedComponentGroups\["flow-chat"\]\}/);
assert.match(styles, /\.lab-nav-group-chevron\[data-expanded\]/);
assert.match(styles, /\.lab-component-links\[hidden\]/);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2691,7 +2691,7 @@
"viewId": "shortcuts"
},
"evidence": [
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#checkConflicts",
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#buildFinalConflictMap",
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#buildStoredKeybindings({})"
]
},
Expand Down
2 changes: 1 addition & 1 deletion src/shared/interactive-capabilities/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -5929,7 +5929,7 @@
"reasonEn": "“Record new shortcuts, detect conflicts, revert one binding, or reset all custom bindings” spans multiple live-state-dependent steps and currently has no single structured Command that can deterministically complete the whole workflow; the Agent opens the exact entry and keeps the remaining interaction visible to the user."
},
"evidence": [
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#checkConflicts",
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#buildFinalConflictMap",
"source:src/web-ui/src/app/scenes/settings/components/KeyboardShortcutsTab.tsx#buildStoredKeybindings({})"
]
}
Expand Down
3 changes: 2 additions & 1 deletion src/web-ui/src/app/components/AboutDialog/AboutDialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@
font-weight: var(--bf-type-label-sm-font-weight);
line-height: var(--bf-type-modifier-leading-ui-line-height);

svg {
svg,
[data-bf-component='icon'] {
flex: 0 0 auto;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@bitfun/ui';
import React, { useCallback, useEffect, useState } from 'react';
import { useI18n } from '@/infrastructure/i18n';
import { CalendarDays, Code2, ShieldCheck, Sparkle, Tag } from 'lucide-react';
import { CalendarDays, Code2, ShieldCheck, Tag } from 'lucide-react';
import {
formatBuildDate,
formatDisplayedVersion,
Expand Down Expand Up @@ -470,7 +470,7 @@ export const AboutDialog: React.FC<AboutDialogProps> = ({
<div className="bitfun-about-dialog__star-copy">
<h2 id="bitfun-about-star-title" className="bitfun-about-dialog__star-title">
<span>{t('about.githubStarTitle')}</span>
<Sparkle size={13} fill="currentColor" aria-hidden="true" />
<Icon name="spark" size="lg" style={{ width: 13, height: 13 }} aria-hidden="true" />
</h2>
<p className="bitfun-about-dialog__star-description">
{t('about.githubStarDescription')}
Expand Down
3 changes: 3 additions & 0 deletions src/web-ui/src/app/components/NavBar/NavBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
&--collapsed {
justify-content: flex-start;
padding-inline: 7px;
// The collapsed toolbar floats above the scene surface. Keep the sidebar
// recipe for the expanded navigation only so this does not paint a tile.
background: transparent;
}

&--macos {
Expand Down
4 changes: 2 additions & 2 deletions src/web-ui/src/app/components/NavPanel/MainNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from '@bitfun/ui';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime';
import { Plus, FolderOpen, FolderPlus, History, Users, Network } from 'lucide-react';
import { FolderOpen, FolderPlus, History, Users, Network } from 'lucide-react';
// import { PanelsTopLeft } from 'lucide-react'; // temporarily hidden: Pages nav entry
import { useSceneManager } from '../../hooks/useSceneManager';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
Expand Down Expand Up @@ -414,7 +414,7 @@ const MainNav: React.FC<MainNavProps> = ({
aria-label={createSessionLabel}
data-testid="nav-new-session-btn"
>
<Plus size={15} aria-hidden="true" />
<Icon name="plus" size="lg" style={{ width: 15, height: 15 }} aria-hidden="true" />
</button>
</Tooltip>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, { lazy, Suspense, useState, useCallback, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import {
PictureInPicture2,
} from 'lucide-react';

import {
Icon,
IconButton,
Expand Down Expand Up @@ -246,7 +244,7 @@ const PersistentFooterActions: React.FC = () => {
}}
>
<MenuItem
leading={<PictureInPicture2 size={14} aria-hidden="true" />}
leading={<Icon name="floating-window" size="sm" aria-hidden="true" />}
onClick={handleFloatingMode}
data-testid="nav-settings-floating-item"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ListFilter, RotateCcw } from 'lucide-react';
import { RotateCcw } from 'lucide-react';

import { useI18n } from '@/infrastructure/i18n';
import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost';
Expand Down Expand Up @@ -358,7 +358,7 @@ const WorkspaceSessionFilterMenu: React.FC = () => {
onClick={() => setOpen(current => !current)}
data-testid="nav-session-filter-btn"
>
<ListFilter size={13} />
<Icon name="filter" size="lg" style={{ width: 13, height: 13 }} />
</button>
</Tooltip>
{menu}
Expand Down
Loading