@withpotter/components 0.13.0-next.84 → 0.13.0-next.85
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/dist/dashboard/activity-feed.d.ts +2 -0
- package/dist/dashboard/activity-feed.js +3 -1
- package/dist/dashboard/activity-feed.js.map +1 -1
- package/dist/dashboard/activity-paid.d.ts +10 -0
- package/dist/dashboard/activity-paid.js +7 -0
- package/dist/dashboard/activity-paid.js.map +1 -0
- package/dist/dashboard/home/ask-controls.js +1 -0
- package/dist/dashboard/home/ask-controls.js.map +1 -1
- package/dist/dashboard/home/ask-mode-chip.js +4 -4
- package/dist/dashboard/home/ask-mode-chip.js.map +1 -1
- package/dist/dashboard/home/home-ask.js +3 -3
- package/dist/dashboard/home/home-ask.js.map +1 -1
- package/dist/dashboard/home/home-quick-actions.js +9 -1
- package/dist/dashboard/home/home-quick-actions.js.map +1 -1
- package/dist/dashboard/home/host-quick-actions.d.ts +19 -0
- package/dist/dashboard/home/host-quick-actions.js +12 -0
- package/dist/dashboard/home/host-quick-actions.js.map +1 -0
- package/dist/dashboard/home/widgets/activity-widget.js +7 -1
- package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
- package/dist/features/gift-card-artwork.js +1 -1
- package/dist/features/gift-card-artwork.js.map +1 -1
- package/dist/features/gift-cards/gift-card-logic.d.ts +41 -0
- package/dist/features/gift-cards/gift-card-logic.js +65 -0
- package/dist/features/gift-cards/gift-card-logic.js.map +1 -0
- package/dist/features/gift-cards/gift-cards-feature.js +656 -518
- package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
- package/dist/features/gift-cards/hint.d.ts +9 -0
- package/dist/features/gift-cards/hint.js +25 -0
- package/dist/features/gift-cards/hint.js.map +1 -0
- package/dist/features/product-form.js +1 -1
- package/dist/features/product-form.js.map +1 -1
- package/dist/sidebar/nav-renderers.js +2 -0
- package/dist/sidebar/nav-renderers.js.map +1 -1
- package/dist/sidebar/nav-section.js +5 -3
- package/dist/sidebar/nav-section.js.map +1 -1
- package/package.json +5 -5
|
@@ -17,5 +17,7 @@ export interface ActivityItem {
|
|
|
17
17
|
/** Retained for the mobile feed; the desktop timeline renders an icon. */
|
|
18
18
|
emoji: string;
|
|
19
19
|
href?: string;
|
|
20
|
+
/** A placed order whose payment has not arrived; the Home card quiets it. */
|
|
21
|
+
unpaid?: boolean;
|
|
20
22
|
}
|
|
21
23
|
export declare function buildActivityItems(recentContent: ContentItem[], recentOrders: Order[], packages: PackageType[], contentTypeSingular: string, primaryContentKey: string, recentMembers: Member[], recentSubscriptions: CustomerSubscription[] | undefined, inventoryProducts: InventoryProduct[] | undefined, recentInvoices: Invoice[] | undefined, formatCurrency: (amount: number, currencyOverride?: string) => string, industryKey?: string | null): ActivityItem[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOrderPaid } from "./activity-paid";
|
|
1
2
|
import * as copy from "@withpotter/lib/timeline-copy";
|
|
2
3
|
function formatRelativeTime(dateStr) {
|
|
3
4
|
const now = Date.now();
|
|
@@ -65,11 +66,12 @@ function buildActivityItems(recentContent, recentOrders, packages, contentTypeSi
|
|
|
65
66
|
order.orderNumber,
|
|
66
67
|
formatCurrency(order.total),
|
|
67
68
|
industryKey,
|
|
68
|
-
order.paymentStatus
|
|
69
|
+
isOrderPaid(order.paymentStatus)
|
|
69
70
|
);
|
|
70
71
|
items.push({
|
|
71
72
|
id: `order-${order.id}`,
|
|
72
73
|
type: isCompleted ? "order_completed" : "order_placed",
|
|
74
|
+
...!isCompleted && !isOrderPaid(order.paymentStatus) ? { unpaid: true } : {},
|
|
73
75
|
...msg,
|
|
74
76
|
timestamp: isCompleted ? order.completedAt || order.updatedAt : order.createdAt,
|
|
75
77
|
href: `/dashboard/orders/${order.id}`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/dashboard/activity-feed.tsx"],"sourcesContent":["import type {\n Order,\n Package as PackageType,\n ContentItem,\n Member,\n CustomerSubscription,\n InventoryProduct,\n Invoice,\n} from '@withpotter/lib/api/types';\nimport * as copy from '@withpotter/lib/timeline-copy';\n\n// ─── Types ──────────────────────────────────────────────────────────\n\n/**\n * A single thing that happened to the business. Built here, rendered by\n * `home/home-timeline.tsx`.\n *\n * The React component that used to live in this file is gone: the home merged\n * its feed with \"Next up\" and \"Needs you\" into one timeline, so the events and\n * the open work read as a single list with now as the pivot. What survives is\n * the builder, which is the part several surfaces actually share.\n */\nexport interface ActivityItem {\n id: string;\n type:\n | 'product_added'\n | 'product_updated'\n | 'order_placed'\n | 'order_completed'\n | 'member_signup'\n | 'member_converted'\n | 'subscription_created'\n | 'invoice_paid'\n | 'invoice_sent';\n title: string;\n description: string;\n timestamp: string;\n /** Retained for the mobile feed; the desktop timeline renders an icon. */\n emoji: string;\n href?: string;\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────\n\nfunction formatRelativeTime(dateStr: string): string {\n const now = Date.now();\n const date = new Date(dateStr).getTime();\n const diffMs = now - date;\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffSec < 60) return 'Just now';\n if (diffMin < 60) return `${diffMin}m ago`;\n if (diffHr < 24) return `${diffHr}h ago`;\n if (diffDay < 7) return `${diffDay}d ago`;\n return new Date(dateStr).toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n });\n}\n\nexport function buildActivityItems(\n recentContent: ContentItem[],\n recentOrders: Order[],\n packages: PackageType[],\n contentTypeSingular: string,\n primaryContentKey: string,\n recentMembers: Member[],\n recentSubscriptions: CustomerSubscription[] = [],\n inventoryProducts: InventoryProduct[] = [],\n recentInvoices: Invoice[] = [],\n formatCurrency: (amount: number, currencyOverride?: string) => string,\n industryKey?: string | null,\n): ActivityItem[] {\n const items: ActivityItem[] = [];\n\n // Product / content activity\n for (const item of recentContent) {\n const isUpdate = item.updatedAt !== item.createdAt;\n const msg = isUpdate\n ? copy.productUpdated(item.title, contentTypeSingular)\n : copy.productAdded(item.title, contentTypeSingular);\n items.push({\n id: `content-${item.id}`,\n type: isUpdate ? 'product_updated' : 'product_added',\n ...msg,\n timestamp: item.updatedAt || item.createdAt,\n href: `/dashboard/content/${primaryContentKey}/${item.id}/edit`,\n });\n }\n\n // Inventory product activity (deduplicate against content items by title)\n const contentTitles = new Set(recentContent.map((c) => c.title));\n for (const prod of inventoryProducts) {\n if (contentTitles.has(prod.title)) continue;\n const created = prod.createdAt ? String(prod.createdAt) : '';\n const updated = prod.updatedAt ? String(prod.updatedAt) : created;\n const isUpdate = updated !== created;\n const msg = isUpdate\n ? copy.productUpdated(prod.title, contentTypeSingular)\n : copy.productAdded(prod.title, contentTypeSingular);\n items.push({\n id: `inv-${prod.id}`,\n type: isUpdate ? 'product_updated' : 'product_added',\n ...msg,\n timestamp: updated || created,\n href: `/dashboard/catalog/${prod.id}`,\n });\n }\n\n // Package activity (only if no content items — avoids duplicates)\n if (recentContent.length === 0 && inventoryProducts.length === 0) {\n for (const pkg of packages) {\n const isUpdate = pkg.updatedAt !== pkg.createdAt;\n const msg = isUpdate\n ? copy.productUpdated(pkg.title, contentTypeSingular)\n : copy.productAdded(pkg.title, contentTypeSingular);\n items.push({\n id: `pkg-${pkg.id}`,\n type: isUpdate ? 'product_updated' : 'product_added',\n ...msg,\n timestamp: pkg.updatedAt || pkg.createdAt,\n href: `/dashboard/content/${primaryContentKey}/${pkg.documentId}/edit`,\n });\n }\n }\n\n // Order activity\n for (const order of recentOrders) {\n const isCompleted = order.status === 'completed';\n const customerName =\n order.customerName || order.customerEmail?.split('@')[0] || 'Someone';\n const msg = isCompleted\n ? copy.orderCompleted(customerName, order.orderNumber)\n : copy.orderPlaced(\n customerName,\n order.orderNumber,\n formatCurrency(order.total),\n industryKey,\n order.paymentStatus === 'paid' ||\n order.paymentStatus === 'partially_refunded' ||\n order.paymentStatus === 'refunded',\n );\n items.push({\n id: `order-${order.id}`,\n type: isCompleted ? 'order_completed' : 'order_placed',\n ...msg,\n timestamp: isCompleted\n ? order.completedAt || order.updatedAt\n : order.createdAt,\n href: `/dashboard/orders/${order.id}`,\n });\n }\n\n // Member signup / conversion activity\n for (const member of recentMembers) {\n const isConverted = !!member.convertedAt;\n const name = member.name || member.email?.split('@')[0] || 'A visitor';\n const msg = isConverted\n ? copy.memberConverted(name, member.email)\n : copy.memberSignup(name, member.email, industryKey);\n items.push({\n id: `member-${member.id}`,\n type: isConverted ? 'member_converted' : 'member_signup',\n ...msg,\n timestamp: isConverted ? member.convertedAt! : member.createdAt,\n href: `/dashboard/customers/${member.id}`,\n });\n }\n\n // Subscription activity\n for (const sub of recentSubscriptions) {\n const name = sub.customerEmail?.split('@')[0] || 'Someone';\n const msg = copy.subscriptionCreated(\n name,\n sub.plan?.name || 'a plan',\n formatCurrency(sub.amount / 100),\n );\n items.push({\n id: `sub-${sub.id}`,\n type: 'subscription_created',\n ...msg,\n timestamp: sub.createdAt,\n href: '/dashboard/subscriptions',\n });\n }\n\n // Invoice activity\n for (const inv of recentInvoices) {\n const customerName =\n inv.customerName || inv.customerEmail?.split('@')[0] || 'Someone';\n const isPaid = inv.status === 'paid' && !!inv.paidAt;\n const isSent = inv.status === 'sent' && !!inv.sentAt;\n if (isPaid) {\n const msg = copy.invoicePaid(\n customerName,\n inv.invoiceNumber,\n formatCurrency(inv.amountPaid || inv.total, inv.currency),\n );\n items.push({\n id: `inv-paid-${inv.id}`,\n type: 'invoice_paid',\n ...msg,\n timestamp: inv.paidAt!,\n href: `/dashboard/invoices/${inv.id}`,\n });\n } else if (isSent) {\n const msg = copy.invoiceSent(\n customerName,\n inv.invoiceNumber,\n formatCurrency(inv.total, inv.currency),\n );\n items.push({\n id: `inv-sent-${inv.id}`,\n type: 'invoice_sent',\n ...msg,\n timestamp: inv.sentAt!,\n href: `/dashboard/invoices/${inv.id}`,\n });\n }\n }\n\n // Sort by timestamp descending\n items.sort(\n (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),\n );\n return items;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/dashboard/activity-feed.tsx"],"sourcesContent":["import type {\n Order,\n Package as PackageType,\n ContentItem,\n Member,\n CustomerSubscription,\n InventoryProduct,\n Invoice,\n} from '@withpotter/lib/api/types';\nimport { isOrderPaid } from './activity-paid';\nimport * as copy from '@withpotter/lib/timeline-copy';\n\n// ─── Types ──────────────────────────────────────────────────────────\n\n/**\n * A single thing that happened to the business. Built here, rendered by\n * `home/home-timeline.tsx`.\n *\n * The React component that used to live in this file is gone: the home merged\n * its feed with \"Next up\" and \"Needs you\" into one timeline, so the events and\n * the open work read as a single list with now as the pivot. What survives is\n * the builder, which is the part several surfaces actually share.\n */\nexport interface ActivityItem {\n id: string;\n type:\n | 'product_added'\n | 'product_updated'\n | 'order_placed'\n | 'order_completed'\n | 'member_signup'\n | 'member_converted'\n | 'subscription_created'\n | 'invoice_paid'\n | 'invoice_sent';\n title: string;\n description: string;\n timestamp: string;\n /** Retained for the mobile feed; the desktop timeline renders an icon. */\n emoji: string;\n href?: string;\n /** A placed order whose payment has not arrived; the Home card quiets it. */\n unpaid?: boolean;\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────\n\nfunction formatRelativeTime(dateStr: string): string {\n const now = Date.now();\n const date = new Date(dateStr).getTime();\n const diffMs = now - date;\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffSec < 60) return 'Just now';\n if (diffMin < 60) return `${diffMin}m ago`;\n if (diffHr < 24) return `${diffHr}h ago`;\n if (diffDay < 7) return `${diffDay}d ago`;\n return new Date(dateStr).toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n });\n}\n\nexport function buildActivityItems(\n recentContent: ContentItem[],\n recentOrders: Order[],\n packages: PackageType[],\n contentTypeSingular: string,\n primaryContentKey: string,\n recentMembers: Member[],\n recentSubscriptions: CustomerSubscription[] = [],\n inventoryProducts: InventoryProduct[] = [],\n recentInvoices: Invoice[] = [],\n formatCurrency: (amount: number, currencyOverride?: string) => string,\n industryKey?: string | null,\n): ActivityItem[] {\n const items: ActivityItem[] = [];\n\n // Product / content activity\n for (const item of recentContent) {\n const isUpdate = item.updatedAt !== item.createdAt;\n const msg = isUpdate\n ? copy.productUpdated(item.title, contentTypeSingular)\n : copy.productAdded(item.title, contentTypeSingular);\n items.push({\n id: `content-${item.id}`,\n type: isUpdate ? 'product_updated' : 'product_added',\n ...msg,\n timestamp: item.updatedAt || item.createdAt,\n href: `/dashboard/content/${primaryContentKey}/${item.id}/edit`,\n });\n }\n\n // Inventory product activity (deduplicate against content items by title)\n const contentTitles = new Set(recentContent.map((c) => c.title));\n for (const prod of inventoryProducts) {\n if (contentTitles.has(prod.title)) continue;\n const created = prod.createdAt ? String(prod.createdAt) : '';\n const updated = prod.updatedAt ? String(prod.updatedAt) : created;\n const isUpdate = updated !== created;\n const msg = isUpdate\n ? copy.productUpdated(prod.title, contentTypeSingular)\n : copy.productAdded(prod.title, contentTypeSingular);\n items.push({\n id: `inv-${prod.id}`,\n type: isUpdate ? 'product_updated' : 'product_added',\n ...msg,\n timestamp: updated || created,\n href: `/dashboard/catalog/${prod.id}`,\n });\n }\n\n // Package activity (only if no content items — avoids duplicates)\n if (recentContent.length === 0 && inventoryProducts.length === 0) {\n for (const pkg of packages) {\n const isUpdate = pkg.updatedAt !== pkg.createdAt;\n const msg = isUpdate\n ? copy.productUpdated(pkg.title, contentTypeSingular)\n : copy.productAdded(pkg.title, contentTypeSingular);\n items.push({\n id: `pkg-${pkg.id}`,\n type: isUpdate ? 'product_updated' : 'product_added',\n ...msg,\n timestamp: pkg.updatedAt || pkg.createdAt,\n href: `/dashboard/content/${primaryContentKey}/${pkg.documentId}/edit`,\n });\n }\n }\n\n // Order activity\n for (const order of recentOrders) {\n const isCompleted = order.status === 'completed';\n const customerName =\n order.customerName || order.customerEmail?.split('@')[0] || 'Someone';\n const msg = isCompleted\n ? copy.orderCompleted(customerName, order.orderNumber)\n : copy.orderPlaced(\n customerName,\n order.orderNumber,\n formatCurrency(order.total),\n industryKey,\n isOrderPaid(order.paymentStatus),\n );\n items.push({\n id: `order-${order.id}`,\n type: isCompleted ? 'order_completed' : 'order_placed',\n ...(!isCompleted && !isOrderPaid(order.paymentStatus)\n ? { unpaid: true }\n : {}),\n ...msg,\n timestamp: isCompleted\n ? order.completedAt || order.updatedAt\n : order.createdAt,\n href: `/dashboard/orders/${order.id}`,\n });\n }\n\n // Member signup / conversion activity\n for (const member of recentMembers) {\n const isConverted = !!member.convertedAt;\n const name = member.name || member.email?.split('@')[0] || 'A visitor';\n const msg = isConverted\n ? copy.memberConverted(name, member.email)\n : copy.memberSignup(name, member.email, industryKey);\n items.push({\n id: `member-${member.id}`,\n type: isConverted ? 'member_converted' : 'member_signup',\n ...msg,\n timestamp: isConverted ? member.convertedAt! : member.createdAt,\n href: `/dashboard/customers/${member.id}`,\n });\n }\n\n // Subscription activity\n for (const sub of recentSubscriptions) {\n const name = sub.customerEmail?.split('@')[0] || 'Someone';\n const msg = copy.subscriptionCreated(\n name,\n sub.plan?.name || 'a plan',\n formatCurrency(sub.amount / 100),\n );\n items.push({\n id: `sub-${sub.id}`,\n type: 'subscription_created',\n ...msg,\n timestamp: sub.createdAt,\n href: '/dashboard/subscriptions',\n });\n }\n\n // Invoice activity\n for (const inv of recentInvoices) {\n const customerName =\n inv.customerName || inv.customerEmail?.split('@')[0] || 'Someone';\n const isPaid = inv.status === 'paid' && !!inv.paidAt;\n const isSent = inv.status === 'sent' && !!inv.sentAt;\n if (isPaid) {\n const msg = copy.invoicePaid(\n customerName,\n inv.invoiceNumber,\n formatCurrency(inv.amountPaid || inv.total, inv.currency),\n );\n items.push({\n id: `inv-paid-${inv.id}`,\n type: 'invoice_paid',\n ...msg,\n timestamp: inv.paidAt!,\n href: `/dashboard/invoices/${inv.id}`,\n });\n } else if (isSent) {\n const msg = copy.invoiceSent(\n customerName,\n inv.invoiceNumber,\n formatCurrency(inv.total, inv.currency),\n );\n items.push({\n id: `inv-sent-${inv.id}`,\n type: 'invoice_sent',\n ...msg,\n timestamp: inv.sentAt!,\n href: `/dashboard/invoices/${inv.id}`,\n });\n }\n }\n\n // Sort by timestamp descending\n items.sort(\n (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),\n );\n return items;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the money for an order has actually arrived.
|
|
3
|
+
*
|
|
4
|
+
* One answer for the activity feed's wording ("started a ticket order" versus
|
|
5
|
+
* "bought a ticket") and for how quiet its row looks, so the two cannot drift.
|
|
6
|
+
* The signal is the order's own `paymentStatus`, never the wording of a title.
|
|
7
|
+
* An order with no payment status is treated as unpaid, which is what the
|
|
8
|
+
* wording has always done.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isOrderPaid(paymentStatus: string | null | undefined): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/dashboard/activity-paid.ts"],"sourcesContent":["/**\n * Whether the money for an order has actually arrived.\n *\n * One answer for the activity feed's wording (\"started a ticket order\" versus\n * \"bought a ticket\") and for how quiet its row looks, so the two cannot drift.\n * The signal is the order's own `paymentStatus`, never the wording of a title.\n * An order with no payment status is treated as unpaid, which is what the\n * wording has always done.\n */\nexport function isOrderPaid(paymentStatus: string | null | undefined): boolean {\n return (\n paymentStatus === 'paid' ||\n paymentStatus === 'partially_refunded' ||\n paymentStatus === 'refunded'\n );\n}\n"],"mappings":"AASO,SAAS,YAAY,eAAmD;AAC7E,SACE,kBAAkB,UAClB,kBAAkB,wBAClB,kBAAkB;AAEtB;","names":[]}
|
|
@@ -28,6 +28,7 @@ function AskControls({
|
|
|
28
28
|
},
|
|
29
29
|
disabled: voice.disabled && !voice.active,
|
|
30
30
|
"aria-label": voice.active ? voiceLabels.stop : voiceLabels.start,
|
|
31
|
+
title: voice.active ? voiceLabels.stop : voiceLabels.start,
|
|
31
32
|
"aria-pressed": voice.active,
|
|
32
33
|
className: `grid size-8 shrink-0 place-items-center rounded-lg border transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${voice.active ? "border-transparent bg-[var(--error)] text-white" : "border-[color-mix(in_srgb,var(--border-subtle)_55%,transparent)] bg-[var(--bg-muted)] text-[var(--text-tertiary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]"}`,
|
|
33
34
|
children: voice.active ? /* @__PURE__ */ jsx(Square, { className: "h-[9px] w-[9px] fill-current" }) : /* @__PURE__ */ jsx(Mic, {})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/ask-controls.tsx"],"sourcesContent":["'use client';\n\nimport { ArrowUp, Mic, Square, X } from 'lucide-react';\n\n/**\n * The three controls at the right end of the ask bar.\n *\n * Lifted out of `home-ask` for room, and they belong together anyway: they are\n * the only things in that bar that are pressed rather than typed into, they\n * share one frame and one size, and the rules about which of them is showing\n * are rules about the same cluster.\n */\nexport function AskControls({\n voice,\n value,\n canSend,\n engaged,\n inputRef,\n onSubmit,\n onClose,\n send,\n voiceLabels = { start: 'Talk to Box', stop: 'Stop voice mode' },\n}: {\n voice: {\n supported: boolean;\n active: boolean;\n start: () => Promise<void> | void;\n stop: () => void;\n /** Pressing it does nothing, e.g. a clip is already waiting to be sent. */\n disabled?: boolean;\n };\n /** Replaces the default round send button. */\n send?: React.ReactNode;\n /** What the mic says it will do, for the person using a screen reader. */\n voiceLabels?: { start: string; stop: string };\n /** There is a sentence or an attachment to send. */\n canSend: boolean;\n value: string;\n /** Anything is open under the bar: suggestions, results or a conversation. */\n engaged: boolean;\n inputRef: React.RefObject<HTMLTextAreaElement | null>;\n onSubmit: (q: string) => void;\n onClose: () => void;\n}) {\n return (\n <>\n {/* Hidden where the browser has no recogniser, rather than shown and\n dead. The icon becomes a stop square so the control says what\n pressing it will do — and in voice mode that is a bigger claim than\n it was for dictation, because pressing it ends a conversation rather\n than closing a microphone. */}\n {voice.supported && (\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n // Both branches play their sound on the PRESS rather than on the\n // recogniser being ready — `getUserMedia` can sit behind a\n // permission prompt, and a confirmation that arrives late\n // confirms nothing. Owned by the hook so every entrance to voice\n // mode sounds the same, including the automatic one at the end of\n // Box's reply, which has no button to hang a sound on.\n if (voice.active) {\n voice.stop();\n } else {\n inputRef.current?.focus();\n void voice.start();\n }\n }}\n disabled={voice.disabled && !voice.active}\n aria-label={voice.active ? voiceLabels.stop : voiceLabels.start}\n aria-pressed={voice.active}\n // FRAMED, like Send. Two adjacent controls where one sits in a\n // circle and the other floats bare read as two different KINDS of\n // control — one a button, one a decoration — when they are both\n // buttons. Same frame, same size, different job.\n className={`grid size-8 shrink-0 place-items-center rounded-lg border transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${\n voice.active\n ? 'border-transparent bg-[var(--error)] text-white'\n : 'border-[color-mix(in_srgb,var(--border-subtle)_55%,transparent)] bg-[var(--bg-muted)] text-[var(--text-tertiary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]'\n }`}\n >\n {voice.active ? (\n <Square className=\"h-[9px] w-[9px] fill-current\" />\n ) : (\n <Mic />\n )}\n </button>\n )}\n\n {/* THE WAY BACK, on the bar, in every engaged state.\n\n Focusing hides the whole board — deliberately; the suggestions want\n the room. But the exits were Escape and a click on dead space, and\n once you typed a word the reward for losing the board was one grey\n line of \"Nothing matched\". John, twice: *\"i cant seem to close a\n chat\"*, then *\"you cant close it when you start prompting\"*. The\n first answer was an X above the thread, which exists only once a\n thread does.\n\n The bar is the one element present in all four engaged states, and\n a control that ends something must not be rendered by the thing it\n ends. */}\n {engaged && (\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={onClose}\n aria-label=\"Close\"\n /* RED ON HOVER, like every other control in this product that undoes\n something. The board's own remove control already answers a hover\n with `--error-bg` and `--error`; this one answered with the same\n neutral wash as the mic beside it, so the one control in the row\n that ENDS something looked exactly like the two that start\n something. A hover state is where a control says what it is about\n to do, and this one was declining to. */\n className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-[var(--bg-muted)] text-[var(--text-tertiary)] transition-[background-color,color] duration-[var(--motion-fast)] hover:bg-[var(--error-bg)] hover:text-[var(--error)]\"\n >\n <X />\n </button>\n )}\n\n {send ?? (\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => onSubmit(value)}\n aria-label=\"Send\"\n disabled={!canSend}\n // LIVE means BLUE. It was muted in both states, so the one control\n // that commits looked equally dead whether or not it would do\n // anything — the single most useful thing a send button can say is\n // \"there is something to send now\", and colour is how it says it.\n className={`grid size-8 shrink-0 place-items-center rounded-full transition-[background-color,color] duration-[var(--motion-fast)] ${\n canSend\n ? 'bg-brand-500 text-white hover:bg-brand-600'\n : 'bg-[var(--bg-muted)] text-[var(--text-muted)]'\n }`}\n >\n <ArrowUp />\n </button>\n )}\n </>\n );\n}\n"],"mappings":";AA6CI,
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/ask-controls.tsx"],"sourcesContent":["'use client';\n\nimport { ArrowUp, Mic, Square, X } from 'lucide-react';\n\n/**\n * The three controls at the right end of the ask bar.\n *\n * Lifted out of `home-ask` for room, and they belong together anyway: they are\n * the only things in that bar that are pressed rather than typed into, they\n * share one frame and one size, and the rules about which of them is showing\n * are rules about the same cluster.\n */\nexport function AskControls({\n voice,\n value,\n canSend,\n engaged,\n inputRef,\n onSubmit,\n onClose,\n send,\n voiceLabels = { start: 'Talk to Box', stop: 'Stop voice mode' },\n}: {\n voice: {\n supported: boolean;\n active: boolean;\n start: () => Promise<void> | void;\n stop: () => void;\n /** Pressing it does nothing, e.g. a clip is already waiting to be sent. */\n disabled?: boolean;\n };\n /** Replaces the default round send button. */\n send?: React.ReactNode;\n /** What the mic says it will do, for the person using a screen reader. */\n voiceLabels?: { start: string; stop: string };\n /** There is a sentence or an attachment to send. */\n canSend: boolean;\n value: string;\n /** Anything is open under the bar: suggestions, results or a conversation. */\n engaged: boolean;\n inputRef: React.RefObject<HTMLTextAreaElement | null>;\n onSubmit: (q: string) => void;\n onClose: () => void;\n}) {\n return (\n <>\n {/* Hidden where the browser has no recogniser, rather than shown and\n dead. The icon becomes a stop square so the control says what\n pressing it will do — and in voice mode that is a bigger claim than\n it was for dictation, because pressing it ends a conversation rather\n than closing a microphone. */}\n {voice.supported && (\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n // Both branches play their sound on the PRESS rather than on the\n // recogniser being ready — `getUserMedia` can sit behind a\n // permission prompt, and a confirmation that arrives late\n // confirms nothing. Owned by the hook so every entrance to voice\n // mode sounds the same, including the automatic one at the end of\n // Box's reply, which has no button to hang a sound on.\n if (voice.active) {\n voice.stop();\n } else {\n inputRef.current?.focus();\n void voice.start();\n }\n }}\n disabled={voice.disabled && !voice.active}\n aria-label={voice.active ? voiceLabels.stop : voiceLabels.start}\n title={voice.active ? voiceLabels.stop : voiceLabels.start}\n aria-pressed={voice.active}\n // FRAMED, like Send. Two adjacent controls where one sits in a\n // circle and the other floats bare read as two different KINDS of\n // control — one a button, one a decoration — when they are both\n // buttons. Same frame, same size, different job.\n className={`grid size-8 shrink-0 place-items-center rounded-lg border transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${\n voice.active\n ? 'border-transparent bg-[var(--error)] text-white'\n : 'border-[color-mix(in_srgb,var(--border-subtle)_55%,transparent)] bg-[var(--bg-muted)] text-[var(--text-tertiary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]'\n }`}\n >\n {voice.active ? (\n <Square className=\"h-[9px] w-[9px] fill-current\" />\n ) : (\n <Mic />\n )}\n </button>\n )}\n\n {/* THE WAY BACK, on the bar, in every engaged state.\n\n Focusing hides the whole board — deliberately; the suggestions want\n the room. But the exits were Escape and a click on dead space, and\n once you typed a word the reward for losing the board was one grey\n line of \"Nothing matched\". John, twice: *\"i cant seem to close a\n chat\"*, then *\"you cant close it when you start prompting\"*. The\n first answer was an X above the thread, which exists only once a\n thread does.\n\n The bar is the one element present in all four engaged states, and\n a control that ends something must not be rendered by the thing it\n ends. */}\n {engaged && (\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={onClose}\n aria-label=\"Close\"\n /* RED ON HOVER, like every other control in this product that undoes\n something. The board's own remove control already answers a hover\n with `--error-bg` and `--error`; this one answered with the same\n neutral wash as the mic beside it, so the one control in the row\n that ENDS something looked exactly like the two that start\n something. A hover state is where a control says what it is about\n to do, and this one was declining to. */\n className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-[var(--bg-muted)] text-[var(--text-tertiary)] transition-[background-color,color] duration-[var(--motion-fast)] hover:bg-[var(--error-bg)] hover:text-[var(--error)]\"\n >\n <X />\n </button>\n )}\n\n {send ?? (\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => onSubmit(value)}\n aria-label=\"Send\"\n disabled={!canSend}\n // LIVE means BLUE. It was muted in both states, so the one control\n // that commits looked equally dead whether or not it would do\n // anything — the single most useful thing a send button can say is\n // \"there is something to send now\", and colour is how it says it.\n className={`grid size-8 shrink-0 place-items-center rounded-full transition-[background-color,color] duration-[var(--motion-fast)] ${\n canSend\n ? 'bg-brand-500 text-white hover:bg-brand-600'\n : 'bg-[var(--bg-muted)] text-[var(--text-muted)]'\n }`}\n >\n <ArrowUp />\n </button>\n )}\n </>\n );\n}\n"],"mappings":";AA6CI,mBAuCQ,KAvCR;AA3CJ,SAAS,SAAS,KAAK,QAAQ,SAAS;AAUjC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc,EAAE,OAAO,eAAe,MAAM,kBAAkB;AAChE,GAqBG;AACD,SACE,iCAMG;AAAA,UAAM,aACL;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,SAAS,MAAM;AAOb,cAAI,MAAM,QAAQ;AAChB,kBAAM,KAAK;AAAA,UACb,OAAO;AACL,qBAAS,SAAS,MAAM;AACxB,iBAAK,MAAM,MAAM;AAAA,UACnB;AAAA,QACF;AAAA,QACA,UAAU,MAAM,YAAY,CAAC,MAAM;AAAA,QACnC,cAAY,MAAM,SAAS,YAAY,OAAO,YAAY;AAAA,QAC1D,OAAO,MAAM,SAAS,YAAY,OAAO,YAAY;AAAA,QACrD,gBAAc,MAAM;AAAA,QAKpB,WAAW,+HACT,MAAM,SACF,oDACA,wLACN;AAAA,QAEC,gBAAM,SACL,oBAAC,UAAO,WAAU,gCAA+B,IAEjD,oBAAC,OAAI;AAAA;AAAA,IAET;AAAA,IAgBD,WACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,SAAS;AAAA,QACT,cAAW;AAAA,QAQX,WAAU;AAAA,QAEV,8BAAC,KAAE;AAAA;AAAA,IACL;AAAA,IAGD,QACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,SAAS,MAAM,SAAS,KAAK;AAAA,QAC7B,cAAW;AAAA,QACX,UAAU,CAAC;AAAA,QAKX,WAAW,0HACT,UACI,+CACA,+CACN;AAAA,QAEA,8BAAC,WAAQ;AAAA;AAAA,IACX;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -3,8 +3,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as Popover from "@radix-ui/react-popover";
|
|
4
4
|
import { Check, ChevronDown, Headset, Search } from "lucide-react";
|
|
5
5
|
const MODES = [
|
|
6
|
-
{ id: "search", label: "Search", Icon: Search },
|
|
7
|
-
{ id: "support", label: "
|
|
6
|
+
{ id: "search", label: "Search your business", Icon: Search },
|
|
7
|
+
{ id: "support", label: "Message support", Icon: Headset }
|
|
8
8
|
];
|
|
9
9
|
function AskModeChip({
|
|
10
10
|
mode,
|
|
@@ -19,7 +19,7 @@ function AskModeChip({
|
|
|
19
19
|
{
|
|
20
20
|
type: "button",
|
|
21
21
|
onMouseDown: (e) => e.preventDefault(),
|
|
22
|
-
"aria-label": "Choose
|
|
22
|
+
"aria-label": "Choose between searching your business and messaging support",
|
|
23
23
|
title: locked ? "Remove the attachment to search again" : void 0,
|
|
24
24
|
className: "flex h-[30px] shrink-0 items-center gap-1 rounded-lg border border-[color-mix(in_srgb,var(--border-subtle)_55%,transparent)] bg-[var(--bg-muted)] px-2 text-xs text-[var(--text-primary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-hover)] disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-[var(--bg-muted)]",
|
|
25
25
|
children: [
|
|
@@ -40,7 +40,7 @@ function AskModeChip({
|
|
|
40
40
|
{
|
|
41
41
|
align: "start",
|
|
42
42
|
sideOffset: 6,
|
|
43
|
-
className: "surface-pinned z-[100] w-
|
|
43
|
+
className: "surface-pinned z-[100] w-56 !p-1",
|
|
44
44
|
children: MODES.map(({ id, label, Icon: ModeIcon }) => /* @__PURE__ */ jsx(Popover.Close, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
45
45
|
"button",
|
|
46
46
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/ask-mode-chip.tsx"],"sourcesContent":["'use client';\n\nimport * as Popover from '@radix-ui/react-popover';\nimport { Check, ChevronDown, Headset, Search } from 'lucide-react';\n\nexport type AskMode = 'search' | 'support';\n\nconst MODES = [\n { id: 'search', label: 'Search', Icon: Search },\n { id: 'support', label: '
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/ask-mode-chip.tsx"],"sourcesContent":["'use client';\n\nimport * as Popover from '@radix-ui/react-popover';\nimport { Check, ChevronDown, Headset, Search } from 'lucide-react';\n\nexport type AskMode = 'search' | 'support';\n\nconst MODES = [\n { id: 'search', label: 'Search your business', Icon: Search },\n { id: 'support', label: 'Message support', Icon: Headset },\n] as const;\n\n/**\n * The picker beside the paperclip: what the box is for right now.\n *\n * It is the pill AI products put in their input to change model, with the\n * same job done honestly for this box: it does not choose a model, it chooses\n * where the words go. Search finds records and takes you to them; Support\n * opens a case with whatever is in the box.\n *\n * IT LOCKS TO SUPPORT while a voice note or a picture is on the card, because\n * neither can be searched for and neither can be sent anywhere else. A pill\n * that let you flip back to Search would silently park the attachment and\n * turn a message to support into a query with a picture stuck to it.\n */\nexport function AskModeChip({\n mode,\n onChange,\n locked,\n}: {\n mode: AskMode;\n onChange: (next: AskMode) => void;\n /** An attachment is on the card, so the mode cannot be changed. */\n locked?: boolean;\n}) {\n const selected = MODES.find((m) => m.id === mode) ?? MODES[0];\n const Icon = selected.Icon;\n return (\n <Popover.Root>\n <Popover.Trigger asChild disabled={locked}>\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n aria-label=\"Choose between searching your business and messaging support\"\n title={locked ? 'Remove the attachment to search again' : undefined}\n className=\"flex h-[30px] shrink-0 items-center gap-1 rounded-lg border border-[color-mix(in_srgb,var(--border-subtle)_55%,transparent)] bg-[var(--bg-muted)] px-2 text-xs text-[var(--text-primary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-hover)] disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-[var(--bg-muted)]\"\n >\n <Icon aria-hidden className=\"size-3.5 shrink-0 text-brand-500\" />\n {selected.label}\n {!locked && (\n <ChevronDown\n aria-hidden\n className=\"size-3.5 shrink-0 text-[var(--text-tertiary)]\"\n />\n )}\n </button>\n </Popover.Trigger>\n <Popover.Portal>\n <Popover.Content\n align=\"start\"\n sideOffset={6}\n className=\"surface-pinned z-[100] w-56 !p-1\"\n >\n {MODES.map(({ id, label, Icon: ModeIcon }) => (\n <Popover.Close asChild key={id}>\n <button\n type=\"button\"\n onClick={() => onChange(id)}\n className=\"flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm text-[var(--text-primary)] hover:bg-[var(--interactive-hover)]\"\n >\n <ModeIcon\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-tertiary)]\"\n />\n <span className=\"flex-1\">{label}</span>\n {id === mode && <Check aria-hidden className=\"size-4\" />}\n </button>\n </Popover.Close>\n ))}\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n );\n}\n"],"mappings":";AAwCQ,SAOE,KAPF;AAtCR,YAAY,aAAa;AACzB,SAAS,OAAO,aAAa,SAAS,cAAc;AAIpD,MAAM,QAAQ;AAAA,EACZ,EAAE,IAAI,UAAU,OAAO,wBAAwB,MAAM,OAAO;AAAA,EAC5D,EAAE,IAAI,WAAW,OAAO,mBAAmB,MAAM,QAAQ;AAC3D;AAeO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,WAAW,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,IAAI,KAAK,MAAM,CAAC;AAC5D,QAAM,OAAO,SAAS;AACtB,SACE,qBAAC,QAAQ,MAAR,EACC;AAAA,wBAAC,QAAQ,SAAR,EAAgB,SAAO,MAAC,UAAU,QACjC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,cAAW;AAAA,QACX,OAAO,SAAS,0CAA0C;AAAA,QAC1D,WAAU;AAAA,QAEV;AAAA,8BAAC,QAAK,eAAW,MAAC,WAAU,oCAAmC;AAAA,UAC9D,SAAS;AAAA,UACT,CAAC,UACA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAU;AAAA;AAAA,UACZ;AAAA;AAAA;AAAA,IAEJ,GACF;AAAA,IACA,oBAAC,QAAQ,QAAR,EACC;AAAA,MAAC,QAAQ;AAAA,MAAR;AAAA,QACC,OAAM;AAAA,QACN,YAAY;AAAA,QACZ,WAAU;AAAA,QAET,gBAAM,IAAI,CAAC,EAAE,IAAI,OAAO,MAAM,SAAS,MACtC,oBAAC,QAAQ,OAAR,EAAc,SAAO,MACpB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,SAAS,EAAE;AAAA,YAC1B,WAAU;AAAA,YAEV;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAW;AAAA,kBACX,WAAU;AAAA;AAAA,cACZ;AAAA,cACA,oBAAC,UAAK,WAAU,UAAU,iBAAM;AAAA,cAC/B,OAAO,QAAQ,oBAAC,SAAM,eAAW,MAAC,WAAU,UAAS;AAAA;AAAA;AAAA,QACxD,KAZ0B,EAa5B,CACD;AAAA;AAAA,IACH,GACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { Info, X } from "lucide-react";
|
|
@@ -201,7 +201,7 @@ function HomeAsk({
|
|
|
201
201
|
inputRef,
|
|
202
202
|
onSubmit: submit,
|
|
203
203
|
onClose: () => inputRef.current?.blur(),
|
|
204
|
-
send: /* @__PURE__ */ jsx(
|
|
204
|
+
send: !inSupport ? /* @__PURE__ */ jsx(Fragment, {}) : /* @__PURE__ */ jsx(
|
|
205
205
|
AskSendButton,
|
|
206
206
|
{
|
|
207
207
|
support: inSupport,
|
|
@@ -209,7 +209,7 @@ function HomeAsk({
|
|
|
209
209
|
sending: support.sending,
|
|
210
210
|
sent: support.sent,
|
|
211
211
|
title: support.needsCaption ? "Add a few words about the picture first" : void 0,
|
|
212
|
-
onClick:
|
|
212
|
+
onClick: () => void support.send()
|
|
213
213
|
}
|
|
214
214
|
)
|
|
215
215
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/home-ask.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState, type RefObject } from 'react';\nimport Link from 'next/link';\nimport { Info, X } from 'lucide-react';\nimport { AutoGrowTextarea } from '@withpotter/ui-kit/auto-grow-textarea';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { AskControls } from './ask-controls';\nimport { AskGlow } from './ask-glow';\nimport { AskModeChip, type AskMode } from './ask-mode-chip';\nimport { AskSendButton } from './ask-send-button';\nimport { AskSupportAttachments } from './ask-support-attachments';\nimport { useAskSupport } from './use-ask-support';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { SUPPORT_NAME } from '@withpotter/lib/support';\nimport { AskResults } from './ask-results';\nimport { SearchTips } from './search-tips';\nimport { HomeQuickActions } from './home-quick-actions';\n\n/**\n * The home's one box: a search, and a way to reach support.\n *\n * SEARCH IS THE DEFAULT. Clicking the field folds the board away and fills the\n * room with what has been happening, then with what matches as you type, and a\n * row opens that record. See `AskResults` and `SearchTips`.\n *\n * SUPPORT IS A MODE, chosen on the pill beside the paperclip or entered by\n * using the mic or the paperclip, neither of which can be searched for. In it\n * the round send button grows into \"Send to support\" and opens a case through\n * the same call the Support page uses, with the message, and the voice note or\n * picture, attached. A voice note can go with no words; a picture cannot, and\n * the card says so under it. The mechanics live in `useAskSupport`.\n *\n * ENTER SENDS, SHIFT+ENTER BREAKS THE LINE. In search it only keeps the caret\n * where it is, because the list under the box is the answer; an IME\n * composition's Enter is neither and is left alone.\n *\n * THE NOTICE STRIP IS DRIVEN BY `usage`, and renders nothing without it. There\n * is no usage figure to read yet, so the home passes none; when there is,\n * handing it in is the whole of the wiring.\n */\nexport function HomeAsk({\n /** Optional handle, so the page header's Box control can focus this. */\n inputRef: externalRef,\n usage,\n onOpenChange,\n}: {\n /** The search opened or closed; the page folds the board away on open. */\n onOpenChange?: (open: boolean) => void;\n inputRef?: RefObject<HTMLTextAreaElement | null>;\n /** Share of the allowance used, and where \"View usage\" goes. */\n usage?: { percent: number; href: string };\n}) {\n const orderLabel = useEntityLabel('order');\n const itemLabel = useEntityLabel('item');\n /* SEARCH WORDING, NOT QUESTIONS. This field finds records, so what it\n suggests is what can be found; `askPlaceholders` are things to ask the\n assistant and no longer belong here. */\n const prompts = useMemo(\n () => [\n `Search ${orderLabel.plural.toLowerCase()} by customer name`,\n 'Search customers by name or email',\n `Search ${itemLabel.plural.toLowerCase()} by name or SKU`,\n `Find an invoice by number`,\n ],\n [orderLabel, itemLabel],\n );\n const focusedPrompt = `Search ${orderLabel.plural.toLowerCase()}, customers, ${itemLabel.plural.toLowerCase()}…`;\n const [i, setI] = useState(0);\n const [value, setValue] = useState('');\n const [focused, setFocused] = useState(false);\n const [noticeDismissed, setNoticeDismissed] = useState(false);\n /* What the box is for. Search unless someone chose Support, or put a voice\n note or a picture on the card, which can only go to support. */\n const [mode, setMode] = useState<AskMode>('search');\n const ownRef = useRef<HTMLTextAreaElement>(null);\n const inputRef = externalRef ?? ownRef;\n const root = useRef<HTMLDivElement>(null);\n\n /* THE SEARCH, NOT A CONVERSATION. Focusing the field opens it: the page\n folds the board away and the room holds what has been happening, then what\n matches as you type. It stays open across a blur (pressing a result blurs\n the field before the click lands) and closes on Escape, a press outside\n this card and its list, or navigating. */\n const [searching, setSearching] = useState(false);\n const setOpen = (next: boolean) => {\n setSearching(next);\n onOpenChange?.(next);\n };\n\n /* After a case goes and the green \"Sent\" has had its moment, the box is\n back to what it normally is: a search. */\n const support = useAskSupport(value, setValue, () => setMode('search'));\n const inSupport = mode === 'support' || support.engaged;\n const recording = support.composer.recordingNow;\n const voice = {\n supported: support.voiceSupported,\n active: recording,\n start: () => {\n setMode('support');\n support.startVoice();\n },\n stop: support.stopVoice,\n /* One clip at a time: remove it to record another. */\n disabled:\n Boolean(support.composer.voice) || support.composer.recorder.isBusy,\n };\n /* The latest of these, for the handlers below that were created on an\n earlier render and would otherwise close over stale ones. */\n const latest = useRef({ mode, engaged: false, value: '' });\n useEffect(() => {\n latest.current = { mode, engaged: support.engaged, value };\n });\n /* CLOSING THE SEARCH ENDS A SUPPORT MODE NOBODY IS USING. Support is sticky\n on purpose while there is something in the box, but a pill left on Support\n after the box was emptied and closed hid the carousel next time with no\n sign of why, and the bottom of the page just sat blank. If there are words\n or an attachment, the mode stays; if not, the box goes back to searching. */\n const closeSearch = () => {\n setOpen(false);\n const l = latest.current;\n if (l.mode === 'support' && !l.engaged && !l.value.trim()) {\n setMode('search');\n }\n };\n useEffect(() => {\n if (!searching) return;\n /* STAYS OPEN ONLY FOR A PRESS ON SOMETHING. The search block is wider and\n taller than anything in it: it holds the empty gap under the box and the\n carousel's own background, and a press there is a press on nothing. So\n \"inside\" is the card itself or a real control in this block (a link, a\n button, a field), plus the Add menu, which is portaled out of it. */\n const onDown = (e: PointerEvent) => {\n const t = e.target as HTMLElement;\n const inside =\n !!root.current?.contains(t) &&\n !!t.closest('[data-ask-card],a,button,input,textarea,[role=\"menu\"]');\n const inMenu = !!t.closest('[role=\"menu\"]');\n if (!inside && !inMenu) closeSearch();\n };\n document.addEventListener('pointerdown', onDown);\n return () => document.removeEventListener('pointerdown', onDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [searching]);\n\n const supportPrompt = recording\n ? 'Listening. What you say appears here…'\n : 'Tell support what you need…';\n\n /* The rotation stops the moment there is a caret in the field: a placeholder\n that keeps changing under one reads as the field arguing with you. */\n useEffect(() => {\n if (focused || value || inSupport) return;\n const t = setInterval(() => setI((n) => (n + 1) % prompts.length), 4200);\n return () => clearInterval(t);\n }, [focused, value, inSupport, prompts.length]);\n\n const canSearchSend = value.trim().length > 0;\n\n /* No hand-off to the ask screen: this field searches. Enter keeps the\n caret where it is and the list under it is the answer. */\n const submit = () => {\n inputRef.current?.focus();\n };\n\n const showNotice = !!usage && !noticeDismissed;\n\n return (\n <div ref={root} className=\"relative isolate w-full\">\n {showNotice && (\n <div className=\"flex h-11 items-center gap-3 rounded-t-[var(--radius-card)] bg-[var(--bg-muted)] pb-3 pl-4 pr-3 text-sm text-[var(--text-primary)]\">\n <Info\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-tertiary)]\"\n />\n <p className=\"min-w-0 flex-1 truncate\">\n You've used {Math.round(usage.percent)}% of your tokens.{' '}\n <Link href={usage.href} className=\"text-brand-500 hover:underline\">\n View usage\n </Link>\n </p>\n <button\n type=\"button\"\n onClick={() => setNoticeDismissed(true)}\n aria-label=\"Dismiss\"\n className=\"grid size-6 shrink-0 place-items-center rounded-md text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n )}\n\n {/* The card overlaps the strip's bottom edge, which is why the strip\n carries a padding-bottom it never shows: the card's top border runs\n through the strip, as drawn. */}\n <div\n data-ask-card\n className={`group relative rounded-[var(--radius-card)] border border-[var(--border-default)] bg-[color-mix(in_srgb,var(--bg-muted)_55%,var(--bg-base))] transition-colors focus-within:border-[color-mix(in_srgb,var(--brand-500)_45%,var(--text-muted))] ${\n showNotice ? '-mt-3' : ''\n }`}\n >\n {/* The rings sit at -z-10 behind this card's OPAQUE fill, which is what\n keeps the centre clear. They bloom on hover and on focus. */}\n <AskGlow />\n <AskSupportAttachments support={support} />\n <div className=\"relative px-4 pt-3\">\n <AutoGrowTextarea\n ref={inputRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => {\n setFocused(true);\n setOpen(true);\n }}\n onBlur={() => setFocused(false)}\n /* Focus alone is not enough: close the search with the box still\n focused (a press on a non-focusable part of the page can leave\n it so) and clicking back in fires no focus event, so nothing\n opened and the bottom stayed blank. */\n onClick={() => {\n if (!searching) setOpen(true);\n }}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n if (inSupport) void support.send();\n else submit();\n }\n if (e.key === 'Escape') {\n /* A message to support is somebody's words: closing the box\n does not throw them away. */\n if (!inSupport) setValue('');\n closeSearch();\n e.currentTarget.blur();\n }\n }}\n aria-label={inSupport ? 'Message to support' : 'Search'}\n className=\"block min-h-[36px] w-full bg-transparent text-md text-[var(--text-primary)] outline-none\"\n />\n {/* Crossfading placeholders, stacked so the card never changes\n height and the rotation cannot reflow it. Rendered beside the\n field rather than as its `placeholder`, which cannot animate. */}\n {!value && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-4 top-3 flex items-start\"\n >\n {(inSupport\n ? [supportPrompt]\n : focused\n ? [focusedPrompt]\n : prompts\n ).map((p, n) => (\n <span\n key={p}\n className={`block truncate text-md text-[var(--text-muted)] transition-opacity duration-[var(--motion-slow)] ${\n focused || inSupport || n === i\n ? 'opacity-100'\n : 'absolute inset-0 opacity-0'\n }`}\n >\n {p}\n </span>\n ))}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 px-3 pb-2 pt-1\">\n {/* The plus that attached a picture is out of this row for now, on\n John's call, 2026-10-09. Sending a picture to support still works\n underneath (`support.composer.pickImage`, and the card's\n attachment frame); put a button back here to reach it. */}\n <AskModeChip\n mode={inSupport ? 'support' : 'search'}\n onChange={setMode}\n locked={support.engaged}\n />\n\n <div className=\"ml-auto flex items-center gap-2\">\n <AskControls\n voice={voice}\n voiceLabels={{\n start: 'Record a voice note for support',\n stop: 'Stop recording',\n }}\n value={value}\n canSend={canSearchSend}\n engaged={false}\n inputRef={inputRef}\n onSubmit={submit}\n onClose={() => inputRef.current?.blur()}\n send={\n <AskSendButton\n support={inSupport}\n canSend={inSupport ? support.canSend : canSearchSend}\n sending={support.sending}\n sent={support.sent}\n title={\n support.needsCaption\n ? 'Add a few words about the picture first'\n : undefined\n }\n onClick={inSupport ? () => void support.send() : submit}\n />\n }\n />\n </div>\n </div>\n </div>\n {/* THE QUICK ACTIONS STEP ASIDE while the search is open. They are the\n most-used things on the page, so they are there whenever the box is\n closed; once you click in, the room under it belongs to the search.\n `popLayout` takes them out of the flow at once and fades them where\n they stood, so the carousel does not wait for them to finish leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {!searching && (\n <motion.div\n key=\"quick\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n transition={{ duration: 0.2 }}\n >\n <HomeQuickActions />\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* The results rise in from below as the board lifts away, and sink back\n out of the way when you click off, so the page never just blinks\n between states. `popLayout`, not `wait`, so the results start at once\n instead of after the old state has finished leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {searching && inSupport && (\n <motion.div\n key=\"support\"\n className=\"mt-3 px-1 text-center text-sm text-[var(--text-tertiary)]\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n This goes to {SUPPORT_NAME} support. We reply in Support and by email.\n </motion.div>\n )}\n {searching && !inSupport && (\n <motion.div\n key=\"search\"\n className=\"mt-3\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n {/* Before anything is typed: suggestions that move on their own. */}\n {!value.trim() && (\n /* 260px from the card to the top of the first suggestion on a tall\n window. The quick actions are hidden while the search is open,\n so the only thing between is the rail's own 8px of top padding\n and this margin: 252. On a short window the gap gives way\n instead of the page scrolling: 100vh less what sits above the\n rail (about 271px of heading and box) and the rail itself\n (296px) and the container's inset, never below 24px. The list's\n 12px margin above collapses into this one, so it adds nothing. */\n <div\n style={{\n marginTop: 'clamp(24px, calc(100vh - 585px), 252px)',\n }}\n >\n <SearchTips />\n </div>\n )}\n {/* Results only once there is something to look for. With an empty\n box the carousel above is the whole panel: a list of recent\n orders and customers under it read as a second, unrelated\n thing, and it cost three requests on every focus. */}\n {value.trim() && (\n <AskResults\n query={value}\n onNavigate={() => {\n setOpen(false);\n setValue('');\n }}\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/home-ask.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState, type RefObject } from 'react';\nimport Link from 'next/link';\nimport { Info, X } from 'lucide-react';\nimport { AutoGrowTextarea } from '@withpotter/ui-kit/auto-grow-textarea';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { AskControls } from './ask-controls';\nimport { AskGlow } from './ask-glow';\nimport { AskModeChip, type AskMode } from './ask-mode-chip';\nimport { AskSendButton } from './ask-send-button';\nimport { AskSupportAttachments } from './ask-support-attachments';\nimport { useAskSupport } from './use-ask-support';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { SUPPORT_NAME } from '@withpotter/lib/support';\nimport { AskResults } from './ask-results';\nimport { SearchTips } from './search-tips';\nimport { HomeQuickActions } from './home-quick-actions';\n\n/**\n * The home's one box: a search, and a way to reach support.\n *\n * SEARCH IS THE DEFAULT. Clicking the field folds the board away and fills the\n * room with what has been happening, then with what matches as you type, and a\n * row opens that record. See `AskResults` and `SearchTips`.\n *\n * SUPPORT IS A MODE, chosen on the pill beside the paperclip or entered by\n * using the mic or the paperclip, neither of which can be searched for. In it\n * the round send button grows into \"Send to support\" and opens a case through\n * the same call the Support page uses, with the message, and the voice note or\n * picture, attached. A voice note can go with no words; a picture cannot, and\n * the card says so under it. The mechanics live in `useAskSupport`.\n *\n * ENTER SENDS, SHIFT+ENTER BREAKS THE LINE. In search it only keeps the caret\n * where it is, because the list under the box is the answer; an IME\n * composition's Enter is neither and is left alone.\n *\n * THE NOTICE STRIP IS DRIVEN BY `usage`, and renders nothing without it. There\n * is no usage figure to read yet, so the home passes none; when there is,\n * handing it in is the whole of the wiring.\n */\nexport function HomeAsk({\n /** Optional handle, so the page header's Box control can focus this. */\n inputRef: externalRef,\n usage,\n onOpenChange,\n}: {\n /** The search opened or closed; the page folds the board away on open. */\n onOpenChange?: (open: boolean) => void;\n inputRef?: RefObject<HTMLTextAreaElement | null>;\n /** Share of the allowance used, and where \"View usage\" goes. */\n usage?: { percent: number; href: string };\n}) {\n const orderLabel = useEntityLabel('order');\n const itemLabel = useEntityLabel('item');\n /* SEARCH WORDING, NOT QUESTIONS. This field finds records, so what it\n suggests is what can be found; `askPlaceholders` are things to ask the\n assistant and no longer belong here. */\n const prompts = useMemo(\n () => [\n `Search ${orderLabel.plural.toLowerCase()} by customer name`,\n 'Search customers by name or email',\n `Search ${itemLabel.plural.toLowerCase()} by name or SKU`,\n `Find an invoice by number`,\n ],\n [orderLabel, itemLabel],\n );\n const focusedPrompt = `Search ${orderLabel.plural.toLowerCase()}, customers, ${itemLabel.plural.toLowerCase()}…`;\n const [i, setI] = useState(0);\n const [value, setValue] = useState('');\n const [focused, setFocused] = useState(false);\n const [noticeDismissed, setNoticeDismissed] = useState(false);\n /* What the box is for. Search unless someone chose Support, or put a voice\n note or a picture on the card, which can only go to support. */\n const [mode, setMode] = useState<AskMode>('search');\n const ownRef = useRef<HTMLTextAreaElement>(null);\n const inputRef = externalRef ?? ownRef;\n const root = useRef<HTMLDivElement>(null);\n\n /* THE SEARCH, NOT A CONVERSATION. Focusing the field opens it: the page\n folds the board away and the room holds what has been happening, then what\n matches as you type. It stays open across a blur (pressing a result blurs\n the field before the click lands) and closes on Escape, a press outside\n this card and its list, or navigating. */\n const [searching, setSearching] = useState(false);\n const setOpen = (next: boolean) => {\n setSearching(next);\n onOpenChange?.(next);\n };\n\n /* After a case goes and the green \"Sent\" has had its moment, the box is\n back to what it normally is: a search. */\n const support = useAskSupport(value, setValue, () => setMode('search'));\n const inSupport = mode === 'support' || support.engaged;\n const recording = support.composer.recordingNow;\n const voice = {\n supported: support.voiceSupported,\n active: recording,\n start: () => {\n setMode('support');\n support.startVoice();\n },\n stop: support.stopVoice,\n /* One clip at a time: remove it to record another. */\n disabled:\n Boolean(support.composer.voice) || support.composer.recorder.isBusy,\n };\n /* The latest of these, for the handlers below that were created on an\n earlier render and would otherwise close over stale ones. */\n const latest = useRef({ mode, engaged: false, value: '' });\n useEffect(() => {\n latest.current = { mode, engaged: support.engaged, value };\n });\n /* CLOSING THE SEARCH ENDS A SUPPORT MODE NOBODY IS USING. Support is sticky\n on purpose while there is something in the box, but a pill left on Support\n after the box was emptied and closed hid the carousel next time with no\n sign of why, and the bottom of the page just sat blank. If there are words\n or an attachment, the mode stays; if not, the box goes back to searching. */\n const closeSearch = () => {\n setOpen(false);\n const l = latest.current;\n if (l.mode === 'support' && !l.engaged && !l.value.trim()) {\n setMode('search');\n }\n };\n useEffect(() => {\n if (!searching) return;\n /* STAYS OPEN ONLY FOR A PRESS ON SOMETHING. The search block is wider and\n taller than anything in it: it holds the empty gap under the box and the\n carousel's own background, and a press there is a press on nothing. So\n \"inside\" is the card itself or a real control in this block (a link, a\n button, a field), plus the Add menu, which is portaled out of it. */\n const onDown = (e: PointerEvent) => {\n const t = e.target as HTMLElement;\n const inside =\n !!root.current?.contains(t) &&\n !!t.closest('[data-ask-card],a,button,input,textarea,[role=\"menu\"]');\n const inMenu = !!t.closest('[role=\"menu\"]');\n if (!inside && !inMenu) closeSearch();\n };\n document.addEventListener('pointerdown', onDown);\n return () => document.removeEventListener('pointerdown', onDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [searching]);\n\n const supportPrompt = recording\n ? 'Listening. What you say appears here…'\n : 'Tell support what you need…';\n\n /* The rotation stops the moment there is a caret in the field: a placeholder\n that keeps changing under one reads as the field arguing with you. */\n useEffect(() => {\n if (focused || value || inSupport) return;\n const t = setInterval(() => setI((n) => (n + 1) % prompts.length), 4200);\n return () => clearInterval(t);\n }, [focused, value, inSupport, prompts.length]);\n\n const canSearchSend = value.trim().length > 0;\n\n /* No hand-off to the ask screen: this field searches. Enter keeps the\n caret where it is and the list under it is the answer. */\n const submit = () => {\n inputRef.current?.focus();\n };\n\n const showNotice = !!usage && !noticeDismissed;\n\n return (\n <div ref={root} className=\"relative isolate w-full\">\n {showNotice && (\n <div className=\"flex h-11 items-center gap-3 rounded-t-[var(--radius-card)] bg-[var(--bg-muted)] pb-3 pl-4 pr-3 text-sm text-[var(--text-primary)]\">\n <Info\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-tertiary)]\"\n />\n <p className=\"min-w-0 flex-1 truncate\">\n You've used {Math.round(usage.percent)}% of your tokens.{' '}\n <Link href={usage.href} className=\"text-brand-500 hover:underline\">\n View usage\n </Link>\n </p>\n <button\n type=\"button\"\n onClick={() => setNoticeDismissed(true)}\n aria-label=\"Dismiss\"\n className=\"grid size-6 shrink-0 place-items-center rounded-md text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n )}\n\n {/* The card overlaps the strip's bottom edge, which is why the strip\n carries a padding-bottom it never shows: the card's top border runs\n through the strip, as drawn. */}\n <div\n data-ask-card\n className={`group relative rounded-[var(--radius-card)] border border-[var(--border-default)] bg-[color-mix(in_srgb,var(--bg-muted)_55%,var(--bg-base))] transition-colors focus-within:border-[color-mix(in_srgb,var(--brand-500)_45%,var(--text-muted))] ${\n showNotice ? '-mt-3' : ''\n }`}\n >\n {/* The rings sit at -z-10 behind this card's OPAQUE fill, which is what\n keeps the centre clear. They bloom on hover and on focus. */}\n <AskGlow />\n <AskSupportAttachments support={support} />\n <div className=\"relative px-4 pt-3\">\n <AutoGrowTextarea\n ref={inputRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => {\n setFocused(true);\n setOpen(true);\n }}\n onBlur={() => setFocused(false)}\n /* Focus alone is not enough: close the search with the box still\n focused (a press on a non-focusable part of the page can leave\n it so) and clicking back in fires no focus event, so nothing\n opened and the bottom stayed blank. */\n onClick={() => {\n if (!searching) setOpen(true);\n }}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n if (inSupport) void support.send();\n else submit();\n }\n if (e.key === 'Escape') {\n /* A message to support is somebody's words: closing the box\n does not throw them away. */\n if (!inSupport) setValue('');\n closeSearch();\n e.currentTarget.blur();\n }\n }}\n aria-label={inSupport ? 'Message to support' : 'Search'}\n className=\"block min-h-[36px] w-full bg-transparent text-md text-[var(--text-primary)] outline-none\"\n />\n {/* Crossfading placeholders, stacked so the card never changes\n height and the rotation cannot reflow it. Rendered beside the\n field rather than as its `placeholder`, which cannot animate. */}\n {!value && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-4 top-3 flex items-start\"\n >\n {(inSupport\n ? [supportPrompt]\n : focused\n ? [focusedPrompt]\n : prompts\n ).map((p, n) => (\n <span\n key={p}\n className={`block truncate text-md text-[var(--text-muted)] transition-opacity duration-[var(--motion-slow)] ${\n focused || inSupport || n === i\n ? 'opacity-100'\n : 'absolute inset-0 opacity-0'\n }`}\n >\n {p}\n </span>\n ))}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 px-3 pb-2 pt-1\">\n {/* The plus that attached a picture is out of this row for now, on\n John's call, 2026-10-09. Sending a picture to support still works\n underneath (`support.composer.pickImage`, and the card's\n attachment frame); put a button back here to reach it. */}\n <AskModeChip\n mode={inSupport ? 'support' : 'search'}\n onChange={setMode}\n locked={support.engaged}\n />\n\n <div className=\"ml-auto flex items-center gap-2\">\n <AskControls\n voice={voice}\n voiceLabels={{\n start: 'Record a voice note for support',\n stop: 'Stop recording',\n }}\n value={value}\n canSend={canSearchSend}\n engaged={false}\n inputRef={inputRef}\n onSubmit={submit}\n onClose={() => inputRef.current?.blur()}\n /* SEARCH HAS NO SEND BUTTON. Results update as you type, so an\n arrow there was a control that did nothing and made the box\n read as a chat input. It appears in Message support, where\n pressing it really sends. */\n send={\n !inSupport ? (\n <></>\n ) : (\n <AskSendButton\n support={inSupport}\n canSend={inSupport ? support.canSend : canSearchSend}\n sending={support.sending}\n sent={support.sent}\n title={\n support.needsCaption\n ? 'Add a few words about the picture first'\n : undefined\n }\n onClick={() => void support.send()}\n />\n )\n }\n />\n </div>\n </div>\n </div>\n {/* THE QUICK ACTIONS STEP ASIDE while the search is open. They are the\n most-used things on the page, so they are there whenever the box is\n closed; once you click in, the room under it belongs to the search.\n `popLayout` takes them out of the flow at once and fades them where\n they stood, so the carousel does not wait for them to finish leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {!searching && (\n <motion.div\n key=\"quick\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n transition={{ duration: 0.2 }}\n >\n <HomeQuickActions />\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* The results rise in from below as the board lifts away, and sink back\n out of the way when you click off, so the page never just blinks\n between states. `popLayout`, not `wait`, so the results start at once\n instead of after the old state has finished leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {searching && inSupport && (\n <motion.div\n key=\"support\"\n className=\"mt-3 px-1 text-center text-sm text-[var(--text-tertiary)]\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n This goes to {SUPPORT_NAME} support. We reply in Support and by email.\n </motion.div>\n )}\n {searching && !inSupport && (\n <motion.div\n key=\"search\"\n className=\"mt-3\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n {/* Before anything is typed: suggestions that move on their own. */}\n {!value.trim() && (\n /* 260px from the card to the top of the first suggestion on a tall\n window. The quick actions are hidden while the search is open,\n so the only thing between is the rail's own 8px of top padding\n and this margin: 252. On a short window the gap gives way\n instead of the page scrolling: 100vh less what sits above the\n rail (about 271px of heading and box) and the rail itself\n (296px) and the container's inset, never below 24px. The list's\n 12px margin above collapses into this one, so it adds nothing. */\n <div\n style={{\n marginTop: 'clamp(24px, calc(100vh - 585px), 252px)',\n }}\n >\n <SearchTips />\n </div>\n )}\n {/* Results only once there is something to look for. With an empty\n box the carousel above is the whole panel: a list of recent\n orders and customers under it read as a second, unrelated\n thing, and it cost three requests on every focus. */}\n {value.trim() && (\n <AskResults\n query={value}\n onNavigate={() => {\n setOpen(false);\n setValue('');\n }}\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -12,6 +12,7 @@ import { useEntityLabel } from "@withpotter/lib/intents/use-entity-label";
|
|
|
12
12
|
import { useIntentConfig } from "@withpotter/lib/intents/use-intent-config";
|
|
13
13
|
import { usePermissions } from "@withpotter/lib/use-permissions";
|
|
14
14
|
import { useTakePayment } from "../take-payment-context";
|
|
15
|
+
import { hostQuickActions } from "./host-quick-actions";
|
|
15
16
|
import { homeAddItemIntent, homeAddItemLinks } from "./home-add-item-links";
|
|
16
17
|
const PILL = "flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]";
|
|
17
18
|
function HomeQuickActions() {
|
|
@@ -44,7 +45,14 @@ function HomeQuickActions() {
|
|
|
44
45
|
Icon: Plus,
|
|
45
46
|
run: () => router.push(links.create)
|
|
46
47
|
}
|
|
47
|
-
] : []
|
|
48
|
+
] : [],
|
|
49
|
+
// Whatever the host registered, after the built-in three.
|
|
50
|
+
...hostQuickActions().filter((a) => !a.allowed || a.allowed((p) => can(p))).map((a) => ({
|
|
51
|
+
id: a.id,
|
|
52
|
+
label: a.label,
|
|
53
|
+
Icon: a.Icon,
|
|
54
|
+
run: () => router.push(a.href)
|
|
55
|
+
}))
|
|
48
56
|
];
|
|
49
57
|
if (actions.length === 0) return null;
|
|
50
58
|
return /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/home-quick-actions.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport {\n ChevronDown,\n CreditCard,\n FilePlus2,\n Plus,\n type LucideIcon,\n} from 'lucide-react';\nimport { OverflowMenu, OverflowItem } from '@withpotter/ui-kit/overflow-menu';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { useTakePayment } from '../take-payment-context';\nimport { homeAddItemIntent, homeAddItemLinks } from './home-add-item-links';\n\nconst PILL =\n 'flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]';\n\n/**\n * Take payment, Send invoice, Add item: the three things done most, as pills\n * under the ask card.\n *\n * They replaced Ask, Write and Organize, which were a look: choosing one moved a\n * highlight and nothing read it. These do something. Take payment opens the same\n * modal the rail used to carry; Send invoice and Add item go to the screens that\n * make them. \"Add item\" says the business's own word (`useEntityLabel`), so a\n * club is told to add a service and a shop a product.\n *\n * The first two write money, so they follow the home's own gate\n * (`canManageFinance`); Add item follows the catalogue's (`canManageProducts`).\n * Add item opens a two-way menu, the same as on the catalogue — typing one or\n * importing many is decided at the button, not on a blank form.\n * Nothing is drawn while permissions resolve, so the row appears whole.\n */\nexport function HomeQuickActions() {\n const router = useRouter();\n const takePayment = useTakePayment();\n const item = useEntityLabel('item');\n const intentConfig = useIntentConfig();\n const { can, loading } = usePermissions();\n if (loading) return null;\n // Named, so the item lands on the list the sidebar opens; see the helper.\n const links = homeAddItemLinks(homeAddItemIntent(intentConfig, item));\n\n const actions: {\n id: string;\n label: string;\n Icon: LucideIcon;\n run: () => void;\n }[] = [\n ...(can('canManageFinance')\n ? [\n {\n id: 'payment',\n label: 'Take payment',\n Icon: CreditCard,\n run: takePayment,\n },\n {\n id: 'invoice',\n label: 'Send invoice',\n Icon: FilePlus2,\n run: () => router.push('/dashboard/invoices/new'),\n },\n ]\n : []),\n ...(can('canManageProducts')\n ? [\n {\n id: 'item',\n label: `Add ${item.singular.toLowerCase()}`,\n Icon: Plus,\n run: () => router.push(links.create),\n },\n ]\n : []),\n ];\n if (actions.length === 0) return null;\n\n return (\n <div\n role=\"group\"\n aria-label=\"Quick actions\"\n className=\"mt-3 flex flex-wrap items-center justify-center gap-3\"\n >\n {actions.map(({ id, label, Icon, run }) =>\n id === 'item' ? (\n <OverflowMenu\n key={id}\n label={label}\n align=\"start\"\n trigger={(props) => (\n <button type=\"button\" {...props} className={PILL}>\n <Icon\n aria-hidden\n className=\"size-4 text-[var(--text-tertiary)]\"\n />\n {label}\n <ChevronDown aria-hidden className=\"-ml-1 size-3.5\" />\n </button>\n )}\n >\n <OverflowItem onClick={run}>Add manually</OverflowItem>\n <OverflowItem\n onClick={() => router.push(links.importSheet)}\n >\n Import from CSV\n </OverflowItem>\n </OverflowMenu>\n ) : (\n <button key={id} type=\"button\" onClick={run} className={PILL}>\n <Icon aria-hidden className=\"size-4 text-[var(--text-tertiary)]\" />\n {label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/home-quick-actions.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport {\n ChevronDown,\n CreditCard,\n FilePlus2,\n Plus,\n type LucideIcon,\n} from 'lucide-react';\nimport { OverflowMenu, OverflowItem } from '@withpotter/ui-kit/overflow-menu';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { useTakePayment } from '../take-payment-context';\nimport { hostQuickActions } from './host-quick-actions';\nimport { homeAddItemIntent, homeAddItemLinks } from './home-add-item-links';\n\nconst PILL =\n 'flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]';\n\n/**\n * Take payment, Send invoice, Add item: the three things done most, as pills\n * under the ask card.\n *\n * They replaced Ask, Write and Organize, which were a look: choosing one moved a\n * highlight and nothing read it. These do something. Take payment opens the same\n * modal the rail used to carry; Send invoice and Add item go to the screens that\n * make them. \"Add item\" says the business's own word (`useEntityLabel`), so a\n * club is told to add a service and a shop a product.\n *\n * The first two write money, so they follow the home's own gate\n * (`canManageFinance`); Add item follows the catalogue's (`canManageProducts`).\n * Add item opens a two-way menu, the same as on the catalogue — typing one or\n * importing many is decided at the button, not on a blank form.\n * Nothing is drawn while permissions resolve, so the row appears whole.\n */\nexport function HomeQuickActions() {\n const router = useRouter();\n const takePayment = useTakePayment();\n const item = useEntityLabel('item');\n const intentConfig = useIntentConfig();\n const { can, loading } = usePermissions();\n if (loading) return null;\n // Named, so the item lands on the list the sidebar opens; see the helper.\n const links = homeAddItemLinks(homeAddItemIntent(intentConfig, item));\n\n const actions: {\n id: string;\n label: string;\n Icon: LucideIcon;\n run: () => void;\n }[] = [\n ...(can('canManageFinance')\n ? [\n {\n id: 'payment',\n label: 'Take payment',\n Icon: CreditCard,\n run: takePayment,\n },\n {\n id: 'invoice',\n label: 'Send invoice',\n Icon: FilePlus2,\n run: () => router.push('/dashboard/invoices/new'),\n },\n ]\n : []),\n ...(can('canManageProducts')\n ? [\n {\n id: 'item',\n label: `Add ${item.singular.toLowerCase()}`,\n Icon: Plus,\n run: () => router.push(links.create),\n },\n ]\n : []),\n // Whatever the host registered, after the built-in three.\n ...hostQuickActions()\n .filter((a) => !a.allowed || a.allowed((p) => can(p as never)))\n .map((a) => ({\n id: a.id,\n label: a.label,\n Icon: a.Icon,\n run: () => router.push(a.href),\n })),\n ];\n if (actions.length === 0) return null;\n\n return (\n <div\n role=\"group\"\n aria-label=\"Quick actions\"\n className=\"mt-3 flex flex-wrap items-center justify-center gap-3\"\n >\n {actions.map(({ id, label, Icon, run }) =>\n id === 'item' ? (\n <OverflowMenu\n key={id}\n label={label}\n align=\"start\"\n trigger={(props) => (\n <button type=\"button\" {...props} className={PILL}>\n <Icon\n aria-hidden\n className=\"size-4 text-[var(--text-tertiary)]\"\n />\n {label}\n <ChevronDown aria-hidden className=\"-ml-1 size-3.5\" />\n </button>\n )}\n >\n <OverflowItem onClick={run}>Add manually</OverflowItem>\n <OverflowItem\n onClick={() => router.push(links.importSheet)}\n >\n Import from CSV\n </OverflowItem>\n </OverflowMenu>\n ) : (\n <button key={id} type=\"button\" onClick={run} className={PILL}>\n <Icon aria-hidden className=\"size-4 text-[var(--text-tertiary)]\" />\n {label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";AAwGc,SACE,KADF;AAtGd,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,cAAc,oBAAoB;AAC3C,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,mBAAmB,wBAAwB;AAEpD,MAAM,OACJ;AAkBK,SAAS,mBAAmB;AACjC,QAAM,SAAS,UAAU;AACzB,QAAM,cAAc,eAAe;AACnC,QAAM,OAAO,eAAe,MAAM;AAClC,QAAM,eAAe,gBAAgB;AACrC,QAAM,EAAE,KAAK,QAAQ,IAAI,eAAe;AACxC,MAAI,QAAS,QAAO;AAEpB,QAAM,QAAQ,iBAAiB,kBAAkB,cAAc,IAAI,CAAC;AAEpE,QAAM,UAKA;AAAA,IACJ,GAAI,IAAI,kBAAkB,IACtB;AAAA,MACE;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK;AAAA,MACP;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK,MAAM,OAAO,KAAK,yBAAyB;AAAA,MAClD;AAAA,IACF,IACA,CAAC;AAAA,IACL,GAAI,IAAI,mBAAmB,IACvB;AAAA,MACE;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,OAAO,KAAK,SAAS,YAAY,CAAC;AAAA,QACzC,MAAM;AAAA,QACN,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM;AAAA,MACrC;AAAA,IACF,IACA,CAAC;AAAA;AAAA,IAEL,GAAG,iBAAiB,EACjB,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,MAAM,IAAI,CAAU,CAAC,CAAC,EAC7D,IAAI,CAAC,OAAO;AAAA,MACX,IAAI,EAAE;AAAA,MACN,OAAO,EAAE;AAAA,MACT,MAAM,EAAE;AAAA,MACR,KAAK,MAAM,OAAO,KAAK,EAAE,IAAI;AAAA,IAC/B,EAAE;AAAA,EACN;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAW;AAAA,MACX,WAAU;AAAA,MAET,kBAAQ;AAAA,QAAI,CAAC,EAAE,IAAI,OAAO,MAAM,IAAI,MACnC,OAAO,SACL;AAAA,UAAC;AAAA;AAAA,YAEC;AAAA,YACA,OAAM;AAAA,YACN,SAAS,CAAC,UACR,qBAAC,YAAO,MAAK,UAAU,GAAG,OAAO,WAAW,MAC1C;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAW;AAAA,kBACX,WAAU;AAAA;AAAA,cACZ;AAAA,cACC;AAAA,cACD,oBAAC,eAAY,eAAW,MAAC,WAAU,kBAAiB;AAAA,eACtD;AAAA,YAGF;AAAA,kCAAC,gBAAa,SAAS,KAAK,0BAAY;AAAA,cACxC;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,MAAM,OAAO,KAAK,MAAM,WAAW;AAAA,kBAC7C;AAAA;AAAA,cAED;AAAA;AAAA;AAAA,UAnBK;AAAA,QAoBP,IAEA,qBAAC,YAAgB,MAAK,UAAS,SAAS,KAAK,WAAW,MACtD;AAAA,8BAAC,QAAK,eAAW,MAAC,WAAU,sCAAqC;AAAA,UAChE;AAAA,aAFU,EAGb;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
/**
|
|
3
|
+
* A quick action a host adds under the Home search box, such as "Send money"
|
|
4
|
+
* from an engine this package knows nothing of. Registered once, where the
|
|
5
|
+
* host builds its dashboard layout, like `registerHostWidget`. Registering the
|
|
6
|
+
* same id again replaces it.
|
|
7
|
+
*/
|
|
8
|
+
export interface HostQuickAction {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
Icon: LucideIcon;
|
|
12
|
+
/** Where it goes. */
|
|
13
|
+
href: string;
|
|
14
|
+
/** Hide it from members who may not use it. Shown to everyone when absent. */
|
|
15
|
+
allowed?: (can: (permission: string) => boolean) => boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function registerHostQuickAction(action: HostQuickAction): void;
|
|
18
|
+
/** The host's quick actions, in the order they were registered. */
|
|
19
|
+
export declare function hostQuickActions(): HostQuickAction[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const HOST_ACTIONS = /* @__PURE__ */ new Map();
|
|
2
|
+
function registerHostQuickAction(action) {
|
|
3
|
+
HOST_ACTIONS.set(action.id, action);
|
|
4
|
+
}
|
|
5
|
+
function hostQuickActions() {
|
|
6
|
+
return [...HOST_ACTIONS.values()];
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
hostQuickActions,
|
|
10
|
+
registerHostQuickAction
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=host-quick-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/host-quick-actions.ts"],"sourcesContent":["import type { LucideIcon } from 'lucide-react';\n\n/**\n * A quick action a host adds under the Home search box, such as \"Send money\"\n * from an engine this package knows nothing of. Registered once, where the\n * host builds its dashboard layout, like `registerHostWidget`. Registering the\n * same id again replaces it.\n */\nexport interface HostQuickAction {\n id: string;\n label: string;\n Icon: LucideIcon;\n /** Where it goes. */\n href: string;\n /** Hide it from members who may not use it. Shown to everyone when absent. */\n allowed?: (can: (permission: string) => boolean) => boolean;\n}\n\nconst HOST_ACTIONS = new Map<string, HostQuickAction>();\n\nexport function registerHostQuickAction(action: HostQuickAction): void {\n HOST_ACTIONS.set(action.id, action);\n}\n\n/** The host's quick actions, in the order they were registered. */\nexport function hostQuickActions(): HostQuickAction[] {\n return [...HOST_ACTIONS.values()];\n}\n"],"mappings":"AAkBA,MAAM,eAAe,oBAAI,IAA6B;AAE/C,SAAS,wBAAwB,QAA+B;AACrE,eAAa,IAAI,OAAO,IAAI,MAAM;AACpC;AAGO,SAAS,mBAAsC;AACpD,SAAO,CAAC,GAAG,aAAa,OAAO,CAAC;AAClC;","names":[]}
|
|
@@ -138,7 +138,13 @@ function ActivityWidget({
|
|
|
138
138
|
] }),
|
|
139
139
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 pb-3", children: [
|
|
140
140
|
/* @__PURE__ */ jsxs("p", { className: "flex items-baseline justify-between gap-2", children: [
|
|
141
|
-
/* @__PURE__ */ jsx(
|
|
141
|
+
/* @__PURE__ */ jsx(
|
|
142
|
+
"span",
|
|
143
|
+
{
|
|
144
|
+
className: `min-w-0 truncate text-sm ${a.unpaid ? "text-[var(--text-tertiary)]" : "font-medium text-[var(--text-primary)]"}`,
|
|
145
|
+
children: a.title
|
|
146
|
+
}
|
|
147
|
+
),
|
|
142
148
|
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-2xs tabular-nums text-[var(--text-muted)]", children: timeLabel(a.timestamp) })
|
|
143
149
|
] }),
|
|
144
150
|
/* @__PURE__ */ jsx("p", { className: "truncate text-sm text-[var(--text-tertiary)]", children: a.description })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/dashboard/home/widgets/activity-widget.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport {\n Package,\n PencilLine,\n ShoppingBag,\n CircleCheck,\n UserPlus,\n UserCheck,\n RefreshCw,\n Receipt,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport type { ActivityItem } from '@withpotter/components/dashboard/activity-feed';\nimport type { NextUpItem } from '../next-up-sources';\nimport { Button } from '@withpotter/ui-kit';\nimport { Panel, PanelEmpty } from '../panel';\nimport { Badge } from '@withpotter/ui-kit';\n\n/**\n * What has happened, as a timeline.\n *\n * WHY IT EXISTS ALONGSIDE \"Today\". Those two cards answer different questions\n * and the home needs both: Today is what is WAITING on you, Activity is what\n * already went. Merging them was tried and is the mistake this replaces — a\n * single list with now as the pivot puts \"Tuesday 22:00, Tunde Ogunleye\" and\n * \"invoice paid 20 minutes ago\" in one column under one heading, so the reader\n * has to check each row's tense before they know whether it is a task.\n *\n * WHAT MAKES IT A TIMELINE RATHER THAN A LIST, and this is the depth the old\n * feed never had:\n *\n * A RAIL. A hairline runs behind the icon column and stops at the last node,\n * so the rows read as one sequence rather than as five unrelated rows that\n * happen to be stacked. It is the cheapest possible signal that these are\n * ordered in time and it is the one every activity feed worth copying has.\n *\n * TYPED, COLOURED NODES. `ActivityItem.type` already carries nine kinds and\n * nothing rendered them — the desktop feed dropped the emoji and drew\n * nothing in its place. Money is green, because money arriving is the one\n * event on this list a merchant scans for; everything else is neutral, so the\n * green means something. Colour that appears on every row means nothing.\n *\n * DAY HEADINGS. Cloudflare's Recents and Square's Tasks both fail here —\n * \"3d ago\" on row four with no break above it makes a five-row list look\n * like five things that happened this morning. A heading per day is what\n * turns a stack of timestamps into a history.\n *\n * The rows are links where the object still exists and plain rows where it does\n * not, rather than every row being a link to somewhere approximate.\n *\n * BLOCKERS RIDE ON TOP, and they have to, because this card replaced Today on\n * the default board and Today was carrying them. Losing them would have made\n * the home stop answering the second half of its own question — \"what do I do\n * next, AND is anything wrong?\" — which is the half a merchant cannot find\n * anywhere else.\n *\n * They are Square's ranked task rows: a title, one line of CONSEQUENCE, and a\n * real button with a verb on it. `NextUpItem` already carries all three\n * (`title`, `meta`, `action`), and the treatment is deliberately unlike the\n * timeline below — a blocker is not something that happened, it is something\n * that is still happening to you. Only the first gets a primary button, as on\n * Square: two primaries side by side is two things that are both the most\n * important thing.\n */\n\n/**\n * Nine kinds, nine marks. `emoji` is on the type and is deliberately not used:\n * it exists for the mobile feed, and an emoji at 13px in a dense column reads\n * as a smudge rather than as a symbol.\n */\nconst MARK: Record<\n ActivityItem['type'],\n { icon: LucideIcon; money?: boolean }\n> = {\n product_added: { icon: Package },\n product_updated: { icon: PencilLine },\n order_placed: { icon: ShoppingBag },\n order_completed: { icon: CircleCheck, money: true },\n member_signup: { icon: UserPlus },\n member_converted: { icon: UserCheck },\n subscription_created: { icon: RefreshCw, money: true },\n invoice_paid: { icon: Receipt, money: true },\n invoice_sent: { icon: Send },\n};\n\n/**\n * Which day a timestamp belongs to, in the reader's own words.\n *\n * Deliberately only three answers. \"Wednesday\" for something eight days old is\n * a puzzle, not a heading — past a week the date is the only honest label.\n */\nfunction dayLabel(iso: string): string {\n const d = new Date(iso);\n const today = new Date();\n const startOf = (x: Date) =>\n new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();\n const days = Math.round((startOf(today) - startOf(d)) / 86_400_000);\n if (days <= 0) return 'Today';\n if (days === 1) return 'Yesterday';\n if (days < 7) return d.toLocaleDateString(undefined, { weekday: 'long' });\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\nfunction timeLabel(iso: string): string {\n return new Date(iso).toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n });\n}\n\n/**\n * How overdue something is, in whole days, or null when it has no clock.\n *\n * Only for items carrying a real `at`. A blocker like \"set up payouts\" has no\n * due date and never gets a countdown — Square can say \"2 days until offline\n * payments are automatically allowed\" because a real policy deadline exists\n * behind it, and manufacturing an equivalent here would be inventing urgency.\n */\nfunction overdueLabel(item: NextUpItem): string | null {\n if (!item.overdue || !item.at) return null;\n const days = Math.floor(\n (Date.now() - new Date(item.at).getTime()) / 86_400_000,\n );\n if (days < 1) return 'due today';\n return `${days}d overdue`;\n}\n\n/**\n * The blockers the go-live dialog can actually resolve.\n *\n * Both are \"your store cannot sell\", and both are fixable in two or three\n * fields without leaving the page — so they open the dialog rather than\n * navigating to a settings screen. Anything else keeps its link.\n */\nconst FIXABLE_IN_PLACE = new Set(['alert-delivery', 'alert-payouts']);\n\nexport function ActivityWidget({\n items,\n blockers = [],\n limit,\n onFixBlocker,\n}: {\n items: ActivityItem[];\n /** Things stopping money. Pinned above the timeline; never sorted into it. */\n blockers?: NextUpItem[];\n /** Narrow cards hold fewer rows than wide ones. */\n limit: number;\n /**\n * Opens the go-live checks. Undefined on a surface that cannot host the\n * dialog, in which case every blocker falls back to its own href — which is\n * a real settings screen, not a dead end.\n */\n onFixBlocker?: () => void;\n}) {\n // Two at most. A card that opens with four chores is a to-do list wearing a\n // history's title, and the rest are still on the screens that own them.\n const blocked = blockers.slice(0, 2);\n // Blockers eat rows: the card is a fixed height and cannot grow to fit both.\n const shown = items.slice(0, Math.max(1, limit - blocked.length));\n\n const blockedBlock = blocked.length > 0 && (\n <ul className=\"mb-3 space-y-2 border-b border-[var(--border-subtle)] pb-3\">\n {blocked.map((b, i) => (\n <li key={b.id} className=\"flex items-start justify-between gap-3\">\n <span className=\"min-w-0\">\n <span className=\"flex items-center gap-2\">\n <span className=\"min-w-0 truncate text-sm font-medium text-[var(--text-primary)]\">\n {b.title}\n </span>\n {/* SQUARE'S `Required` PILL, and the flag was already on the data.\n `NextUpItem.required` has been set on the payouts blocker all\n along and nothing has ever rendered it, so the one task that\n genuinely stops money read exactly like the one that does not.\n A blocker list where every row looks equally urgent is a list\n with no ranking, whatever order it is sorted in. */}\n {b.required && (\n <Badge variant=\"error\" className=\"shrink-0\">\n Required\n </Badge>\n )}\n {/* HOW LONG, where the clock is real. Square's tasks carry a\n deadline — \"2 days until offline payments are automatically\n allowed\" — and ours carried none. The honest half of that is\n elapsed time on items that HAVE a date: an invoice knows when\n it was due. Blockers have no date and are given no countdown,\n because inventing one would be the fabrication this avoids. */}\n {overdueLabel(b) && (\n <span className=\"shrink-0 text-2xs font-medium tabular-nums text-[var(--error)]\">\n {overdueLabel(b)}\n </span>\n )}\n </span>\n {b.meta && (\n <span className=\"block truncate text-sm text-[var(--text-tertiary)]\">\n {b.meta}\n </span>\n )}\n </span>\n {onFixBlocker && FIXABLE_IN_PLACE.has(b.id) ? (\n <Button\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n onClick={onFixBlocker}\n >\n {b.action ?? 'Fix'}\n </Button>\n ) : (\n <Button\n asChild\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n >\n <Link href={b.href}>{b.action ?? 'Fix'}</Link>\n </Button>\n )}\n </li>\n ))}\n </ul>\n );\n\n if (shown.length === 0 || items.length === 0) {\n return (\n <Panel title=\"Recent activity\">\n {blockedBlock}\n <PanelEmpty\n title=\"Nothing has happened yet\"\n description=\"Orders, payments and new customers will appear here as they come in.\"\n />\n </Panel>\n );\n }\n\n /**\n * Day headings resolved BEFORE the JSX, by comparing each row to the one\n * above it, rather than by carrying a `lastDay` variable through the map.\n * A mutable binding written inside a render callback is exactly what the\n * immutability rule exists to catch: it survives across renders and makes\n * the output depend on how many times the list has been walked.\n */\n const rows = shown.map((item, i) => {\n const day = dayLabel(item.timestamp);\n return {\n item,\n day,\n showDay: i === 0 || day !== dayLabel(shown[i - 1].timestamp),\n last: i === shown.length - 1,\n };\n });\n\n return (\n <Panel\n title=\"Recent activity\"\n /* The card scrolls. A row cut off at its edge looked like a mistake, so the last 20px fades\n out and the card has that much empty padding: with every row in view the fade falls on\n nothing, and with more below, the half row dissolves instead of ending mid-sentence. */\n bodyClassName=\"overflow-y-auto pt-1 pb-5 [-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)] [mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)]\"\n >\n {blockedBlock}\n <ol className=\"relative\">\n {rows.map(({ item: a, day, showDay: newDay, last }) => {\n const { icon: Icon, money } = MARK[a.type] ?? MARK.order_placed;\n\n const row = (\n <div className=\"flex gap-3\">\n {/* The rail and the node. The line is drawn by the node's own\n ::before via a sibling span rather than as a border on the\n row, so it can stop at the last item instead of running into\n the card's bottom padding and looking cut off. */}\n <div className=\"relative flex w-5 shrink-0 justify-center\">\n {!last && (\n <span\n aria-hidden\n className=\"absolute left-1/2 top-5 h-[calc(100%-8px)] w-px -translate-x-1/2 bg-[var(--border-subtle)]\"\n />\n )}\n <span\n aria-hidden\n className={`relative z-10 mt-1 grid h-5 w-5 place-items-center rounded-full ${\n money\n ? 'bg-[var(--success-bg,var(--bg-muted))] text-[var(--success)]'\n : 'bg-[var(--bg-muted)] text-[var(--text-tertiary)]'\n }`}\n >\n <Icon className=\"h-[11px] w-[11px]\" strokeWidth={2} />\n </span>\n </div>\n\n <div className=\"min-w-0 flex-1 pb-3\">\n <p className=\"flex items-baseline justify-between gap-2\">\n <span className=\"min-w-0 truncate text-sm font-medium text-[var(--text-primary)]\">\n {a.title}\n </span>\n <span className=\"shrink-0 text-2xs tabular-nums text-[var(--text-muted)]\">\n {timeLabel(a.timestamp)}\n </span>\n </p>\n <p className=\"truncate text-sm text-[var(--text-tertiary)]\">\n {a.description}\n </p>\n </div>\n </div>\n );\n\n return (\n <li key={a.id}>\n {newDay && (\n <p className=\"pb-2 pt-1 text-2xs font-medium uppercase tracking-wide text-[var(--text-muted)] first:pt-0\">\n {day}\n </p>\n )}\n {a.href ? (\n <Link\n href={a.href}\n className=\"-mx-2 block rounded-lg px-2 transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {row}\n </Link>\n ) : (\n <div className=\"-mx-2 px-2\">{row}</div>\n )}\n </li>\n );\n })}\n </ol>\n </Panel>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/home/widgets/activity-widget.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport {\n Package,\n PencilLine,\n ShoppingBag,\n CircleCheck,\n UserPlus,\n UserCheck,\n RefreshCw,\n Receipt,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport type { ActivityItem } from '@withpotter/components/dashboard/activity-feed';\nimport type { NextUpItem } from '../next-up-sources';\nimport { Button } from '@withpotter/ui-kit';\nimport { Panel, PanelEmpty } from '../panel';\nimport { Badge } from '@withpotter/ui-kit';\n\n/**\n * What has happened, as a timeline.\n *\n * WHY IT EXISTS ALONGSIDE \"Today\". Those two cards answer different questions\n * and the home needs both: Today is what is WAITING on you, Activity is what\n * already went. Merging them was tried and is the mistake this replaces — a\n * single list with now as the pivot puts \"Tuesday 22:00, Tunde Ogunleye\" and\n * \"invoice paid 20 minutes ago\" in one column under one heading, so the reader\n * has to check each row's tense before they know whether it is a task.\n *\n * WHAT MAKES IT A TIMELINE RATHER THAN A LIST, and this is the depth the old\n * feed never had:\n *\n * A RAIL. A hairline runs behind the icon column and stops at the last node,\n * so the rows read as one sequence rather than as five unrelated rows that\n * happen to be stacked. It is the cheapest possible signal that these are\n * ordered in time and it is the one every activity feed worth copying has.\n *\n * TYPED, COLOURED NODES. `ActivityItem.type` already carries nine kinds and\n * nothing rendered them — the desktop feed dropped the emoji and drew\n * nothing in its place. Money is green, because money arriving is the one\n * event on this list a merchant scans for; everything else is neutral, so the\n * green means something. Colour that appears on every row means nothing.\n *\n * DAY HEADINGS. Cloudflare's Recents and Square's Tasks both fail here —\n * \"3d ago\" on row four with no break above it makes a five-row list look\n * like five things that happened this morning. A heading per day is what\n * turns a stack of timestamps into a history.\n *\n * The rows are links where the object still exists and plain rows where it does\n * not, rather than every row being a link to somewhere approximate.\n *\n * BLOCKERS RIDE ON TOP, and they have to, because this card replaced Today on\n * the default board and Today was carrying them. Losing them would have made\n * the home stop answering the second half of its own question — \"what do I do\n * next, AND is anything wrong?\" — which is the half a merchant cannot find\n * anywhere else.\n *\n * They are Square's ranked task rows: a title, one line of CONSEQUENCE, and a\n * real button with a verb on it. `NextUpItem` already carries all three\n * (`title`, `meta`, `action`), and the treatment is deliberately unlike the\n * timeline below — a blocker is not something that happened, it is something\n * that is still happening to you. Only the first gets a primary button, as on\n * Square: two primaries side by side is two things that are both the most\n * important thing.\n */\n\n/**\n * Nine kinds, nine marks. `emoji` is on the type and is deliberately not used:\n * it exists for the mobile feed, and an emoji at 13px in a dense column reads\n * as a smudge rather than as a symbol.\n */\nconst MARK: Record<\n ActivityItem['type'],\n { icon: LucideIcon; money?: boolean }\n> = {\n product_added: { icon: Package },\n product_updated: { icon: PencilLine },\n order_placed: { icon: ShoppingBag },\n order_completed: { icon: CircleCheck, money: true },\n member_signup: { icon: UserPlus },\n member_converted: { icon: UserCheck },\n subscription_created: { icon: RefreshCw, money: true },\n invoice_paid: { icon: Receipt, money: true },\n invoice_sent: { icon: Send },\n};\n\n/**\n * Which day a timestamp belongs to, in the reader's own words.\n *\n * Deliberately only three answers. \"Wednesday\" for something eight days old is\n * a puzzle, not a heading — past a week the date is the only honest label.\n */\nfunction dayLabel(iso: string): string {\n const d = new Date(iso);\n const today = new Date();\n const startOf = (x: Date) =>\n new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();\n const days = Math.round((startOf(today) - startOf(d)) / 86_400_000);\n if (days <= 0) return 'Today';\n if (days === 1) return 'Yesterday';\n if (days < 7) return d.toLocaleDateString(undefined, { weekday: 'long' });\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\nfunction timeLabel(iso: string): string {\n return new Date(iso).toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n });\n}\n\n/**\n * How overdue something is, in whole days, or null when it has no clock.\n *\n * Only for items carrying a real `at`. A blocker like \"set up payouts\" has no\n * due date and never gets a countdown — Square can say \"2 days until offline\n * payments are automatically allowed\" because a real policy deadline exists\n * behind it, and manufacturing an equivalent here would be inventing urgency.\n */\nfunction overdueLabel(item: NextUpItem): string | null {\n if (!item.overdue || !item.at) return null;\n const days = Math.floor(\n (Date.now() - new Date(item.at).getTime()) / 86_400_000,\n );\n if (days < 1) return 'due today';\n return `${days}d overdue`;\n}\n\n/**\n * The blockers the go-live dialog can actually resolve.\n *\n * Both are \"your store cannot sell\", and both are fixable in two or three\n * fields without leaving the page — so they open the dialog rather than\n * navigating to a settings screen. Anything else keeps its link.\n */\nconst FIXABLE_IN_PLACE = new Set(['alert-delivery', 'alert-payouts']);\n\nexport function ActivityWidget({\n items,\n blockers = [],\n limit,\n onFixBlocker,\n}: {\n items: ActivityItem[];\n /** Things stopping money. Pinned above the timeline; never sorted into it. */\n blockers?: NextUpItem[];\n /** Narrow cards hold fewer rows than wide ones. */\n limit: number;\n /**\n * Opens the go-live checks. Undefined on a surface that cannot host the\n * dialog, in which case every blocker falls back to its own href — which is\n * a real settings screen, not a dead end.\n */\n onFixBlocker?: () => void;\n}) {\n // Two at most. A card that opens with four chores is a to-do list wearing a\n // history's title, and the rest are still on the screens that own them.\n const blocked = blockers.slice(0, 2);\n // Blockers eat rows: the card is a fixed height and cannot grow to fit both.\n const shown = items.slice(0, Math.max(1, limit - blocked.length));\n\n const blockedBlock = blocked.length > 0 && (\n <ul className=\"mb-3 space-y-2 border-b border-[var(--border-subtle)] pb-3\">\n {blocked.map((b, i) => (\n <li key={b.id} className=\"flex items-start justify-between gap-3\">\n <span className=\"min-w-0\">\n <span className=\"flex items-center gap-2\">\n <span className=\"min-w-0 truncate text-sm font-medium text-[var(--text-primary)]\">\n {b.title}\n </span>\n {/* SQUARE'S `Required` PILL, and the flag was already on the data.\n `NextUpItem.required` has been set on the payouts blocker all\n along and nothing has ever rendered it, so the one task that\n genuinely stops money read exactly like the one that does not.\n A blocker list where every row looks equally urgent is a list\n with no ranking, whatever order it is sorted in. */}\n {b.required && (\n <Badge variant=\"error\" className=\"shrink-0\">\n Required\n </Badge>\n )}\n {/* HOW LONG, where the clock is real. Square's tasks carry a\n deadline — \"2 days until offline payments are automatically\n allowed\" — and ours carried none. The honest half of that is\n elapsed time on items that HAVE a date: an invoice knows when\n it was due. Blockers have no date and are given no countdown,\n because inventing one would be the fabrication this avoids. */}\n {overdueLabel(b) && (\n <span className=\"shrink-0 text-2xs font-medium tabular-nums text-[var(--error)]\">\n {overdueLabel(b)}\n </span>\n )}\n </span>\n {b.meta && (\n <span className=\"block truncate text-sm text-[var(--text-tertiary)]\">\n {b.meta}\n </span>\n )}\n </span>\n {onFixBlocker && FIXABLE_IN_PLACE.has(b.id) ? (\n <Button\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n onClick={onFixBlocker}\n >\n {b.action ?? 'Fix'}\n </Button>\n ) : (\n <Button\n asChild\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n >\n <Link href={b.href}>{b.action ?? 'Fix'}</Link>\n </Button>\n )}\n </li>\n ))}\n </ul>\n );\n\n if (shown.length === 0 || items.length === 0) {\n return (\n <Panel title=\"Recent activity\">\n {blockedBlock}\n <PanelEmpty\n title=\"Nothing has happened yet\"\n description=\"Orders, payments and new customers will appear here as they come in.\"\n />\n </Panel>\n );\n }\n\n /**\n * Day headings resolved BEFORE the JSX, by comparing each row to the one\n * above it, rather than by carrying a `lastDay` variable through the map.\n * A mutable binding written inside a render callback is exactly what the\n * immutability rule exists to catch: it survives across renders and makes\n * the output depend on how many times the list has been walked.\n */\n const rows = shown.map((item, i) => {\n const day = dayLabel(item.timestamp);\n return {\n item,\n day,\n showDay: i === 0 || day !== dayLabel(shown[i - 1].timestamp),\n last: i === shown.length - 1,\n };\n });\n\n return (\n <Panel\n title=\"Recent activity\"\n /* The card scrolls. A row cut off at its edge looked like a mistake, so the last 20px fades\n out and the card has that much empty padding: with every row in view the fade falls on\n nothing, and with more below, the half row dissolves instead of ending mid-sentence. */\n bodyClassName=\"overflow-y-auto pt-1 pb-5 [-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)] [mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)]\"\n >\n {blockedBlock}\n <ol className=\"relative\">\n {rows.map(({ item: a, day, showDay: newDay, last }) => {\n const { icon: Icon, money } = MARK[a.type] ?? MARK.order_placed;\n\n const row = (\n <div className=\"flex gap-3\">\n {/* The rail and the node. The line is drawn by the node's own\n ::before via a sibling span rather than as a border on the\n row, so it can stop at the last item instead of running into\n the card's bottom padding and looking cut off. */}\n <div className=\"relative flex w-5 shrink-0 justify-center\">\n {!last && (\n <span\n aria-hidden\n className=\"absolute left-1/2 top-5 h-[calc(100%-8px)] w-px -translate-x-1/2 bg-[var(--border-subtle)]\"\n />\n )}\n <span\n aria-hidden\n className={`relative z-10 mt-1 grid h-5 w-5 place-items-center rounded-full ${\n money\n ? 'bg-[var(--success-bg,var(--bg-muted))] text-[var(--success)]'\n : 'bg-[var(--bg-muted)] text-[var(--text-tertiary)]'\n }`}\n >\n <Icon className=\"h-[11px] w-[11px]\" strokeWidth={2} />\n </span>\n </div>\n\n <div className=\"min-w-0 flex-1 pb-3\">\n <p className=\"flex items-baseline justify-between gap-2\">\n <span\n className={`min-w-0 truncate text-sm ${\n a.unpaid\n ? 'text-[var(--text-tertiary)]'\n : 'font-medium text-[var(--text-primary)]'\n }`}\n >\n {a.title}\n </span>\n <span className=\"shrink-0 text-2xs tabular-nums text-[var(--text-muted)]\">\n {timeLabel(a.timestamp)}\n </span>\n </p>\n <p className=\"truncate text-sm text-[var(--text-tertiary)]\">\n {a.description}\n </p>\n </div>\n </div>\n );\n\n return (\n <li key={a.id}>\n {newDay && (\n <p className=\"pb-2 pt-1 text-2xs font-medium uppercase tracking-wide text-[var(--text-muted)] first:pt-0\">\n {day}\n </p>\n )}\n {a.href ? (\n <Link\n href={a.href}\n className=\"-mx-2 block rounded-lg px-2 transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {row}\n </Link>\n ) : (\n <div className=\"-mx-2 px-2\">{row}</div>\n )}\n </li>\n );\n })}\n </ol>\n </Panel>\n );\n}\n"],"mappings":"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","names":[]}
|