@withpotter/components 0.13.0-next.82 → 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 +56 -43
- package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
- package/dist/dashboard/home/widgets/home-widgets.js +3 -2
- package/dist/dashboard/home/widgets/home-widgets.js.map +1 -1
- package/dist/dashboard/home/widgets/host-widgets.d.ts +6 -0
- package/dist/dashboard/home/widgets/host-widgets.js +16 -0
- package/dist/dashboard/home/widgets/host-widgets.js.map +1 -0
- package/dist/dashboard/home/widgets/member-label.d.ts +11 -0
- package/dist/dashboard/home/widgets/member-label.js +10 -0
- package/dist/dashboard/home/widgets/member-label.js.map +1 -0
- package/dist/dashboard/home/widgets/middle-ellipsis.d.ts +5 -0
- package/dist/dashboard/home/widgets/middle-ellipsis.js +11 -0
- package/dist/dashboard/home/widgets/middle-ellipsis.js.map +1 -0
- package/dist/dashboard/home/widgets/money-widgets.js +9 -4
- package/dist/dashboard/home/widgets/money-widgets.js.map +1 -1
- package/dist/dashboard/home/widgets/registry.d.ts +6 -3
- package/dist/dashboard/home/widgets/registry.js +12 -2
- package/dist/dashboard/home/widgets/registry.js.map +1 -1
- package/dist/dashboard/home/widgets/revenue-series.d.ts +12 -0
- package/dist/dashboard/home/widgets/revenue-series.js +26 -0
- package/dist/dashboard/home/widgets/revenue-series.js.map +1 -0
- package/dist/dashboard/home/widgets/team-widget.js +2 -1
- package/dist/dashboard/home/widgets/team-widget.js.map +1 -1
- package/dist/dashboard/home/widgets/types.d.ts +13 -1
- package/dist/dashboard/home/widgets/types.js.map +1 -1
- package/dist/dashboard/home/widgets/use-home-layout.js +9 -4
- package/dist/dashboard/home/widgets/use-home-layout.js.map +1 -1
- package/dist/dashboard/home/widgets/website-widget.js +9 -1
- package/dist/dashboard/home/widgets/website-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 +4 -4
|
@@ -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 title=\"Recent activity\" bodyClassName=\"overflow-y-auto pt-1\">\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":[]}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "motion/react";
|
|
10
10
|
import { WIDGET_HEIGHT } from "./types";
|
|
11
11
|
import { eases } from "@withpotter/lib/motion";
|
|
12
|
-
import {
|
|
12
|
+
import { widgetDef, widgetsFor, coveredHrefs } from "./registry";
|
|
13
13
|
import { WidgetFrame } from "./widget-frame";
|
|
14
14
|
import { WidgetGallery } from "./widget-gallery";
|
|
15
15
|
import { WidgetEmptyState } from "./widget-empty";
|
|
@@ -153,7 +153,8 @@ function HomeWidgets({
|
|
|
153
153
|
layout: settled,
|
|
154
154
|
className: "grid grid-cols-1 gap-4 md:grid-cols-6",
|
|
155
155
|
children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "popLayout", initial: false, children: widgets.map((w, index) => {
|
|
156
|
-
const def =
|
|
156
|
+
const def = widgetDef(w.id);
|
|
157
|
+
if (!def) return null;
|
|
157
158
|
const zone = dragging && dragging !== w.id && overIndex === index ? /* @__PURE__ */ jsx(
|
|
158
159
|
motion.div,
|
|
159
160
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/dashboard/home/widgets/home-widgets.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport {\n AnimatePresence,\n motion,\n useDragControls,\n useReducedMotion,\n} from 'motion/react';\nimport { WIDGET_HEIGHT } from './types';\nimport { eases } from '@withpotter/lib/motion';\nimport { WIDGETS, widgetsFor, coveredHrefs } from './registry';\nimport { WidgetFrame } from './widget-frame';\nimport { WidgetGallery } from './widget-gallery';\nimport { WidgetEmptyState } from './widget-empty';\nimport { useHomeLayout, sizeForIndex } from './use-home-layout';\nimport { MoneyBoardProvider } from './money-context';\nimport { useQuietRegulars } from './regulars-widget';\nimport type { WidgetContext, WidgetDef, WidgetId } from './types';\n\n/**\n * The board.\n *\n * REMOVAL, ported from Shopify's numbers. Theirs is\n * `transition: height .35s, transform .35s, box-shadow .35s` on\n * `cubic-bezier(0.34, 1.8, 0.64, 1)` — a back-out with a real overshoot, with\n * the NEIGHBOURS moving on the identical curve so the grid resettles instead of\n * snapping shut. We already ship that curve as `eases.overshoot`\n * (0.34, 1.56, 0.64, 1); it is fractionally gentler than theirs and it is ours,\n * so no second version of the same idea enters the system.\n *\n * `mode=\"popLayout\"` is what buys the neighbours' half of it: the leaving widget\n * is pulled out of flow immediately, so every `layout` sibling animates to its\n * new position over the same 350ms rather than waiting for the exit to finish.\n * The exit itself is Shopify's `_fade-out-shrink`, verbatim: opacity to 0,\n * `translateY(-1.25rem)`, `scale(0.9)`.\n *\n * Entrance is their `_card-entrance`: 8px rise and a fade, so a widget added\n * from the gallery arrives the way the board's own widgets do.\n *\n * BLOCKED IS NOT HERE. It renders above this component, always, and cannot be\n * removed or resized. iOS widgets are glanceable and read-only, which is a good\n * model for \"how are we doing\" and a bad one for \"what is stopping me\"; if the\n * whole page becomes arrangeable, the home quietly turns back into the report\n * it was taken apart from. Pinning the blockers is what earns everything below\n * them the freedom to move.\n */\n/**\n * One card on the board, and everything about picking it up.\n *\n * IT IS ITS OWN COMPONENT SO IT CAN HOLD A TIMER. Hold-to-lift needs\n * `useDragControls` and a pending timeout per card, and hooks cannot live\n * inside a `.map` callback — which is the only reason this is not inline.\n *\n * HOLD TO LIFT, ~140ms. A card used to detach the instant the pointer moved,\n * which made every click inside one a coin toss: brush the pointer while\n * pressing a link and you were dragging instead of following it. A short press\n * before it comes loose makes the pickup deliberate, and it is the same\n * gesture every phone home screen already teaches.\n *\n * The timer is cancelled by pointer-up and by leaving the card, so a quick\n * click never arms it — and `dragListener={false}` means Framer starts nothing\n * on its own; the hold is the only way in.\n */\nfunction BoardCard({\n id,\n span,\n dimmed,\n disabled,\n animateLayout,\n onLift,\n onDrag,\n onDrop,\n children,\n}: {\n id: WidgetId;\n span: string;\n /** False until the board has assembled — see `settled` in HomeWidgets. */\n animateLayout: boolean;\n /** Every card except the one in hand fades while a drag is running. */\n dimmed: boolean;\n disabled: boolean;\n onLift: () => void;\n onDrag: () => void;\n onDrop: () => void;\n children: ReactNode;\n}) {\n const controls = useDragControls();\n const holdRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const cancelHold = () => {\n if (holdRef.current) clearTimeout(holdRef.current);\n holdRef.current = null;\n };\n\n return (\n <motion.div\n data-widget={id}\n layout={animateLayout}\n initial={false}\n animate={{ opacity: dimmed ? 0.6 : 1 }}\n exit={{ opacity: 0, y: -20, scale: 0.9 }}\n /**\n * LAND, DO NOT STOP. `layout` carries the card into its new slot on the\n * board's own overshoot curve, so it settles rather than arriving —\n * `dragSnapToOrigin` returns the element while this animates the slot,\n * and the two together read as one movement.\n */\n transition={{\n layout: { duration: 0.35, ease: eases.overshoot },\n opacity: { duration: 0.2, ease: eases.out },\n }}\n drag={!disabled}\n dragListener={false}\n dragControls={controls}\n dragSnapToOrigin\n dragElastic={0.12}\n dragMomentum={false}\n onPointerDown={(e) => {\n if (disabled) return;\n const ev = e;\n cancelHold();\n holdRef.current = setTimeout(() => {\n onLift();\n controls.start(ev);\n }, 140);\n }}\n onPointerUp={cancelHold}\n onPointerLeave={cancelHold}\n onDrag={onDrag}\n onDragEnd={() => {\n cancelHold();\n onDrop();\n }}\n whileDrag={{\n // Lifted out of the page and out of its own row's stacking order, or\n // the neighbours paint over the card being carried.\n zIndex: 40,\n scale: 1.02,\n boxShadow: 'var(--shadow-xl)',\n cursor: 'grabbing',\n }}\n className={span}\n >\n {children}\n </motion.div>\n );\n}\n\nexport function HomeWidgets({\n ctx: pageCtx,\n accountId,\n tenantSlug,\n galleryOpen,\n onGalleryOpenChange,\n}: {\n ctx: WidgetContext;\n accountId?: string;\n tenantSlug?: string;\n /**\n * The gallery is opened from the PAGE HEADER now, not from a tile in the\n * grid. Adding a widget is a control and the header is where this app puts\n * controls; a dashed cell sitting inside the board it edits was the odd one\n * out, and it also cost a slot that a real card now uses.\n */\n galleryOpen: boolean;\n onGalleryOpenChange: (open: boolean) => void;\n}) {\n const reduceMotion = useReducedMotion() ?? false;\n // The one thing the board adds to what the page resolved: whether the Gone\n // quiet card has anybody to name. Asked here, from the card's own list, so it\n // can gate the card's slot; a card that decided this for itself could only\n // leave an empty frame behind.\n const { quiet } = useQuietRegulars();\n const ctx: WidgetContext = {\n ...pageCtx,\n hasQuietRegulars: quiet ? quiet.length > 0 : undefined,\n };\n const { widgets, placed, add, move } = useHomeLayout(\n accountId,\n tenantSlug,\n ctx,\n );\n\n /**\n * NOTHING ANIMATES UNTIL THE BOARD HAS FINISHED ASSEMBLING.\n *\n * John, 2026-08-23: \"why is it that when i open the home the gone quiet card\n * flys, it shouldn't be flying.\"\n *\n * `layout` on a card is not an entrance — `initial={false}` already suppresses\n * that — it is a promise to animate to any NEW position. On a cold open the\n * board's composition is still resolving: the saved arrangement arrives from\n * storage, permissions land and decide whether Team is available at all, and\n * the setup checklist arrives and decides whether that card exists. Every one\n * of those re-flows the grid, and `layout` faithfully flew the cards to their\n * new slots on a 350ms overshoot — which is right after a drag and reads as a\n * glitch on arrival, because nobody asked for it.\n *\n * So the board earns its motion. One frame after mount the arrangement is\n * settled and `layout` switches on, which is when it starts being about the\n * merchant moving something rather than about data landing. A drag, a removal\n * and an add all still animate exactly as before; those all happen long after\n * this flips.\n */\n const [settled, setSettled] = useState(false);\n useEffect(() => {\n // Two frames: the first paints the resolved arrangement, the second is the\n // earliest moment at which a further change is genuinely a change.\n const id = requestAnimationFrame(() =>\n requestAnimationFrame(() => setSettled(true)),\n );\n return () => cancelAnimationFrame(id);\n }, []);\n\n const candidates: WidgetDef[] = widgetsFor(ctx).filter(\n (d) => !placed.has(d.id),\n );\n\n /**\n * The context each widget actually renders from, with the blockers already\n * shown elsewhere on the board taken out.\n *\n * The home kept saying things twice: \"Set up payouts\" was a blocker pinned\n * above Activity AND the entire Revenue card; \"3 products low on stock\" was a\n * blocker AND the Low stock widget. Resolving it HERE rather than inside\n * Activity is what keeps it honest — this is the only place that knows which\n * widgets are actually placed, and a chore covered by a widget the merchant\n * has since removed is not covered at all.\n */\n const covered = coveredHrefs(ctx, placed);\n const boardCtx: WidgetContext =\n covered.size === 0\n ? ctx\n : { ...ctx, today: ctx.today.filter((i) => !covered.has(i.href)) };\n\n /**\n * Which card is in the hand, and where every card sits.\n *\n * The rects are a ref rather than state: they are read during a drag to work\n * out what the pointer is over, and putting them in state would re-render the\n * whole board on every pointer move to compute a number only the drag reads.\n * They are measured on pick-up, which is the last moment they are true — a\n * card dropped earlier may have changed the layout since.\n */\n const [dragging, setDragging] = useState<WidgetId | null>(null);\n /**\n * The slot the card would land in, and the ONLY thing a pointer move changes.\n *\n * The first version reordered and PERSISTED on every `onDrag` tick, which\n * wrote to localStorage sixty times a second and re-rendered the board under\n * the gesture that was driving it — so the cards it was meant to shuffle sat\n * still, because the drag kept being interrupted by its own side effects.\n * Nothing commits until the drop; until then this index is all that moves.\n */\n const [overIndex, setOverIndex] = useState<number | null>(null);\n const rects = useRef<{ id: WidgetId; cx: number; cy: number }[]>([]);\n\n const measure = () => {\n rects.current = widgets.flatMap((w) => {\n const el = document.querySelector<HTMLElement>(`[data-widget=\"${w.id}\"]`);\n if (!el) return [];\n const r = el.getBoundingClientRect();\n return [{ id: w.id, cx: r.left + r.width / 2, cy: r.top + r.height / 2 }];\n });\n };\n\n /**\n * The card whose centre is nearest the pointer, which is what \"drop here\"\n * means on a grid with two different card widths.\n *\n * Nearest-centre rather than \"which rect contains the pointer\": a wide card\n * dragged over the trio row is bigger than the slot under it, so hit-testing\n * containment leaves gaps between cards where nothing is targeted and the\n * drop silently does nothing.\n */\n /**\n * Where the dragged card is NOW, read off the element itself.\n *\n * This replaced `info.point`, which was the bug behind drops that silently\n * did nothing: Framer reports the pointer in PAGE coordinates while\n * `getBoundingClientRect` returns VIEWPORT ones, and the dashboard scrolls\n * inside an inner container — so the moment the page was scrolled the two\n * spaces diverged and every drop hit-tested against the wrong row, usually\n * resolving to the card's own slot.\n *\n * The element's own rect is in the same space as every other rect by\n * construction, so there is no space to get wrong. It is also the more\n * honest question: what matters is where the CARD is, not where the cursor\n * happens to be inside it.\n */\n const centreOf = (id: WidgetId): { x: number; y: number } | null => {\n const el = document.querySelector<HTMLElement>(`[data-widget=\"${id}\"]`);\n if (!el) return null;\n const r = el.getBoundingClientRect();\n return { x: r.left + r.width / 2, y: r.top + r.height / 2 };\n };\n\n const targetAt = (x: number, y: number): WidgetId | null => {\n let best: WidgetId | null = null;\n let bestDist = Infinity;\n for (const r of rects.current) {\n const d = (r.cx - x) ** 2 + (r.cy - y) ** 2;\n if (d < bestDist) {\n bestDist = d;\n best = r.id;\n }\n }\n return best;\n };\n\n return (\n // Wraps the gallery too: a preview showing different figures from the board\n // it previews would be worse than no preview.\n <MoneyBoardProvider\n demo={\n ctx.demo ? { data: ctx.demo.data, prior: ctx.demo.prior } : undefined\n }\n >\n {/* SIX COLUMNS, 20px gap — Shopify's grid, measured. A `wide` card spans\n three of them and two fill a row; a `narrow` spans two and three fill\n a row. Everything collapses to one column below `md`, because 146px\n columns are not a phone layout. */}\n <motion.div\n layout={settled}\n className=\"grid grid-cols-1 gap-4 md:grid-cols-6\"\n >\n <AnimatePresence mode=\"popLayout\" initial={false}>\n {widgets.map((w, index) => {\n const def = WIDGETS[w.id];\n /**\n * THE DROP ZONE, and it is a real grid cell rather than an outline\n * painted over one.\n *\n * Being in the flow is the whole point: inserting it PUSHES the\n * cards after it along, so the board visibly opens a gap where the\n * card will land. An absolutely-positioned overlay would have shown\n * the target without moving anything, which is the complaint it\n * exists to answer — the other widgets sitting still while you hold\n * one over them.\n *\n * It takes the size of the slot it sits in, not of the card being\n * carried, because that is what the card will BECOME once dropped:\n * drag a narrow card into the top row and the zone is wide, which\n * is the rule made visible before you commit to it.\n */\n const zone =\n dragging && dragging !== w.id && overIndex === index ? (\n <motion.div\n key=\"dropzone\"\n layout\n initial={{ opacity: 0, scale: 0.96 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.96 }}\n transition={{ duration: 0.16, ease: eases.out }}\n style={{ height: WIDGET_HEIGHT }}\n className={`${\n sizeForIndex(index) === 'wide'\n ? 'md:col-span-3'\n : 'md:col-span-2'\n } dropzone-breathe rounded-[var(--radius-card)] border-2 border-dashed border-brand-500 bg-brand-500/5`}\n />\n ) : null;\n\n return (\n <Fragment key={w.id}>\n {zone}\n <BoardCard\n id={w.id}\n animateLayout={settled}\n span={w.size === 'wide' ? 'md:col-span-3' : 'md:col-span-2'}\n dimmed={!!dragging && dragging !== w.id && !reduceMotion}\n disabled={reduceMotion}\n onLift={() => {\n measure();\n setDragging(w.id);\n setOverIndex(null);\n }}\n onDrag={() => {\n const c = centreOf(w.id);\n if (!c) return;\n const overId = targetAt(c.x, c.y);\n if (!overId || overId === w.id) return;\n const to = widgets.findIndex((x) => x.id === overId);\n if (to !== -1) setOverIndex(to);\n }}\n onDrop={() => {\n const c = centreOf(w.id);\n const overId = c ? targetAt(c.x, c.y) : null;\n const to =\n overId && overId !== w.id\n ? widgets.findIndex((x) => x.id === overId)\n : overIndex;\n setDragging(null);\n setOverIndex(null);\n if (to != null && to !== -1) move(w.id, to);\n }}\n >\n <WidgetFrame\n name={def.name}\n onMove={(delta) =>\n move(\n w.id,\n widgets.findIndex((x) => x.id === w.id) + delta,\n )\n }\n >\n {/* A widget with nothing yet shows its own empty state, which\n is where setup now lives: the box you fix is the box that\n then fills with your data. */}\n {def.empty?.(boardCtx) ? (\n <WidgetEmptyState empty={def.empty(boardCtx)!} />\n ) : (\n def.render(boardCtx, w.size)\n )}\n </WidgetFrame>\n </BoardCard>\n </Fragment>\n );\n })}\n </AnimatePresence>\n </motion.div>\n\n <WidgetGallery\n open={galleryOpen}\n onOpenChange={onGalleryOpenChange}\n candidates={candidates}\n ctx={ctx}\n onAdd={(def, size) => add(def.id, size)}\n />\n </MoneyBoardProvider>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/home/widgets/home-widgets.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport {\n AnimatePresence,\n motion,\n useDragControls,\n useReducedMotion,\n} from 'motion/react';\nimport { WIDGET_HEIGHT } from './types';\nimport { eases } from '@withpotter/lib/motion';\nimport { widgetDef, widgetsFor, coveredHrefs } from './registry';\nimport { WidgetFrame } from './widget-frame';\nimport { WidgetGallery } from './widget-gallery';\nimport { WidgetEmptyState } from './widget-empty';\nimport { useHomeLayout, sizeForIndex } from './use-home-layout';\nimport { MoneyBoardProvider } from './money-context';\nimport { useQuietRegulars } from './regulars-widget';\nimport type { WidgetContext, WidgetDef, WidgetId } from './types';\n\n/**\n * The board.\n *\n * REMOVAL, ported from Shopify's numbers. Theirs is\n * `transition: height .35s, transform .35s, box-shadow .35s` on\n * `cubic-bezier(0.34, 1.8, 0.64, 1)` — a back-out with a real overshoot, with\n * the NEIGHBOURS moving on the identical curve so the grid resettles instead of\n * snapping shut. We already ship that curve as `eases.overshoot`\n * (0.34, 1.56, 0.64, 1); it is fractionally gentler than theirs and it is ours,\n * so no second version of the same idea enters the system.\n *\n * `mode=\"popLayout\"` is what buys the neighbours' half of it: the leaving widget\n * is pulled out of flow immediately, so every `layout` sibling animates to its\n * new position over the same 350ms rather than waiting for the exit to finish.\n * The exit itself is Shopify's `_fade-out-shrink`, verbatim: opacity to 0,\n * `translateY(-1.25rem)`, `scale(0.9)`.\n *\n * Entrance is their `_card-entrance`: 8px rise and a fade, so a widget added\n * from the gallery arrives the way the board's own widgets do.\n *\n * BLOCKED IS NOT HERE. It renders above this component, always, and cannot be\n * removed or resized. iOS widgets are glanceable and read-only, which is a good\n * model for \"how are we doing\" and a bad one for \"what is stopping me\"; if the\n * whole page becomes arrangeable, the home quietly turns back into the report\n * it was taken apart from. Pinning the blockers is what earns everything below\n * them the freedom to move.\n */\n/**\n * One card on the board, and everything about picking it up.\n *\n * IT IS ITS OWN COMPONENT SO IT CAN HOLD A TIMER. Hold-to-lift needs\n * `useDragControls` and a pending timeout per card, and hooks cannot live\n * inside a `.map` callback — which is the only reason this is not inline.\n *\n * HOLD TO LIFT, ~140ms. A card used to detach the instant the pointer moved,\n * which made every click inside one a coin toss: brush the pointer while\n * pressing a link and you were dragging instead of following it. A short press\n * before it comes loose makes the pickup deliberate, and it is the same\n * gesture every phone home screen already teaches.\n *\n * The timer is cancelled by pointer-up and by leaving the card, so a quick\n * click never arms it — and `dragListener={false}` means Framer starts nothing\n * on its own; the hold is the only way in.\n */\nfunction BoardCard({\n id,\n span,\n dimmed,\n disabled,\n animateLayout,\n onLift,\n onDrag,\n onDrop,\n children,\n}: {\n id: WidgetId;\n span: string;\n /** False until the board has assembled — see `settled` in HomeWidgets. */\n animateLayout: boolean;\n /** Every card except the one in hand fades while a drag is running. */\n dimmed: boolean;\n disabled: boolean;\n onLift: () => void;\n onDrag: () => void;\n onDrop: () => void;\n children: ReactNode;\n}) {\n const controls = useDragControls();\n const holdRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const cancelHold = () => {\n if (holdRef.current) clearTimeout(holdRef.current);\n holdRef.current = null;\n };\n\n return (\n <motion.div\n data-widget={id}\n layout={animateLayout}\n initial={false}\n animate={{ opacity: dimmed ? 0.6 : 1 }}\n exit={{ opacity: 0, y: -20, scale: 0.9 }}\n /**\n * LAND, DO NOT STOP. `layout` carries the card into its new slot on the\n * board's own overshoot curve, so it settles rather than arriving —\n * `dragSnapToOrigin` returns the element while this animates the slot,\n * and the two together read as one movement.\n */\n transition={{\n layout: { duration: 0.35, ease: eases.overshoot },\n opacity: { duration: 0.2, ease: eases.out },\n }}\n drag={!disabled}\n dragListener={false}\n dragControls={controls}\n dragSnapToOrigin\n dragElastic={0.12}\n dragMomentum={false}\n onPointerDown={(e) => {\n if (disabled) return;\n const ev = e;\n cancelHold();\n holdRef.current = setTimeout(() => {\n onLift();\n controls.start(ev);\n }, 140);\n }}\n onPointerUp={cancelHold}\n onPointerLeave={cancelHold}\n onDrag={onDrag}\n onDragEnd={() => {\n cancelHold();\n onDrop();\n }}\n whileDrag={{\n // Lifted out of the page and out of its own row's stacking order, or\n // the neighbours paint over the card being carried.\n zIndex: 40,\n scale: 1.02,\n boxShadow: 'var(--shadow-xl)',\n cursor: 'grabbing',\n }}\n className={span}\n >\n {children}\n </motion.div>\n );\n}\n\nexport function HomeWidgets({\n ctx: pageCtx,\n accountId,\n tenantSlug,\n galleryOpen,\n onGalleryOpenChange,\n}: {\n ctx: WidgetContext;\n accountId?: string;\n tenantSlug?: string;\n /**\n * The gallery is opened from the PAGE HEADER now, not from a tile in the\n * grid. Adding a widget is a control and the header is where this app puts\n * controls; a dashed cell sitting inside the board it edits was the odd one\n * out, and it also cost a slot that a real card now uses.\n */\n galleryOpen: boolean;\n onGalleryOpenChange: (open: boolean) => void;\n}) {\n const reduceMotion = useReducedMotion() ?? false;\n // The one thing the board adds to what the page resolved: whether the Gone\n // quiet card has anybody to name. Asked here, from the card's own list, so it\n // can gate the card's slot; a card that decided this for itself could only\n // leave an empty frame behind.\n const { quiet } = useQuietRegulars();\n const ctx: WidgetContext = {\n ...pageCtx,\n hasQuietRegulars: quiet ? quiet.length > 0 : undefined,\n };\n const { widgets, placed, add, move } = useHomeLayout(\n accountId,\n tenantSlug,\n ctx,\n );\n\n /**\n * NOTHING ANIMATES UNTIL THE BOARD HAS FINISHED ASSEMBLING.\n *\n * John, 2026-08-23: \"why is it that when i open the home the gone quiet card\n * flys, it shouldn't be flying.\"\n *\n * `layout` on a card is not an entrance — `initial={false}` already suppresses\n * that — it is a promise to animate to any NEW position. On a cold open the\n * board's composition is still resolving: the saved arrangement arrives from\n * storage, permissions land and decide whether Team is available at all, and\n * the setup checklist arrives and decides whether that card exists. Every one\n * of those re-flows the grid, and `layout` faithfully flew the cards to their\n * new slots on a 350ms overshoot — which is right after a drag and reads as a\n * glitch on arrival, because nobody asked for it.\n *\n * So the board earns its motion. One frame after mount the arrangement is\n * settled and `layout` switches on, which is when it starts being about the\n * merchant moving something rather than about data landing. A drag, a removal\n * and an add all still animate exactly as before; those all happen long after\n * this flips.\n */\n const [settled, setSettled] = useState(false);\n useEffect(() => {\n // Two frames: the first paints the resolved arrangement, the second is the\n // earliest moment at which a further change is genuinely a change.\n const id = requestAnimationFrame(() =>\n requestAnimationFrame(() => setSettled(true)),\n );\n return () => cancelAnimationFrame(id);\n }, []);\n\n const candidates: WidgetDef[] = widgetsFor(ctx).filter(\n (d) => !placed.has(d.id),\n );\n\n /**\n * The context each widget actually renders from, with the blockers already\n * shown elsewhere on the board taken out.\n *\n * The home kept saying things twice: \"Set up payouts\" was a blocker pinned\n * above Activity AND the entire Revenue card; \"3 products low on stock\" was a\n * blocker AND the Low stock widget. Resolving it HERE rather than inside\n * Activity is what keeps it honest — this is the only place that knows which\n * widgets are actually placed, and a chore covered by a widget the merchant\n * has since removed is not covered at all.\n */\n const covered = coveredHrefs(ctx, placed);\n const boardCtx: WidgetContext =\n covered.size === 0\n ? ctx\n : { ...ctx, today: ctx.today.filter((i) => !covered.has(i.href)) };\n\n /**\n * Which card is in the hand, and where every card sits.\n *\n * The rects are a ref rather than state: they are read during a drag to work\n * out what the pointer is over, and putting them in state would re-render the\n * whole board on every pointer move to compute a number only the drag reads.\n * They are measured on pick-up, which is the last moment they are true — a\n * card dropped earlier may have changed the layout since.\n */\n const [dragging, setDragging] = useState<WidgetId | null>(null);\n /**\n * The slot the card would land in, and the ONLY thing a pointer move changes.\n *\n * The first version reordered and PERSISTED on every `onDrag` tick, which\n * wrote to localStorage sixty times a second and re-rendered the board under\n * the gesture that was driving it — so the cards it was meant to shuffle sat\n * still, because the drag kept being interrupted by its own side effects.\n * Nothing commits until the drop; until then this index is all that moves.\n */\n const [overIndex, setOverIndex] = useState<number | null>(null);\n const rects = useRef<{ id: WidgetId; cx: number; cy: number }[]>([]);\n\n const measure = () => {\n rects.current = widgets.flatMap((w) => {\n const el = document.querySelector<HTMLElement>(`[data-widget=\"${w.id}\"]`);\n if (!el) return [];\n const r = el.getBoundingClientRect();\n return [{ id: w.id, cx: r.left + r.width / 2, cy: r.top + r.height / 2 }];\n });\n };\n\n /**\n * The card whose centre is nearest the pointer, which is what \"drop here\"\n * means on a grid with two different card widths.\n *\n * Nearest-centre rather than \"which rect contains the pointer\": a wide card\n * dragged over the trio row is bigger than the slot under it, so hit-testing\n * containment leaves gaps between cards where nothing is targeted and the\n * drop silently does nothing.\n */\n /**\n * Where the dragged card is NOW, read off the element itself.\n *\n * This replaced `info.point`, which was the bug behind drops that silently\n * did nothing: Framer reports the pointer in PAGE coordinates while\n * `getBoundingClientRect` returns VIEWPORT ones, and the dashboard scrolls\n * inside an inner container — so the moment the page was scrolled the two\n * spaces diverged and every drop hit-tested against the wrong row, usually\n * resolving to the card's own slot.\n *\n * The element's own rect is in the same space as every other rect by\n * construction, so there is no space to get wrong. It is also the more\n * honest question: what matters is where the CARD is, not where the cursor\n * happens to be inside it.\n */\n const centreOf = (id: WidgetId): { x: number; y: number } | null => {\n const el = document.querySelector<HTMLElement>(`[data-widget=\"${id}\"]`);\n if (!el) return null;\n const r = el.getBoundingClientRect();\n return { x: r.left + r.width / 2, y: r.top + r.height / 2 };\n };\n\n const targetAt = (x: number, y: number): WidgetId | null => {\n let best: WidgetId | null = null;\n let bestDist = Infinity;\n for (const r of rects.current) {\n const d = (r.cx - x) ** 2 + (r.cy - y) ** 2;\n if (d < bestDist) {\n bestDist = d;\n best = r.id;\n }\n }\n return best;\n };\n\n return (\n // Wraps the gallery too: a preview showing different figures from the board\n // it previews would be worse than no preview.\n <MoneyBoardProvider\n demo={\n ctx.demo ? { data: ctx.demo.data, prior: ctx.demo.prior } : undefined\n }\n >\n {/* SIX COLUMNS, 20px gap — Shopify's grid, measured. A `wide` card spans\n three of them and two fill a row; a `narrow` spans two and three fill\n a row. Everything collapses to one column below `md`, because 146px\n columns are not a phone layout. */}\n <motion.div\n layout={settled}\n className=\"grid grid-cols-1 gap-4 md:grid-cols-6\"\n >\n <AnimatePresence mode=\"popLayout\" initial={false}>\n {widgets.map((w, index) => {\n const def = widgetDef(w.id);\n if (!def) return null;\n /**\n * THE DROP ZONE, and it is a real grid cell rather than an outline\n * painted over one.\n *\n * Being in the flow is the whole point: inserting it PUSHES the\n * cards after it along, so the board visibly opens a gap where the\n * card will land. An absolutely-positioned overlay would have shown\n * the target without moving anything, which is the complaint it\n * exists to answer — the other widgets sitting still while you hold\n * one over them.\n *\n * It takes the size of the slot it sits in, not of the card being\n * carried, because that is what the card will BECOME once dropped:\n * drag a narrow card into the top row and the zone is wide, which\n * is the rule made visible before you commit to it.\n */\n const zone =\n dragging && dragging !== w.id && overIndex === index ? (\n <motion.div\n key=\"dropzone\"\n layout\n initial={{ opacity: 0, scale: 0.96 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.96 }}\n transition={{ duration: 0.16, ease: eases.out }}\n style={{ height: WIDGET_HEIGHT }}\n className={`${\n sizeForIndex(index) === 'wide'\n ? 'md:col-span-3'\n : 'md:col-span-2'\n } dropzone-breathe rounded-[var(--radius-card)] border-2 border-dashed border-brand-500 bg-brand-500/5`}\n />\n ) : null;\n\n return (\n <Fragment key={w.id}>\n {zone}\n <BoardCard\n id={w.id}\n animateLayout={settled}\n span={w.size === 'wide' ? 'md:col-span-3' : 'md:col-span-2'}\n dimmed={!!dragging && dragging !== w.id && !reduceMotion}\n disabled={reduceMotion}\n onLift={() => {\n measure();\n setDragging(w.id);\n setOverIndex(null);\n }}\n onDrag={() => {\n const c = centreOf(w.id);\n if (!c) return;\n const overId = targetAt(c.x, c.y);\n if (!overId || overId === w.id) return;\n const to = widgets.findIndex((x) => x.id === overId);\n if (to !== -1) setOverIndex(to);\n }}\n onDrop={() => {\n const c = centreOf(w.id);\n const overId = c ? targetAt(c.x, c.y) : null;\n const to =\n overId && overId !== w.id\n ? widgets.findIndex((x) => x.id === overId)\n : overIndex;\n setDragging(null);\n setOverIndex(null);\n if (to != null && to !== -1) move(w.id, to);\n }}\n >\n <WidgetFrame\n name={def.name}\n onMove={(delta) =>\n move(\n w.id,\n widgets.findIndex((x) => x.id === w.id) + delta,\n )\n }\n >\n {/* A widget with nothing yet shows its own empty state, which\n is where setup now lives: the box you fix is the box that\n then fills with your data. */}\n {def.empty?.(boardCtx) ? (\n <WidgetEmptyState empty={def.empty(boardCtx)!} />\n ) : (\n def.render(boardCtx, w.size)\n )}\n </WidgetFrame>\n </BoardCard>\n </Fragment>\n );\n })}\n </AnimatePresence>\n </motion.div>\n\n <WidgetGallery\n open={galleryOpen}\n onOpenChange={onGalleryOpenChange}\n candidates={candidates}\n ctx={ctx}\n onAdd={(def, size) => add(def.id, size)}\n />\n </MoneyBoardProvider>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WidgetDef } from './types';
|
|
2
|
+
export declare function registerHostWidget(def: WidgetDef): void;
|
|
3
|
+
/** The host's widgets, in the order they were registered. */
|
|
4
|
+
export declare function hostWidgets(): WidgetDef[];
|
|
5
|
+
/** A host's widget by id, or undefined. */
|
|
6
|
+
export declare function hostWidget(id: string): WidgetDef | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const HOST_WIDGETS = /* @__PURE__ */ new Map();
|
|
2
|
+
function registerHostWidget(def) {
|
|
3
|
+
HOST_WIDGETS.set(def.id, def);
|
|
4
|
+
}
|
|
5
|
+
function hostWidgets() {
|
|
6
|
+
return [...HOST_WIDGETS.values()];
|
|
7
|
+
}
|
|
8
|
+
function hostWidget(id) {
|
|
9
|
+
return HOST_WIDGETS.get(id);
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
hostWidget,
|
|
13
|
+
hostWidgets,
|
|
14
|
+
registerHostWidget
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=host-widgets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/home/widgets/host-widgets.ts"],"sourcesContent":["import type { WidgetDef } from './types';\n\n/**\n * Widgets a host adds to the board. The catalogue in registry.tsx is what this package draws; a host with\n * something of its own to say (a wallet balance, from an engine this package knows nothing of)\n * registers it once, where its dashboard layout is built. Registering the same id again replaces it.\n */\nconst HOST_WIDGETS = new Map<string, WidgetDef>();\n\nexport function registerHostWidget(def: WidgetDef): void {\n HOST_WIDGETS.set(def.id, def);\n}\n\n/** The host's widgets, in the order they were registered. */\nexport function hostWidgets(): WidgetDef[] {\n return [...HOST_WIDGETS.values()];\n}\n\n/** A host's widget by id, or undefined. */\nexport function hostWidget(id: string): WidgetDef | undefined {\n return HOST_WIDGETS.get(id);\n}\n"],"mappings":"AAOA,MAAM,eAAe,oBAAI,IAAuB;AAEzC,SAAS,mBAAmB,KAAsB;AACvD,eAAa,IAAI,IAAI,IAAI,GAAG;AAC9B;AAGO,SAAS,cAA2B;AACzC,SAAO,CAAC,GAAG,aAAa,OAAO,CAAC;AAClC;AAGO,SAAS,WAAW,IAAmC;AAC5D,SAAO,aAAa,IAAI,EAAE;AAC5B;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who a row is. An engine that lists people by the address they were invited with and gives no name
|
|
3
|
+
* (Olive's) still has something to show: the part before the @, as the team page names them. Only a
|
|
4
|
+
* person with no name and no address at all is a "Team member".
|
|
5
|
+
*/
|
|
6
|
+
export declare function memberLabel(m: {
|
|
7
|
+
name?: string | null;
|
|
8
|
+
email?: string | null;
|
|
9
|
+
inviteEmail?: string | null;
|
|
10
|
+
role?: string;
|
|
11
|
+
}): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function memberLabel(m) {
|
|
2
|
+
const address = (m.email || m.inviteEmail || "").trim();
|
|
3
|
+
if (m.name?.trim()) return m.name.trim();
|
|
4
|
+
if (address) return address.includes("@") ? address.split("@")[0] : address;
|
|
5
|
+
return m.role === "owner" ? "Owner" : "Team member";
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
memberLabel
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=member-label.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/home/widgets/member-label.ts"],"sourcesContent":["/**\n * Who a row is. An engine that lists people by the address they were invited with and gives no name\n * (Olive's) still has something to show: the part before the @, as the team page names them. Only a\n * person with no name and no address at all is a \"Team member\".\n */\nexport function memberLabel(m: {\n name?: string | null;\n email?: string | null;\n inviteEmail?: string | null;\n role?: string;\n}): string {\n const address = (m.email || m.inviteEmail || '').trim();\n if (m.name?.trim()) return m.name.trim();\n if (address) return address.includes('@') ? address.split('@')[0] : address;\n return m.role === 'owner' ? 'Owner' : 'Team member';\n}\n\n"],"mappings":"AAKO,SAAS,YAAY,GAKjB;AACT,QAAM,WAAW,EAAE,SAAS,EAAE,eAAe,IAAI,KAAK;AACtD,MAAI,EAAE,MAAM,KAAK,EAAG,QAAO,EAAE,KAAK,KAAK;AACvC,MAAI,QAAS,QAAO,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,CAAC,IAAI;AACpE,SAAO,EAAE,SAAS,UAAU,UAAU;AACxC;","names":[]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A long address shortened in the middle, keeping both ends: the start says whose it is and the end
|
|
3
|
+
* says where it lives. Cut at the end, "fubi.box-9825bee8a691fdce..." lost the part that matters.
|
|
4
|
+
*/
|
|
5
|
+
export declare function middleEllipsis(text: string, max?: number): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function middleEllipsis(text, max = 28) {
|
|
2
|
+
if (max < 5 || text.length <= max) return text;
|
|
3
|
+
const keep = max - 1;
|
|
4
|
+
const head = Math.ceil(keep * 0.6);
|
|
5
|
+
const tail = keep - head;
|
|
6
|
+
return `${text.slice(0, head)}\u2026${text.slice(text.length - tail)}`;
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
middleEllipsis
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=middle-ellipsis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/home/widgets/middle-ellipsis.ts"],"sourcesContent":["/**\n * A long address shortened in the middle, keeping both ends: the start says whose it is and the end\n * says where it lives. Cut at the end, \"fubi.box-9825bee8a691fdce...\" lost the part that matters.\n */\nexport function middleEllipsis(text: string, max = 28): string {\n if (max < 5 || text.length <= max) return text;\n const keep = max - 1;\n const head = Math.ceil(keep * 0.6);\n const tail = keep - head;\n return `${text.slice(0, head)}\\u2026${text.slice(text.length - tail)}`;\n}\n"],"mappings":"AAIO,SAAS,eAAe,MAAc,MAAM,IAAY;AAC7D,MAAI,MAAM,KAAK,KAAK,UAAU,IAAK,QAAO;AAC1C,QAAM,OAAO,MAAM;AACnB,QAAM,OAAO,KAAK,KAAK,OAAO,GAAG;AACjC,QAAM,OAAO,OAAO;AACpB,SAAO,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,SAAS,KAAK,MAAM,KAAK,SAAS,IAAI,CAAC;AACtE;","names":[]}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@withpotter/pages-analytics/dashboard/(analytics)/analytics/helpers";
|
|
7
7
|
import { useMoneyBoard, RANGE_LABEL } from "./money-context";
|
|
8
8
|
import { Panel } from "../panel";
|
|
9
|
+
import { spreadByDay } from "./revenue-series";
|
|
9
10
|
const RANGES = ["7d", "30d", "90d"];
|
|
10
11
|
function Delta({
|
|
11
12
|
dir,
|
|
@@ -28,7 +29,7 @@ function Delta({
|
|
|
28
29
|
}
|
|
29
30
|
function Series({ series }) {
|
|
30
31
|
const max = series.reduce((m, p) => Math.max(m, p.value), 0);
|
|
31
|
-
return /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "mt-auto flex h-
|
|
32
|
+
return /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "mt-auto flex h-24 w-full items-end gap-px pt-3", children: series.map((p) => /* @__PURE__ */ jsx(
|
|
32
33
|
"div",
|
|
33
34
|
{
|
|
34
35
|
style: { height: `${Math.max(2, p.value / max * 100)}%` },
|
|
@@ -58,7 +59,8 @@ function MoneyWidget({
|
|
|
58
59
|
series,
|
|
59
60
|
size,
|
|
60
61
|
showRange,
|
|
61
|
-
empty
|
|
62
|
+
empty,
|
|
63
|
+
caption
|
|
62
64
|
}) {
|
|
63
65
|
const { range, pending, failed, retry } = useMoneyBoard();
|
|
64
66
|
const big = size !== "narrow";
|
|
@@ -106,16 +108,18 @@ function MoneyWidget({
|
|
|
106
108
|
),
|
|
107
109
|
delta && /* @__PURE__ */ jsx(Delta, { ...delta })
|
|
108
110
|
] }),
|
|
109
|
-
|
|
111
|
+
caption && /* @__PURE__ */ jsx("p", { className: "mt-1 text-2xs text-[var(--text-muted)]", children: caption }),
|
|
112
|
+
size === "wide" && series?.length ? /* @__PURE__ */ jsx(Series, { series: spreadByDay(series, Number.parseInt(range, 10)) }) : null
|
|
110
113
|
] })
|
|
111
114
|
]
|
|
112
115
|
}
|
|
113
116
|
);
|
|
114
117
|
}
|
|
115
118
|
function RevenueWidget({ size }) {
|
|
116
|
-
const { data, prior, formatCurrency } = useMoneyBoard();
|
|
119
|
+
const { data, prior, range, formatCurrency } = useMoneyBoard();
|
|
117
120
|
const revenue = data?.sales.totalRevenue ?? 0;
|
|
118
121
|
const series = data?.revenueTimeSeries ?? [];
|
|
122
|
+
const caption = revenue > 0 && prior != null && (prior.sales.totalRevenue ?? 0) === 0 ? `Nothing in the previous ${RANGE_LABEL[range]}` : void 0;
|
|
119
123
|
return /* @__PURE__ */ jsx(
|
|
120
124
|
MoneyWidget,
|
|
121
125
|
{
|
|
@@ -124,6 +128,7 @@ function RevenueWidget({ size }) {
|
|
|
124
128
|
showRange: true,
|
|
125
129
|
value: formatCurrency(Math.round(revenue)),
|
|
126
130
|
delta: computeDelta(revenue, prior?.sales.totalRevenue ?? 0),
|
|
131
|
+
caption,
|
|
127
132
|
series,
|
|
128
133
|
empty: data != null && revenue === 0 && !series.some((p) => p.value > 0)
|
|
129
134
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/dashboard/home/widgets/money-widgets.tsx"],"sourcesContent":["'use client';\n\nimport { ArrowUpRight, ArrowDownRight } from 'lucide-react';\nimport {\n computeDelta,\n type DeltaDirection,\n} from '@withpotter/pages-analytics/dashboard/(analytics)/analytics/helpers';\nimport type { TimeSeriesPoint } from '@withpotter/lib/api/types';\nimport { useMoneyBoard, RANGE_LABEL, type MetricsRange } from './money-context';\nimport type { WidgetSize } from './types';\nimport { Panel } from '../panel';\n\nconst RANGES: MetricsRange[] = ['7d', '30d', '90d'];\n\nfunction Delta({\n dir,\n pct,\n hasBaseline,\n}: {\n dir: DeltaDirection;\n pct: number;\n hasBaseline: boolean;\n}) {\n if (!hasBaseline || dir === 'flat') return null;\n const good = dir === 'up';\n return (\n <span\n className={`inline-flex items-center gap-0.5 text-sm font-semibold tabular-nums ${\n good ? 'text-[var(--success)]' : 'text-[var(--error)]'\n }`}\n >\n {good ? <ArrowUpRight /> : <ArrowDownRight />}\n {pct.toFixed(pct >= 10 ? 0 : 1)}%\n </span>\n );\n}\n\n/** Bars, because a day is a discrete thing and a line between two invents one. */\nfunction Series({ series }: { series: TimeSeriesPoint[] }) {\n const max = series.reduce((m, p) => Math.max(m, p.value), 0);\n return (\n <div aria-hidden className=\"mt-auto flex h-16 w-full items-end gap-px pt-3\">\n {series.map((p) => (\n <div\n key={p.date}\n style={{ height: `${Math.max(2, (p.value / max) * 100)}%` }}\n className=\"min-w-px flex-1 rounded-t-[1px] bg-[var(--color-brand-500)]/35\"\n />\n ))}\n </div>\n );\n}\n\n/**\n * The range control, rendered by whichever money widget is large enough to hold\n * it. It writes to shared state, so moving it moves every money widget at once.\n * At `sm` the widget states its window as text instead: a segmented control in a\n * 148px box would be most of the box.\n */\nfunction RangeControl() {\n const { range, setRange } = useMoneyBoard();\n return (\n <div className=\"surface-pinned flex items-center gap-0.5 p-0.5\">\n {RANGES.map((r) => (\n <button\n key={r}\n type=\"button\"\n onClick={() => setRange(r)}\n aria-pressed={r === range}\n className={`rounded-md px-2 py-1 text-2xs font-medium transition-colors ${\n r === range\n ? 'bg-[var(--interactive-hover)] text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'\n }`}\n >\n {RANGE_LABEL[r]}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * The shared shell for every money widget.\n *\n * An empty window says so. Cloudflare writes \"Not enough data\" and Square writes\n * \"No data available for timeframe selected\"; both refuse to let nothing\n * masquerade as a measurement, and a bare `₦0` under the word Revenue tells a\n * merchant they sold nothing when the truth is that nobody could have bought\n * anything yet.\n */\nfunction MoneyWidget({\n label,\n value,\n delta,\n series,\n size,\n showRange,\n empty,\n}: {\n label: string;\n value: string;\n delta?: { dir: DeltaDirection; pct: number; hasBaseline: boolean };\n series?: TimeSeriesPoint[];\n size: WidgetSize;\n /** Only one widget on the board needs to own the control. */\n showRange?: boolean;\n empty: boolean;\n}) {\n const { range, pending, failed, retry } = useMoneyBoard();\n const big = size !== 'narrow';\n\n return (\n <Panel\n title={label}\n control={big && showRange ? <RangeControl /> : undefined}\n action={\n big ? { label: 'Analytics', href: '/dashboard/analytics' } : undefined\n }\n className=\"h-full\"\n bodyClassName=\"flex flex-col\"\n >\n {!big && (\n <p className=\"text-2xs text-[var(--text-muted)]\">\n {RANGE_LABEL[range]}\n </p>\n )}\n\n {pending ? (\n <span\n aria-hidden\n className=\"mt-2 inline-block h-[28px] w-32 animate-pulse rounded bg-[var(--bg-subtle)]\"\n />\n ) : failed ? (\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Couldn’t load this just now.{' '}\n <button\n type=\"button\"\n onClick={retry}\n className=\"font-medium text-[var(--text-secondary)] underline underline-offset-2 hover:text-[var(--text-primary)]\"\n >\n Try again\n </button>\n </p>\n ) : empty ? (\n /* An empty window is not a measurement. Cloudflare writes \"Not enough\n data\" and Square writes \"No data available for timeframe selected\";\n both refuse to let nothing wear the shape of a result. */\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Not enough data\n </p>\n ) : (\n <>\n <div className=\"mt-1 flex flex-wrap items-baseline gap-x-2\">\n <span\n className={`font-semibold leading-none tracking-[-0.02em] tabular-nums text-[var(--text-primary)] ${\n big ? 'text-3xl' : 'text-2xl'\n }`}\n >\n {value}\n </span>\n {delta && <Delta {...delta} />}\n </div>\n {size === 'wide' && series?.length ? (\n <Series series={series} />\n ) : null}\n </>\n )}\n </Panel>\n );\n}\n\nexport function RevenueWidget({ size }: { size: WidgetSize }) {\n const { data, prior, formatCurrency } = useMoneyBoard();\n const revenue = data?.sales.totalRevenue ?? 0;\n const series = data?.revenueTimeSeries ?? [];\n return (\n <MoneyWidget\n label=\"Revenue\"\n size={size}\n showRange\n value={formatCurrency(Math.round(revenue))}\n delta={computeDelta(revenue, prior?.sales.totalRevenue ?? 0)}\n series={series}\n empty={data != null && revenue === 0 && !series.some((p) => p.value > 0)}\n />\n );\n}\n\nexport function SalesCountWidget({ size }: { size: WidgetSize }) {\n const { data, prior, language } = useMoneyBoard();\n const orders = data?.sales.totalOrders ?? 0;\n return (\n <MoneyWidget\n label={language.countLabel}\n size={size}\n // No chart. `getHomeSummary` returns one series and it is revenue's; a\n // picture of the wrong quantity is worse than no picture.\n value={orders.toLocaleString()}\n delta={computeDelta(orders, prior?.sales.totalOrders ?? 0)}\n empty={data != null && orders === 0}\n />\n );\n}\n\nexport function AvgOrderWidget({ size }: { size: WidgetSize }) {\n const { data, formatCurrency, language } = useMoneyBoard();\n const aov = data?.sales.averageOrderValue ?? 0;\n return (\n <MoneyWidget\n label={language.averageLabel}\n size={size}\n value={formatCurrency(aov)}\n empty={data != null && aov === 0}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/home/widgets/money-widgets.tsx"],"sourcesContent":["'use client';\n\nimport { ArrowUpRight, ArrowDownRight } from 'lucide-react';\nimport {\n computeDelta,\n type DeltaDirection,\n} from '@withpotter/pages-analytics/dashboard/(analytics)/analytics/helpers';\nimport type { TimeSeriesPoint } from '@withpotter/lib/api/types';\nimport { useMoneyBoard, RANGE_LABEL, type MetricsRange } from './money-context';\nimport type { WidgetSize } from './types';\nimport { Panel } from '../panel';\nimport { spreadByDay } from './revenue-series';\n\nconst RANGES: MetricsRange[] = ['7d', '30d', '90d'];\n\nfunction Delta({\n dir,\n pct,\n hasBaseline,\n}: {\n dir: DeltaDirection;\n pct: number;\n hasBaseline: boolean;\n}) {\n if (!hasBaseline || dir === 'flat') return null;\n const good = dir === 'up';\n return (\n <span\n className={`inline-flex items-center gap-0.5 text-sm font-semibold tabular-nums ${\n good ? 'text-[var(--success)]' : 'text-[var(--error)]'\n }`}\n >\n {good ? <ArrowUpRight /> : <ArrowDownRight />}\n {pct.toFixed(pct >= 10 ? 0 : 1)}%\n </span>\n );\n}\n\n/** Bars, because a day is a discrete thing and a line between two invents one. */\nfunction Series({ series }: { series: TimeSeriesPoint[] }) {\n const max = series.reduce((m, p) => Math.max(m, p.value), 0);\n return (\n <div aria-hidden className=\"mt-auto flex h-24 w-full items-end gap-px pt-3\">\n {series.map((p) => (\n <div\n key={p.date}\n style={{ height: `${Math.max(2, (p.value / max) * 100)}%` }}\n className=\"min-w-px flex-1 rounded-t-[1px] bg-[var(--color-brand-500)]/35\"\n />\n ))}\n </div>\n );\n}\n\n/**\n * The range control, rendered by whichever money widget is large enough to hold\n * it. It writes to shared state, so moving it moves every money widget at once.\n * At `sm` the widget states its window as text instead: a segmented control in a\n * 148px box would be most of the box.\n */\nfunction RangeControl() {\n const { range, setRange } = useMoneyBoard();\n return (\n <div className=\"surface-pinned flex items-center gap-0.5 p-0.5\">\n {RANGES.map((r) => (\n <button\n key={r}\n type=\"button\"\n onClick={() => setRange(r)}\n aria-pressed={r === range}\n className={`rounded-md px-2 py-1 text-2xs font-medium transition-colors ${\n r === range\n ? 'bg-[var(--interactive-hover)] text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'\n }`}\n >\n {RANGE_LABEL[r]}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * The shared shell for every money widget.\n *\n * An empty window says so. Cloudflare writes \"Not enough data\" and Square writes\n * \"No data available for timeframe selected\"; both refuse to let nothing\n * masquerade as a measurement, and a bare `₦0` under the word Revenue tells a\n * merchant they sold nothing when the truth is that nobody could have bought\n * anything yet.\n */\nfunction MoneyWidget({\n label,\n value,\n delta,\n series,\n size,\n showRange,\n empty,\n caption,\n}: {\n label: string;\n value: string;\n delta?: { dir: DeltaDirection; pct: number; hasBaseline: boolean };\n series?: TimeSeriesPoint[];\n size: WidgetSize;\n /** Only one widget on the board needs to own the control. */\n showRange?: boolean;\n empty: boolean;\n /** One line under the figure, when the figure alone does not say enough. */\n caption?: string;\n}) {\n const { range, pending, failed, retry } = useMoneyBoard();\n const big = size !== 'narrow';\n\n return (\n <Panel\n title={label}\n control={big && showRange ? <RangeControl /> : undefined}\n action={\n big ? { label: 'Analytics', href: '/dashboard/analytics' } : undefined\n }\n className=\"h-full\"\n bodyClassName=\"flex flex-col\"\n >\n {!big && (\n <p className=\"text-2xs text-[var(--text-muted)]\">\n {RANGE_LABEL[range]}\n </p>\n )}\n\n {pending ? (\n <span\n aria-hidden\n className=\"mt-2 inline-block h-[28px] w-32 animate-pulse rounded bg-[var(--bg-subtle)]\"\n />\n ) : failed ? (\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Couldn’t load this just now.{' '}\n <button\n type=\"button\"\n onClick={retry}\n className=\"font-medium text-[var(--text-secondary)] underline underline-offset-2 hover:text-[var(--text-primary)]\"\n >\n Try again\n </button>\n </p>\n ) : empty ? (\n /* An empty window is not a measurement. Cloudflare writes \"Not enough\n data\" and Square writes \"No data available for timeframe selected\";\n both refuse to let nothing wear the shape of a result. */\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Not enough data\n </p>\n ) : (\n <>\n <div className=\"mt-1 flex flex-wrap items-baseline gap-x-2\">\n <span\n className={`font-semibold leading-none tracking-[-0.02em] tabular-nums text-[var(--text-primary)] ${\n big ? 'text-3xl' : 'text-2xl'\n }`}\n >\n {value}\n </span>\n {delta && <Delta {...delta} />}\n </div>\n {caption && (\n <p className=\"mt-1 text-2xs text-[var(--text-muted)]\">{caption}</p>\n )}\n {size === 'wide' && series?.length ? (\n <Series series={spreadByDay(series, Number.parseInt(range, 10))} />\n ) : null}\n </>\n )}\n </Panel>\n );\n}\n\nexport function RevenueWidget({ size }: { size: WidgetSize }) {\n const { data, prior, range, formatCurrency } = useMoneyBoard();\n const revenue = data?.sales.totalRevenue ?? 0;\n const series = data?.revenueTimeSeries ?? [];\n // With nothing to compare to there is no arrow, and a bare figure says nothing about whether it\n // is good. Say what the comparison is when there is none to draw.\n const caption =\n revenue > 0 && prior != null && (prior.sales.totalRevenue ?? 0) === 0\n ? `Nothing in the previous ${RANGE_LABEL[range]}`\n : undefined;\n return (\n <MoneyWidget\n label=\"Revenue\"\n size={size}\n showRange\n value={formatCurrency(Math.round(revenue))}\n delta={computeDelta(revenue, prior?.sales.totalRevenue ?? 0)}\n caption={caption}\n series={series}\n empty={data != null && revenue === 0 && !series.some((p) => p.value > 0)}\n />\n );\n}\n\nexport function SalesCountWidget({ size }: { size: WidgetSize }) {\n const { data, prior, language } = useMoneyBoard();\n const orders = data?.sales.totalOrders ?? 0;\n return (\n <MoneyWidget\n label={language.countLabel}\n size={size}\n // No chart. `getHomeSummary` returns one series and it is revenue's; a\n // picture of the wrong quantity is worse than no picture.\n value={orders.toLocaleString()}\n delta={computeDelta(orders, prior?.sales.totalOrders ?? 0)}\n empty={data != null && orders === 0}\n />\n );\n}\n\nexport function AvgOrderWidget({ size }: { size: WidgetSize }) {\n const { data, formatCurrency, language } = useMoneyBoard();\n const aov = data?.sales.averageOrderValue ?? 0;\n return (\n <MoneyWidget\n label={language.averageLabel}\n size={size}\n value={formatCurrency(aov)}\n empty={data != null && aov === 0}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
export { registerHostWidget, hostWidgets } from './host-widgets';
|
|
2
|
+
import type { BuiltInWidgetId, WidgetDef, WidgetId, WidgetContext } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* The catalogue.
|
|
4
5
|
*
|
|
@@ -18,8 +19,8 @@ import type { WidgetDef, WidgetId, WidgetContext } from './types';
|
|
|
18
19
|
*
|
|
19
20
|
* `Blocked` is deliberately NOT here. See `home-widgets.tsx`.
|
|
20
21
|
*/
|
|
21
|
-
export declare const WIDGETS: Record<
|
|
22
|
-
export declare const WIDGET_ORDER:
|
|
22
|
+
export declare const WIDGETS: Record<BuiltInWidgetId, WidgetDef>;
|
|
23
|
+
export declare const WIDGET_ORDER: BuiltInWidgetId[];
|
|
23
24
|
/**
|
|
24
25
|
* Every destination the board is already showing, from placed widgets only.
|
|
25
26
|
*
|
|
@@ -33,4 +34,6 @@ export declare const WIDGET_ORDER: WidgetId[];
|
|
|
33
34
|
* it would be how the home quietly stops mentioning that you cannot be paid.
|
|
34
35
|
*/
|
|
35
36
|
export declare function coveredHrefs(ctx: WidgetContext, placed: Iterable<WidgetId>): Set<string>;
|
|
37
|
+
/** A widget by id: built in first, then a host's. Undefined for an id nobody defines any more. */
|
|
38
|
+
export declare function widgetDef(id: WidgetId): WidgetDef | undefined;
|
|
36
39
|
export declare function widgetsFor(ctx: WidgetContext): WidgetDef[];
|
|
@@ -13,6 +13,8 @@ import { BillWidget } from "./bill-widget";
|
|
|
13
13
|
import { WebsiteWidget } from "./website-widget";
|
|
14
14
|
import { isSettled, orderSteps } from "./setup-steps";
|
|
15
15
|
import { ThemeMiniature, Pointer } from "./widget-art";
|
|
16
|
+
import { hostWidget, hostWidgets } from "./host-widgets";
|
|
17
|
+
import { registerHostWidget, hostWidgets as hostWidgets2 } from "./host-widgets";
|
|
16
18
|
const WIDGETS = {
|
|
17
19
|
/**
|
|
18
20
|
* Money, as three widgets rather than one block.
|
|
@@ -362,7 +364,7 @@ const WIDGET_ORDER = [
|
|
|
362
364
|
function coveredHrefs(ctx, placed) {
|
|
363
365
|
const out = /* @__PURE__ */ new Set();
|
|
364
366
|
for (const id of placed) {
|
|
365
|
-
const def =
|
|
367
|
+
const def = widgetDef(id);
|
|
366
368
|
if (!def) continue;
|
|
367
369
|
for (const href of def.covers?.(ctx) ?? []) out.add(href);
|
|
368
370
|
const empty = def.empty?.(ctx);
|
|
@@ -370,13 +372,21 @@ function coveredHrefs(ctx, placed) {
|
|
|
370
372
|
}
|
|
371
373
|
return out;
|
|
372
374
|
}
|
|
375
|
+
function widgetDef(id) {
|
|
376
|
+
return WIDGETS[id] ?? hostWidget(id);
|
|
377
|
+
}
|
|
373
378
|
function widgetsFor(ctx) {
|
|
374
|
-
return WIDGET_ORDER.map((id) => WIDGETS[id])
|
|
379
|
+
return [...WIDGET_ORDER.map((id) => WIDGETS[id]), ...hostWidgets()].filter(
|
|
380
|
+
(w) => w.available(ctx)
|
|
381
|
+
);
|
|
375
382
|
}
|
|
376
383
|
export {
|
|
377
384
|
WIDGETS,
|
|
378
385
|
WIDGET_ORDER,
|
|
379
386
|
coveredHrefs,
|
|
387
|
+
hostWidgets2 as hostWidgets,
|
|
388
|
+
registerHostWidget,
|
|
389
|
+
widgetDef,
|
|
380
390
|
widgetsFor
|
|
381
391
|
};
|
|
382
392
|
//# sourceMappingURL=registry.js.map
|