diff --git a/design-system/_ds_manifest.json b/design-system/_ds_manifest.json index d4ad86f7..52108b1c 100644 --- a/design-system/_ds_manifest.json +++ b/design-system/_ds_manifest.json @@ -37,10 +37,30 @@ "name": "DataTable", "sourcePath": "components/data/DataTable.jsx" }, + { + "name": "StatGroup", + "sourcePath": "components/data/StatGroup.jsx" + }, + { + "name": "Alert", + "sourcePath": "components/feedback/Alert.jsx" + }, + { + "name": "EmptyState", + "sourcePath": "components/feedback/EmptyState.jsx" + }, + { + "name": "Rating", + "sourcePath": "components/feedback/Rating.jsx" + }, { "name": "Skeleton", "sourcePath": "components/feedback/Skeleton.jsx" }, + { + "name": "Spinner", + "sourcePath": "components/feedback/Spinner.jsx" + }, { "name": "StatusPill", "sourcePath": "components/feedback/StatusPill.jsx" @@ -65,10 +85,18 @@ "name": "Textarea", "sourcePath": "components/forms/Textarea.jsx" }, + { + "name": "Toggle", + "sourcePath": "components/forms/Toggle.jsx" + }, { "name": "AppCard", "sourcePath": "components/launcher/AppCard.jsx" }, + { + "name": "AppTile", + "sourcePath": "components/launcher/AppTile.jsx" + }, { "name": "HealthDot", "sourcePath": "components/launcher/HealthDot.jsx" @@ -77,6 +105,22 @@ "name": "IntegrationBadge", "sourcePath": "components/launcher/IntegrationBadge.jsx" }, + { + "name": "CenteredCard", + "sourcePath": "components/layout/CenteredCard.jsx" + }, + { + "name": "Eyebrow", + "sourcePath": "components/layout/Eyebrow.jsx" + }, + { + "name": "InfoRow", + "sourcePath": "components/layout/InfoRow.jsx" + }, + { + "name": "SectionHead", + "sourcePath": "components/layout/SectionHead.jsx" + }, { "name": "BottomNav", "sourcePath": "components/mobile/BottomNav.jsx" @@ -89,10 +133,18 @@ "name": "Drawer", "sourcePath": "components/mobile/Drawer.jsx" }, + { + "name": "Pagination", + "sourcePath": "components/navigation/Pagination.jsx" + }, { "name": "Modal", "sourcePath": "components/overlay/Modal.jsx" }, + { + "name": "AnnouncementBar", + "sourcePath": "components/shell/AnnouncementBar.jsx" + }, { "name": "MenuItem", "sourcePath": "components/shell/MenuItem.jsx" @@ -195,16 +247,16 @@ { "path": "guidelines/mobile-layout.card.html", "group": "Mobile", - "viewport": "480x520", - "name": "Mobile Layout — BottomNav, Drawer, BottomSheet", - "subtitle": "xs/sm layout primitives: bottom nav, slide-in drawer, bottom sheet" + "viewport": "800x520", + "name": "Mobile layout", + "subtitle": "breakpoints, shell patterns, tokens" }, { "path": "guidelines/motion.card.html", "group": "Motion", - "viewport": "720x300", - "name": "Motion — Easing & Duration", - "subtitle": "Standard, springy-seam, enter/exit timing" + "viewport": "800x480", + "name": "Motion", + "subtitle": "seam animations, easing, focus" }, { "path": "guidelines/radii-shadows.card.html", @@ -558,6 +610,12 @@ "kind": "shadow", "definedIn": "tokens/colors.css" }, + { + "name": "--scrim", + "value": "rgba(0, 0, 0, 0.55)", + "kind": "color", + "definedIn": "tokens/colors.css" + }, { "name": "--seam", "value": "linear-gradient(90deg, var(--accent-color) 0%, var(--accent-2) 100%)", @@ -972,6 +1030,12 @@ "kind": "spacing", "definedIn": "tokens/spacing.css" }, + { + "name": "--space-7", + "value": "1.75rem", + "kind": "spacing", + "definedIn": "tokens/spacing.css" + }, { "name": "--space-8", "value": "2rem", @@ -1074,6 +1138,60 @@ "kind": "shadow", "definedIn": "tokens/spacing.css" }, + { + "name": "--shadow-pop", + "value": "0 12px 32px -12px var(--shadow), 0 2px 8px -4px var(--shadow)", + "kind": "shadow", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-base", + "value": "0", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-raised", + "value": "10", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-sticky", + "value": "100", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-dropdown", + "value": "1000", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-overlay", + "value": "1100", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-modal", + "value": "1200", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-toast", + "value": "1300", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--z-tooltip", + "value": "1400", + "kind": "other", + "definedIn": "tokens/spacing.css" + }, { "name": "--top-bar-height", "value": "60px", @@ -1105,99 +1223,111 @@ "definedIn": "tokens/spacing.css" }, { - "name": "--ease-standard", - "value": "cubic-bezier(0.4, 0, 0.2, 1)", - "kind": "other", + "name": "--top-bar-height-mobile", + "value": "52px", + "kind": "spacing", "definedIn": "tokens/spacing.css" }, { - "name": "--ease-out", - "value": "cubic-bezier(0.16, 1, 0.3, 1)", + "name": "--bottom-nav-height", + "value": "56px", + "kind": "spacing", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--drawer-width", + "value": "280px", + "kind": "spacing", + "definedIn": "tokens/spacing.css" + }, + { + "name": "--sheet-max-h", + "value": "80dvh", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--duration-fast", - "value": "120ms", + "name": "--touch-target", + "value": "44px", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--duration-base", - "value": "200ms", + "name": "--safe-area-top", + "value": "env(safe-area-inset-top, 0px)", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--duration-slow", - "value": "320ms", + "name": "--safe-area-bottom", + "value": "env(safe-area-inset-bottom, 0px)", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--duration-enter", - "value": "260ms", + "name": "--safe-area-left", + "value": "env(safe-area-inset-left, 0px)", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--duration-exit", - "value": "180ms", + "name": "--safe-area-right", + "value": "env(safe-area-inset-right, 0px)", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--duration-seam", - "value": "400ms", + "name": "--ease-standard", + "value": "cubic-bezier(0.4, 0, 0.2, 1)", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--ease-seam", + "name": "--ease-out", "value": "cubic-bezier(0.16, 1, 0.3, 1)", "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--space-7", - "value": "1.75rem", - "kind": "spacing", + "name": "--ease-seam", + "value": "cubic-bezier(0.16, 1, 0.3, 1)", + "kind": "color", "definedIn": "tokens/spacing.css" }, { - "name": "--top-bar-height-mobile", - "value": "52px", - "kind": "spacing", + "name": "--duration-fast", + "value": "120ms", + "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--bottom-nav-height", - "value": "56px", - "kind": "spacing", + "name": "--duration-base", + "value": "200ms", + "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--drawer-width", - "value": "280px", - "kind": "spacing", + "name": "--duration-slow", + "value": "320ms", + "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--sheet-max-h", - "value": "80dvh", - "kind": "spacing", + "name": "--duration-enter", + "value": "260ms", + "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--touch-target", - "value": "44px", - "kind": "spacing", + "name": "--duration-exit", + "value": "180ms", + "kind": "other", "definedIn": "tokens/spacing.css" }, { - "name": "--shadow-pop", - "value": "0 12px 32px -12px var(--shadow), 0 2px 8px -4px var(--shadow)", - "kind": "shadow", + "name": "--duration-seam", + "value": "400ms", + "kind": "color", "definedIn": "tokens/spacing.css" } ], diff --git a/design-system/components/launcher/AppTile.d.ts b/design-system/components/launcher/AppTile.d.ts new file mode 100644 index 00000000..642d1783 --- /dev/null +++ b/design-system/components/launcher/AppTile.d.ts @@ -0,0 +1,25 @@ +import * as React from "react"; + +/** + * The launcher app tile — a compact icon + name cell for the 9-dots app grid, + * modeled on the Google app launcher. No description, no integration badge; + * offline apps render grayscaled, dimmed and non-interactive. Use `AppCard` + * for the full-detail dashboard / management surfaces. + */ +export interface AppTileProps + extends Omit, "onClick"> { + /** The app's display name (menu label), rendered beneath the icon. */ + name: string; + /** Runtime integration strategy — drives the icon fallback hue/emoji. */ + integrationType?: "module-federation" | "iframe" | "web-component" | string; + /** App icon image URL; falls back to a per-type emoji glyph on error or when absent. */ + iconUrl?: string; + /** Emoji glyph (e.g. from a manifest `Icon.kind = "emoji"`); overrides the per-type fallback emoji when no `iconUrl`. */ + iconGlyph?: string; + /** When false, the tile is grayscaled, dimmed and inert. */ + isHealthy?: boolean; + /** Fired on click / Enter / Space — only when the app is healthy. */ + onClick?: (event: React.SyntheticEvent) => void; +} + +export function AppTile(props: AppTileProps): JSX.Element; diff --git a/design-system/components/launcher/AppTile.jsx b/design-system/components/launcher/AppTile.jsx new file mode 100644 index 00000000..273dd750 --- /dev/null +++ b/design-system/components/launcher/AppTile.jsx @@ -0,0 +1,138 @@ +import React, { useState } from "react"; +import { HealthDot } from "./HealthDot.jsx"; + +// The per-integration-type icon fallback, tuned to the cool runtime palette — +// mirrors so the launcher grid and the dashboard cards agree. +const TYPE_GRADIENT = { + "module-federation": "linear-gradient(135deg, #15414f, var(--accent-2))", + iframe: "linear-gradient(135deg, #4a3a12, var(--warning-color))", + "web-component": "linear-gradient(135deg, #2f1f4f, var(--accent-color))", + other: "linear-gradient(135deg, #2f1f4f, var(--accent-color))", +}; +const TYPE_EMOJI = { + "module-federation": "🔗", + iframe: "🖼️", + "web-component": "🧩", + other: "📱", +}; + +/** + * The launcher app tile — a compact icon + name cell for the 9-dots app grid, + * modeled on the Google app launcher: no description and no integration badge, + * just the manifest icon and menu label. Offline apps are grayscaled, dimmed + * and inert (a corner health dot marks the state). Use instead for + * the full-detail dashboard / management surfaces. + */ +export function AppTile({ + name, + integrationType = "other", + iconUrl, + iconGlyph, + isHealthy = true, + onClick, + style, + ...rest +}) { + // A manifest may declare an emoji glyph (Icon.kind = "emoji"); it takes + // precedence over the per-integration-type fallback emoji. + const glyph = iconGlyph || TYPE_EMOJI[integrationType] || TYPE_EMOJI.other; + const gradient = TYPE_GRADIENT[integrationType] || TYPE_GRADIENT.other; + // A broken icon URL falls back to the emoji glyph, per the documented + // contract — swap the whole node, don't just hide the . + const [iconFailed, setIconFailed] = useState(false); + const showImage = Boolean(iconUrl) && !iconFailed; + + const highlight = (e) => { + if (!isHealthy) return; + e.currentTarget.style.backgroundColor = "var(--bg-quaternary)"; + }; + const settle = (e) => { + e.currentTarget.style.backgroundColor = "transparent"; + }; + + return ( +
{ + if (!isHealthy || !onClick) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(e); + } + }} + onMouseEnter={highlight} + onMouseLeave={settle} + style={{ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "var(--space-2)", + padding: "var(--space-3) var(--space-2)", + borderRadius: "var(--radius-md)", + background: "transparent", + cursor: isHealthy ? "pointer" : "not-allowed", + opacity: isHealthy ? 1 : 0.6, + filter: isHealthy ? "none" : "grayscale(0.5)", + transition: "background-color var(--duration-base) var(--ease-standard)", + textAlign: "center", + ...style, + }} + {...rest} + > +
+ {showImage ? ( + setIconFailed(true)} + /> + ) : ( + + )} + +
+ + + {name} + +
+ ); +} diff --git a/design-system/components/launcher/AppTile.prompt.md b/design-system/components/launcher/AppTile.prompt.md new file mode 100644 index 00000000..a07f6dd5 --- /dev/null +++ b/design-system/components/launcher/AppTile.prompt.md @@ -0,0 +1,14 @@ +The launcher app tile — a compact icon + name cell for the 9-dots app grid, modeled on the Google app launcher. Unlike `AppCard`, it shows only the app icon and its menu label — no description, no integration badge — so a dense grid of apps stays scannable. Offline apps go grayscale + dimmed + inert, with a corner health dot. + +```jsx + navigate("/app/fuzeclock")} +/> + +``` + +Props: `name`, `integrationType` (`module-federation` | `iframe` | `web-component` | other — sets the fallback icon hue/emoji), `iconUrl` (emoji fallback on error), `iconGlyph` (manifest emoji glyph), `isHealthy` (offline = grayscale/dim/inert), `onClick` (fires on click/Enter/Space when healthy). For the full-detail dashboard/management card, use `AppCard`. diff --git a/design-system/index.d.ts b/design-system/index.d.ts index e8e42fd5..5ce1b3ef 100644 --- a/design-system/index.d.ts +++ b/design-system/index.d.ts @@ -1,35 +1,83 @@ -/* AUTO-GENERATED type barrel — mirrors index.js exports */ -export * from './components/access/RoleBadge'; -export * from './components/feedback/Alert'; -export * from './components/feedback/EmptyState'; -export * from './components/feedback/Spinner'; -export * from './components/layout/CenteredCard'; -export * from './components/layout/InfoRow'; -export * from './components/forms/Toggle'; -export * from './components/billing/PricingCard'; -export * from './components/brand/BrandMark'; -export * from './components/brand/SeamDivider'; -export * from './components/core/Avatar'; -export * from './components/core/Badge'; -export * from './components/core/Button'; -export * from './components/core/IconButton'; -export * from './components/data/DataTable'; -export * from './components/feedback/Skeleton'; -export * from './components/feedback/StatusPill'; -export * from './components/feedback/Toast'; -export * from './components/forms/FileDropZone'; -export * from './components/forms/Input'; -export * from './components/forms/Select'; -export * from './components/forms/Textarea'; -export * from './components/launcher/AppCard'; -export * from './components/launcher/HealthDot'; -export * from './components/launcher/IntegrationBadge'; -export * from './components/mobile/BottomNav'; -export * from './components/mobile/BottomSheet'; -export * from './components/mobile/Drawer'; -export * from './components/overlay/Modal'; -export * from './components/shell/MenuItem'; -export * from './components/shell/Tabs'; -export * from './components/shell/ThemeToggle'; -export * from './components/shell/TopBar'; -export * from './components/shell/UserMenu'; +/* AUTO-GENERATED by build.mjs — do not edit. The public type surface. */ +export { RoleBadge } from './components/access/RoleBadge' +export type * from './components/access/RoleBadge' +export { PricingCard } from './components/billing/PricingCard' +export type * from './components/billing/PricingCard' +export { BrandMark } from './components/brand/BrandMark' +export type * from './components/brand/BrandMark' +export { SeamDivider } from './components/brand/SeamDivider' +export type * from './components/brand/SeamDivider' +export { Avatar } from './components/core/Avatar' +export type * from './components/core/Avatar' +export { Badge } from './components/core/Badge' +export type * from './components/core/Badge' +export { Button } from './components/core/Button' +export type * from './components/core/Button' +export { IconButton } from './components/core/IconButton' +export type * from './components/core/IconButton' +export { DataTable } from './components/data/DataTable' +export type * from './components/data/DataTable' +export { StatGroup } from './components/data/StatGroup' +export type * from './components/data/StatGroup' +export { Alert } from './components/feedback/Alert' +export type * from './components/feedback/Alert' +export { EmptyState } from './components/feedback/EmptyState' +export type * from './components/feedback/EmptyState' +export { Rating } from './components/feedback/Rating' +export type * from './components/feedback/Rating' +export { Skeleton } from './components/feedback/Skeleton' +export type * from './components/feedback/Skeleton' +export { Spinner } from './components/feedback/Spinner' +export type * from './components/feedback/Spinner' +export { StatusPill } from './components/feedback/StatusPill' +export type * from './components/feedback/StatusPill' +export { Toast } from './components/feedback/Toast' +export type * from './components/feedback/Toast' +export { FileDropZone } from './components/forms/FileDropZone' +export type * from './components/forms/FileDropZone' +export { Input } from './components/forms/Input' +export type * from './components/forms/Input' +export { Select } from './components/forms/Select' +export type * from './components/forms/Select' +export { Textarea } from './components/forms/Textarea' +export type * from './components/forms/Textarea' +export { Toggle } from './components/forms/Toggle' +export type * from './components/forms/Toggle' +export { AppCard } from './components/launcher/AppCard' +export type * from './components/launcher/AppCard' +export { AppTile } from './components/launcher/AppTile' +export type * from './components/launcher/AppTile' +export { HealthDot } from './components/launcher/HealthDot' +export type * from './components/launcher/HealthDot' +export { IntegrationBadge } from './components/launcher/IntegrationBadge' +export type * from './components/launcher/IntegrationBadge' +export { CenteredCard } from './components/layout/CenteredCard' +export type * from './components/layout/CenteredCard' +export { Eyebrow } from './components/layout/Eyebrow' +export type * from './components/layout/Eyebrow' +export { InfoRow } from './components/layout/InfoRow' +export type * from './components/layout/InfoRow' +export { SectionHead } from './components/layout/SectionHead' +export type * from './components/layout/SectionHead' +export { BottomNav } from './components/mobile/BottomNav' +export type * from './components/mobile/BottomNav' +export { BottomSheet } from './components/mobile/BottomSheet' +export type * from './components/mobile/BottomSheet' +export { Drawer } from './components/mobile/Drawer' +export type * from './components/mobile/Drawer' +export { Pagination } from './components/navigation/Pagination' +export type * from './components/navigation/Pagination' +export { Modal } from './components/overlay/Modal' +export type * from './components/overlay/Modal' +export { AnnouncementBar } from './components/shell/AnnouncementBar' +export type * from './components/shell/AnnouncementBar' +export { MenuItem } from './components/shell/MenuItem' +export type * from './components/shell/MenuItem' +export { Tabs } from './components/shell/Tabs' +export type * from './components/shell/Tabs' +export { ThemeToggle } from './components/shell/ThemeToggle' +export type * from './components/shell/ThemeToggle' +export { TopBar } from './components/shell/TopBar' +export type * from './components/shell/TopBar' +export { UserMenu } from './components/shell/UserMenu' +export type * from './components/shell/UserMenu' diff --git a/design-system/index.js b/design-system/index.js index c62ec2de..2cb0592c 100644 --- a/design-system/index.js +++ b/design-system/index.js @@ -2,12 +2,6 @@ Consumers: import { Button, AppCard } from '@fuzefront/design-system' */ export { RoleBadge } from './components/access/RoleBadge.jsx' -export { Alert } from './components/feedback/Alert.jsx' -export { EmptyState } from './components/feedback/EmptyState.jsx' -export { Spinner } from './components/feedback/Spinner.jsx' -export { CenteredCard } from './components/layout/CenteredCard.jsx' -export { InfoRow } from './components/layout/InfoRow.jsx' -export { Toggle } from './components/forms/Toggle.jsx' export { PricingCard } from './components/billing/PricingCard.jsx' export { BrandMark } from './components/brand/BrandMark.jsx' export { SeamDivider } from './components/brand/SeamDivider.jsx' @@ -16,20 +10,33 @@ export { Badge } from './components/core/Badge.jsx' export { Button } from './components/core/Button.jsx' export { IconButton } from './components/core/IconButton.jsx' export { DataTable } from './components/data/DataTable.jsx' +export { StatGroup } from './components/data/StatGroup.jsx' +export { Alert } from './components/feedback/Alert.jsx' +export { EmptyState } from './components/feedback/EmptyState.jsx' +export { Rating } from './components/feedback/Rating.jsx' export { Skeleton } from './components/feedback/Skeleton.jsx' +export { Spinner } from './components/feedback/Spinner.jsx' export { StatusPill } from './components/feedback/StatusPill.jsx' export { Toast } from './components/feedback/Toast.jsx' export { FileDropZone } from './components/forms/FileDropZone.jsx' export { Input } from './components/forms/Input.jsx' export { Select } from './components/forms/Select.jsx' export { Textarea } from './components/forms/Textarea.jsx' +export { Toggle } from './components/forms/Toggle.jsx' export { AppCard } from './components/launcher/AppCard.jsx' +export { AppTile } from './components/launcher/AppTile.jsx' export { HealthDot } from './components/launcher/HealthDot.jsx' export { IntegrationBadge } from './components/launcher/IntegrationBadge.jsx' +export { CenteredCard } from './components/layout/CenteredCard.jsx' +export { Eyebrow } from './components/layout/Eyebrow.jsx' +export { InfoRow } from './components/layout/InfoRow.jsx' +export { SectionHead } from './components/layout/SectionHead.jsx' export { BottomNav } from './components/mobile/BottomNav.jsx' export { BottomSheet } from './components/mobile/BottomSheet.jsx' export { Drawer } from './components/mobile/Drawer.jsx' +export { Pagination } from './components/navigation/Pagination.jsx' export { Modal } from './components/overlay/Modal.jsx' +export { AnnouncementBar } from './components/shell/AnnouncementBar.jsx' export { MenuItem } from './components/shell/MenuItem.jsx' export { Tabs } from './components/shell/Tabs.jsx' export { ThemeToggle } from './components/shell/ThemeToggle.jsx' diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c13205ee..16bca14e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -62,7 +62,7 @@ }, "../packages/chat-client": { "name": "@fuzefront/chat-client", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "eventsource-parser": "^1.1.2" @@ -78,7 +78,7 @@ }, "../packages/chat-ui": { "name": "@fuzefront/chat-ui", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "devDependencies": { "@fuzefront/chat-client": "file:../chat-client", @@ -96,7 +96,7 @@ "vitest": "^1.6.0" }, "peerDependencies": { - "@fuzefront/chat-client": "^1.0.0", + "@fuzefront/chat-client": "^1.1.0", "react": "^18.0.0", "react-dom": "^18.0.0" } diff --git a/frontend/src/components/AppSelector.tsx b/frontend/src/components/AppSelector.tsx index 914c7d32..44e7ea52 100644 --- a/frontend/src/components/AppSelector.tsx +++ b/frontend/src/components/AppSelector.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import type { App } from '@fuzefront/app-registry-client' -import { AppCard } from '@fuzefront/design-system' +import { AppTile } from '@fuzefront/design-system' import { useLanguage } from '../contexts/LanguageContext' import { useRegisteredApps } from '../platform/appRegistry' import { @@ -12,10 +12,11 @@ import { } from '../platform/appManifest' /** - * The top-bar app launcher. Renders activated apps from the app-registry - * (manifest icon + menuLabel) using the design-system , matching - * frame 01-app-menu. Clicking an app navigates to its surface (portal mount or - * standalone surface), per the manifest. + * The top-bar app launcher (the 9-dots menu). Renders activated apps from the + * app-registry (manifest icon + menuLabel) as a compact icon+name grid using + * the design-system — Google-launcher style, no per-app details. + * Clicking an app navigates to its surface (portal mount or standalone + * surface), per the manifest. */ function AppSelector() { const { t } = useLanguage() @@ -158,17 +159,16 @@ function AppSelector() {
{apps.map(app => ( -