@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,41 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils'
|
|
2
|
+
import type { SiteHeaderCategory } from './site-header-types'
|
|
3
|
+
|
|
4
|
+
export interface CategoryStripProps {
|
|
5
|
+
categories: readonly SiteHeaderCategory[]
|
|
6
|
+
className?: string
|
|
7
|
+
onSelect?: (category: SiteHeaderCategory) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CategoryStrip({ categories, className, onSelect }: CategoryStripProps) {
|
|
11
|
+
if (!categories || categories.length === 0) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn('w-full border-t border-border/40 bg-muted/20 select-none', className)}>
|
|
15
|
+
<div className="mx-auto flex h-10 w-full max-w-7xl items-center overflow-x-auto px-4 scrollbar-none sm:px-6 lg:px-8">
|
|
16
|
+
<nav aria-label="Category navigation" className="flex items-center gap-1.5 min-w-max">
|
|
17
|
+
{categories.map((cat) => (
|
|
18
|
+
<a
|
|
19
|
+
className={cn(
|
|
20
|
+
'rounded-lg px-2.5 py-1 text-xs font-medium transition-colors',
|
|
21
|
+
cat.active
|
|
22
|
+
? 'bg-background text-foreground font-semibold shadow-2xs'
|
|
23
|
+
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground',
|
|
24
|
+
)}
|
|
25
|
+
href={cat.href}
|
|
26
|
+
key={cat.id}
|
|
27
|
+
onClick={(e) => {
|
|
28
|
+
if (onSelect) {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
onSelect(cat)
|
|
31
|
+
}
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{cat.label}
|
|
35
|
+
</a>
|
|
36
|
+
))}
|
|
37
|
+
</nav>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { SiteHeader } from './site-header'
|
|
2
|
+
export { AnnouncementBar } from './announcement-bar'
|
|
3
|
+
export { MainNavbar } from './main-navbar'
|
|
4
|
+
export { CategoryStrip } from './category-strip'
|
|
5
|
+
export { MobileDrawer } from './mobile-drawer'
|
|
6
|
+
export type {
|
|
7
|
+
SiteAnnouncementProps,
|
|
8
|
+
SiteHeaderActions,
|
|
9
|
+
SiteHeaderBrand,
|
|
10
|
+
SiteHeaderCategory,
|
|
11
|
+
SiteHeaderNavLink,
|
|
12
|
+
SiteHeaderProps,
|
|
13
|
+
} from './site-header-types'
|
|
14
|
+
export type { MainNavbarProps } from './main-navbar'
|
|
15
|
+
export type { CategoryStripProps } from './category-strip'
|
|
16
|
+
export type { MobileDrawerProps } from './mobile-drawer'
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Menu, Search, ShoppingBag, SunMoon, User } from 'lucide-react'
|
|
2
|
+
import { Badge } from '../../components/badge'
|
|
3
|
+
import { Button } from '../../components/button'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import type { SiteHeaderActions, SiteHeaderBrand, SiteHeaderNavLink } from './site-header-types'
|
|
6
|
+
|
|
7
|
+
export interface MainNavbarProps {
|
|
8
|
+
actions?: SiteHeaderActions
|
|
9
|
+
brand: SiteHeaderBrand
|
|
10
|
+
links?: readonly SiteHeaderNavLink[]
|
|
11
|
+
onOpenMobileMenu?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function MainNavbar({ actions = {}, brand, links = [], onOpenMobileMenu }: MainNavbarProps) {
|
|
15
|
+
const {
|
|
16
|
+
accountHref = '/account',
|
|
17
|
+
cartCount = 0,
|
|
18
|
+
ctaHref,
|
|
19
|
+
ctaLabel = 'Get Started',
|
|
20
|
+
onAccountClick,
|
|
21
|
+
onCartClick,
|
|
22
|
+
onCtaClick,
|
|
23
|
+
onSearchClick,
|
|
24
|
+
onThemeToggle,
|
|
25
|
+
searchPlaceholder = 'Search...',
|
|
26
|
+
showAccount = true,
|
|
27
|
+
showCart = true,
|
|
28
|
+
showCta = true,
|
|
29
|
+
showSearch = true,
|
|
30
|
+
showThemeToggle = true,
|
|
31
|
+
} = actions
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
|
|
35
|
+
{/* Brand */}
|
|
36
|
+
<div className="flex items-center gap-3">
|
|
37
|
+
<a
|
|
38
|
+
className="flex items-center gap-2.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
39
|
+
href={brand.href ?? '/'}
|
|
40
|
+
>
|
|
41
|
+
{brand.logo ? (
|
|
42
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground font-bold">
|
|
43
|
+
{brand.logo}
|
|
44
|
+
</div>
|
|
45
|
+
) : (
|
|
46
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground font-bold text-sm">
|
|
47
|
+
{brand.title.slice(0, 2).toUpperCase()}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
<div className="flex flex-col">
|
|
52
|
+
<div className="flex items-center gap-2">
|
|
53
|
+
<span className="text-base font-bold tracking-tight text-foreground">
|
|
54
|
+
{brand.title}
|
|
55
|
+
</span>
|
|
56
|
+
{brand.badge && (
|
|
57
|
+
<Badge className="text-[10px] font-semibold" variant="secondary">
|
|
58
|
+
{brand.badge}
|
|
59
|
+
</Badge>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
{brand.tagline && (
|
|
63
|
+
<span className="text-[11px] text-muted-foreground hidden sm:inline-block">
|
|
64
|
+
{brand.tagline}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</a>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Centered Navigation */}
|
|
72
|
+
{links.length > 0 && (
|
|
73
|
+
<nav aria-label="Main navigation" className="hidden md:flex items-center gap-1">
|
|
74
|
+
{links.map((link) => (
|
|
75
|
+
<a
|
|
76
|
+
className={cn(
|
|
77
|
+
'relative flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors select-none',
|
|
78
|
+
link.active
|
|
79
|
+
? 'text-primary font-semibold'
|
|
80
|
+
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground',
|
|
81
|
+
)}
|
|
82
|
+
href={link.href}
|
|
83
|
+
key={link.href}
|
|
84
|
+
rel={link.external ? 'noopener noreferrer' : undefined}
|
|
85
|
+
target={link.external ? '_blank' : undefined}
|
|
86
|
+
>
|
|
87
|
+
{link.label}
|
|
88
|
+
{link.badge && (
|
|
89
|
+
<span className="rounded-full bg-primary/10 px-1.5 py-0.2 text-[10px] font-semibold text-primary">
|
|
90
|
+
{link.badge}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
</a>
|
|
94
|
+
))}
|
|
95
|
+
</nav>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* Action Cluster */}
|
|
99
|
+
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
100
|
+
{showSearch && (
|
|
101
|
+
<button
|
|
102
|
+
aria-label="Search"
|
|
103
|
+
className="flex items-center gap-2 rounded-xl border border-border/80 bg-muted/40 px-2.5 py-1.5 text-xs text-muted-foreground transition-colors hover:border-foreground/30 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={onSearchClick}
|
|
106
|
+
>
|
|
107
|
+
<Search className="size-4" />
|
|
108
|
+
<span className="hidden lg:inline-block">{searchPlaceholder}</span>
|
|
109
|
+
<kbd className="hidden lg:inline-flex rounded border bg-background px-1 text-[10px] font-mono">
|
|
110
|
+
⌘K
|
|
111
|
+
</kbd>
|
|
112
|
+
</button>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{showThemeToggle && onThemeToggle && (
|
|
116
|
+
<Button
|
|
117
|
+
aria-label="Toggle theme mode"
|
|
118
|
+
className="size-8.5 rounded-xl text-muted-foreground hover:text-foreground"
|
|
119
|
+
size="icon"
|
|
120
|
+
variant="ghost"
|
|
121
|
+
onClick={onThemeToggle}
|
|
122
|
+
>
|
|
123
|
+
<SunMoon className="size-4" />
|
|
124
|
+
</Button>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{showAccount && (
|
|
128
|
+
<a
|
|
129
|
+
aria-label="Account profile"
|
|
130
|
+
className="inline-flex size-8.5 items-center justify-center rounded-xl text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
131
|
+
href={accountHref}
|
|
132
|
+
onClick={onAccountClick}
|
|
133
|
+
>
|
|
134
|
+
<User className="size-4" />
|
|
135
|
+
</a>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{showCart && (
|
|
139
|
+
<button
|
|
140
|
+
aria-label={`Shopping cart with ${cartCount} items`}
|
|
141
|
+
className="relative inline-flex size-8.5 items-center justify-center rounded-xl text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={onCartClick}
|
|
144
|
+
>
|
|
145
|
+
<ShoppingBag className="size-4" />
|
|
146
|
+
{cartCount > 0 && (
|
|
147
|
+
<span className="absolute -right-1 -top-1 flex size-4.5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground shadow-xs animate-in zoom-in">
|
|
148
|
+
{cartCount > 99 ? '99+' : cartCount}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</button>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{showCta && (
|
|
155
|
+
<Button
|
|
156
|
+
className="hidden sm:inline-flex rounded-xl font-semibold"
|
|
157
|
+
render={ctaHref ? <a href={ctaHref} /> : undefined}
|
|
158
|
+
size="sm"
|
|
159
|
+
variant="default"
|
|
160
|
+
onClick={onCtaClick}
|
|
161
|
+
>
|
|
162
|
+
{ctaLabel}
|
|
163
|
+
</Button>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/* Mobile menu trigger */}
|
|
167
|
+
<Button
|
|
168
|
+
aria-label="Open navigation menu"
|
|
169
|
+
className="md:hidden size-8.5 rounded-xl text-muted-foreground hover:text-foreground"
|
|
170
|
+
size="icon"
|
|
171
|
+
variant="ghost"
|
|
172
|
+
onClick={onOpenMobileMenu}
|
|
173
|
+
>
|
|
174
|
+
<Menu className="size-4.5" />
|
|
175
|
+
</Button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Search, X } from 'lucide-react'
|
|
2
|
+
import { Button } from '../../components/button'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import type {
|
|
5
|
+
SiteHeaderActions,
|
|
6
|
+
SiteHeaderBrand,
|
|
7
|
+
SiteHeaderCategory,
|
|
8
|
+
SiteHeaderNavLink,
|
|
9
|
+
} from './site-header-types'
|
|
10
|
+
|
|
11
|
+
export interface MobileDrawerProps {
|
|
12
|
+
actions?: SiteHeaderActions
|
|
13
|
+
brand: SiteHeaderBrand
|
|
14
|
+
categories?: readonly SiteHeaderCategory[]
|
|
15
|
+
isOpen: boolean
|
|
16
|
+
links?: readonly SiteHeaderNavLink[]
|
|
17
|
+
onClose: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function MobileDrawer({
|
|
21
|
+
actions = {},
|
|
22
|
+
brand,
|
|
23
|
+
categories = [],
|
|
24
|
+
isOpen,
|
|
25
|
+
links = [],
|
|
26
|
+
onClose,
|
|
27
|
+
}: MobileDrawerProps) {
|
|
28
|
+
if (!isOpen) return null
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
aria-label="Mobile navigation"
|
|
33
|
+
aria-modal="true"
|
|
34
|
+
className="fixed inset-0 z-50 flex md:hidden"
|
|
35
|
+
role="dialog"
|
|
36
|
+
>
|
|
37
|
+
{/* Backdrop */}
|
|
38
|
+
<div
|
|
39
|
+
className="fixed inset-0 bg-background/80 backdrop-blur-xs transition-opacity"
|
|
40
|
+
onClick={onClose}
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
{/* Drawer surface */}
|
|
44
|
+
<div className="relative ml-auto flex h-full w-full max-w-xs flex-col gap-4 border-l border-border/80 bg-background p-5 shadow-xl animate-in slide-in-from-right duration-200">
|
|
45
|
+
<div className="flex items-center justify-between border-b border-border/60 pb-3">
|
|
46
|
+
<span className="font-bold tracking-tight text-foreground">{brand.title}</span>
|
|
47
|
+
<button
|
|
48
|
+
aria-label="Close navigation"
|
|
49
|
+
className="rounded-lg p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={onClose}
|
|
52
|
+
>
|
|
53
|
+
<X className="size-5" />
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{actions.showSearch && (
|
|
58
|
+
<div className="relative">
|
|
59
|
+
<Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground/60" />
|
|
60
|
+
<input
|
|
61
|
+
className="h-9 w-full rounded-xl border border-border/80 bg-muted/30 pl-8 pr-3 text-xs text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:bg-background focus:outline-none"
|
|
62
|
+
placeholder={actions.searchPlaceholder || 'Search products & projects...'}
|
|
63
|
+
type="text"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
<div className="flex flex-1 flex-col gap-4 overflow-y-auto pr-1">
|
|
69
|
+
{/* Main links */}
|
|
70
|
+
<nav className="flex flex-col gap-1">
|
|
71
|
+
<span className="px-2 text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
72
|
+
Navigation
|
|
73
|
+
</span>
|
|
74
|
+
{links.map((link) => (
|
|
75
|
+
<a
|
|
76
|
+
className={cn(
|
|
77
|
+
'flex items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
78
|
+
link.active
|
|
79
|
+
? 'bg-primary/10 text-primary font-semibold'
|
|
80
|
+
: 'text-foreground/80 hover:bg-muted hover:text-foreground',
|
|
81
|
+
)}
|
|
82
|
+
href={link.href}
|
|
83
|
+
key={link.href}
|
|
84
|
+
onClick={onClose}
|
|
85
|
+
>
|
|
86
|
+
<span>{link.label}</span>
|
|
87
|
+
{link.badge && (
|
|
88
|
+
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary">
|
|
89
|
+
{link.badge}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</a>
|
|
93
|
+
))}
|
|
94
|
+
</nav>
|
|
95
|
+
|
|
96
|
+
{/* Categories */}
|
|
97
|
+
{categories.length > 0 && (
|
|
98
|
+
<div className="flex flex-col gap-1 border-t border-border/40 pt-3">
|
|
99
|
+
<span className="px-2 text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
100
|
+
Categories
|
|
101
|
+
</span>
|
|
102
|
+
<div className="flex flex-wrap gap-1.5 px-2 pt-1">
|
|
103
|
+
{categories.map((cat) => (
|
|
104
|
+
<a
|
|
105
|
+
className={cn(
|
|
106
|
+
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
|
|
107
|
+
cat.active
|
|
108
|
+
? 'bg-primary text-primary-foreground font-semibold'
|
|
109
|
+
: 'bg-muted/70 text-foreground/80 hover:bg-muted',
|
|
110
|
+
)}
|
|
111
|
+
href={cat.href}
|
|
112
|
+
key={cat.id}
|
|
113
|
+
onClick={onClose}
|
|
114
|
+
>
|
|
115
|
+
{cat.label}
|
|
116
|
+
</a>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{actions.showCta && (
|
|
124
|
+
<div className="border-t border-border/60 pt-3">
|
|
125
|
+
<Button
|
|
126
|
+
className="w-full rounded-xl font-semibold"
|
|
127
|
+
render={actions.ctaHref ? <a href={actions.ctaHref} /> : undefined}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
actions.onCtaClick?.()
|
|
130
|
+
onClose()
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{actions.ctaLabel || 'Get Started'}
|
|
134
|
+
</Button>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SiteHeaderNavLink {
|
|
4
|
+
active?: boolean
|
|
5
|
+
badge?: string
|
|
6
|
+
external?: boolean
|
|
7
|
+
href: string
|
|
8
|
+
label: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SiteHeaderCategory {
|
|
12
|
+
active?: boolean
|
|
13
|
+
href: string
|
|
14
|
+
id: string
|
|
15
|
+
label: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SiteAnnouncementProps {
|
|
19
|
+
actionLabel?: string
|
|
20
|
+
actionUrl?: string
|
|
21
|
+
dismissible?: boolean
|
|
22
|
+
message: string
|
|
23
|
+
onDismiss?: () => void
|
|
24
|
+
show?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SiteHeaderBrand {
|
|
28
|
+
badge?: string
|
|
29
|
+
href?: string
|
|
30
|
+
logo?: ReactNode
|
|
31
|
+
tagline?: string
|
|
32
|
+
title: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SiteHeaderActions {
|
|
36
|
+
accountHref?: string
|
|
37
|
+
cartCount?: number
|
|
38
|
+
ctaHref?: string
|
|
39
|
+
ctaLabel?: string
|
|
40
|
+
onAccountClick?: () => void
|
|
41
|
+
onCartClick?: () => void
|
|
42
|
+
onCtaClick?: () => void
|
|
43
|
+
onSearchClick?: () => void
|
|
44
|
+
onThemeToggle?: () => void
|
|
45
|
+
searchPlaceholder?: string
|
|
46
|
+
showAccount?: boolean
|
|
47
|
+
showCart?: boolean
|
|
48
|
+
showCta?: boolean
|
|
49
|
+
showSearch?: boolean
|
|
50
|
+
showThemeToggle?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SiteHeaderProps {
|
|
54
|
+
actions?: SiteHeaderActions
|
|
55
|
+
announcement?: SiteAnnouncementProps
|
|
56
|
+
brand: SiteHeaderBrand
|
|
57
|
+
categories?: readonly SiteHeaderCategory[]
|
|
58
|
+
className?: string
|
|
59
|
+
links?: readonly SiteHeaderNavLink[]
|
|
60
|
+
onCategorySelect?: (category: SiteHeaderCategory) => void
|
|
61
|
+
showCategories?: boolean
|
|
62
|
+
sticky?: boolean
|
|
63
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
import { AnnouncementBar } from './announcement-bar'
|
|
4
|
+
import { CategoryStrip } from './category-strip'
|
|
5
|
+
import { MainNavbar } from './main-navbar'
|
|
6
|
+
import { MobileDrawer } from './mobile-drawer'
|
|
7
|
+
import type { SiteHeaderProps } from './site-header-types'
|
|
8
|
+
|
|
9
|
+
export function SiteHeader({
|
|
10
|
+
actions,
|
|
11
|
+
announcement,
|
|
12
|
+
brand,
|
|
13
|
+
categories = [],
|
|
14
|
+
className,
|
|
15
|
+
links = [],
|
|
16
|
+
onCategorySelect,
|
|
17
|
+
showCategories = true,
|
|
18
|
+
sticky = true,
|
|
19
|
+
}: SiteHeaderProps) {
|
|
20
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<header
|
|
24
|
+
className={cn(
|
|
25
|
+
'w-full flex flex-col z-40 transition-shadow',
|
|
26
|
+
sticky &&
|
|
27
|
+
'sticky top-0 bg-background/90 backdrop-blur-md border-b border-border/70 shadow-2xs',
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{/* 1. Top Announcement Bar */}
|
|
32
|
+
{announcement && <AnnouncementBar {...announcement} />}
|
|
33
|
+
|
|
34
|
+
{/* 2. Primary Navigation Bar */}
|
|
35
|
+
<MainNavbar
|
|
36
|
+
actions={actions}
|
|
37
|
+
brand={brand}
|
|
38
|
+
links={links}
|
|
39
|
+
onOpenMobileMenu={() => setMobileMenuOpen(true)}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
{/* 3. Sub-navigation Category Strip */}
|
|
43
|
+
{showCategories && categories.length > 0 && (
|
|
44
|
+
<CategoryStrip categories={categories} onSelect={onCategorySelect} />
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{/* 4. Responsive Mobile Drawer */}
|
|
48
|
+
<MobileDrawer
|
|
49
|
+
actions={actions}
|
|
50
|
+
brand={brand}
|
|
51
|
+
categories={categories}
|
|
52
|
+
isOpen={mobileMenuOpen}
|
|
53
|
+
links={links}
|
|
54
|
+
onClose={() => setMobileMenuOpen(false)}
|
|
55
|
+
/>
|
|
56
|
+
</header>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns an RFC 4122 version 4 identifier without requiring `crypto.randomUUID`.
|
|
3
|
+
*
|
|
4
|
+
* Older browsers can provide `getRandomValues` but not `randomUUID`; the final
|
|
5
|
+
* fallback keeps non-security-critical client correlation IDs working as well.
|
|
6
|
+
*/
|
|
7
|
+
export function createRandomId(): string {
|
|
8
|
+
const browserCrypto = globalThis.crypto;
|
|
9
|
+
if (typeof browserCrypto?.randomUUID === "function") return browserCrypto.randomUUID();
|
|
10
|
+
|
|
11
|
+
const bytes = new Uint8Array(16);
|
|
12
|
+
if (typeof browserCrypto?.getRandomValues === "function") browserCrypto.getRandomValues(bytes);
|
|
13
|
+
else for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
|
|
14
|
+
|
|
15
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
16
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
17
|
+
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
19
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jsPDF } from "jspdf";
|
|
2
|
+
|
|
3
|
+
export type PdfDocumentSource = {
|
|
4
|
+
content: string;
|
|
5
|
+
title: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type PdfShareChannel = "email" | "whatsapp";
|
|
9
|
+
|
|
10
|
+
export class PdfProvider {
|
|
11
|
+
static async copyText(content: string): Promise<void> {
|
|
12
|
+
if (navigator.clipboard?.writeText) {
|
|
13
|
+
try {
|
|
14
|
+
await navigator.clipboard.writeText(content);
|
|
15
|
+
return;
|
|
16
|
+
} catch {
|
|
17
|
+
// Browser clipboard permissions can be unavailable in embedded desktop webviews.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const textarea = window.document.createElement("textarea");
|
|
22
|
+
textarea.value = content;
|
|
23
|
+
textarea.style.position = "fixed";
|
|
24
|
+
textarea.style.opacity = "0";
|
|
25
|
+
window.document.body.append(textarea);
|
|
26
|
+
textarea.select();
|
|
27
|
+
window.document.execCommand("copy");
|
|
28
|
+
textarea.remove();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static async shareText(source: PdfDocumentSource): Promise<void> {
|
|
32
|
+
if (navigator.share) {
|
|
33
|
+
try {
|
|
34
|
+
await navigator.share({ text: source.content, title: source.title });
|
|
35
|
+
return;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as { name?: string }).name === "AbortError") return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await this.copyText(source.content);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async create(source: PdfDocumentSource): Promise<File> {
|
|
44
|
+
const document = new jsPDF({ format: "a4", unit: "mm" });
|
|
45
|
+
const margin = 18;
|
|
46
|
+
const pageWidth = document.internal.pageSize.getWidth() - margin * 2;
|
|
47
|
+
const pageHeight = document.internal.pageSize.getHeight() - margin * 2;
|
|
48
|
+
const title = source.title.trim() || "Zetro response";
|
|
49
|
+
const lines = document.splitTextToSize(toPlainText(source.content), pageWidth);
|
|
50
|
+
let cursor = margin;
|
|
51
|
+
|
|
52
|
+
document.setFont("helvetica", "bold");
|
|
53
|
+
document.setFontSize(16);
|
|
54
|
+
document.text(title, margin, cursor);
|
|
55
|
+
cursor += 10;
|
|
56
|
+
document.setFont("helvetica", "normal");
|
|
57
|
+
document.setFontSize(10);
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (cursor > pageHeight) {
|
|
61
|
+
document.addPage();
|
|
62
|
+
cursor = margin;
|
|
63
|
+
}
|
|
64
|
+
document.text(line, margin, cursor);
|
|
65
|
+
cursor += 5;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const pageCount = document.getNumberOfPages();
|
|
69
|
+
for (let page = 1; page <= pageCount; page += 1) {
|
|
70
|
+
document.setPage(page);
|
|
71
|
+
document.setFontSize(8);
|
|
72
|
+
document.setTextColor(110);
|
|
73
|
+
document.text(`Zetro · ${page} / ${pageCount}`, margin, document.internal.pageSize.getHeight() - 10);
|
|
74
|
+
document.setTextColor(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new File([document.output("blob")], `${toFileName(title)}.pdf`, { type: "application/pdf" });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static async download(source: PdfDocumentSource): Promise<File> {
|
|
81
|
+
const file = await this.create(source);
|
|
82
|
+
const url = URL.createObjectURL(file);
|
|
83
|
+
const anchor = window.document.createElement("a");
|
|
84
|
+
anchor.href = url;
|
|
85
|
+
anchor.download = file.name;
|
|
86
|
+
anchor.click();
|
|
87
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
88
|
+
return file;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static print(source: PdfDocumentSource): void {
|
|
92
|
+
const printWindow = window.open("", "_blank", "popup");
|
|
93
|
+
if (!printWindow) throw new Error("Allow pop-ups to print this response.");
|
|
94
|
+
printWindow.document.write(printDocument(source));
|
|
95
|
+
printWindow.document.close();
|
|
96
|
+
printWindow.focus();
|
|
97
|
+
printWindow.print();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static async share(source: PdfDocumentSource, channel: PdfShareChannel): Promise<void> {
|
|
101
|
+
const file = await this.create(source);
|
|
102
|
+
if (navigator.share && navigator.canShare?.({ files: [file] })) {
|
|
103
|
+
await navigator.share({ files: [file], text: source.title, title: source.title });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await this.download(source);
|
|
108
|
+
const message = encodeURIComponent(`${source.title}\n\nThe PDF has downloaded and is ready to attach.`);
|
|
109
|
+
if (channel === "email") {
|
|
110
|
+
window.location.assign(`mailto:?subject=${encodeURIComponent(source.title)}&body=${message}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
window.open(`https://wa.me/?text=${message}`, "_blank", "noopener,noreferrer");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toPlainText(value: string): string {
|
|
118
|
+
const parser = new DOMParser();
|
|
119
|
+
return (parser.parseFromString(value, "text/html").body.textContent || value)
|
|
120
|
+
.replace(/```[\s\S]*?```/g, (block) => block.replace(/```/g, ""))
|
|
121
|
+
.replace(/[`*_#]/g, "")
|
|
122
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
123
|
+
.trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toFileName(value: string): string {
|
|
127
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "zetro-response";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printDocument(source: PdfDocumentSource): string {
|
|
131
|
+
const content = escapeHtml(source.content).replace(/\n/g, "<br />");
|
|
132
|
+
return `<!doctype html><html><head><title>${escapeHtml(source.title)}</title><style>@page { margin: 18mm; } body { color: #111; font: 11pt/1.6 Arial, sans-serif; } h1 { font-size: 18pt; margin: 0 0 12mm; } article { white-space: normal; } </style></head><body><h1>${escapeHtml(source.title)}</h1><article>${content}</article></body></html>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function escapeHtml(value: string): string {
|
|
136
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
137
|
+
}
|