@withpotter/components 0.13.0-next.124 → 0.13.0-next.58

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.
Files changed (132) hide show
  1. package/dist/billing/overview-tab.js +3 -4
  2. package/dist/billing/overview-tab.js.map +1 -1
  3. package/dist/billing/plans-tab.js +12 -6
  4. package/dist/billing/plans-tab.js.map +1 -1
  5. package/dist/charts/index.d.ts +1 -1
  6. package/dist/charts/index.js +1 -3
  7. package/dist/charts/index.js.map +1 -1
  8. package/dist/dashboard/activity-feed.d.ts +0 -2
  9. package/dist/dashboard/activity-feed.js +1 -4
  10. package/dist/dashboard/activity-feed.js.map +1 -1
  11. package/dist/dashboard/home/ask-controls.js +0 -1
  12. package/dist/dashboard/home/ask-controls.js.map +1 -1
  13. package/dist/dashboard/home/ask-mode-chip.js +4 -4
  14. package/dist/dashboard/home/ask-mode-chip.js.map +1 -1
  15. package/dist/dashboard/home/home-ask.js +3 -3
  16. package/dist/dashboard/home/home-ask.js.map +1 -1
  17. package/dist/dashboard/home/home-quick-actions.js +1 -9
  18. package/dist/dashboard/home/home-quick-actions.js.map +1 -1
  19. package/dist/dashboard/home/widgets/activity-widget.js +43 -56
  20. package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
  21. package/dist/dashboard/home/widgets/home-widgets.js +2 -3
  22. package/dist/dashboard/home/widgets/home-widgets.js.map +1 -1
  23. package/dist/dashboard/home/widgets/money-widgets.js +4 -9
  24. package/dist/dashboard/home/widgets/money-widgets.js.map +1 -1
  25. package/dist/dashboard/home/widgets/registry.d.ts +3 -6
  26. package/dist/dashboard/home/widgets/registry.js +2 -12
  27. package/dist/dashboard/home/widgets/registry.js.map +1 -1
  28. package/dist/dashboard/home/widgets/team-widget.js +3 -15
  29. package/dist/dashboard/home/widgets/team-widget.js.map +1 -1
  30. package/dist/dashboard/home/widgets/types.d.ts +1 -13
  31. package/dist/dashboard/home/widgets/types.js.map +1 -1
  32. package/dist/dashboard/home/widgets/use-home-layout.js +4 -9
  33. package/dist/dashboard/home/widgets/use-home-layout.js.map +1 -1
  34. package/dist/dashboard/home/widgets/website-widget.js +1 -9
  35. package/dist/dashboard/home/widgets/website-widget.js.map +1 -1
  36. package/dist/dashboard/page-transition.d.ts +0 -11
  37. package/dist/dashboard/page-transition.js +2 -9
  38. package/dist/dashboard/page-transition.js.map +1 -1
  39. package/dist/features/gift-card-artwork.js +1 -1
  40. package/dist/features/gift-card-artwork.js.map +1 -1
  41. package/dist/features/gift-cards/gift-cards-feature.js +518 -656
  42. package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
  43. package/dist/features/product-form.js +1 -1
  44. package/dist/features/product-form.js.map +1 -1
  45. package/dist/mobile/screens/billing-flows.js +6 -7
  46. package/dist/mobile/screens/billing-flows.js.map +1 -1
  47. package/dist/mobile/screens/billing.js +2 -2
  48. package/dist/mobile/screens/billing.js.map +1 -1
  49. package/dist/notifications-dropdown.js +1 -11
  50. package/dist/notifications-dropdown.js.map +1 -1
  51. package/dist/shell/notification-kinds.d.ts +2 -11
  52. package/dist/shell/notification-kinds.js +1 -16
  53. package/dist/shell/notification-kinds.js.map +1 -1
  54. package/dist/shell/notification-row.d.ts +2 -6
  55. package/dist/shell/notification-row.js +33 -59
  56. package/dist/shell/notification-row.js.map +1 -1
  57. package/dist/shell/product-switcher.js +0 -3
  58. package/dist/shell/product-switcher.js.map +1 -1
  59. package/dist/shell/settings-modal-context.d.ts +0 -2
  60. package/dist/shell/settings-modal-context.js +2 -4
  61. package/dist/shell/settings-modal-context.js.map +1 -1
  62. package/dist/shell/settings-panels.js +9 -29
  63. package/dist/shell/settings-panels.js.map +1 -1
  64. package/dist/shell/settings-rows.d.ts +2 -7
  65. package/dist/shell/settings-rows.js +36 -93
  66. package/dist/shell/settings-rows.js.map +1 -1
  67. package/dist/shell/shell.css +0 -192
  68. package/dist/shell/sidebar-footer.js +2 -6
  69. package/dist/shell/sidebar-footer.js.map +1 -1
  70. package/dist/shell/unified-settings-modal.d.ts +2 -3
  71. package/dist/shell/unified-settings-modal.js +74 -151
  72. package/dist/shell/unified-settings-modal.js.map +1 -1
  73. package/dist/sidebar/config.js +3 -19
  74. package/dist/sidebar/config.js.map +1 -1
  75. package/dist/sidebar/more-group.js +1 -1
  76. package/dist/sidebar/more-group.js.map +1 -1
  77. package/dist/sidebar/nav-renderers.js +0 -2
  78. package/dist/sidebar/nav-renderers.js.map +1 -1
  79. package/dist/sidebar/nav-section.js +3 -5
  80. package/dist/sidebar/nav-section.js.map +1 -1
  81. package/dist/sidebar/setup-progress.d.ts +1 -3
  82. package/dist/sidebar/setup-progress.js +2 -2
  83. package/dist/sidebar/setup-progress.js.map +1 -1
  84. package/dist/sidebar/spine-groups.js +1 -7
  85. package/dist/sidebar/spine-groups.js.map +1 -1
  86. package/dist/sidebar.js +5 -5
  87. package/dist/sidebar.js.map +1 -1
  88. package/dist/templates/template-live-thumb.js +5 -16
  89. package/dist/templates/template-live-thumb.js.map +1 -1
  90. package/package.json +5 -5
  91. package/dist/dashboard/activity-paid.d.ts +0 -10
  92. package/dist/dashboard/activity-paid.js +0 -7
  93. package/dist/dashboard/activity-paid.js.map +0 -1
  94. package/dist/dashboard/home/host-quick-actions.d.ts +0 -19
  95. package/dist/dashboard/home/host-quick-actions.js +0 -12
  96. package/dist/dashboard/home/host-quick-actions.js.map +0 -1
  97. package/dist/dashboard/home/widgets/host-widgets.d.ts +0 -6
  98. package/dist/dashboard/home/widgets/host-widgets.js +0 -16
  99. package/dist/dashboard/home/widgets/host-widgets.js.map +0 -1
  100. package/dist/dashboard/home/widgets/member-label.d.ts +0 -11
  101. package/dist/dashboard/home/widgets/member-label.js +0 -10
  102. package/dist/dashboard/home/widgets/member-label.js.map +0 -1
  103. package/dist/dashboard/home/widgets/middle-ellipsis.d.ts +0 -5
  104. package/dist/dashboard/home/widgets/middle-ellipsis.js +0 -11
  105. package/dist/dashboard/home/widgets/middle-ellipsis.js.map +0 -1
  106. package/dist/dashboard/home/widgets/revenue-series.d.ts +0 -12
  107. package/dist/dashboard/home/widgets/revenue-series.js +0 -26
  108. package/dist/dashboard/home/widgets/revenue-series.js.map +0 -1
  109. package/dist/dashboard/page-transition-key.d.ts +0 -7
  110. package/dist/dashboard/page-transition-key.js +0 -11
  111. package/dist/dashboard/page-transition-key.js.map +0 -1
  112. package/dist/features/gift-cards/gift-card-logic.d.ts +0 -41
  113. package/dist/features/gift-cards/gift-card-logic.js +0 -65
  114. package/dist/features/gift-cards/gift-card-logic.js.map +0 -1
  115. package/dist/features/gift-cards/hint.d.ts +0 -9
  116. package/dist/features/gift-cards/hint.js +0 -25
  117. package/dist/features/gift-cards/hint.js.map +0 -1
  118. package/dist/shell/notification-actions.d.ts +0 -12
  119. package/dist/shell/notification-actions.js +0 -18
  120. package/dist/shell/notification-actions.js.map +0 -1
  121. package/dist/shell/settings-groups.d.ts +0 -23
  122. package/dist/shell/settings-groups.js +0 -22
  123. package/dist/shell/settings-groups.js.map +0 -1
  124. package/dist/shell/settings-host.d.ts +0 -39
  125. package/dist/shell/settings-host.js +0 -37
  126. package/dist/shell/settings-host.js.map +0 -1
  127. package/dist/shell/sidebar-cards.d.ts +0 -32
  128. package/dist/shell/sidebar-cards.js +0 -37
  129. package/dist/shell/sidebar-cards.js.map +0 -1
  130. package/dist/templates/template-tile-source.d.ts +0 -24
  131. package/dist/templates/template-tile-source.js +0 -18
  132. package/dist/templates/template-tile-source.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/dashboard/home/widgets/website-widget.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport {\n ArrowRight,\n ArrowUpRight,\n Eye,\n Globe,\n Lock,\n ShoppingBag,\n TrendingDown,\n TrendingUp,\n} from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { analyticsApi } from '@withpotter/lib/api/analytics';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { middleEllipsis } from './middle-ellipsis';\nimport { Panel, PanelAction } from '../panel';\n\n/**\n * The storefront, once there is one.\n *\n * WHY THIS WIDGET EXISTS. Deleting the \"Finish setting up\" panel only works if\n * every chore it held has somewhere better to be. Three of the four did:\n * \"add your first product\" belongs to Today, \"take payments\" is already in\n * Blocked because it stops money outright, and both are boxes whose empty state\n * the chore can occupy. \"Design your site\" and \"get a domain\" had nowhere — no\n * widget on the board was ever going to show their result — so the panel could\n * only have been relocated, not removed. This is the box those two fill.\n */\n/**\n * Views over the trailing week.\n *\n * IT FETCHES ITS OWN, a deliberate exception to the board's rule against\n * per-widget requests. That rule exists because six widgets reading money would\n * mean six copies of one request, which is why `MoneyBoardProvider` exists.\n * Nothing else on the board wants page views, so a provider for one consumer\n * would be ceremony — and `getHomeSummary`, which the money widgets share, does\n * not carry them.\n *\n * SEVEN DAYS, not the money board's window, and the two do not have to agree.\n * The money widgets share one range precisely so revenue-over-30 never appears\n * beside orders-over-7; this card is not part of that sentence.\n */\ninterface Traffic {\n /** Visits over the trailing seven days. */\n views: number;\n /** The same window, one week earlier, for the change. */\n prior: number;\n /** Distinct people, as opposed to page loads. */\n visitors: number;\n /** One point per day, oldest first. Drives the sparkline. */\n series: number[];\n /** Where most of them came from, and what share that was. */\n topSource?: { source: string; percentage: number };\n /** The page they looked at most. */\n topPage?: string;\n /** Sessions that reached a cart, and sessions that bought. */\n carts: number;\n orders: number;\n}\n\nfunction useWeeklyTraffic(enabled: boolean) {\n /*\n * Two windows, so the number can say which way it is going. A visit count\n * with no comparison is a fact you cannot act on: 40 is good or bad only\n * against last week.\n *\n * Through the query cache rather than an effect. `/analytics/dashboard` is a\n * twenty-odd-field payload and this asked for two of them on every mount of\n * the home, with nothing holding the answer — dev's double invoke made it\n * four requests for a card most merchants scroll past. Keyed by the day,\n * which is the resolution a week-long comparison has, so returning to the\n * home inside the same day costs nothing.\n */\n const iso = (d: Date) => d.toISOString().slice(0, 10);\n const today = iso(new Date());\n const windows = useMemo(() => {\n const end = new Date(`${today}T00:00:00.000Z`);\n const days = (n: number) => new Date(end.getTime() - n * 86_400_000);\n return {\n now: { startDate: iso(days(6)), endDate: today },\n before: { startDate: iso(days(13)), endDate: iso(days(7)) },\n };\n }, [today]);\n\n // Silent on failure, both of them: this card's job is the storefront, and a\n // failed traffic read must not turn a working Website card into an error.\n const { data: now } = useApiQuery(\n ['home-traffic', today],\n (slug) => analyticsApi.getDashboard(slug, windows.now),\n { enabled, retry: false },\n );\n const { data: before } = useApiQuery(\n ['home-traffic-previous', today],\n (slug) => analyticsApi.getDashboard(slug, windows.before),\n { enabled, retry: false },\n );\n\n return useMemo<Traffic | null>(() => {\n if (!now) return null;\n // All of this arrives in the SAME response as the view count, so the card\n // gets four more facts for no extra request. It would have been a waste to\n // fetch `overview` and throw the rest of the payload away.\n const sources = now.trafficSources ?? [];\n const pages = now.topPages ?? [];\n return {\n views: now.overview?.totalPageViews ?? 0,\n prior: before?.overview?.totalPageViews ?? 0,\n visitors: now.overview?.uniqueVisitors ?? 0,\n series: (now.pageViewsTimeSeries ?? []).map((p) => p.value ?? 0),\n topSource: sources.length\n ? { source: sources[0].source, percentage: sources[0].percentage }\n : undefined,\n topPage: pages.length ? pages[0].pageUrl : undefined,\n carts: now.funnel?.addToCart ?? 0,\n orders: now.funnel?.checkoutComplete ?? 0,\n };\n }, [now, before]);\n}\n\n/**\n * Week on week, and it stays silent when it cannot be honest.\n *\n * A first week has no prior to compare against, and rendering \"+100%\" against\n * zero is a number that means nothing dressed as a number that means something.\n * The analytics rule is that a figure ships only if it can change what someone\n * does next; a delta off no baseline cannot.\n */\nfunction TrafficDelta({ now, prior }: { now: number; prior: number }) {\n if (prior <= 0 || now === prior) return null;\n const pct = Math.round(((now - prior) / prior) * 100);\n const up = pct > 0;\n return (\n <span\n className={`ml-auto inline-flex items-center gap-0.5 text-sm font-medium tabular-nums ${\n up ? 'text-[var(--success)]' : 'text-[var(--text-tertiary)]'\n }`}\n >\n {up ? (\n <TrendingUp className=\"h-[11px] w-[11px]\" />\n ) : (\n <TrendingDown className=\"h-[11px] w-[11px]\" />\n )}\n {Math.abs(pct)}%\n </span>\n );\n}\n\n/**\n * Below this, a conversion rate is arithmetic on noise. One sale in nine visits\n * is 11.1%, which reads as a triumph and means nothing.\n */\nconst MIN_FOR_RATE = 30;\n\n/** One step of the funnel: the count, then what it counts. */\nfunction Step({ value, label }: { value: number; label: string }) {\n return (\n <span className=\"shrink-0\">\n <span className=\"font-medium tabular-nums text-[var(--text-secondary)]\">\n {value.toLocaleString()}\n </span>{' '}\n {label}\n </span>\n );\n}\n\n/**\n * A headline count and what it counts, for the pre-conversion state.\n *\n * THE GLYPH LEADS, and it is the one its own screen already uses: `Eye` is\n * what the Traffic tab marks views with, `ShoppingBag` is what the Activity\n * widget draws beside `order_placed`. Two counts sitting side by side are\n * told apart at a glance by shape long before anyone reads either label —\n * which is the whole reason to have them — and picking a third icon for a\n * concept the app has already settled would cost that.\n *\n * `self-center` because the row is baseline-aligned: the number and its label\n * share a baseline, which is the detail that makes them read as one phrase,\n * and a 16px glyph hung off that same line sits low against a 24px figure.\n */\nfunction Count({\n icon: Icon,\n value,\n label,\n}: {\n icon: LucideIcon;\n value: number;\n label: string;\n}) {\n return (\n <p className=\"flex items-baseline gap-2\">\n <Icon\n aria-hidden\n className=\"size-4 shrink-0 self-center text-[var(--text-muted)]\"\n />\n {/* 17px, not 20. Two of these sit side by side now, and at `text-2xl`\n they were the loudest thing in a card whose subject is the site\n above them — a week's visit count competing with the storefront it\n describes. One step down the scale (John, 2026-08-25) still reads as\n a figure against a 13px label. */}\n <span className=\"text-xl font-semibold tabular-nums tracking-[-0.02em] text-[var(--text-primary)]\">\n {value.toLocaleString()}\n </span>\n <span className=\"text-sm text-[var(--text-tertiary)]\">{label}</span>\n </p>\n );\n}\n\nexport function WebsiteWidget({\n storefrontUrl,\n hasCustomDomain,\n siteImage,\n businessName,\n}: {\n storefrontUrl?: string | null;\n hasCustomDomain: boolean;\n /** The business's own image. Absent is normal and draws a mark instead. */\n siteImage?: string | null;\n businessName?: string;\n}) {\n const { tenant } = useAuth();\n const isOpen = tenant?.status === 'active';\n const host = storefrontUrl?.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');\n // Only asked for once the site is published: there is no traffic to a\n // storefront that does not exist, and asking would spend a request to be\n // told zero.\n const traffic = useWeeklyTraffic(!!storefrontUrl);\n\n return (\n <Panel\n title=\"Website\"\n className=\"h-full\"\n bodyClassName=\"flex flex-col min-h-0\"\n >\n {/*\n IT IS A LINK PREVIEW — the card that appears when somebody pastes your\n address into WhatsApp or Slack.\n\n Two designs came before this and both did too much. The first was four\n unrelated rows: a pill, a number, a link, a button. The second made the\n card a browser window — chrome, viewport, the image standing upright\n inside it — which looked like a site but answered nothing, because a\n merchant does not need to be shown that websites live in browsers.\n\n The link preview is the right frame because it is the one a merchant\n already knows and the one that matters: it is how the business appears\n the moment anyone shares it. Horizontal, image left, name and address\n right — so the card is literally a rehearsal of the thing it describes.\n\n Three facts, in the order they are wanted: what it looks like shared,\n where it lives, and whether anyone came. Nothing else.\n */}\n <a\n href={storefrontUrl ?? '#'}\n target=\"_blank\"\n rel=\"noreferrer\"\n /* THE PREVIEW IS GREY, not the panel's own fill. It carried\n `surface-interactive`, whose background is `--card-bg` — the exact\n fill of the `Panel` it sits inside — so the only thing separating\n the two was a hairline, and the card read as having no background at\n all. `--bg-muted` is the grey either side of that: #efefef on white,\n #202023 on the dark ground. The chip stands ON the panel, which is\n what a thing you can click should look like. */\n className=\"surface-interactive group/site flex items-stretch gap-0 overflow-hidden bg-[var(--bg-muted)]\"\n >\n {/* Square, and fixed. A share card's thumbnail never grows with the\n title; letting it would make two businesses' cards different shapes\n for no reason a reader could name. */}\n <span className=\"grid h-[68px] w-[68px] shrink-0 place-items-center overflow-hidden bg-[var(--bg-base)]\">\n {siteImage ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={siteImage}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-[var(--motion-slow)] ease-out group-hover/site:scale-105\"\n />\n ) : (\n <Globe className=\"size-5 text-[var(--text-muted)]\"\n strokeWidth={1.5}\n />\n )}\n </span>\n\n <span className=\"flex min-w-0 flex-1 flex-col justify-center gap-0.5 px-3 py-2\">\n <span className=\"truncate text-md font-medium text-[var(--text-primary)]\">\n {businessName ?? 'Your website'}\n </span>\n <span className=\"flex min-w-0 items-center gap-1\">\n {isOpen && host ? (\n <>\n <Lock\n aria-hidden\n className=\"h-[9px] w-[9px] shrink-0 text-[var(--success)]\"\n />\n <span\n title={host}\n className=\"min-w-0 truncate text-sm text-[var(--text-tertiary)]\"\n >\n {middleEllipsis(host)}\n </span>\n </>\n ) : (\n /* \"Not published yet\" was a FACT with no way to act on it, and\n the only control that could open the site was a modal that\n appeared once and never again. A merchant who dismissed it had\n no route to publishing from the dashboard at all.\n\n Shopify's own onboarding guidance is dismissible cards rather\n than modals, and this is already the card about the site. So\n the state and the act that resolves it are in one place. */\n /* The address exists before the site is open, and showing it\n under a padlock said the opposite of the truth. While it is\n shut the line says so; the address is no use until it is. */\n <span className=\"min-w-0 truncate text-sm text-[var(--text-tertiary)]\">\n Not open yet\n </span>\n )}\n </span>\n </span>\n\n <span className=\"grid w-8 shrink-0 place-items-center text-[var(--text-muted)] transition-colors group-hover/site:text-[var(--text-primary)]\">\n <ArrowUpRight />\n </span>\n </a>\n\n {/* NO CONTROL UNDER THE PREVIEW.\n\n There was an \"Open my site\" button here, then — after John cut it —\n the same act surviving as an \"Open it now\" text link. Both are gone\n on his call, 2026-08-25. The card's job is to SHOW the storefront;\n \"Not open yet\" is the line that states it, and publishing is a\n once-ever act that was holding a permanent row on a board where\n every other card leads with content. Opening the site lives on the\n Website hub, which the preview itself links into. */}\n\n {/* THE QUESTION TRAFFIC IS ACTUALLY ASKING.\n A visit count is a vanity number: 480 is good or bad depending\n entirely on what the 480 did, and a merchant cannot act on it. What\n they can act on is the CONVERSION — the share of people who came and\n bought — because a high number says do more of whatever brought them\n and a low one says the site is not closing.\n\n So conversion leads and visits are the context under it, which is the\n inverse of what this card did. The funnel arrives in the same request\n as the view count and was being thrown away.\n\n Only when there is enough traffic to divide by. A rate off nine\n visits is noise with a decimal point, and reporting \"11.1%\" from one\n sale in nine would be worse than reporting nothing. */}\n {traffic && traffic.views >= MIN_FOR_RATE ? (\n <div className=\"mt-3\">\n <p className=\"flex items-baseline gap-2\">\n <span className=\"text-3xl font-semibold leading-none tracking-[-0.02em] tabular-nums text-[var(--text-primary)]\">\n {((traffic.orders / traffic.views) * 100).toFixed(1)}%\n </span>\n <span className=\"text-sm text-[var(--text-tertiary)]\">bought</span>\n <TrafficDelta now={traffic.views} prior={traffic.prior} />\n </p>\n\n {/* The drop-off, as three counts on one line. Where people leave is\n the whole diagnosis: visits high and carts low is a catalogue\n problem, carts high and orders low is a checkout problem, and one\n rate alone cannot tell those apart. */}\n <p className=\"mt-2 flex items-center gap-2 text-sm text-[var(--text-tertiary)]\">\n <Step value={traffic.views} label=\"visits\" />\n <ArrowRight aria-hidden className=\"h-[11px] w-[11px] shrink-0\" />\n <Step value={traffic.carts} label=\"carts\" />\n <ArrowRight aria-hidden className=\"h-[11px] w-[11px] shrink-0\" />\n <Step value={traffic.orders} label=\"orders\" />\n </p>\n\n {traffic.topSource && (\n <p className=\"mt-2 truncate text-sm text-[var(--text-muted)]\">\n Most from{' '}\n <span className=\"font-medium text-[var(--text-secondary)]\">\n {traffic.topSource.source}\n </span>{' '}\n · {Math.round(traffic.topSource.percentage)}%\n </p>\n )}\n </div>\n ) : (\n traffic && (\n // Too little traffic to divide by, so it reports the count and says\n // so rather than dressing a tiny sample as a rate.\n <div className=\"mt-3\">\n {/* TWO COUNTS, not one. Visits alone says people arrived and\n stops there; the merchant's actual question underneath it is\n whether any of them bought. Below `MIN_FOR_RATE` the two\n cannot honestly be divided into a percentage — that is what\n the line beneath says — but they can both be stated, and a\n week of 14 visits and 2 orders is a different week from 14\n and 0 in every way that matters. Same window, same request:\n `checkoutComplete` off the funnel already in the payload. */}\n {/* NO \"not enough traffic to measure conversion\" UNDER THIS.\n Cut on John's call, 2026-08-25. It explained an absence — why\n the card was not showing a percentage — to someone who had not\n asked for one and could not act on the answer. The counts are\n the card's whole claim at this stage; a line apologising for\n the number it is not showing makes a week of real visits read\n as a shortfall. The rate appears on its own once there is\n enough traffic to divide by, and needs no trailer before then. */}\n <div className=\"flex flex-wrap items-baseline gap-x-6 gap-y-1\">\n <Count\n icon={Eye}\n value={traffic.views}\n label={`${traffic.views === 1 ? 'visit' : 'visits'} this week`}\n />\n <Count\n icon={ShoppingBag}\n value={traffic.orders}\n label={traffic.orders === 1 ? 'order' : 'orders'}\n />\n </div>\n </div>\n )\n )}\n\n {/* No \"View site\" button.\n The share-card preview above IS the link — it is an `<a>` to the\n storefront and always has been — so a button underneath was a second\n control for the same click, on a card whose whole argument is that it\n shows you the thing rather than describing it. John, looking at it:\n *\"the open site button on the website card is not needed, people can\n legit click on section that shows the site\"*.\n\n \"Add your own domain\" stays, and is not the same case: it is the one\n job on this card the preview cannot do. */}\n {!hasCustomDomain && (\n <div className=\"mt-auto pt-3\">\n <PanelAction href=\"/dashboard/business/domains\">\n Add your own domain\n </PanelAction>\n </div>\n )}\n </Panel>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/dashboard/home/widgets/website-widget.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport {\n ArrowRight,\n ArrowUpRight,\n Eye,\n Globe,\n Lock,\n ShoppingBag,\n TrendingDown,\n TrendingUp,\n} from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { analyticsApi } from '@withpotter/lib/api/analytics';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { Panel, PanelAction } from '../panel';\n\n/**\n * The storefront, once there is one.\n *\n * WHY THIS WIDGET EXISTS. Deleting the \"Finish setting up\" panel only works if\n * every chore it held has somewhere better to be. Three of the four did:\n * \"add your first product\" belongs to Today, \"take payments\" is already in\n * Blocked because it stops money outright, and both are boxes whose empty state\n * the chore can occupy. \"Design your site\" and \"get a domain\" had nowhere — no\n * widget on the board was ever going to show their result — so the panel could\n * only have been relocated, not removed. This is the box those two fill.\n */\n/**\n * Views over the trailing week.\n *\n * IT FETCHES ITS OWN, a deliberate exception to the board's rule against\n * per-widget requests. That rule exists because six widgets reading money would\n * mean six copies of one request, which is why `MoneyBoardProvider` exists.\n * Nothing else on the board wants page views, so a provider for one consumer\n * would be ceremony — and `getHomeSummary`, which the money widgets share, does\n * not carry them.\n *\n * SEVEN DAYS, not the money board's window, and the two do not have to agree.\n * The money widgets share one range precisely so revenue-over-30 never appears\n * beside orders-over-7; this card is not part of that sentence.\n */\ninterface Traffic {\n /** Visits over the trailing seven days. */\n views: number;\n /** The same window, one week earlier, for the change. */\n prior: number;\n /** Distinct people, as opposed to page loads. */\n visitors: number;\n /** One point per day, oldest first. Drives the sparkline. */\n series: number[];\n /** Where most of them came from, and what share that was. */\n topSource?: { source: string; percentage: number };\n /** The page they looked at most. */\n topPage?: string;\n /** Sessions that reached a cart, and sessions that bought. */\n carts: number;\n orders: number;\n}\n\nfunction useWeeklyTraffic(enabled: boolean) {\n /*\n * Two windows, so the number can say which way it is going. A visit count\n * with no comparison is a fact you cannot act on: 40 is good or bad only\n * against last week.\n *\n * Through the query cache rather than an effect. `/analytics/dashboard` is a\n * twenty-odd-field payload and this asked for two of them on every mount of\n * the home, with nothing holding the answer — dev's double invoke made it\n * four requests for a card most merchants scroll past. Keyed by the day,\n * which is the resolution a week-long comparison has, so returning to the\n * home inside the same day costs nothing.\n */\n const iso = (d: Date) => d.toISOString().slice(0, 10);\n const today = iso(new Date());\n const windows = useMemo(() => {\n const end = new Date(`${today}T00:00:00.000Z`);\n const days = (n: number) => new Date(end.getTime() - n * 86_400_000);\n return {\n now: { startDate: iso(days(6)), endDate: today },\n before: { startDate: iso(days(13)), endDate: iso(days(7)) },\n };\n }, [today]);\n\n // Silent on failure, both of them: this card's job is the storefront, and a\n // failed traffic read must not turn a working Website card into an error.\n const { data: now } = useApiQuery(\n ['home-traffic', today],\n (slug) => analyticsApi.getDashboard(slug, windows.now),\n { enabled, retry: false },\n );\n const { data: before } = useApiQuery(\n ['home-traffic-previous', today],\n (slug) => analyticsApi.getDashboard(slug, windows.before),\n { enabled, retry: false },\n );\n\n return useMemo<Traffic | null>(() => {\n if (!now) return null;\n // All of this arrives in the SAME response as the view count, so the card\n // gets four more facts for no extra request. It would have been a waste to\n // fetch `overview` and throw the rest of the payload away.\n const sources = now.trafficSources ?? [];\n const pages = now.topPages ?? [];\n return {\n views: now.overview?.totalPageViews ?? 0,\n prior: before?.overview?.totalPageViews ?? 0,\n visitors: now.overview?.uniqueVisitors ?? 0,\n series: (now.pageViewsTimeSeries ?? []).map((p) => p.value ?? 0),\n topSource: sources.length\n ? { source: sources[0].source, percentage: sources[0].percentage }\n : undefined,\n topPage: pages.length ? pages[0].pageUrl : undefined,\n carts: now.funnel?.addToCart ?? 0,\n orders: now.funnel?.checkoutComplete ?? 0,\n };\n }, [now, before]);\n}\n\n/**\n * Week on week, and it stays silent when it cannot be honest.\n *\n * A first week has no prior to compare against, and rendering \"+100%\" against\n * zero is a number that means nothing dressed as a number that means something.\n * The analytics rule is that a figure ships only if it can change what someone\n * does next; a delta off no baseline cannot.\n */\nfunction TrafficDelta({ now, prior }: { now: number; prior: number }) {\n if (prior <= 0 || now === prior) return null;\n const pct = Math.round(((now - prior) / prior) * 100);\n const up = pct > 0;\n return (\n <span\n className={`ml-auto inline-flex items-center gap-0.5 text-sm font-medium tabular-nums ${\n up ? 'text-[var(--success)]' : 'text-[var(--text-tertiary)]'\n }`}\n >\n {up ? (\n <TrendingUp className=\"h-[11px] w-[11px]\" />\n ) : (\n <TrendingDown className=\"h-[11px] w-[11px]\" />\n )}\n {Math.abs(pct)}%\n </span>\n );\n}\n\n/**\n * Below this, a conversion rate is arithmetic on noise. One sale in nine visits\n * is 11.1%, which reads as a triumph and means nothing.\n */\nconst MIN_FOR_RATE = 30;\n\n/** One step of the funnel: the count, then what it counts. */\nfunction Step({ value, label }: { value: number; label: string }) {\n return (\n <span className=\"shrink-0\">\n <span className=\"font-medium tabular-nums text-[var(--text-secondary)]\">\n {value.toLocaleString()}\n </span>{' '}\n {label}\n </span>\n );\n}\n\n/**\n * A headline count and what it counts, for the pre-conversion state.\n *\n * THE GLYPH LEADS, and it is the one its own screen already uses: `Eye` is\n * what the Traffic tab marks views with, `ShoppingBag` is what the Activity\n * widget draws beside `order_placed`. Two counts sitting side by side are\n * told apart at a glance by shape long before anyone reads either label —\n * which is the whole reason to have them — and picking a third icon for a\n * concept the app has already settled would cost that.\n *\n * `self-center` because the row is baseline-aligned: the number and its label\n * share a baseline, which is the detail that makes them read as one phrase,\n * and a 16px glyph hung off that same line sits low against a 24px figure.\n */\nfunction Count({\n icon: Icon,\n value,\n label,\n}: {\n icon: LucideIcon;\n value: number;\n label: string;\n}) {\n return (\n <p className=\"flex items-baseline gap-2\">\n <Icon\n aria-hidden\n className=\"size-4 shrink-0 self-center text-[var(--text-muted)]\"\n />\n {/* 17px, not 20. Two of these sit side by side now, and at `text-2xl`\n they were the loudest thing in a card whose subject is the site\n above them — a week's visit count competing with the storefront it\n describes. One step down the scale (John, 2026-08-25) still reads as\n a figure against a 13px label. */}\n <span className=\"text-xl font-semibold tabular-nums tracking-[-0.02em] text-[var(--text-primary)]\">\n {value.toLocaleString()}\n </span>\n <span className=\"text-sm text-[var(--text-tertiary)]\">{label}</span>\n </p>\n );\n}\n\nexport function WebsiteWidget({\n storefrontUrl,\n hasCustomDomain,\n siteImage,\n businessName,\n}: {\n storefrontUrl?: string | null;\n hasCustomDomain: boolean;\n /** The business's own image. Absent is normal and draws a mark instead. */\n siteImage?: string | null;\n businessName?: string;\n}) {\n const { tenant } = useAuth();\n const isOpen = tenant?.status === 'active';\n const host = storefrontUrl?.replace(/^https?:\\/\\//, '').replace(/\\/$/, '');\n // Only asked for once the site is published: there is no traffic to a\n // storefront that does not exist, and asking would spend a request to be\n // told zero.\n const traffic = useWeeklyTraffic(!!storefrontUrl);\n\n return (\n <Panel\n title=\"Website\"\n className=\"h-full\"\n bodyClassName=\"flex flex-col min-h-0\"\n >\n {/*\n IT IS A LINK PREVIEW — the card that appears when somebody pastes your\n address into WhatsApp or Slack.\n\n Two designs came before this and both did too much. The first was four\n unrelated rows: a pill, a number, a link, a button. The second made the\n card a browser window — chrome, viewport, the image standing upright\n inside it — which looked like a site but answered nothing, because a\n merchant does not need to be shown that websites live in browsers.\n\n The link preview is the right frame because it is the one a merchant\n already knows and the one that matters: it is how the business appears\n the moment anyone shares it. Horizontal, image left, name and address\n right — so the card is literally a rehearsal of the thing it describes.\n\n Three facts, in the order they are wanted: what it looks like shared,\n where it lives, and whether anyone came. Nothing else.\n */}\n <a\n href={storefrontUrl ?? '#'}\n target=\"_blank\"\n rel=\"noreferrer\"\n /* THE PREVIEW IS GREY, not the panel's own fill. It carried\n `surface-interactive`, whose background is `--card-bg` — the exact\n fill of the `Panel` it sits inside — so the only thing separating\n the two was a hairline, and the card read as having no background at\n all. `--bg-muted` is the grey either side of that: #efefef on white,\n #202023 on the dark ground. The chip stands ON the panel, which is\n what a thing you can click should look like. */\n className=\"surface-interactive group/site flex items-stretch gap-0 overflow-hidden bg-[var(--bg-muted)]\"\n >\n {/* Square, and fixed. A share card's thumbnail never grows with the\n title; letting it would make two businesses' cards different shapes\n for no reason a reader could name. */}\n <span className=\"grid h-[68px] w-[68px] shrink-0 place-items-center overflow-hidden bg-[var(--bg-base)]\">\n {siteImage ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={siteImage}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-[var(--motion-slow)] ease-out group-hover/site:scale-105\"\n />\n ) : (\n <Globe className=\"size-5 text-[var(--text-muted)]\"\n strokeWidth={1.5}\n />\n )}\n </span>\n\n <span className=\"flex min-w-0 flex-1 flex-col justify-center gap-0.5 px-3 py-2\">\n <span className=\"truncate text-md font-medium text-[var(--text-primary)]\">\n {businessName ?? 'Your website'}\n </span>\n <span className=\"flex min-w-0 items-center gap-1\">\n {isOpen && host ? (\n <>\n <Lock\n aria-hidden\n className=\"h-[9px] w-[9px] shrink-0 text-[var(--success)]\"\n />\n <span className=\"min-w-0 truncate text-sm text-[var(--text-tertiary)]\">\n {host}\n </span>\n </>\n ) : (\n /* \"Not published yet\" was a FACT with no way to act on it, and\n the only control that could open the site was a modal that\n appeared once and never again. A merchant who dismissed it had\n no route to publishing from the dashboard at all.\n\n Shopify's own onboarding guidance is dismissible cards rather\n than modals, and this is already the card about the site. So\n the state and the act that resolves it are in one place. */\n /* The address exists before the site is open, and showing it\n under a padlock said the opposite of the truth. While it is\n shut the line says so; the address is no use until it is. */\n <span className=\"min-w-0 truncate text-sm text-[var(--text-tertiary)]\">\n Not open yet\n </span>\n )}\n </span>\n </span>\n\n <span className=\"grid w-8 shrink-0 place-items-center text-[var(--text-muted)] transition-colors group-hover/site:text-[var(--text-primary)]\">\n <ArrowUpRight />\n </span>\n </a>\n\n {/* NO CONTROL UNDER THE PREVIEW.\n\n There was an \"Open my site\" button here, then — after John cut it —\n the same act surviving as an \"Open it now\" text link. Both are gone\n on his call, 2026-08-25. The card's job is to SHOW the storefront;\n \"Not open yet\" is the line that states it, and publishing is a\n once-ever act that was holding a permanent row on a board where\n every other card leads with content. Opening the site lives on the\n Website hub, which the preview itself links into. */}\n\n {/* THE QUESTION TRAFFIC IS ACTUALLY ASKING.\n A visit count is a vanity number: 480 is good or bad depending\n entirely on what the 480 did, and a merchant cannot act on it. What\n they can act on is the CONVERSION — the share of people who came and\n bought — because a high number says do more of whatever brought them\n and a low one says the site is not closing.\n\n So conversion leads and visits are the context under it, which is the\n inverse of what this card did. The funnel arrives in the same request\n as the view count and was being thrown away.\n\n Only when there is enough traffic to divide by. A rate off nine\n visits is noise with a decimal point, and reporting \"11.1%\" from one\n sale in nine would be worse than reporting nothing. */}\n {traffic && traffic.views >= MIN_FOR_RATE ? (\n <div className=\"mt-3\">\n <p className=\"flex items-baseline gap-2\">\n <span className=\"text-3xl font-semibold leading-none tracking-[-0.02em] tabular-nums text-[var(--text-primary)]\">\n {((traffic.orders / traffic.views) * 100).toFixed(1)}%\n </span>\n <span className=\"text-sm text-[var(--text-tertiary)]\">bought</span>\n <TrafficDelta now={traffic.views} prior={traffic.prior} />\n </p>\n\n {/* The drop-off, as three counts on one line. Where people leave is\n the whole diagnosis: visits high and carts low is a catalogue\n problem, carts high and orders low is a checkout problem, and one\n rate alone cannot tell those apart. */}\n <p className=\"mt-2 flex items-center gap-2 text-sm text-[var(--text-tertiary)]\">\n <Step value={traffic.views} label=\"visits\" />\n <ArrowRight aria-hidden className=\"h-[11px] w-[11px] shrink-0\" />\n <Step value={traffic.carts} label=\"carts\" />\n <ArrowRight aria-hidden className=\"h-[11px] w-[11px] shrink-0\" />\n <Step value={traffic.orders} label=\"orders\" />\n </p>\n\n {traffic.topSource && (\n <p className=\"mt-2 truncate text-sm text-[var(--text-muted)]\">\n Most from{' '}\n <span className=\"font-medium text-[var(--text-secondary)]\">\n {traffic.topSource.source}\n </span>{' '}\n · {Math.round(traffic.topSource.percentage)}%\n </p>\n )}\n </div>\n ) : (\n traffic && (\n // Too little traffic to divide by, so it reports the count and says\n // so rather than dressing a tiny sample as a rate.\n <div className=\"mt-3\">\n {/* TWO COUNTS, not one. Visits alone says people arrived and\n stops there; the merchant's actual question underneath it is\n whether any of them bought. Below `MIN_FOR_RATE` the two\n cannot honestly be divided into a percentage — that is what\n the line beneath says — but they can both be stated, and a\n week of 14 visits and 2 orders is a different week from 14\n and 0 in every way that matters. Same window, same request:\n `checkoutComplete` off the funnel already in the payload. */}\n {/* NO \"not enough traffic to measure conversion\" UNDER THIS.\n Cut on John's call, 2026-08-25. It explained an absence — why\n the card was not showing a percentage — to someone who had not\n asked for one and could not act on the answer. The counts are\n the card's whole claim at this stage; a line apologising for\n the number it is not showing makes a week of real visits read\n as a shortfall. The rate appears on its own once there is\n enough traffic to divide by, and needs no trailer before then. */}\n <div className=\"flex flex-wrap items-baseline gap-x-6 gap-y-1\">\n <Count\n icon={Eye}\n value={traffic.views}\n label={`${traffic.views === 1 ? 'visit' : 'visits'} this week`}\n />\n <Count\n icon={ShoppingBag}\n value={traffic.orders}\n label={traffic.orders === 1 ? 'order' : 'orders'}\n />\n </div>\n </div>\n )\n )}\n\n {/* No \"View site\" button.\n The share-card preview above IS the link — it is an `<a>` to the\n storefront and always has been — so a button underneath was a second\n control for the same click, on a card whose whole argument is that it\n shows you the thing rather than describing it. John, looking at it:\n *\"the open site button on the website card is not needed, people can\n legit click on section that shows the site\"*.\n\n \"Add your own domain\" stays, and is not the same case: it is the one\n job on this card the preview cannot do. */}\n {!hasCustomDomain && (\n <div className=\"mt-auto pt-3\">\n <PanelAction href=\"/dashboard/business/domains\">\n Add your own domain\n </PanelAction>\n </div>\n )}\n </Panel>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +1,3 @@
1
- /**
2
- * Sections whose pages are tabs of one place, by their first path (`/dashboard/staff`). Moving
3
- * between their tabs is not an arrival: the frame stays, the content changes under it. A host puts
4
- * this around the dashboard; with none, every path arrives on its own as before.
5
- */
6
- export declare const PageTransitionGroups: import("react").Provider<readonly string[]>;
7
1
  /**
8
2
  * The arrival of a route's content.
9
3
  *
@@ -29,11 +23,6 @@ export declare const PageTransitionGroups: import("react").Provider<readonly str
29
23
  * the app was the one arrival with no motion on it. Nothing new is needed for
30
24
  * it: changing business IS an arrival, and it should arrive the way every
31
25
  * other one does.
32
- *
33
- * TABS OF ONE PLACE ARE ONE ARRIVAL. A host names such sections with `PageTransitionGroups`
34
- * (see `page-transition-key.ts`); moving between their tabs keeps the key, so the frame the tabs
35
- * share stays on screen and only what is under it changes, instead of the whole page fading in
36
- * again on every tab.
37
26
  */
38
27
  export declare function PageTransition({ children }: {
39
28
  children: React.ReactNode;
@@ -1,18 +1,12 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { createContext, useContext } from "react";
4
3
  import { usePathname } from "@withpotter/lib/use-pathname";
5
4
  import { AnimatePresence, motion } from "motion/react";
6
5
  import { springs } from "@withpotter/lib/motion";
7
6
  import { useAuth } from "@withpotter/lib/auth-context";
8
- import { transitionPath } from "./page-transition-key";
9
- const NO_GROUPS = [];
10
- const GroupsContext = createContext(NO_GROUPS);
11
- const PageTransitionGroups = GroupsContext.Provider;
12
7
  function PageTransition({ children }) {
13
8
  const pathname = usePathname();
14
9
  const { tenant } = useAuth();
15
- const groups = useContext(GroupsContext);
16
10
  return /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx(
17
11
  motion.div,
18
12
  {
@@ -22,11 +16,10 @@ function PageTransition({ children }) {
22
16
  className: "motion-reduce:transform-none",
23
17
  children
24
18
  },
25
- `${tenant?.slug ?? ""}:${transitionPath(pathname, groups)}`
19
+ `${tenant?.slug ?? ""}:${pathname}`
26
20
  ) });
27
21
  }
28
22
  export {
29
- PageTransition,
30
- PageTransitionGroups
23
+ PageTransition
31
24
  };
32
25
  //# sourceMappingURL=page-transition.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dashboard/page-transition.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext } from 'react';\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { transitionPath } from './page-transition-key';\n\nconst NO_GROUPS: readonly string[] = [];\nconst GroupsContext = createContext<readonly string[]>(NO_GROUPS);\n\n/**\n * Sections whose pages are tabs of one place, by their first path (`/dashboard/staff`). Moving\n * between their tabs is not an arrival: the frame stays, the content changes under it. A host puts\n * this around the dashboard; with none, every path arrives on its own as before.\n */\nexport const PageTransitionGroups = GroupsContext.Provider;\n\n/**\n * The arrival of a route's content.\n *\n * Navigation used to be a hard swap: the old page's pixels were replaced by the\n * new page's pixels on the same frame, which reads as a flicker rather than a\n * move — you cannot tell whether something loaded or the screen glitched.\n *\n * This is one of the cases `lib/motion.ts` reserves a spring for: the content\n * genuinely travels, so it carries velocity and settles rather than stopping\n * dead on a fixed curve. `springs.gentle` is the surface preset — cards, panels\n * and page blocks — so a route arrives the same way a sheet does.\n *\n * The travel is deliberately small (6px) and there is no exit animation: making\n * the outgoing page linger delays the incoming one, and the brand direction is\n * explicit that motion is never load-bearing. `prefers-reduced-motion` drops it\n * to a plain opacity settle.\n *\n * THE WORKSPACE IS PART OF THE KEY, not just the path.\n * John, 2026-08-23: *\"when you change a workspace why is there no motion or\n * spring physics, it just loads sharp.\"* Switching businesses is a soft swap —\n * the tenant changes in context and every consumer re-renders — so the URL\n * never moves, the key never changed, and the single biggest content change in\n * the app was the one arrival with no motion on it. Nothing new is needed for\n * it: changing business IS an arrival, and it should arrive the way every\n * other one does.\n *\n * TABS OF ONE PLACE ARE ONE ARRIVAL. A host names such sections with `PageTransitionGroups`\n * (see `page-transition-key.ts`); moving between their tabs keeps the key, so the frame the tabs\n * share stays on screen and only what is under it changes, instead of the whole page fading in\n * again on every tab.\n */\nexport function PageTransition({ children }: { children: React.ReactNode }) {\n const pathname = usePathname();\n const { tenant } = useAuth();\n const groups = useContext(GroupsContext);\n\n return (\n <AnimatePresence mode=\"wait\" initial={false}>\n <motion.div\n key={`${tenant?.slug ?? ''}:${transitionPath(pathname, groups)}`}\n initial={{ opacity: 0, y: 6 }}\n animate={{ opacity: 1, y: 0 }}\n transition={springs.gentle}\n className=\"motion-reduce:transform-none\"\n >\n {children}\n </motion.div>\n </AnimatePresence>\n );\n}\n"],"mappings":";AAyDM;AAvDN,SAAS,eAAe,kBAAkB;AAC1C,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB,cAAc;AACxC,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAE/B,MAAM,YAA+B,CAAC;AACtC,MAAM,gBAAgB,cAAiC,SAAS;AAOzD,MAAM,uBAAuB,cAAc;AAiC3C,SAAS,eAAe,EAAE,SAAS,GAAkC;AAC1E,QAAM,WAAW,YAAY;AAC7B,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,SAAS,WAAW,aAAa;AAEvC,SACE,oBAAC,mBAAgB,MAAK,QAAO,SAAS,OACpC;AAAA,IAAC,OAAO;AAAA,IAAP;AAAA,MAEC,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,YAAY,QAAQ;AAAA,MACpB,WAAU;AAAA,MAET;AAAA;AAAA,IANI,GAAG,QAAQ,QAAQ,EAAE,IAAI,eAAe,UAAU,MAAM,CAAC;AAAA,EAOhE,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/dashboard/page-transition.tsx"],"sourcesContent":["'use client';\n\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { useAuth } from '@withpotter/lib/auth-context';\n\n/**\n * The arrival of a route's content.\n *\n * Navigation used to be a hard swap: the old page's pixels were replaced by the\n * new page's pixels on the same frame, which reads as a flicker rather than a\n * move — you cannot tell whether something loaded or the screen glitched.\n *\n * This is one of the cases `lib/motion.ts` reserves a spring for: the content\n * genuinely travels, so it carries velocity and settles rather than stopping\n * dead on a fixed curve. `springs.gentle` is the surface preset — cards, panels\n * and page blocks — so a route arrives the same way a sheet does.\n *\n * The travel is deliberately small (6px) and there is no exit animation: making\n * the outgoing page linger delays the incoming one, and the brand direction is\n * explicit that motion is never load-bearing. `prefers-reduced-motion` drops it\n * to a plain opacity settle.\n *\n * THE WORKSPACE IS PART OF THE KEY, not just the path.\n * John, 2026-08-23: *\"when you change a workspace why is there no motion or\n * spring physics, it just loads sharp.\"* Switching businesses is a soft swap —\n * the tenant changes in context and every consumer re-renders — so the URL\n * never moves, the key never changed, and the single biggest content change in\n * the app was the one arrival with no motion on it. Nothing new is needed for\n * it: changing business IS an arrival, and it should arrive the way every\n * other one does.\n */\nexport function PageTransition({ children }: { children: React.ReactNode }) {\n const pathname = usePathname();\n const { tenant } = useAuth();\n\n return (\n <AnimatePresence mode=\"wait\" initial={false}>\n <motion.div\n key={`${tenant?.slug ?? ''}:${pathname}`}\n initial={{ opacity: 0, y: 6 }}\n animate={{ opacity: 1, y: 0 }}\n transition={springs.gentle}\n className=\"motion-reduce:transform-none\"\n >\n {children}\n </motion.div>\n </AnimatePresence>\n );\n}\n"],"mappings":";AAuCM;AArCN,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB,cAAc;AACxC,SAAS,eAAe;AACxB,SAAS,eAAe;AA4BjB,SAAS,eAAe,EAAE,SAAS,GAAkC;AAC1E,QAAM,WAAW,YAAY;AAC7B,QAAM,EAAE,OAAO,IAAI,QAAQ;AAE3B,SACE,oBAAC,mBAAgB,MAAK,QAAO,SAAS,OACpC;AAAA,IAAC,OAAO;AAAA,IAAP;AAAA,MAEC,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,YAAY,QAAQ;AAAA,MACpB,WAAU;AAAA,MAET;AAAA;AAAA,IANI,GAAG,QAAQ,QAAQ,EAAE,IAAI,QAAQ;AAAA,EAOxC,GACF;AAEJ;","names":[]}
@@ -6,7 +6,7 @@ const GIFT_CARD_PRESETS = [
6
6
  name: "Gold",
7
7
  imageUrl: null,
8
8
  palette: {
9
- background: "#a16207",
9
+ background: "#713f12",
10
10
  foreground: "#fffbeb",
11
11
  accent: "#fbbf24"
12
12
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/features/gift-card-artwork.tsx"],"sourcesContent":["import { Gift } from 'lucide-react';\nimport type { GiftCardDesignSnapshot } from '@withpotter/lib/api';\n\nexport const GIFT_CARD_PRESETS: GiftCardDesignSnapshot[] = [\n {\n designId: 'gold',\n name: 'Gold',\n imageUrl: null,\n palette: {\n background: '#a16207',\n foreground: '#fffbeb',\n accent: '#fbbf24',\n },\n occasion: 'general',\n },\n {\n designId: 'platinum',\n name: 'Platinum',\n imageUrl: null,\n palette: {\n background: '#334155',\n foreground: '#f8fafc',\n accent: '#cbd5e1',\n },\n occasion: 'general',\n },\n {\n designId: 'bronze',\n name: 'Bronze',\n imageUrl: null,\n palette: {\n background: '#7c2d12',\n foreground: '#fff7ed',\n accent: '#fb923c',\n },\n occasion: 'general',\n },\n {\n designId: 'silver',\n name: 'Silver',\n imageUrl: null,\n palette: {\n background: '#475569',\n foreground: '#f8fafc',\n accent: '#e2e8f0',\n },\n occasion: 'general',\n },\n];\n\ninterface GiftCardArtworkProps {\n design: GiftCardDesignSnapshot;\n merchantName: string;\n amount?: string;\n recipientName?: string;\n senderName?: string;\n code?: string;\n className?: string;\n}\n\nexport interface GiftCardArtworkData\n extends Omit<GiftCardArtworkProps, 'className'> {}\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&apos;');\n}\n\nexport function createGiftCardSvg({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n}: GiftCardArtworkData): string {\n const foreground = design.palette.foreground;\n const background = design.palette.background;\n const accent = design.palette.accent;\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"756\" viewBox=\"0 0 1200 756\">\n <defs><pattern id=\"lines\" width=\"72\" height=\"72\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\"><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"72\" stroke=\"${accent}\" stroke-opacity=\"0.14\" stroke-width=\"3\"/></pattern></defs>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"${background}\"/>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"url(#lines)\"/>\n <rect width=\"1200\" height=\"12\" rx=\"6\" fill=\"${accent}\"/>\n <circle cx=\"78\" cy=\"82\" r=\"28\" fill=\"${accent}\"/>\n <text x=\"126\" y=\"72\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"30\" font-weight=\"700\">${escapeXml(merchantName)}</text>\n <text x=\"126\" y=\"104\" fill=\"${foreground}\" fill-opacity=\"0.7\" font-family=\"Arial, sans-serif\" font-size=\"20\">GIFT CARD</text>\n ${amount ? `<text x=\"1120\" y=\"82\" text-anchor=\"end\" fill=\"${foreground}\" fill-opacity=\"0.82\" font-family=\"Arial, sans-serif\" font-size=\"26\" font-weight=\"600\">${escapeXml(amount)}</text>` : ''}\n <text x=\"72\" y=\"330\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"58\" font-weight=\"700\">${escapeXml(design.name)}</text>\n <text x=\"72\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">TO</text>\n <text x=\"72\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(recipientName || 'Someone special')}</text>\n <text x=\"600\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">FROM</text>\n <text x=\"600\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(senderName || merchantName)}</text>\n ${code ? `<text x=\"72\" y=\"664\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">REDEEM WITH</text><text x=\"72\" y=\"704\" fill=\"${foreground}\" font-family=\"monospace\" font-size=\"30\" font-weight=\"700\">${escapeXml(code)}</text>` : ''}\n</svg>`;\n}\n\nexport async function downloadGiftCardArtwork(\n data: GiftCardArtworkData,\n): Promise<void> {\n const svgBlob = new Blob([createGiftCardSvg(data)], {\n type: 'image/svg+xml;charset=utf-8',\n });\n const svgUrl = URL.createObjectURL(svgBlob);\n const image = new Image();\n await new Promise<void>((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = () => reject(new Error('Unable to render gift card image'));\n image.src = svgUrl;\n });\n\n const canvas = document.createElement('canvas');\n canvas.width = 1200;\n canvas.height = 756;\n const context = canvas.getContext('2d');\n if (!context) {\n URL.revokeObjectURL(svgUrl);\n throw new Error('Unable to create gift card image');\n }\n context.drawImage(image, 0, 0, canvas.width, canvas.height);\n URL.revokeObjectURL(svgUrl);\n\n const pngBlob = await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) =>\n blob\n ? resolve(blob)\n : reject(new Error('Unable to encode gift card image')),\n 'image/png',\n );\n });\n const url = URL.createObjectURL(pngBlob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `gift-card-${data.code || 'preview'}.png`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n}\n\nexport function createGiftCardEmailHtml(data: GiftCardArtworkData): string {\n const { design, merchantName, amount, recipientName, senderName, code } = data;\n return `<div style=\"margin:0;padding:32px 16px;background:#f4f4f5;font-family:Arial,sans-serif;color:#18181b\">\n <div style=\"max-width:620px;margin:0 auto\">\n <p style=\"margin:0 0 16px;font-size:16px\">${escapeXml(senderName || merchantName)} sent you a gift card.</p>\n <div style=\"padding:28px;border-radius:12px;background:${design.palette.background};color:${design.palette.foreground};border-top:6px solid ${design.palette.accent}\">\n <div style=\"font-size:20px;font-weight:700\">${escapeXml(merchantName)}</div>\n <div style=\"margin-top:4px;font-size:12px;opacity:.7\">GIFT CARD</div>\n <div style=\"margin-top:56px;font-size:30px;font-weight:700\">${escapeXml(design.name)}</div>\n <table role=\"presentation\" style=\"width:100%;margin-top:40px;color:inherit\"><tr>\n <td><div style=\"font-size:11px;opacity:.6\">TO</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(recipientName || 'Someone special')}</div></td>\n <td><div style=\"font-size:11px;opacity:.6\">FROM</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(senderName || merchantName)}</div></td>\n ${amount ? `<td style=\"text-align:right;font-size:16px;font-weight:600\">${escapeXml(amount)}</td>` : ''}\n </tr></table>\n ${code ? `<div style=\"margin-top:36px;font-size:11px;opacity:.6\">REDEEM WITH</div><div style=\"margin-top:6px;font-family:monospace;font-size:20px;font-weight:700\">${escapeXml(code)}</div>` : ''}\n </div>\n </div>\n</div>`;\n}\n\nexport function GiftCardArtwork({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n className = '',\n}: GiftCardArtworkProps) {\n return (\n <div\n className={`relative aspect-[1.586/1] w-full overflow-hidden rounded-md p-5 ${className}`}\n style={{\n color: design.palette.foreground,\n backgroundColor: design.palette.background,\n backgroundImage: `repeating-linear-gradient(135deg, transparent 0 18px, ${design.palette.accent}22 18px 19px)`,\n }}\n >\n {design.imageUrl && (\n <img\n src={design.imageUrl}\n alt=\"\"\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n )}\n <div\n className=\"absolute inset-x-0 top-0 h-1\"\n style={{ backgroundColor: design.palette.accent }}\n />\n <div className=\"relative flex h-full flex-col justify-between\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span\n className=\"flex h-7 w-7 items-center justify-center rounded-full\"\n style={{\n color: design.palette.background,\n backgroundColor: design.palette.accent,\n }}\n >\n <Gift className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold\">{merchantName}</p>\n <p className=\"text-[10px] font-medium uppercase opacity-65\">\n Gift card\n </p>\n </div>\n </div>\n {amount && (\n <span className=\"shrink-0 text-sm font-medium opacity-80\">\n {amount}\n </span>\n )}\n </div>\n <div>\n <p className=\"text-lg font-semibold\">{design.name}</p>\n <div className=\"mt-3 grid grid-cols-2 gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">To</p>\n <p className=\"truncate text-sm font-medium\">\n {recipientName || 'Someone special'}\n </p>\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">\n From\n </p>\n <p className=\"truncate text-sm font-medium\">\n {senderName || merchantName}\n </p>\n </div>\n </div>\n {code && <p className=\"mt-2 truncate font-mono text-xs opacity-70\">{code}</p>}\n </div>\n </div>\n </div>\n );\n}"],"mappings":"AAyLQ,cAsBI,YAtBJ;AAzLR,SAAS,YAAY;AAGd,MAAM,oBAA8C;AAAA,EACzD;AAAA,IACE,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV;AAAA,IACA,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV;AAAA,IACA,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV;AAAA,IACA,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAeA,SAAS,UAAU,OAAuB;AACxC,SAAO,MACJ,WAAW,KAAK,OAAO,EACvB,WAAW,KAAK,MAAM,EACtB,WAAW,KAAK,MAAM,EACtB,WAAW,KAAK,QAAQ,EACxB,WAAW,KAAK,QAAQ;AAC7B;AAEO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,aAAa,OAAO,QAAQ;AAClC,QAAM,aAAa,OAAO,QAAQ;AAClC,QAAM,SAAS,OAAO,QAAQ;AAE9B,SAAO;AAAA,4JACmJ,MAAM;AAAA,kDAChH,UAAU;AAAA;AAAA,gDAEZ,MAAM;AAAA,yCACb,MAAM;AAAA,+BAChB,UAAU,sEAAsE,UAAU,YAAY,CAAC;AAAA,gCACtG,UAAU;AAAA,IACtC,SAAS,iDAAiD,UAAU,0FAA0F,UAAU,MAAM,CAAC,YAAY,EAAE;AAAA,+BAClK,UAAU,sEAAsE,UAAU,OAAO,IAAI,CAAC;AAAA,+BACtG,UAAU;AAAA,+BACV,UAAU,sEAAsE,UAAU,iBAAiB,iBAAiB,CAAC;AAAA,gCAC5H,UAAU;AAAA,gCACV,UAAU,sEAAsE,UAAU,cAAc,YAAY,CAAC;AAAA,IACjJ,OAAO,8BAA8B,UAAU,uIAAuI,UAAU,8DAA8D,UAAU,IAAI,CAAC,YAAY,EAAE;AAAA;AAE/R;AAEA,eAAsB,wBACpB,MACe;AACf,QAAM,UAAU,IAAI,KAAK,CAAC,kBAAkB,IAAI,CAAC,GAAG;AAAA,IAClD,MAAM;AAAA,EACR,CAAC;AACD,QAAM,SAAS,IAAI,gBAAgB,OAAO;AAC1C,QAAM,QAAQ,IAAI,MAAM;AACxB,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,UAAM,SAAS,MAAM,QAAQ;AAC7B,UAAM,UAAU,MAAM,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAC1E,UAAM,MAAM;AAAA,EACd,CAAC;AAED,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ;AACf,SAAO,SAAS;AAChB,QAAM,UAAU,OAAO,WAAW,IAAI;AACtC,MAAI,CAAC,SAAS;AACZ,QAAI,gBAAgB,MAAM;AAC1B,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AACA,UAAQ,UAAU,OAAO,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAC1D,MAAI,gBAAgB,MAAM;AAE1B,QAAM,UAAU,MAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3D,WAAO;AAAA,MACL,CAAC,SACC,OACI,QAAQ,IAAI,IACZ,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,MAAM,IAAI,gBAAgB,OAAO;AACvC,QAAM,OAAO,SAAS,cAAc,GAAG;AACvC,OAAK,OAAO;AACZ,OAAK,WAAW,aAAa,KAAK,QAAQ,SAAS;AACnD,WAAS,KAAK,YAAY,IAAI;AAC9B,OAAK,MAAM;AACX,OAAK,OAAO;AACZ,aAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,CAAC;AAC9C;AAEO,SAAS,wBAAwB,MAAmC;AACzE,QAAM,EAAE,QAAQ,cAAc,QAAQ,eAAe,YAAY,KAAK,IAAI;AAC1E,SAAO;AAAA;AAAA,gDAEuC,UAAU,cAAc,YAAY,CAAC;AAAA,6DACxB,OAAO,QAAQ,UAAU,UAAU,OAAO,QAAQ,UAAU,yBAAyB,OAAO,QAAQ,MAAM;AAAA,oDACnH,UAAU,YAAY,CAAC;AAAA;AAAA,oEAEP,UAAU,OAAO,IAAI,CAAC;AAAA;AAAA,wHAE8B,UAAU,iBAAiB,iBAAiB,CAAC;AAAA,0HAC3C,UAAU,cAAc,YAAY,CAAC;AAAA,UACrJ,SAAS,+DAA+D,UAAU,MAAM,CAAC,UAAU,EAAE;AAAA;AAAA,QAEvG,OAAO,4JAA4J,UAAU,IAAI,CAAC,WAAW,EAAE;AAAA;AAAA;AAAA;AAIvM;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAAyB;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,mEAAmE,SAAS;AAAA,MACvF,OAAO;AAAA,QACL,OAAO,OAAO,QAAQ;AAAA,QACtB,iBAAiB,OAAO,QAAQ;AAAA,QAChC,iBAAiB,yDAAyD,OAAO,QAAQ,MAAM;AAAA,MACjG;AAAA,MAEC;AAAA,eAAO,YACN;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,OAAO;AAAA,YACZ,KAAI;AAAA,YACJ,WAAU;AAAA;AAAA,QACZ;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,EAAE,iBAAiB,OAAO,QAAQ,OAAO;AAAA;AAAA,QAClD;AAAA,QACA,qBAAC,SAAI,WAAU,iDACb;AAAA,+BAAC,SAAI,WAAU,2CACb;AAAA,iCAAC,SAAI,WAAU,mCACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,OAAO;AAAA,oBACL,OAAO,OAAO,QAAQ;AAAA,oBACtB,iBAAiB,OAAO,QAAQ;AAAA,kBAClC;AAAA,kBAEA,8BAAC,QAAK,WAAU,WAAU;AAAA;AAAA,cAC5B;AAAA,cACA,qBAAC,SAAI,WAAU,WACb;AAAA,oCAAC,OAAE,WAAU,kCAAkC,wBAAa;AAAA,gBAC5D,oBAAC,OAAE,WAAU,gDAA+C,uBAE5D;AAAA,iBACF;AAAA,eACF;AAAA,YACC,UACC,oBAAC,UAAK,WAAU,2CACb,kBACH;AAAA,aAEJ;AAAA,UACA,qBAAC,SACC;AAAA,gCAAC,OAAE,WAAU,yBAAyB,iBAAO,MAAK;AAAA,YAClD,qBAAC,SAAI,WAAU,+BACb;AAAA,mCAAC,SAAI,WAAU,WACb;AAAA,oCAAC,OAAE,WAAU,kDAAiD,gBAAE;AAAA,gBAChE,oBAAC,OAAE,WAAU,gCACV,2BAAiB,mBACpB;AAAA,iBACF;AAAA,cACA,qBAAC,SAAI,WAAU,WACb;AAAA,oCAAC,OAAE,WAAU,kDAAiD,kBAE9D;AAAA,gBACA,oBAAC,OAAE,WAAU,gCACV,wBAAc,cACjB;AAAA,iBACF;AAAA,eACF;AAAA,YACC,QAAQ,oBAAC,OAAE,WAAU,8CAA8C,gBAAK;AAAA,aAC3E;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/features/gift-card-artwork.tsx"],"sourcesContent":["import { Gift } from 'lucide-react';\nimport type { GiftCardDesignSnapshot } from '@withpotter/lib/api';\n\nexport const GIFT_CARD_PRESETS: GiftCardDesignSnapshot[] = [\n {\n designId: 'gold',\n name: 'Gold',\n imageUrl: null,\n palette: {\n background: '#713f12',\n foreground: '#fffbeb',\n accent: '#fbbf24',\n },\n occasion: 'general',\n },\n {\n designId: 'platinum',\n name: 'Platinum',\n imageUrl: null,\n palette: {\n background: '#334155',\n foreground: '#f8fafc',\n accent: '#cbd5e1',\n },\n occasion: 'general',\n },\n {\n designId: 'bronze',\n name: 'Bronze',\n imageUrl: null,\n palette: {\n background: '#7c2d12',\n foreground: '#fff7ed',\n accent: '#fb923c',\n },\n occasion: 'general',\n },\n {\n designId: 'silver',\n name: 'Silver',\n imageUrl: null,\n palette: {\n background: '#475569',\n foreground: '#f8fafc',\n accent: '#e2e8f0',\n },\n occasion: 'general',\n },\n];\n\ninterface GiftCardArtworkProps {\n design: GiftCardDesignSnapshot;\n merchantName: string;\n amount?: string;\n recipientName?: string;\n senderName?: string;\n code?: string;\n className?: string;\n}\n\nexport interface GiftCardArtworkData\n extends Omit<GiftCardArtworkProps, 'className'> {}\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&apos;');\n}\n\nexport function createGiftCardSvg({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n}: GiftCardArtworkData): string {\n const foreground = design.palette.foreground;\n const background = design.palette.background;\n const accent = design.palette.accent;\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"756\" viewBox=\"0 0 1200 756\">\n <defs><pattern id=\"lines\" width=\"72\" height=\"72\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\"><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"72\" stroke=\"${accent}\" stroke-opacity=\"0.14\" stroke-width=\"3\"/></pattern></defs>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"${background}\"/>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"url(#lines)\"/>\n <rect width=\"1200\" height=\"12\" rx=\"6\" fill=\"${accent}\"/>\n <circle cx=\"78\" cy=\"82\" r=\"28\" fill=\"${accent}\"/>\n <text x=\"126\" y=\"72\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"30\" font-weight=\"700\">${escapeXml(merchantName)}</text>\n <text x=\"126\" y=\"104\" fill=\"${foreground}\" fill-opacity=\"0.7\" font-family=\"Arial, sans-serif\" font-size=\"20\">GIFT CARD</text>\n ${amount ? `<text x=\"1120\" y=\"82\" text-anchor=\"end\" fill=\"${foreground}\" fill-opacity=\"0.82\" font-family=\"Arial, sans-serif\" font-size=\"26\" font-weight=\"600\">${escapeXml(amount)}</text>` : ''}\n <text x=\"72\" y=\"330\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"58\" font-weight=\"700\">${escapeXml(design.name)}</text>\n <text x=\"72\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">TO</text>\n <text x=\"72\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(recipientName || 'Someone special')}</text>\n <text x=\"600\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">FROM</text>\n <text x=\"600\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(senderName || merchantName)}</text>\n ${code ? `<text x=\"72\" y=\"664\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">REDEEM WITH</text><text x=\"72\" y=\"704\" fill=\"${foreground}\" font-family=\"monospace\" font-size=\"30\" font-weight=\"700\">${escapeXml(code)}</text>` : ''}\n</svg>`;\n}\n\nexport async function downloadGiftCardArtwork(\n data: GiftCardArtworkData,\n): Promise<void> {\n const svgBlob = new Blob([createGiftCardSvg(data)], {\n type: 'image/svg+xml;charset=utf-8',\n });\n const svgUrl = URL.createObjectURL(svgBlob);\n const image = new Image();\n await new Promise<void>((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = () => reject(new Error('Unable to render gift card image'));\n image.src = svgUrl;\n });\n\n const canvas = document.createElement('canvas');\n canvas.width = 1200;\n canvas.height = 756;\n const context = canvas.getContext('2d');\n if (!context) {\n URL.revokeObjectURL(svgUrl);\n throw new Error('Unable to create gift card image');\n }\n context.drawImage(image, 0, 0, canvas.width, canvas.height);\n URL.revokeObjectURL(svgUrl);\n\n const pngBlob = await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) =>\n blob\n ? resolve(blob)\n : reject(new Error('Unable to encode gift card image')),\n 'image/png',\n );\n });\n const url = URL.createObjectURL(pngBlob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `gift-card-${data.code || 'preview'}.png`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n}\n\nexport function createGiftCardEmailHtml(data: GiftCardArtworkData): string {\n const { design, merchantName, amount, recipientName, senderName, code } = data;\n return `<div style=\"margin:0;padding:32px 16px;background:#f4f4f5;font-family:Arial,sans-serif;color:#18181b\">\n <div style=\"max-width:620px;margin:0 auto\">\n <p style=\"margin:0 0 16px;font-size:16px\">${escapeXml(senderName || merchantName)} sent you a gift card.</p>\n <div style=\"padding:28px;border-radius:12px;background:${design.palette.background};color:${design.palette.foreground};border-top:6px solid ${design.palette.accent}\">\n <div style=\"font-size:20px;font-weight:700\">${escapeXml(merchantName)}</div>\n <div style=\"margin-top:4px;font-size:12px;opacity:.7\">GIFT CARD</div>\n <div style=\"margin-top:56px;font-size:30px;font-weight:700\">${escapeXml(design.name)}</div>\n <table role=\"presentation\" style=\"width:100%;margin-top:40px;color:inherit\"><tr>\n <td><div style=\"font-size:11px;opacity:.6\">TO</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(recipientName || 'Someone special')}</div></td>\n <td><div style=\"font-size:11px;opacity:.6\">FROM</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(senderName || merchantName)}</div></td>\n ${amount ? `<td style=\"text-align:right;font-size:16px;font-weight:600\">${escapeXml(amount)}</td>` : ''}\n </tr></table>\n ${code ? `<div style=\"margin-top:36px;font-size:11px;opacity:.6\">REDEEM WITH</div><div style=\"margin-top:6px;font-family:monospace;font-size:20px;font-weight:700\">${escapeXml(code)}</div>` : ''}\n </div>\n </div>\n</div>`;\n}\n\nexport function GiftCardArtwork({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n className = '',\n}: GiftCardArtworkProps) {\n return (\n <div\n className={`relative aspect-[1.586/1] w-full overflow-hidden rounded-md p-5 ${className}`}\n style={{\n color: design.palette.foreground,\n backgroundColor: design.palette.background,\n backgroundImage: `repeating-linear-gradient(135deg, transparent 0 18px, ${design.palette.accent}22 18px 19px)`,\n }}\n >\n {design.imageUrl && (\n <img\n src={design.imageUrl}\n alt=\"\"\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n )}\n <div\n className=\"absolute inset-x-0 top-0 h-1\"\n style={{ backgroundColor: design.palette.accent }}\n />\n <div className=\"relative flex h-full flex-col justify-between\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span\n className=\"flex h-7 w-7 items-center justify-center rounded-full\"\n style={{\n color: design.palette.background,\n backgroundColor: design.palette.accent,\n }}\n >\n <Gift className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold\">{merchantName}</p>\n <p className=\"text-[10px] font-medium uppercase opacity-65\">\n Gift card\n </p>\n </div>\n </div>\n {amount && (\n <span className=\"shrink-0 text-sm font-medium opacity-80\">\n {amount}\n </span>\n )}\n </div>\n <div>\n <p className=\"text-lg font-semibold\">{design.name}</p>\n <div className=\"mt-3 grid grid-cols-2 gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">To</p>\n <p className=\"truncate text-sm font-medium\">\n {recipientName || 'Someone special'}\n </p>\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">\n From\n </p>\n <p className=\"truncate text-sm font-medium\">\n {senderName || merchantName}\n </p>\n </div>\n </div>\n {code && <p className=\"mt-2 truncate font-mono text-xs opacity-70\">{code}</p>}\n </div>\n </div>\n </div>\n );\n}"],"mappings":"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","names":[]}