@devxcrew/react-ui 0.0.0-stage → 0.1.7
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/README.md +85 -2
- package/package.json +105 -4
- package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
- package/src/blocks/agent-chat-workspace/index.ts +2 -0
- package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
- package/src/blocks/agent-provider-settings/index.ts +2 -0
- package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
- package/src/blocks/agent-task-workspace/index.ts +2 -0
- package/src/blocks/analysis-context/analysis-context.tsx +6 -0
- package/src/blocks/analysis-context/index.ts +1 -0
- package/src/blocks/app-header/app-header.tsx +159 -0
- package/src/blocks/app-header/index.ts +2 -0
- package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
- package/src/blocks/archived-chat-workspace/index.ts +2 -0
- package/src/blocks/auth/README.md +32 -0
- package/src/blocks/auth/admin-login-page.tsx +15 -0
- package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/auth-page-layout.tsx +90 -0
- package/src/blocks/auth/client-login-page.tsx +20 -0
- package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/identity-management-desk.tsx +530 -0
- package/src/blocks/auth/index.ts +12 -0
- package/src/blocks/auth/login-page.tsx +206 -0
- package/src/blocks/auth/password-forgot-page.tsx +60 -0
- package/src/blocks/auth/portal-pages.tsx +32 -0
- package/src/blocks/auth/privileged-desk.tsx +55 -0
- package/src/blocks/auth/register-page.tsx +117 -0
- package/src/blocks/auth/session-boundary.tsx +296 -0
- package/src/blocks/auth/super-admin-login-page.tsx +15 -0
- package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
- package/src/blocks/blog/blog-card.tsx +156 -0
- package/src/blocks/blog/blog-reader.tsx +175 -0
- package/src/blocks/blog/blog-types.ts +47 -0
- package/src/blocks/blog/index.ts +3 -0
- package/src/blocks/chat-composer/chat-composer.tsx +140 -0
- package/src/blocks/chat-composer/index.ts +1 -0
- package/src/blocks/chat-history/chat-history.tsx +59 -0
- package/src/blocks/chat-history/index.ts +1 -0
- package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
- package/src/blocks/chat-response-progress/index.ts +2 -0
- package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
- package/src/blocks/chat-runtime-controls/index.ts +2 -0
- package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
- package/src/blocks/chat-runtime-trace/index.ts +2 -0
- package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
- package/src/blocks/codex-connection-settings/index.ts +1 -0
- package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
- package/src/blocks/dropzone/dropzone-types.ts +32 -0
- package/src/blocks/dropzone/dropzone.tsx +216 -0
- package/src/blocks/dropzone/index.ts +3 -0
- package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
- package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
- package/src/blocks/ecommerce/cart/index.ts +2 -0
- package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
- package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
- package/src/blocks/ecommerce/categories/index.ts +2 -0
- package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
- package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
- package/src/blocks/ecommerce/checkout/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
- package/src/blocks/ecommerce/comparison/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
- package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
- package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
- package/src/blocks/ecommerce/index.ts +12 -0
- package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
- package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
- package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
- package/src/blocks/ecommerce/price-history/index.ts +2 -0
- package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
- package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
- package/src/blocks/ecommerce/reviews/index.ts +2 -0
- package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
- package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
- package/src/blocks/ecommerce/wishlist/index.ts +2 -0
- package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
- package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
- package/src/blocks/execution-status/README.md +25 -0
- package/src/blocks/execution-status/execution-status.tsx +135 -0
- package/src/blocks/execution-status/index.ts +2 -0
- package/src/blocks/file-tree/file-icons.tsx +84 -0
- package/src/blocks/file-tree/file-tree-node.tsx +158 -0
- package/src/blocks/file-tree/file-tree-types.ts +33 -0
- package/src/blocks/file-tree/file-tree.tsx +163 -0
- package/src/blocks/file-tree/index.ts +5 -0
- package/src/blocks/filter-builder/filter-builder.tsx +169 -0
- package/src/blocks/filter-builder/filter-row.tsx +164 -0
- package/src/blocks/filter-builder/filter-types.ts +61 -0
- package/src/blocks/filter-builder/index.ts +12 -0
- package/src/blocks/footer/footer-types.ts +35 -0
- package/src/blocks/footer/index.ts +2 -0
- package/src/blocks/footer/site-footer.tsx +212 -0
- package/src/blocks/form/form-block.tsx +113 -0
- package/src/blocks/form/form-lookup-field.tsx +60 -0
- package/src/blocks/form/index.ts +4 -0
- package/src/blocks/handover-stack/handover-stack.tsx +41 -0
- package/src/blocks/handover-stack/index.ts +1 -0
- package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
- package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
- package/src/blocks/idea-handover/index.ts +4 -0
- package/src/blocks/kanban/index.ts +11 -0
- package/src/blocks/kanban/kanban-board.tsx +146 -0
- package/src/blocks/kanban/kanban-card.tsx +149 -0
- package/src/blocks/kanban/kanban-column.tsx +102 -0
- package/src/blocks/kanban/kanban-types.ts +38 -0
- package/src/blocks/loader/global-loader.tsx +90 -0
- package/src/blocks/loader/index.ts +1 -0
- package/src/blocks/mascot/README.md +21 -0
- package/src/blocks/mascot/index.ts +2 -0
- package/src/blocks/mascot/mascot.tsx +202 -0
- package/src/blocks/master-list/README.md +21 -0
- package/src/blocks/master-list/index.ts +12 -0
- package/src/blocks/master-list/master-form.tsx +157 -0
- package/src/blocks/master-list/master-list-desk.tsx +370 -0
- package/src/blocks/master-list/master-list.tsx +189 -0
- package/src/blocks/master-list/types.ts +18 -0
- package/src/blocks/mermaid-preview/index.ts +1 -0
- package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
- package/src/blocks/notifications/README.md +7 -0
- package/src/blocks/notifications/index.ts +1 -0
- package/src/blocks/notifications/notification-center-page.tsx +73 -0
- package/src/blocks/pricing/index.ts +7 -0
- package/src/blocks/pricing/pricing-table.tsx +173 -0
- package/src/blocks/pricing/pricing-types.ts +34 -0
- package/src/blocks/product-card/index.ts +2 -0
- package/src/blocks/product-card/product-card-types.ts +35 -0
- package/src/blocks/product-card/product-card.tsx +183 -0
- package/src/blocks/table/data-table-block.tsx +313 -0
- package/src/blocks/table/data-table-filter-menu.tsx +48 -0
- package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
- package/src/blocks/table/data-table-menu-content.tsx +32 -0
- package/src/blocks/table/data-table-pagination.tsx +149 -0
- package/src/blocks/table/data-table-row-actions.tsx +53 -0
- package/src/blocks/table/data-table-status.tsx +42 -0
- package/src/blocks/table/data-table-toolbar.tsx +124 -0
- package/src/blocks/table/data-table-totals.tsx +31 -0
- package/src/blocks/table/index.ts +13 -0
- package/src/blocks/workspace/index.ts +19 -0
- package/src/blocks/workspace/workspace-action-card.tsx +73 -0
- package/src/blocks/workspace/workspace-application-header.tsx +67 -0
- package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
- package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
- package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
- package/src/blocks/workspace/workspace-page-header.tsx +43 -0
- package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
- package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
- package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
- package/src/blocks/workspace/workspace-section-card.tsx +42 -0
- package/src/components/accordion.tsx +65 -0
- package/src/components/alert-dialog.tsx +160 -0
- package/src/components/alert.tsx +68 -0
- package/src/components/aspect-ratio.tsx +22 -0
- package/src/components/attachment.tsx +195 -0
- package/src/components/avatar.tsx +90 -0
- package/src/components/badge.tsx +55 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/bubble.tsx +123 -0
- package/src/components/button-group.tsx +79 -0
- package/src/components/button.tsx +71 -0
- package/src/components/calendar.tsx +185 -0
- package/src/components/card.tsx +100 -0
- package/src/components/carousel.tsx +229 -0
- package/src/components/chart.tsx +338 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +15 -0
- package/src/components/combobox.tsx +269 -0
- package/src/components/command.tsx +181 -0
- package/src/components/compact-model-switcher.tsx +128 -0
- package/src/components/context-menu.tsx +244 -0
- package/src/components/dialog.tsx +138 -0
- package/src/components/direction.tsx +1 -0
- package/src/components/drawer.tsx +208 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/empty.tsx +93 -0
- package/src/components/field.tsx +232 -0
- package/src/components/hover-card.tsx +45 -0
- package/src/components/input-group.tsx +146 -0
- package/src/components/input-otp.tsx +83 -0
- package/src/components/input.tsx +19 -0
- package/src/components/item.tsx +188 -0
- package/src/components/kbd.tsx +26 -0
- package/src/components/label.tsx +17 -0
- package/src/components/markdown-content.tsx +113 -0
- package/src/components/marker.tsx +67 -0
- package/src/components/menubar.tsx +267 -0
- package/src/components/message-scroller.tsx +91 -0
- package/src/components/message.tsx +84 -0
- package/src/components/native-select.tsx +54 -0
- package/src/components/navigation-menu.tsx +160 -0
- package/src/components/pagination.tsx +118 -0
- package/src/components/popover.tsx +74 -0
- package/src/components/progress.tsx +82 -0
- package/src/components/questionnaire.tsx +150 -0
- package/src/components/radio-group.tsx +35 -0
- package/src/components/resizable.tsx +41 -0
- package/src/components/rich-text-editor.tsx +604 -0
- package/src/components/scroll-area.tsx +47 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +18 -0
- package/src/components/sheet.tsx +125 -0
- package/src/components/sidebar.tsx +693 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +51 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/sparkline.tsx +231 -0
- package/src/components/spinner.tsx +20 -0
- package/src/components/status-badge.tsx +114 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +91 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/textarea.tsx +17 -0
- package/src/components/toast.tsx +222 -0
- package/src/components/toggle-group.tsx +85 -0
- package/src/components/toggle.tsx +42 -0
- package/src/components/tooltip.tsx +53 -0
- package/src/design-system/README.md +55 -0
- package/src/design-system/action-intents.ts +21 -0
- package/src/design-system/agent-manifest.ts +83 -0
- package/src/design-system/block-registry.ts +272 -0
- package/src/design-system/component-registry.ts +142 -0
- package/src/design-system/contracts.ts +69 -0
- package/src/design-system/defaults.ts +3 -0
- package/src/design-system/index.ts +29 -0
- package/src/design-system/mcp-server.ts +156 -0
- package/src/design-system/page-registry.ts +74 -0
- package/src/design-system/selection.ts +67 -0
- package/src/design-system/template-registry.ts +33 -0
- package/src/features/interface-topology/index.ts +14 -0
- package/src/features/interface-topology/interface-topology.types.ts +40 -0
- package/src/features/interface-topology/topology-control.tsx +18 -0
- package/src/features/interface-topology/topology-inspector.tsx +228 -0
- package/src/features/interface-topology/topology-marker.tsx +47 -0
- package/src/features/interface-topology/topology-region.tsx +44 -0
- package/src/features/interface-topology/use-interface-topology.ts +178 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scroll-follow.ts +22 -0
- package/src/index.ts +11 -0
- package/src/layouts/README.md +65 -0
- package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
- package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
- package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
- package/src/layouts/agent-workspace/index.ts +7 -0
- package/src/layouts/blog-header/blog-header-types.ts +25 -0
- package/src/layouts/blog-header/blog-header.tsx +172 -0
- package/src/layouts/blog-header/index.ts +2 -0
- package/src/layouts/documentation-workspace/README.md +8 -0
- package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
- package/src/layouts/documentation-workspace/index.ts +1 -0
- package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
- package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
- package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
- package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
- package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
- package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
- package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
- package/src/layouts/ecommerce-header/index.ts +2 -0
- package/src/layouts/main-workspace/index.ts +28 -0
- package/src/layouts/mdi/index.ts +1 -0
- package/src/layouts/mdi/mdi.tsx +20 -0
- package/src/layouts/mdi-main/index.ts +27 -0
- package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
- package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
- package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
- package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
- package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
- package/src/layouts/mdi-main/mdi-main.tsx +241 -0
- package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
- package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
- package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
- package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
- package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
- package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
- package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
- package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
- package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
- package/src/layouts/mdi-main/mdi-types.ts +131 -0
- package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
- package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
- package/src/layouts/site-header/announcement-bar.tsx +55 -0
- package/src/layouts/site-header/category-strip.tsx +41 -0
- package/src/layouts/site-header/index.ts +16 -0
- package/src/layouts/site-header/main-navbar.tsx +179 -0
- package/src/layouts/site-header/mobile-drawer.tsx +140 -0
- package/src/layouts/site-header/site-header-types.ts +63 -0
- package/src/layouts/site-header/site-header.tsx +58 -0
- package/src/lib/random-id.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/pdf-provider.ts +137 -0
- package/src/styles/globals.css +374 -0
- package/src/templates/README.md +12 -0
- package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
- package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
- package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
- package/src/templates/dashboard-01/components/data-table.tsx +577 -0
- package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
- package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
- package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
- package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
- package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
- package/src/templates/dashboard-01/components/site-header.tsx +14 -0
- package/src/templates/dashboard-01/data.json +614 -0
- package/src/templates/dashboard-01/index.ts +7 -0
- package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
- package/src/templates/documentation-sidebar/index.ts +2 -0
- package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
- package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
- package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
- package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
- package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
- package/src/templates/sidebar-07/index.ts +1 -0
- package/src/templates/ui-page/index.ts +7 -0
- package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
- package/src/templates/ui-page/ui-template-code.tsx +48 -0
- package/src/templates/ui-page/ui-template-header.tsx +87 -0
- package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
- package/src/templates/ui-page/ui-template-page.tsx +97 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-config.ts +30 -0
- package/src/theme/theme-provider.tsx +142 -0
- package/src/theme/theme-selector.tsx +75 -0
- package/src/tokens/index.ts +15 -0
- package/src/vite-env.d.ts +3 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
InterfaceTopologyController,
|
|
4
|
+
InterfaceTopologyDesk,
|
|
5
|
+
InterfaceTopologySection,
|
|
6
|
+
} from './interface-topology.types';
|
|
7
|
+
|
|
8
|
+
const labelsStorageKey = 'codexsun.ui.ito.labels-visible';
|
|
9
|
+
|
|
10
|
+
export function useInterfaceTopology(desks: readonly InterfaceTopologyDesk[]): InterfaceTopologyController {
|
|
11
|
+
validateInterfaceTopologyDesks(desks);
|
|
12
|
+
const allSections = useMemo(() => desks.flatMap((desk) => desk.sections), [desks]);
|
|
13
|
+
const [scopeRootId, setScopeRootId] = useState<string | null>(null);
|
|
14
|
+
const scopedDesks = useMemo(
|
|
15
|
+
() =>
|
|
16
|
+
scopeRootId
|
|
17
|
+
? desks
|
|
18
|
+
.map((desk) => ({
|
|
19
|
+
...desk,
|
|
20
|
+
sections: desk.sections.filter(({ id }) => id === scopeRootId || id.startsWith(`${scopeRootId}.`)),
|
|
21
|
+
}))
|
|
22
|
+
.filter((desk) => desk.sections.length > 0)
|
|
23
|
+
: desks,
|
|
24
|
+
[desks, scopeRootId],
|
|
25
|
+
);
|
|
26
|
+
const firstDesk = scopedDesks[0];
|
|
27
|
+
const [activeDeskId, setActiveDeskId] = useState(firstDesk?.id ?? '');
|
|
28
|
+
const activeDesk = scopedDesks.find(({ id }) => id === activeDeskId) ?? firstDesk;
|
|
29
|
+
const sections = activeDesk?.sections ?? [];
|
|
30
|
+
const firstId = sections[0]?.id ?? '';
|
|
31
|
+
const [labelsVisible, setLabelsVisible] = useState(readLabelsVisibility);
|
|
32
|
+
const [open, setOpen] = useState(false);
|
|
33
|
+
const [highlighting, setHighlighting] = useState(false);
|
|
34
|
+
const [selected, setSelected] = useState(firstId);
|
|
35
|
+
const sectionIds = useMemo(
|
|
36
|
+
() => new Set(scopedDesks.flatMap((desk) => desk.sections.map(({ id }) => id))),
|
|
37
|
+
[scopedDesks],
|
|
38
|
+
);
|
|
39
|
+
const pageKey = scopedDesks
|
|
40
|
+
.map((desk) => `${desk.id}:${desk.sections.map(({ technicalName }) => technicalName).join('|')}`)
|
|
41
|
+
.join(';');
|
|
42
|
+
|
|
43
|
+
useEffect(() => setHighlighting(false), [pageKey]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!scopedDesks.some(({ id }) => id === activeDeskId)) setActiveDeskId(firstDesk?.id ?? '');
|
|
46
|
+
}, [activeDeskId, firstDesk?.id, scopedDesks]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!sections.some(({ id }) => id === selected)) setSelected(firstId);
|
|
49
|
+
}, [firstId, sections, selected]);
|
|
50
|
+
|
|
51
|
+
function inspect(id: string) {
|
|
52
|
+
const owner = scopedDesks.find((desk) => desk.sections.some((section) => section.id === id));
|
|
53
|
+
if (!owner) return;
|
|
54
|
+
setActiveDeskId(owner.id);
|
|
55
|
+
copyTechnicalName(id);
|
|
56
|
+
setSelected(id);
|
|
57
|
+
setHighlighting(true);
|
|
58
|
+
setOpen(true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function copyTechnicalName(id: string) {
|
|
62
|
+
const section = allSections.find((candidate) => candidate.id === id);
|
|
63
|
+
if (section) copyText(section.technicalName);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function selectDesk(id: string) {
|
|
67
|
+
const desk = scopedDesks.find((candidate) => candidate.id === id);
|
|
68
|
+
if (!desk) return;
|
|
69
|
+
setActiveDeskId(id);
|
|
70
|
+
setSelected(desk.sections[0]?.id ?? '');
|
|
71
|
+
setHighlighting(false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toggleLabels() {
|
|
75
|
+
setLabelsVisible((current) => {
|
|
76
|
+
const next = !current;
|
|
77
|
+
persistLabelsVisibility(next);
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const setScope = useCallback((rootId: string | null) => setScopeRootId(rootId), []);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
activeDeskId,
|
|
86
|
+
allSections,
|
|
87
|
+
close: () => setOpen(false),
|
|
88
|
+
copyTechnicalName,
|
|
89
|
+
desks: scopedDesks,
|
|
90
|
+
highlightClassName: () =>
|
|
91
|
+
'data-[ito-highlighted=true]:ring-2 data-[ito-highlighted=true]:ring-inset data-[ito-highlighted=true]:ring-violet-700',
|
|
92
|
+
highlighting,
|
|
93
|
+
inspect,
|
|
94
|
+
labelsVisible,
|
|
95
|
+
open,
|
|
96
|
+
regionProps: (id) => ({
|
|
97
|
+
'data-ito-highlighted': sectionIds.has(id) && highlighting && selected === id,
|
|
98
|
+
'data-ito-section': id,
|
|
99
|
+
}),
|
|
100
|
+
sections,
|
|
101
|
+
select: setSelected,
|
|
102
|
+
selectDesk,
|
|
103
|
+
setScope,
|
|
104
|
+
selected,
|
|
105
|
+
toggleHighlight: () => setHighlighting((current) => !current),
|
|
106
|
+
toggleLabels,
|
|
107
|
+
toggleOpen: () => setOpen((current) => !current),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validateInterfaceTopologyDesks(desks: readonly InterfaceTopologyDesk[]): void {
|
|
112
|
+
const deskIds = new Set<string>();
|
|
113
|
+
for (const desk of desks) {
|
|
114
|
+
if (deskIds.has(desk.id)) throw new Error(`Duplicate ITO desk id: ${desk.id}`);
|
|
115
|
+
deskIds.add(desk.id);
|
|
116
|
+
}
|
|
117
|
+
validateInterfaceTopologySections(desks.flatMap((desk) => desk.sections));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function validateInterfaceTopologySections(sections: readonly InterfaceTopologySection[]): void {
|
|
121
|
+
const ids = new Set<string>();
|
|
122
|
+
const technicalNames = new Set<string>();
|
|
123
|
+
|
|
124
|
+
for (const section of sections) {
|
|
125
|
+
if (ids.has(section.id)) throw new Error(`Duplicate ITO id: ${section.id}`);
|
|
126
|
+
if (technicalNames.has(section.technicalName)) {
|
|
127
|
+
throw new Error(`Duplicate ITO technical name: ${section.technicalName}`);
|
|
128
|
+
}
|
|
129
|
+
if (!/^[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/.test(section.technicalName)) {
|
|
130
|
+
throw new Error(`ITO technical name must use section.block.control: ${section.technicalName}`);
|
|
131
|
+
}
|
|
132
|
+
ids.add(section.id);
|
|
133
|
+
technicalNames.add(section.technicalName);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const section of sections) {
|
|
137
|
+
const parentId = section.id.includes('.') ? section.id.slice(0, section.id.lastIndexOf('.')) : '';
|
|
138
|
+
if (parentId && !ids.has(parentId)) {
|
|
139
|
+
throw new Error(`ITO item ${section.id} is missing parent ${parentId}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readLabelsVisibility() {
|
|
145
|
+
if (typeof window === 'undefined') return false;
|
|
146
|
+
try {
|
|
147
|
+
return window.localStorage.getItem(labelsStorageKey) === 'true';
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function persistLabelsVisibility(visible: boolean) {
|
|
154
|
+
try {
|
|
155
|
+
window.localStorage.setItem(labelsStorageKey, String(visible));
|
|
156
|
+
} catch {
|
|
157
|
+
// The session state still works when browser storage is unavailable.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function copyText(value: string) {
|
|
162
|
+
if (navigator.clipboard?.writeText) {
|
|
163
|
+
void navigator.clipboard.writeText(value).catch(() => fallbackCopy(value));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
fallbackCopy(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function fallbackCopy(value: string) {
|
|
170
|
+
const target = document.createElement('textarea');
|
|
171
|
+
target.value = value;
|
|
172
|
+
target.setAttribute('readonly', '');
|
|
173
|
+
target.className = 'fixed opacity-0';
|
|
174
|
+
document.body.append(target);
|
|
175
|
+
target.select();
|
|
176
|
+
document.execCommand('copy');
|
|
177
|
+
target.remove();
|
|
178
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener('change', onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener('change', onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Follow updates only while the reader remains near the end of the scroll viewport. */
|
|
4
|
+
export function useScrollFollow(update: unknown, resetKey: unknown) {
|
|
5
|
+
const endRef = useRef<HTMLDivElement>(null)
|
|
6
|
+
const following = useRef(true)
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const viewport = endRef.current?.closest<HTMLElement>('[data-slot="scroll-area-viewport"]')
|
|
9
|
+
if (!viewport) return
|
|
10
|
+
following.current = true
|
|
11
|
+
endRef.current?.scrollIntoView({ block: 'end' })
|
|
12
|
+
const onScroll = () => {
|
|
13
|
+
following.current = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 80
|
|
14
|
+
}
|
|
15
|
+
viewport.addEventListener('scroll', onScroll, { passive: true })
|
|
16
|
+
return () => viewport.removeEventListener('scroll', onScroll)
|
|
17
|
+
}, [resetKey])
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (following.current) endRef.current?.scrollIntoView({ block: 'end' })
|
|
20
|
+
}, [update])
|
|
21
|
+
return endRef
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { cn } from "./lib/utils";
|
|
2
|
+
export { PdfProvider, type PdfDocumentSource, type PdfShareChannel } from "./providers/pdf-provider";
|
|
3
|
+
export { designTokens } from "./tokens/index";
|
|
4
|
+
export { MainWorkspace, type MainWorkspaceProps, type MdiNavigationSection } from "./layouts/main-workspace";
|
|
5
|
+
export { Mdi, type MdiProps } from "./layouts/mdi";
|
|
6
|
+
export { DocumentationWorkspace, type DocumentationWorkspaceProps } from "./layouts/documentation-workspace";
|
|
7
|
+
export {
|
|
8
|
+
WorkspaceApplicationHeader,
|
|
9
|
+
type WorkspaceApplicationHeaderProps,
|
|
10
|
+
} from "./blocks/workspace";
|
|
11
|
+
export { useMdiTopology, type MdiNavigationItem } from "./layouts/main-workspace";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# UI Layouts
|
|
2
|
+
|
|
3
|
+
Layouts are shared application frames. They own reusable navigation chrome and content boundaries,
|
|
4
|
+
but they do not own product routes, business entities, persistence, or workflows.
|
|
5
|
+
|
|
6
|
+
## Layout boundaries
|
|
7
|
+
|
|
8
|
+
- `main-workspace` is the shared shell for desktop application workspaces.
|
|
9
|
+
- `mdi` is a plain centered-label surface for layout previews and empty states.
|
|
10
|
+
- `documentation-workspace` applies Docs defaults to `main-workspace`.
|
|
11
|
+
- `agent-workspace` adds activity rails inside `main-workspace`.
|
|
12
|
+
- `site-header`, `ecommerce-header`, and `blog-header` are standalone page headers. Do not compose
|
|
13
|
+
them into `main-workspace`.
|
|
14
|
+
|
|
15
|
+
Each layout folder exports one public component through `index.ts`. Keep its internal parts in the
|
|
16
|
+
same folder. Name internal parts after their visible job, such as `ecommerce-search` or
|
|
17
|
+
`ecommerce-navigation`.
|
|
18
|
+
|
|
19
|
+
- `main-workspace` provides the CODEXSUN multiple-document application frame. Its public parts
|
|
20
|
+
include the top menu, application switcher, profile menu, shadcn sidebar, status bar, and feature
|
|
21
|
+
settings screen. Pass application-owned navigation data and workspace content through the public
|
|
22
|
+
layout contract. Applications can provide a controlled search value and change callback; the
|
|
23
|
+
layout renders the field but does not own search state or result behavior.
|
|
24
|
+
|
|
25
|
+
When an application omits `children`, MDI leaves the workspace canvas empty.
|
|
26
|
+
|
|
27
|
+
- `agent-workspace` provides a fixed Primary Activity Rail, a center Workspace Canvas, and a fixed
|
|
28
|
+
Secondary Utility Rail. Applications supply typed icon items and actions.
|
|
29
|
+
|
|
30
|
+
Pass `agentWorkspace` to `MainWorkspace` to connect the Agent Workspace inside the shared shell. The
|
|
31
|
+
built-in feature settings show one visibility switch for each activity rail. MDI places the Primary
|
|
32
|
+
Activity Rail before the default sidebar and keeps the utility rail rightmost.
|
|
33
|
+
|
|
34
|
+
Applications can replace the standard primary action and navigation area with `sidebarContent`. They
|
|
35
|
+
can replace the footer with a React node or pass `null` to remove it. An omitted `sidebarFooter`
|
|
36
|
+
keeps the shared Feature settings footer. The MDI shell continues to own sidebar sizing, collapse
|
|
37
|
+
behavior, and the rail toggle.
|
|
38
|
+
|
|
39
|
+
An application can map a parent ITO group to the complete area below the command bar with
|
|
40
|
+
`deskRegionId`. Its workspace and sidebar regions can then be registered as child components of that
|
|
41
|
+
desk.
|
|
42
|
+
|
|
43
|
+
Feature visibility is stored per `applicationId`. The shared feature settings screen can toggle the
|
|
44
|
+
top menu, notifications, application switcher, profile menu, and bottom status bar. Product routes
|
|
45
|
+
and workflows remain app-owned.
|
|
46
|
+
|
|
47
|
+
Applications can pass `statusEnd` to place compact metadata at the right edge of the status bar. The
|
|
48
|
+
application owns the content and its text style.
|
|
49
|
+
|
|
50
|
+
`MainWorkspace` binds `@devxcrew/react-ui/theme` for every application. The appearance control changes
|
|
51
|
+
light, dark, or system mode and the shared accent composition. Applications must use semantic tokens
|
|
52
|
+
instead of storing their own shell theme.
|
|
53
|
+
|
|
54
|
+
`MainWorkspace` also sets the browser document title from `applicationName`. Shared templates and
|
|
55
|
+
application routes must not replace this title.
|
|
56
|
+
|
|
57
|
+
Applications import layouts through their public `@devxcrew/react-ui/layouts/*` entry points.
|
|
58
|
+
|
|
59
|
+
The default MDI application catalog links Platform, UI, Docs, and Zetro. It uses the documented
|
|
60
|
+
local ports when the current host is localhost or `127.0.0.1`. Applications can provide `apps` to
|
|
61
|
+
replace these destinations for a deployed environment.
|
|
62
|
+
|
|
63
|
+
The ITO inspector shows a shadcn Select only when the current page supplies more than one desk.
|
|
64
|
+
`showMdiOverview` adds the shared MDI Overview desk. A normal application desk contains only
|
|
65
|
+
app-owned regions and does not show the desk selector.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cn } from '@devxcrew/react-ui/lib/utils'
|
|
2
|
+
import { Button } from '@devxcrew/react-ui/components/button'
|
|
3
|
+
import {
|
|
4
|
+
Tooltip,
|
|
5
|
+
TooltipContent,
|
|
6
|
+
TooltipProvider,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from '@devxcrew/react-ui/components/tooltip'
|
|
9
|
+
|
|
10
|
+
import type { AgentWorkspaceRail, AgentWorkspaceRailItem } from './agent-workspace.types'
|
|
11
|
+
|
|
12
|
+
type AgentActivityRailProps = {
|
|
13
|
+
rail: AgentWorkspaceRail
|
|
14
|
+
side: 'left' | 'right'
|
|
15
|
+
visible: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AgentActivityRail({ rail, side, visible }: AgentActivityRailProps) {
|
|
19
|
+
return (
|
|
20
|
+
<TooltipProvider>
|
|
21
|
+
<aside
|
|
22
|
+
aria-hidden={!visible}
|
|
23
|
+
aria-label={rail.label}
|
|
24
|
+
className={cn(
|
|
25
|
+
'relative z-20 h-full shrink-0 overflow-hidden bg-sidebar transition-[width,opacity,border-color] duration-200 motion-reduce:transition-none',
|
|
26
|
+
visible
|
|
27
|
+
? cn('w-14 border-border opacity-100', side === 'left' ? 'border-r' : 'border-l')
|
|
28
|
+
: 'pointer-events-none w-0 border-0 opacity-0',
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{visible ? (
|
|
32
|
+
<div className="flex h-full w-14 flex-col items-center gap-2 px-2 py-3">
|
|
33
|
+
<RailItems items={rail.items} side={side} />
|
|
34
|
+
{rail.footerItems?.length ? (
|
|
35
|
+
<div className="flex min-h-0 flex-1 flex-col justify-end">
|
|
36
|
+
<RailItems items={rail.footerItems} side={side} />
|
|
37
|
+
</div>
|
|
38
|
+
) : null}
|
|
39
|
+
</div>
|
|
40
|
+
) : null}
|
|
41
|
+
</aside>
|
|
42
|
+
</TooltipProvider>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function RailItems({
|
|
47
|
+
items,
|
|
48
|
+
side,
|
|
49
|
+
}: {
|
|
50
|
+
items: readonly AgentWorkspaceRailItem[]
|
|
51
|
+
side: 'left' | 'right'
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<nav
|
|
55
|
+
className="flex flex-col gap-2"
|
|
56
|
+
aria-label={side === 'left' ? 'Primary tools' : 'Secondary tools'}
|
|
57
|
+
>
|
|
58
|
+
{items.map((item) => (
|
|
59
|
+
<RailAction item={item} key={item.id} side={side} />
|
|
60
|
+
))}
|
|
61
|
+
</nav>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function RailAction({ item, side }: { item: AgentWorkspaceRailItem; side: 'left' | 'right' }) {
|
|
66
|
+
const Icon = item.icon
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Tooltip>
|
|
70
|
+
<TooltipTrigger
|
|
71
|
+
render={
|
|
72
|
+
<Button
|
|
73
|
+
aria-label={item.label}
|
|
74
|
+
aria-current={item.active ? 'page' : undefined}
|
|
75
|
+
className={cn(
|
|
76
|
+
'relative hover:translate-y-0 active:translate-y-0 motion-reduce:transition-none',
|
|
77
|
+
item.active &&
|
|
78
|
+
'bg-sidebar-accent text-sidebar-accent-foreground ring-1 ring-sidebar-border before:absolute before:inset-y-2 before:-left-1.5 before:w-0.5 before:rounded-full before:bg-primary hover:bg-sidebar-accent',
|
|
79
|
+
)}
|
|
80
|
+
disabled={item.disabled}
|
|
81
|
+
size="icon"
|
|
82
|
+
variant={item.active ? 'secondary' : 'ghost'}
|
|
83
|
+
onClick={item.onSelect}
|
|
84
|
+
/>
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
<Icon />
|
|
88
|
+
{item.badge !== undefined ? <RailBadge value={item.badge} /> : null}
|
|
89
|
+
</TooltipTrigger>
|
|
90
|
+
<TooltipContent side={side === 'left' ? 'right' : 'left'}>{item.label}</TooltipContent>
|
|
91
|
+
</Tooltip>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function RailBadge({ value }: { value: number | string }) {
|
|
96
|
+
return (
|
|
97
|
+
<span className="absolute top-0.5 right-0.5 grid min-h-3.5 min-w-3.5 place-items-center rounded-full bg-primary px-1 text-[10px] leading-none font-semibold text-primary-foreground">
|
|
98
|
+
{value}
|
|
99
|
+
</span>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cn } from '@devxcrew/react-ui/lib/utils'
|
|
2
|
+
|
|
3
|
+
import { AgentActivityRail } from './agent-activity-rail'
|
|
4
|
+
import type { AgentWorkspaceProps } from './agent-workspace.types'
|
|
5
|
+
|
|
6
|
+
export function AgentWorkspace({
|
|
7
|
+
canvasClassName,
|
|
8
|
+
children,
|
|
9
|
+
className,
|
|
10
|
+
primaryRail,
|
|
11
|
+
secondaryRail,
|
|
12
|
+
showPrimaryRail = true,
|
|
13
|
+
showSecondaryRail = true,
|
|
14
|
+
}: AgentWorkspaceProps) {
|
|
15
|
+
return (
|
|
16
|
+
<section
|
|
17
|
+
aria-label="Agent workspace"
|
|
18
|
+
className={cn('flex size-full min-h-0 min-w-0 overflow-hidden bg-background', className)}
|
|
19
|
+
>
|
|
20
|
+
<AgentActivityRail rail={primaryRail} side="left" visible={showPrimaryRail} />
|
|
21
|
+
<div className={cn('relative min-h-0 min-w-0 flex-1 overflow-hidden', canvasClassName)}>
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
<AgentActivityRail rail={secondaryRail} side="right" visible={showSecondaryRail} />
|
|
25
|
+
</section>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export type AgentWorkspaceRailItem = {
|
|
5
|
+
active?: boolean
|
|
6
|
+
badge?: number | string
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
icon: LucideIcon
|
|
9
|
+
id: string
|
|
10
|
+
label: string
|
|
11
|
+
onSelect?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type AgentWorkspaceRail = {
|
|
15
|
+
footerItems?: readonly AgentWorkspaceRailItem[]
|
|
16
|
+
items: readonly AgentWorkspaceRailItem[]
|
|
17
|
+
label: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AgentWorkspaceProps = {
|
|
21
|
+
canvasClassName?: string
|
|
22
|
+
children?: ReactNode
|
|
23
|
+
className?: string
|
|
24
|
+
primaryRail: AgentWorkspaceRail
|
|
25
|
+
secondaryRail: AgentWorkspaceRail
|
|
26
|
+
showPrimaryRail?: boolean
|
|
27
|
+
showSecondaryRail?: boolean
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type BlogTopicTag = {
|
|
4
|
+
readonly active?: boolean
|
|
5
|
+
readonly href: string
|
|
6
|
+
readonly id: string
|
|
7
|
+
readonly label: string
|
|
8
|
+
readonly postCount?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type BlogHeaderProps = {
|
|
12
|
+
readonly backToStoreHref?: string
|
|
13
|
+
readonly backToStoreLabel?: string
|
|
14
|
+
readonly brand: {
|
|
15
|
+
readonly badge?: string
|
|
16
|
+
readonly href?: string
|
|
17
|
+
readonly logo?: ReactNode
|
|
18
|
+
readonly title: string
|
|
19
|
+
}
|
|
20
|
+
readonly className?: string
|
|
21
|
+
readonly onNewsletterClick?: () => void
|
|
22
|
+
readonly onSearch?: (query: string) => void
|
|
23
|
+
readonly readingProgress?: number
|
|
24
|
+
readonly topics?: readonly BlogTopicTag[]
|
|
25
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { ArrowLeftIcon, BookOpenIcon, MailIcon, SearchIcon, XIcon } from 'lucide-react'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Button } from '../../components/button'
|
|
5
|
+
import { Badge } from '../../components/badge'
|
|
6
|
+
import type { BlogHeaderProps } from './blog-header-types'
|
|
7
|
+
|
|
8
|
+
export function BlogHeader({
|
|
9
|
+
backToStoreHref = '/',
|
|
10
|
+
backToStoreLabel = 'Back to Store',
|
|
11
|
+
brand,
|
|
12
|
+
className,
|
|
13
|
+
onNewsletterClick,
|
|
14
|
+
onSearch,
|
|
15
|
+
readingProgress: explicitProgress,
|
|
16
|
+
topics = [
|
|
17
|
+
{ active: true, href: '#all', id: 'all', label: 'All Stories' },
|
|
18
|
+
{ href: '#engineering', id: 'engineering', label: 'Engineering' },
|
|
19
|
+
{ href: '#product-design', id: 'product-design', label: 'Product Design' },
|
|
20
|
+
{ href: '#architecture', id: 'architecture', label: 'Architecture' },
|
|
21
|
+
{ href: '#guides', id: 'guides', label: 'Guides & Case Studies' },
|
|
22
|
+
],
|
|
23
|
+
}: BlogHeaderProps) {
|
|
24
|
+
const [scrollProgress, setScrollProgress] = useState(0)
|
|
25
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
26
|
+
const [showSearch, setShowSearch] = useState(false)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (explicitProgress !== undefined) return
|
|
30
|
+
|
|
31
|
+
const handleScroll = () => {
|
|
32
|
+
const totalHeight = document.documentElement.scrollHeight - window.innerHeight
|
|
33
|
+
if (totalHeight <= 0) {
|
|
34
|
+
setScrollProgress(0)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const currentProgress = (window.scrollY / totalHeight) * 100
|
|
38
|
+
setScrollProgress(Math.min(100, Math.max(0, currentProgress)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
42
|
+
return () => window.removeEventListener('scroll', handleScroll)
|
|
43
|
+
}, [explicitProgress])
|
|
44
|
+
|
|
45
|
+
const progress = explicitProgress !== undefined ? explicitProgress : scrollProgress
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<header
|
|
49
|
+
className={cn(
|
|
50
|
+
'sticky top-0 z-40 w-full border-b border-border/80 bg-background/95 backdrop-blur-md',
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{/* 1. Scroll / Reading Progress Line */}
|
|
55
|
+
<div className="absolute top-0 left-0 right-0 h-1 bg-muted/40">
|
|
56
|
+
<div
|
|
57
|
+
className="h-full bg-primary transition-all duration-150"
|
|
58
|
+
style={{ width: `${progress}%` }}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* 2. Top Bridge & Brand Bar */}
|
|
63
|
+
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-3">
|
|
64
|
+
{/* Back to Store link + Brand */}
|
|
65
|
+
<div className="flex items-center gap-4">
|
|
66
|
+
<a
|
|
67
|
+
href={backToStoreHref}
|
|
68
|
+
className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground hover:text-foreground transition-colors"
|
|
69
|
+
>
|
|
70
|
+
<ArrowLeftIcon className="size-3.5" />
|
|
71
|
+
<span>{backToStoreLabel}</span>
|
|
72
|
+
</a>
|
|
73
|
+
|
|
74
|
+
<div className="h-4 w-px bg-border/80 hidden sm:block" />
|
|
75
|
+
|
|
76
|
+
<a href={brand.href ?? '/blog'} className="flex items-center gap-2">
|
|
77
|
+
{brand.logo ?? <BookOpenIcon className="size-5 text-primary" />}
|
|
78
|
+
<div className="flex items-baseline gap-2">
|
|
79
|
+
<span className="text-lg font-bold tracking-tight text-foreground">
|
|
80
|
+
{brand.title}
|
|
81
|
+
</span>
|
|
82
|
+
{brand.badge && (
|
|
83
|
+
<Badge variant="secondary" className="text-[10px] font-semibold py-0">
|
|
84
|
+
{brand.badge}
|
|
85
|
+
</Badge>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</a>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Search & Newsletter CTA */}
|
|
92
|
+
<div className="flex items-center gap-2">
|
|
93
|
+
{showSearch ? (
|
|
94
|
+
<div className="flex items-center rounded-lg border border-input bg-muted/30 px-2.5 py-1">
|
|
95
|
+
<SearchIcon className="size-3.5 text-muted-foreground mr-1.5" />
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
value={searchQuery}
|
|
99
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
100
|
+
onKeyDown={(e) => {
|
|
101
|
+
if (e.key === 'Enter') onSearch?.(searchQuery)
|
|
102
|
+
}}
|
|
103
|
+
placeholder="Search articles..."
|
|
104
|
+
className="w-36 sm:w-48 bg-transparent text-xs placeholder:text-muted-foreground focus:outline-none"
|
|
105
|
+
autoFocus
|
|
106
|
+
/>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setShowSearch(false)}
|
|
110
|
+
className="text-muted-foreground hover:text-foreground ml-1"
|
|
111
|
+
>
|
|
112
|
+
<XIcon className="size-3.5" />
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
) : (
|
|
116
|
+
<Button
|
|
117
|
+
size="sm"
|
|
118
|
+
variant="ghost"
|
|
119
|
+
className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
|
|
120
|
+
onClick={() => setShowSearch(true)}
|
|
121
|
+
>
|
|
122
|
+
<SearchIcon className="size-4" />
|
|
123
|
+
<span className="hidden sm:inline">Search</span>
|
|
124
|
+
</Button>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
<Button
|
|
128
|
+
size="sm"
|
|
129
|
+
variant="default"
|
|
130
|
+
className="h-8 gap-1.5 text-xs font-medium"
|
|
131
|
+
onClick={onNewsletterClick}
|
|
132
|
+
>
|
|
133
|
+
<MailIcon className="size-3.5" />
|
|
134
|
+
<span>Subscribe</span>
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* 3. Topics Strip */}
|
|
140
|
+
{topics.length > 0 && (
|
|
141
|
+
<div className="border-t border-border/40 bg-muted/15 px-4 py-1.5">
|
|
142
|
+
<div className="mx-auto flex max-w-7xl items-center gap-1.5 overflow-x-auto no-scrollbar">
|
|
143
|
+
{topics.map((t) => (
|
|
144
|
+
<a
|
|
145
|
+
key={t.id}
|
|
146
|
+
href={t.href}
|
|
147
|
+
className={cn(
|
|
148
|
+
'flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-all',
|
|
149
|
+
t.active
|
|
150
|
+
? 'bg-primary text-primary-foreground font-semibold shadow-xs'
|
|
151
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
<span>{t.label}</span>
|
|
155
|
+
{t.postCount !== undefined && (
|
|
156
|
+
<span
|
|
157
|
+
className={cn(
|
|
158
|
+
'text-[10px] opacity-75',
|
|
159
|
+
t.active ? 'text-primary-foreground' : 'text-muted-foreground',
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
({t.postCount})
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
</a>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</header>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Documentation Workspace
|
|
2
|
+
|
|
3
|
+
`DocumentationWorkspace` is the shared MDI shell for documentation and knowledge applications. It
|
|
4
|
+
supplies Docs identity, search, sidebar persistence, status, and workspace defaults while the owning
|
|
5
|
+
application supplies navigation, content, settings, topology, and data loading.
|
|
6
|
+
|
|
7
|
+
Import it from `@devxcrew/react-ui/layouts/documentation-workspace`. Do not put document persistence,
|
|
8
|
+
repository discovery, editing, or application routes in this package layout.
|