@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,212 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ArrowRightIcon, CheckIcon, MailIcon, ShieldCheckIcon } from 'lucide-react'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import { Button } from '../../components/button'
|
|
5
|
+
import type { SiteFooterProps } from './footer-types'
|
|
6
|
+
|
|
7
|
+
const defaultColumns = [
|
|
8
|
+
{
|
|
9
|
+
title: 'Storefront',
|
|
10
|
+
links: [
|
|
11
|
+
{ href: '#products', label: 'All Products' },
|
|
12
|
+
{ href: '#deals', label: 'Flash Deals', badge: 'Hot' },
|
|
13
|
+
{ href: '#bestsellers', label: 'Best Sellers' },
|
|
14
|
+
{ href: '#new', label: 'New Arrivals' },
|
|
15
|
+
{ href: '#compare', label: 'Product Compare' },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: 'Customer Care',
|
|
20
|
+
links: [
|
|
21
|
+
{ href: '#tracking', label: 'Delivery Tracker' },
|
|
22
|
+
{ href: '#returns', label: 'Returns & Refunds' },
|
|
23
|
+
{ href: '#faq', label: 'Help & FAQ' },
|
|
24
|
+
{ href: '#shipping', label: 'Shipping Information' },
|
|
25
|
+
{ href: '#contact', label: 'Contact Support' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: 'Editorial & Story',
|
|
30
|
+
links: [
|
|
31
|
+
{ href: '#blog', label: 'Journal & Stories' },
|
|
32
|
+
{ href: '#guides', label: 'Buying Guides' },
|
|
33
|
+
{ href: '#authors', label: 'Design Team' },
|
|
34
|
+
{ href: '#community', label: 'Community Forum' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'Legal & Security',
|
|
39
|
+
links: [
|
|
40
|
+
{ href: '#privacy', label: 'Privacy Policy' },
|
|
41
|
+
{ href: '#terms', label: 'Terms of Service' },
|
|
42
|
+
{ href: '#security', label: 'Security & Trust' },
|
|
43
|
+
{ href: '#compliance', label: 'Compliance' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export function SiteFooter({
|
|
49
|
+
brand,
|
|
50
|
+
className,
|
|
51
|
+
columns = defaultColumns,
|
|
52
|
+
copyright = `© ${new Date().getFullYear()} ${brand.title}. All rights reserved.`,
|
|
53
|
+
newsletter = {
|
|
54
|
+
description: 'Get weekly design insights, product drops, and exclusive flash sale access.',
|
|
55
|
+
placeholder: 'Enter your email address...',
|
|
56
|
+
title: 'Subscribe to our newsletter',
|
|
57
|
+
},
|
|
58
|
+
paymentBadges = ['Visa', 'Mastercard', 'Amex', 'PayPal', 'Apple Pay', 'Stripe'],
|
|
59
|
+
socialLinks = [],
|
|
60
|
+
}: SiteFooterProps) {
|
|
61
|
+
const [email, setEmail] = useState('')
|
|
62
|
+
const [isSubscribed, setIsSubscribed] = useState(false)
|
|
63
|
+
|
|
64
|
+
const handleSubscribe = (e: React.FormEvent) => {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
if (!email.includes('@')) return
|
|
67
|
+
newsletter.onSubscribe?.(email)
|
|
68
|
+
setIsSubscribed(true)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<footer
|
|
73
|
+
className={cn('w-full border-t border-border/80 bg-background text-foreground', className)}
|
|
74
|
+
>
|
|
75
|
+
{/* 1. Newsletter Strip */}
|
|
76
|
+
{newsletter && (
|
|
77
|
+
<div className="border-b border-border/60 bg-muted/20 px-6 py-10">
|
|
78
|
+
<div className="mx-auto flex max-w-7xl flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
|
|
79
|
+
<div className="space-y-1">
|
|
80
|
+
<h3 className="text-base font-bold tracking-tight text-foreground flex items-center gap-2">
|
|
81
|
+
<MailIcon className="size-4 text-primary" />
|
|
82
|
+
<span>{newsletter.title}</span>
|
|
83
|
+
</h3>
|
|
84
|
+
{newsletter.description && (
|
|
85
|
+
<p className="text-xs text-muted-foreground">{newsletter.description}</p>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<form onSubmit={handleSubscribe} className="flex max-w-md w-full gap-2">
|
|
90
|
+
<input
|
|
91
|
+
type="email"
|
|
92
|
+
required
|
|
93
|
+
value={email}
|
|
94
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
95
|
+
placeholder={newsletter.placeholder ?? 'Enter your email...'}
|
|
96
|
+
className="flex-1 rounded-xl border border-input bg-background px-3.5 py-2 text-xs placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
97
|
+
/>
|
|
98
|
+
<Button
|
|
99
|
+
size="sm"
|
|
100
|
+
type="submit"
|
|
101
|
+
className="gap-1.5 text-xs px-4"
|
|
102
|
+
disabled={isSubscribed}
|
|
103
|
+
>
|
|
104
|
+
{isSubscribed ? (
|
|
105
|
+
<>
|
|
106
|
+
<CheckIcon className="size-3.5" />
|
|
107
|
+
<span>Subscribed</span>
|
|
108
|
+
</>
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
<span>Join</span>
|
|
112
|
+
<ArrowRightIcon className="size-3.5" />
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</Button>
|
|
116
|
+
</form>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* 2. Main Columns Navigation */}
|
|
122
|
+
<div className="mx-auto max-w-7xl px-6 py-12">
|
|
123
|
+
<div className="grid grid-cols-1 md:grid-cols-12 gap-10">
|
|
124
|
+
{/* Brand Column */}
|
|
125
|
+
<div className="md:col-span-4 space-y-4">
|
|
126
|
+
<div className="flex items-center gap-2.5">
|
|
127
|
+
{brand.logo}
|
|
128
|
+
<span className="text-lg font-bold tracking-tight text-foreground">
|
|
129
|
+
{brand.title}
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{brand.description && (
|
|
134
|
+
<p className="text-xs text-muted-foreground leading-relaxed max-w-sm">
|
|
135
|
+
{brand.description}
|
|
136
|
+
</p>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground pt-1">
|
|
140
|
+
<ShieldCheckIcon className="size-4 text-emerald-600" />
|
|
141
|
+
<span>Certified secure checkout & buyer protection</span>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{socialLinks.length > 0 && (
|
|
145
|
+
<div className="flex items-center gap-2 pt-2">
|
|
146
|
+
{socialLinks.map((s) => (
|
|
147
|
+
<a
|
|
148
|
+
key={s.label}
|
|
149
|
+
href={s.href}
|
|
150
|
+
aria-label={s.label}
|
|
151
|
+
className="flex size-8 items-center justify-center rounded-lg border border-border/80 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
|
152
|
+
>
|
|
153
|
+
{s.icon}
|
|
154
|
+
</a>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Links Columns */}
|
|
161
|
+
<div className="md:col-span-8 grid grid-cols-2 sm:grid-cols-4 gap-8">
|
|
162
|
+
{columns.map((col) => (
|
|
163
|
+
<div key={col.title} className="space-y-3">
|
|
164
|
+
<h4 className="text-xs font-bold uppercase tracking-wider text-foreground">
|
|
165
|
+
{col.title}
|
|
166
|
+
</h4>
|
|
167
|
+
<ul className="space-y-2 text-xs">
|
|
168
|
+
{col.links.map((link) => (
|
|
169
|
+
<li key={link.label}>
|
|
170
|
+
<a
|
|
171
|
+
href={link.href}
|
|
172
|
+
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
173
|
+
>
|
|
174
|
+
<span>{link.label}</span>
|
|
175
|
+
{link.badge && (
|
|
176
|
+
<span className="rounded bg-primary/10 px-1 py-0.2 text-[9px] font-bold text-primary">
|
|
177
|
+
{link.badge}
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
</a>
|
|
181
|
+
</li>
|
|
182
|
+
))}
|
|
183
|
+
</ul>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* 3. Bottom Legal & Payment Badges Strip */}
|
|
191
|
+
<div className="border-t border-border/60 bg-muted/10 px-6 py-6">
|
|
192
|
+
<div className="mx-auto flex max-w-7xl flex-col sm:flex-row items-center justify-between gap-4 text-xs text-muted-foreground">
|
|
193
|
+
<p>{copyright}</p>
|
|
194
|
+
|
|
195
|
+
{/* Payment Badges */}
|
|
196
|
+
{paymentBadges.length > 0 && (
|
|
197
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
198
|
+
{paymentBadges.map((badge) => (
|
|
199
|
+
<span
|
|
200
|
+
key={badge}
|
|
201
|
+
className="rounded-md border border-border/60 bg-background px-2 py-0.5 font-mono text-[10px] font-bold text-muted-foreground"
|
|
202
|
+
>
|
|
203
|
+
{badge}
|
|
204
|
+
</span>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</footer>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { ArrowLeft, Save, X } from 'lucide-react'
|
|
3
|
+
import { Button } from '../../components/button'
|
|
4
|
+
import { Switch } from '../../components/switch'
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/tabs'
|
|
6
|
+
|
|
7
|
+
export type FormBlockTab = {
|
|
8
|
+
content: ReactNode
|
|
9
|
+
id: string
|
|
10
|
+
label: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FormBlockProps = {
|
|
14
|
+
active: boolean
|
|
15
|
+
activeLabel?: string
|
|
16
|
+
defaultTab?: string
|
|
17
|
+
description: string
|
|
18
|
+
onActiveChange: (active: boolean) => void
|
|
19
|
+
onBack: () => void
|
|
20
|
+
onCancel: () => void
|
|
21
|
+
onSubmit: () => void
|
|
22
|
+
submitLabel?: string
|
|
23
|
+
tabs: readonly FormBlockTab[]
|
|
24
|
+
title: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const actionMotion =
|
|
28
|
+
'transition-[transform,box-shadow] duration-200 ease-out hover:-translate-y-0.5 hover:shadow-md motion-reduce:transform-none motion-reduce:transition-none'
|
|
29
|
+
|
|
30
|
+
export function FormBlock({
|
|
31
|
+
active,
|
|
32
|
+
activeLabel = 'Active',
|
|
33
|
+
defaultTab,
|
|
34
|
+
description,
|
|
35
|
+
onActiveChange,
|
|
36
|
+
onBack,
|
|
37
|
+
onCancel,
|
|
38
|
+
onSubmit,
|
|
39
|
+
submitLabel = 'Save',
|
|
40
|
+
tabs,
|
|
41
|
+
title,
|
|
42
|
+
}: FormBlockProps) {
|
|
43
|
+
const firstTab = defaultTab ?? tabs[0]?.id
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section className="w-full">
|
|
47
|
+
<form
|
|
48
|
+
className="grid gap-3"
|
|
49
|
+
onSubmit={(event) => {
|
|
50
|
+
event.preventDefault()
|
|
51
|
+
onSubmit()
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<header className="flex min-h-12 flex-wrap items-center justify-between gap-2 rounded-md border bg-card px-4 py-2 shadow-sm">
|
|
55
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
56
|
+
<Button
|
|
57
|
+
className={actionMotion}
|
|
58
|
+
onClick={onBack}
|
|
59
|
+
size="sm"
|
|
60
|
+
type="button"
|
|
61
|
+
variant="outline"
|
|
62
|
+
>
|
|
63
|
+
<ArrowLeft /> Back
|
|
64
|
+
</Button>
|
|
65
|
+
<h1 className="truncate text-sm font-semibold">{title}</h1>
|
|
66
|
+
<p className="sr-only">{description}</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex items-center gap-2">
|
|
69
|
+
<Button
|
|
70
|
+
className={actionMotion}
|
|
71
|
+
onClick={onCancel}
|
|
72
|
+
size="sm"
|
|
73
|
+
type="button"
|
|
74
|
+
variant="outline"
|
|
75
|
+
>
|
|
76
|
+
<X /> Cancel
|
|
77
|
+
</Button>
|
|
78
|
+
<Button className={actionMotion} size="sm" type="submit">
|
|
79
|
+
<Save /> {submitLabel}
|
|
80
|
+
</Button>
|
|
81
|
+
</div>
|
|
82
|
+
</header>
|
|
83
|
+
|
|
84
|
+
<div className="overflow-hidden rounded-md border bg-card shadow-sm">
|
|
85
|
+
<Tabs className="gap-0" defaultValue={firstTab}>
|
|
86
|
+
<TabsList
|
|
87
|
+
className="h-auto w-full justify-start overflow-x-auto rounded-none border-b bg-transparent px-4 pt-2"
|
|
88
|
+
variant="line"
|
|
89
|
+
>
|
|
90
|
+
{tabs.map((tab) => (
|
|
91
|
+
<TabsTrigger className="min-h-10 flex-none px-3" key={tab.id} value={tab.id}>
|
|
92
|
+
{tab.label}
|
|
93
|
+
</TabsTrigger>
|
|
94
|
+
))}
|
|
95
|
+
</TabsList>
|
|
96
|
+
{tabs.map((tab) => (
|
|
97
|
+
<TabsContent className="p-4" key={tab.id} value={tab.id}>
|
|
98
|
+
{tab.content}
|
|
99
|
+
</TabsContent>
|
|
100
|
+
))}
|
|
101
|
+
</Tabs>
|
|
102
|
+
|
|
103
|
+
<div className="px-4 pb-4">
|
|
104
|
+
<label className="flex min-h-12 items-center justify-between gap-4 rounded-md border border-success/35 bg-success/10 px-4 py-2.5 transition-colors hover:bg-success/15">
|
|
105
|
+
<span className="font-medium">{activeLabel}</span>
|
|
106
|
+
<Switch aria-label={activeLabel} checked={active} onCheckedChange={onActiveChange} />
|
|
107
|
+
</label>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</form>
|
|
111
|
+
</section>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Combobox,
|
|
3
|
+
ComboboxContent,
|
|
4
|
+
ComboboxEmpty,
|
|
5
|
+
ComboboxInput,
|
|
6
|
+
ComboboxItem,
|
|
7
|
+
ComboboxList,
|
|
8
|
+
} from '../../components/combobox'
|
|
9
|
+
import { Field, FieldLabel } from '../../components/field'
|
|
10
|
+
|
|
11
|
+
export type FormLookupOption = {
|
|
12
|
+
label: string
|
|
13
|
+
value: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function FormLookupField({
|
|
17
|
+
label,
|
|
18
|
+
onValueChange,
|
|
19
|
+
options,
|
|
20
|
+
placeholder,
|
|
21
|
+
required = false,
|
|
22
|
+
value,
|
|
23
|
+
}: {
|
|
24
|
+
label: string
|
|
25
|
+
onValueChange: (value: string) => void
|
|
26
|
+
options: readonly FormLookupOption[]
|
|
27
|
+
placeholder: string
|
|
28
|
+
required?: boolean
|
|
29
|
+
value: string
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<Field>
|
|
33
|
+
<FieldLabel>
|
|
34
|
+
{label}
|
|
35
|
+
{required ? <span className="text-destructive">*</span> : null}
|
|
36
|
+
</FieldLabel>
|
|
37
|
+
<Combobox
|
|
38
|
+
items={options.map((option) => option.value)}
|
|
39
|
+
onValueChange={(nextValue) => onValueChange(nextValue ?? '')}
|
|
40
|
+
value={value}
|
|
41
|
+
>
|
|
42
|
+
<ComboboxInput className="h-10" placeholder={placeholder} showClear />
|
|
43
|
+
<ComboboxContent>
|
|
44
|
+
<ComboboxEmpty>No matching option.</ComboboxEmpty>
|
|
45
|
+
<ComboboxList>
|
|
46
|
+
{options.map((option) => (
|
|
47
|
+
<ComboboxItem
|
|
48
|
+
className="min-h-10 cursor-pointer px-3"
|
|
49
|
+
key={option.value}
|
|
50
|
+
value={option.value}
|
|
51
|
+
>
|
|
52
|
+
{option.label}
|
|
53
|
+
</ComboboxItem>
|
|
54
|
+
))}
|
|
55
|
+
</ComboboxList>
|
|
56
|
+
</ComboboxContent>
|
|
57
|
+
</Combobox>
|
|
58
|
+
</Field>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ClipboardCheckIcon, Layers3Icon, XIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@devxcrew/react-ui/components/badge";
|
|
4
|
+
import { Button } from "@devxcrew/react-ui/components/button";
|
|
5
|
+
import { ScrollArea } from "@devxcrew/react-ui/components/scroll-area";
|
|
6
|
+
import { Separator } from "@devxcrew/react-ui/components/separator";
|
|
7
|
+
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@devxcrew/react-ui/components/sheet";
|
|
8
|
+
|
|
9
|
+
export type HandoverStackItem = {
|
|
10
|
+
content: string;
|
|
11
|
+
id: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type HandoverStackProps = {
|
|
15
|
+
items: readonly HandoverStackItem[];
|
|
16
|
+
onConsolidate: () => void;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
onRemove: (id: string) => void;
|
|
19
|
+
open: boolean;
|
|
20
|
+
working?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function HandoverStack({ items, onConsolidate, onOpenChange, onRemove, open, working }: HandoverStackProps) {
|
|
24
|
+
return <Sheet open={open} onOpenChange={onOpenChange}>
|
|
25
|
+
<SheetContent className="gap-0 p-0 sm:max-w-md" side="right">
|
|
26
|
+
<SheetHeader className="border-b pr-12">
|
|
27
|
+
<div className="flex items-center gap-2"><SheetTitle>Handover stack</SheetTitle><Badge variant="secondary">{items.length}</Badge></div>
|
|
28
|
+
<SheetDescription>Selected Zetro responses, ready to consolidate into one revised idea.</SheetDescription>
|
|
29
|
+
</SheetHeader>
|
|
30
|
+
<ScrollArea className="min-h-0 flex-1"><div className="flex flex-col p-4">
|
|
31
|
+
{items.length ? items.map((item, index) => <section key={item.id} className="flex flex-col gap-2 py-3 first:pt-0 last:pb-0">
|
|
32
|
+
<div className="flex items-center gap-2"><Layers3Icon className="size-3.5 text-muted-foreground" /><p className="flex-1 text-sm font-medium">Response {index + 1}</p><Button aria-label={`Remove response ${index + 1}`} size="icon-xs" variant="ghost" onClick={() => onRemove(item.id)}><XIcon /></Button></div>
|
|
33
|
+
<p className="line-clamp-5 text-sm leading-6 text-muted-foreground">{item.content}</p>
|
|
34
|
+
<code className="text-xs text-muted-foreground">{item.id}</code>
|
|
35
|
+
{index < items.length - 1 ? <Separator className="mt-1" /> : null}
|
|
36
|
+
</section>) : <div className="flex min-h-40 flex-col items-center justify-center gap-2 text-center"><Layers3Icon className="size-5 text-muted-foreground" /><p className="text-sm font-medium">No responses collected</p><p className="max-w-56 text-sm text-muted-foreground">Use the stack icon below any Zetro response to collect it here.</p></div>}
|
|
37
|
+
</div></ScrollArea>
|
|
38
|
+
<SheetFooter className="border-t"><Button className="w-full" disabled={!items.length || working} onClick={onConsolidate}><ClipboardCheckIcon /> {working ? "Consolidating…" : "Consolidate revised idea"}</Button></SheetFooter>
|
|
39
|
+
</SheetContent>
|
|
40
|
+
</Sheet>;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HandoverStack, type HandoverStackItem, type HandoverStackProps } from "./handover-stack";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IdeaBriefDraft } from "./idea-handover-workspace";
|
|
2
|
+
|
|
3
|
+
export type BriefReadinessItem = {
|
|
4
|
+
complete: boolean;
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getBriefReadiness(brief: IdeaBriefDraft): BriefReadinessItem[] {
|
|
10
|
+
return [
|
|
11
|
+
readinessItem("title", "A clear title", brief.title),
|
|
12
|
+
readinessItem("outcome", "A defined outcome", brief.outcome),
|
|
13
|
+
readinessItem("audience", "A named audience", brief.audience),
|
|
14
|
+
readinessItem("scope", "Scope boundaries", brief.scope),
|
|
15
|
+
readinessItem("exclusions", "Explicit exclusions", brief.exclusions),
|
|
16
|
+
readinessItem("constraints", "Known constraints", brief.constraints),
|
|
17
|
+
readinessItem("risks", "Risks to consider", brief.risks),
|
|
18
|
+
readinessItem("success-signals", "Measurable success signals", brief.successSignals),
|
|
19
|
+
{ complete: brief.sourceMessageIds.length > 0, id: "sources", label: "At least one source response" },
|
|
20
|
+
{ complete: brief.projectScope === "all-projects" || Boolean(brief.projectReference?.trim()), id: "project", label: "A project reference when scoped" },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isBriefReady(brief: IdeaBriefDraft): boolean {
|
|
25
|
+
return getBriefReadiness(brief).every((item) => item.complete);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseAcceptanceCriteria(value: string): string[] {
|
|
29
|
+
return value
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
.map((item) => item.replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function serializeAcceptanceCriteria(items: readonly string[]): string {
|
|
36
|
+
return items.map((item) => item.trim()).filter(Boolean).join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readinessItem(id: string, label: string, value: string): BriefReadinessItem {
|
|
40
|
+
return { complete: Boolean(value.trim()), id, label };
|
|
41
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { ArchiveIcon, CheckCircle2Icon, CheckIcon, ChevronLeftIcon, CircleIcon, ClipboardIcon, EyeIcon, FileCheck2Icon, ListTodoIcon, NetworkIcon, PlusIcon, RefreshCwIcon, SendIcon, Trash2Icon, WandSparklesIcon } from "lucide-react";
|
|
3
|
+
import { Button } from "@devxcrew/react-ui/components/button";
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@devxcrew/react-ui/components/card";
|
|
5
|
+
import { Input } from "@devxcrew/react-ui/components/input";
|
|
6
|
+
import { Label } from "@devxcrew/react-ui/components/label";
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@devxcrew/react-ui/components/select";
|
|
8
|
+
import { Textarea } from "@devxcrew/react-ui/components/textarea";
|
|
9
|
+
import { getBriefReadiness, isBriefReady, parseAcceptanceCriteria, serializeAcceptanceCriteria } from "./idea-handover-readiness";
|
|
10
|
+
|
|
11
|
+
export type IdeaStage = "explore" | "compare" | "revise" | "final";
|
|
12
|
+
|
|
13
|
+
export type IdeaBriefDraft = {
|
|
14
|
+
audience: string;
|
|
15
|
+
constraints: string;
|
|
16
|
+
exclusions: string;
|
|
17
|
+
outcome: string;
|
|
18
|
+
projectReference: string | null;
|
|
19
|
+
projectScope: "project" | "all-projects";
|
|
20
|
+
risks: string;
|
|
21
|
+
scope: string;
|
|
22
|
+
sourceMessageIds: string[];
|
|
23
|
+
status: "draft" | "final";
|
|
24
|
+
successSignals: string;
|
|
25
|
+
title: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type IdeaHandoverTask = {
|
|
29
|
+
acceptanceCriteria?: string;
|
|
30
|
+
deliveredAt?: string | null;
|
|
31
|
+
deliveryError?: string | null;
|
|
32
|
+
id: string;
|
|
33
|
+
projectReference: string | null;
|
|
34
|
+
projectScope: "project" | "all-projects";
|
|
35
|
+
summary?: string;
|
|
36
|
+
status: string;
|
|
37
|
+
title: string;
|
|
38
|
+
zunoHandoffId?: string | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type IdeaTaskDraft = {
|
|
42
|
+
acceptanceCriteria: string;
|
|
43
|
+
priority: "low" | "medium" | "high";
|
|
44
|
+
summary: string;
|
|
45
|
+
title: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type IdeaHandoverSource = {
|
|
49
|
+
content: string;
|
|
50
|
+
id: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type IdeaHandoverWorkspaceProps = {
|
|
54
|
+
brief: IdeaBriefDraft;
|
|
55
|
+
currentStage: IdeaStage;
|
|
56
|
+
handoffPackagePreview: string;
|
|
57
|
+
onAutoFillBrief: () => void;
|
|
58
|
+
onBack: () => void;
|
|
59
|
+
onArchiveConversation?: () => void;
|
|
60
|
+
onBriefChange: (brief: IdeaBriefDraft) => void;
|
|
61
|
+
onCopyHandoffPackage: () => void;
|
|
62
|
+
onCreateTask: () => void;
|
|
63
|
+
onDeliverTask: () => void;
|
|
64
|
+
onSaveBrief: (status: "draft" | "final") => void;
|
|
65
|
+
onStageChange: (stage: IdeaStage) => void;
|
|
66
|
+
saving?: boolean;
|
|
67
|
+
sources: readonly IdeaHandoverSource[];
|
|
68
|
+
task?: IdeaHandoverTask;
|
|
69
|
+
taskDraft: IdeaTaskDraft;
|
|
70
|
+
onTaskDraftChange: (taskDraft: IdeaTaskDraft) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const stages: readonly { id: IdeaStage; label: string }[] = [
|
|
74
|
+
{ id: "explore", label: "Explore" },
|
|
75
|
+
{ id: "compare", label: "Compare" },
|
|
76
|
+
{ id: "revise", label: "Revise" },
|
|
77
|
+
{ id: "final", label: "Final brief" },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export function IdeaHandoverWorkspace({ brief, currentStage, handoffPackagePreview, onArchiveConversation, onAutoFillBrief, onBack, onBriefChange, onCopyHandoffPackage, onCreateTask, onDeliverTask, onSaveBrief, onStageChange, onTaskDraftChange, saving, sources, task, taskDraft }: IdeaHandoverWorkspaceProps) {
|
|
81
|
+
const update = <K extends Exclude<keyof IdeaBriefDraft, "status">>(key: K, value: IdeaBriefDraft[K]) => onBriefChange({ ...brief, [key]: value, status: "draft" });
|
|
82
|
+
const updateTask = <K extends keyof IdeaTaskDraft>(key: K, value: IdeaTaskDraft[K]) => onTaskDraftChange({ ...taskDraft, [key]: value });
|
|
83
|
+
const isProjectScope = brief.projectScope === "project";
|
|
84
|
+
const readiness = getBriefReadiness(brief);
|
|
85
|
+
const canPrepareTask = brief.status === "final" && isBriefReady(brief) && taskDraft.title.trim() && taskDraft.summary.trim() && parseAcceptanceCriteria(taskDraft.acceptanceCriteria).length > 0;
|
|
86
|
+
|
|
87
|
+
return <section className="flex size-full min-h-0 flex-col bg-background">
|
|
88
|
+
<header className="flex shrink-0 items-center gap-3 border-b px-4 py-3 sm:px-6">
|
|
89
|
+
<Button size="sm" variant="ghost" onClick={onBack}><ChevronLeftIcon /> Conversation</Button>
|
|
90
|
+
<div className="min-w-0 flex-1"><h1 className="truncate text-sm font-semibold">Final brief and task handover</h1><p className="text-xs text-muted-foreground">Keep sources and project scope with the idea.</p></div>
|
|
91
|
+
</header>
|
|
92
|
+
<main className="min-h-0 flex-1 overflow-y-auto px-4 py-6 sm:px-6">
|
|
93
|
+
<div className="mx-auto flex w-full max-w-4xl flex-col gap-5">
|
|
94
|
+
<nav aria-label="Idea stages" className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
95
|
+
{stages.map((stage) => <Button key={stage.id} className="justify-start" size="sm" variant={stage.id === currentStage ? "secondary" : "ghost"} onClick={() => onStageChange(stage.id)}>
|
|
96
|
+
{stage.id === "final" ? <FileCheck2Icon /> : <NetworkIcon />} {stage.label}
|
|
97
|
+
</Button>)}
|
|
98
|
+
</nav>
|
|
99
|
+
<Card size="sm"><CardHeader><CardTitle>Final brief</CardTitle><CardDescription>Save the idea with its source responses. Finalize it before creating an agent task.</CardDescription></CardHeader><CardContent className="grid gap-4">
|
|
100
|
+
<Field label="Brief title"><Input value={brief.title} onChange={(event) => update("title", event.target.value)} /></Field>
|
|
101
|
+
<Field label="Outcome"><Textarea value={brief.outcome} onChange={(event) => update("outcome", event.target.value)} /></Field>
|
|
102
|
+
<div className="grid gap-4 sm:grid-cols-2"><Field label="Audience"><Textarea value={brief.audience} onChange={(event) => update("audience", event.target.value)} /></Field><Field label="Scope"><Textarea value={brief.scope} onChange={(event) => update("scope", event.target.value)} /></Field></div>
|
|
103
|
+
<div className="grid gap-4 sm:grid-cols-2"><Field label="Exclusions"><Textarea value={brief.exclusions} onChange={(event) => update("exclusions", event.target.value)} /></Field><Field label="Constraints"><Textarea value={brief.constraints} onChange={(event) => update("constraints", event.target.value)} /></Field></div>
|
|
104
|
+
<div className="grid gap-4 sm:grid-cols-2"><Field label="Risks"><Textarea value={brief.risks} onChange={(event) => update("risks", event.target.value)} /></Field><Field label="Success signals"><Textarea value={brief.successSignals} onChange={(event) => update("successSignals", event.target.value)} /></Field></div>
|
|
105
|
+
<div className="grid gap-4 sm:grid-cols-2"><Field label="Project scope"><Select value={brief.projectScope} onValueChange={(value) => update("projectScope", value as IdeaBriefDraft["projectScope"])}><SelectTrigger className="w-full"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="project">A referred project</SelectItem><SelectItem value="all-projects">Common to all projects</SelectItem></SelectContent></Select></Field>{isProjectScope ? <Field label="Referred project"><Input placeholder="Project name or reference" value={brief.projectReference ?? ""} onChange={(event) => update("projectReference", event.target.value || null)} /></Field> : <p className="self-end text-sm text-muted-foreground">This idea is common to all projects.</p>}</div>
|
|
106
|
+
<SourcePicker selected={brief.sourceMessageIds} sources={sources} onChange={(sourceMessageIds) => update("sourceMessageIds", sourceMessageIds)} />
|
|
107
|
+
<BriefReadiness items={readiness} />
|
|
108
|
+
<div className="flex flex-wrap items-center justify-between gap-2"><Button disabled={saving || !brief.sourceMessageIds.length} variant="ghost" onClick={onAutoFillBrief}><WandSparklesIcon /> Fill empty fields</Button><div className="flex flex-wrap gap-2"><Button disabled={saving} variant="outline" onClick={() => onSaveBrief("draft")}>Save draft</Button><Button disabled={saving || !isBriefReady(brief)} onClick={() => onSaveBrief("final")}><CheckCircle2Icon /> Finalize brief</Button></div></div>
|
|
109
|
+
</CardContent></Card>
|
|
110
|
+
<Card size="sm"><CardHeader><CardTitle>Prepared task</CardTitle><CardDescription>Prepare the handoff for Zuno. This keeps the scope and acceptance criteria but does not start a runner.</CardDescription></CardHeader><CardContent className="grid gap-4">
|
|
111
|
+
{task ? <div className="grid gap-3 rounded-md border bg-muted/40 p-3"><div className="flex flex-wrap items-center gap-3"><ListTodoIcon className="size-4 text-muted-foreground" /><div className="min-w-0 flex-1"><p className="font-medium">{task.title}</p><p className="text-xs text-muted-foreground">{task.status} · {task.projectScope === "all-projects" ? "All projects" : task.projectReference}</p></div>{task.status === "delivered" ? onArchiveConversation ? <Button size="sm" variant="outline" onClick={onArchiveConversation}><ArchiveIcon /> Archive conversation</Button> : null : <Button disabled={saving || task.status === "delivering"} size="sm" onClick={onDeliverTask}>{task.status === "failed" ? <RefreshCwIcon /> : <SendIcon />}{task.status === "failed" ? "Retry delivery" : task.status === "delivering" ? "Delivering..." : "Deliver to Zuno"}</Button>}</div>{task.deliveryError ? <p className="text-sm text-destructive" role="alert">{task.deliveryError}</p> : null}{task.status === "delivered" ? <p className="text-sm text-muted-foreground">Accepted by Zuno{task.deliveredAt ? ` on ${new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(task.deliveredAt))}` : ""}. Receipt: <code>{task.zunoHandoffId}</code></p> : null}</div> : <>
|
|
112
|
+
<div className="grid gap-4 sm:grid-cols-[minmax(0,1fr)_10rem]"><Field label="Task title"><Input value={taskDraft.title} onChange={(event) => updateTask("title", event.target.value)} /></Field><Field label="Priority"><Select value={taskDraft.priority} onValueChange={(value) => updateTask("priority", value as IdeaTaskDraft["priority"])}><SelectTrigger className="w-full"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="low">Low</SelectItem><SelectItem value="medium">Medium</SelectItem><SelectItem value="high">High</SelectItem></SelectContent></Select></Field></div>
|
|
113
|
+
<Field label="Task summary"><Textarea value={taskDraft.summary} onChange={(event) => updateTask("summary", event.target.value)} /></Field>
|
|
114
|
+
<AcceptanceCriteriaEditor value={taskDraft.acceptanceCriteria} onChange={(value) => updateTask("acceptanceCriteria", value)} />
|
|
115
|
+
<details className="overflow-hidden rounded-md border"><summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-sm font-medium hover:bg-muted"><EyeIcon className="size-4 text-muted-foreground" /> Review Zuno package</summary><pre className="max-h-72 overflow-auto border-t bg-muted/30 p-3 text-xs leading-relaxed">{handoffPackagePreview}</pre></details>
|
|
116
|
+
<div className="flex flex-wrap justify-end gap-2"><Button disabled={saving || brief.status !== "final" || !isBriefReady(brief)} variant="outline" onClick={onCopyHandoffPackage}><ClipboardIcon /> Copy Zuno package</Button><Button disabled={saving || !canPrepareTask} onClick={onCreateTask}><SendIcon /> Prepare task</Button></div>
|
|
117
|
+
</>}
|
|
118
|
+
</CardContent></Card>
|
|
119
|
+
</div>
|
|
120
|
+
</main>
|
|
121
|
+
</section>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Field({ children, label }: { children: ReactNode; label: string }) {
|
|
125
|
+
return <div className="grid gap-2"><Label>{label}</Label>{children}</div>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function BriefReadiness({ items }: { items: ReturnType<typeof getBriefReadiness> }) {
|
|
129
|
+
const completeCount = items.filter((item) => item.complete).length;
|
|
130
|
+
return <section aria-label="Brief readiness" className="grid gap-2 border-y py-3"><div className="flex items-center justify-between gap-3"><p className="text-sm font-medium">Brief readiness</p><p className="text-sm text-muted-foreground">{completeCount} of {items.length} complete</p></div><div className="grid gap-x-4 gap-y-2 sm:grid-cols-2">{items.map((item) => <div key={item.id} className="flex min-w-0 items-center gap-2 text-sm">{item.complete ? <CheckIcon className="size-4 shrink-0 text-emerald-600" /> : <CircleIcon className="size-4 shrink-0 text-muted-foreground" />}<span className={item.complete ? undefined : "text-muted-foreground"}>{item.label}</span></div>)}</div></section>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function AcceptanceCriteriaEditor({ onChange, value }: { onChange: (value: string) => void; value: string }) {
|
|
134
|
+
const items = parseAcceptanceCriteria(value);
|
|
135
|
+
const [draft, setDraft] = useState("");
|
|
136
|
+
const updateItem = (index: number, nextValue: string) => onChange(serializeAcceptanceCriteria(items.map((item, itemIndex) => itemIndex === index ? nextValue : item)));
|
|
137
|
+
const removeItem = (index: number) => onChange(serializeAcceptanceCriteria(items.filter((_, itemIndex) => itemIndex !== index)));
|
|
138
|
+
const addItem = () => {
|
|
139
|
+
if (!draft.trim()) return;
|
|
140
|
+
onChange(serializeAcceptanceCriteria([...items, draft]));
|
|
141
|
+
setDraft("");
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return <div className="grid gap-2"><Label>Acceptance criteria</Label>{items.length ? <div className="grid gap-2">{items.map((item, index) => <div key={`${index}-${items.length}`} className="flex items-center gap-2"><span className="w-6 shrink-0 text-center text-sm text-muted-foreground">{index + 1}.</span><Input aria-label={`Acceptance criterion ${index + 1}`} value={item} onChange={(event) => updateItem(index, event.target.value)} /><Button aria-label={`Remove acceptance criterion ${index + 1}`} size="icon-xs" title="Remove criterion" variant="ghost" onClick={() => removeItem(index)}><Trash2Icon /></Button></div>)}</div> : <p className="text-sm text-muted-foreground">Add observable conditions that Zuno can validate after execution.</p>}<div className="flex items-center gap-2"><span className="w-6 shrink-0 text-center text-sm text-muted-foreground">{items.length + 1}.</span><Input aria-label="New acceptance criterion" placeholder="Add a verifiable result" value={draft} onChange={(event) => setDraft(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); addItem(); } }} /><Button aria-label="Add acceptance criterion" disabled={!draft.trim()} size="icon-sm" title="Add criterion" variant="outline" onClick={addItem}><PlusIcon /></Button></div></div>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function SourcePicker({ onChange, selected, sources }: { onChange: (ids: string[]) => void; selected: string[]; sources: readonly IdeaHandoverSource[] }) {
|
|
148
|
+
const toggle = (id: string) => onChange(selected.includes(id) ? selected.filter((item) => item !== id) : [...selected, id]);
|
|
149
|
+
return <div className="grid gap-2"><Label>Source responses</Label><div className="max-h-48 overflow-y-auto rounded-md border p-2">{sources.length ? sources.map((source) => <label key={source.id} className="flex cursor-pointer gap-2 rounded-sm p-2 text-sm hover:bg-muted"><input checked={selected.includes(source.id)} type="checkbox" onChange={() => toggle(source.id)} /><span className="min-w-0"><span className="line-clamp-2">{source.content}</span><code className="text-xs text-muted-foreground">{source.id}</code></span></label>) : <p className="p-2 text-sm text-muted-foreground">Collect or create assistant responses before finalizing the brief.</p>}</div></div>;
|
|
150
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { IdeaHandoverWorkspace } from "./idea-handover-workspace";
|
|
2
|
+
export type { IdeaBriefDraft, IdeaHandoverSource, IdeaHandoverTask, IdeaHandoverWorkspaceProps, IdeaStage, IdeaTaskDraft } from "./idea-handover-workspace";
|
|
3
|
+
export { getBriefReadiness, isBriefReady, parseAcceptanceCriteria, serializeAcceptanceCriteria } from "./idea-handover-readiness";
|
|
4
|
+
export type { BriefReadinessItem } from "./idea-handover-readiness";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { KanbanBoard } from './kanban-board'
|
|
2
|
+
export { KanbanColumn } from './kanban-column'
|
|
3
|
+
export { KanbanCard } from './kanban-card'
|
|
4
|
+
export type {
|
|
5
|
+
KanbanBoardProps,
|
|
6
|
+
KanbanCardItem,
|
|
7
|
+
KanbanColumnItem,
|
|
8
|
+
KanbanPriority,
|
|
9
|
+
} from './kanban-types'
|
|
10
|
+
export type { KanbanCardProps } from './kanban-card'
|
|
11
|
+
export type { KanbanColumnProps } from './kanban-column'
|