@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.
- package/dist/billing/overview-tab.js +3 -4
- package/dist/billing/overview-tab.js.map +1 -1
- package/dist/billing/plans-tab.js +12 -6
- package/dist/billing/plans-tab.js.map +1 -1
- package/dist/charts/index.d.ts +1 -1
- package/dist/charts/index.js +1 -3
- package/dist/charts/index.js.map +1 -1
- package/dist/dashboard/activity-feed.d.ts +0 -2
- package/dist/dashboard/activity-feed.js +1 -4
- package/dist/dashboard/activity-feed.js.map +1 -1
- package/dist/dashboard/home/ask-controls.js +0 -1
- 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 +1 -9
- package/dist/dashboard/home/home-quick-actions.js.map +1 -1
- package/dist/dashboard/home/widgets/activity-widget.js +43 -56
- package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
- package/dist/dashboard/home/widgets/home-widgets.js +2 -3
- package/dist/dashboard/home/widgets/home-widgets.js.map +1 -1
- package/dist/dashboard/home/widgets/money-widgets.js +4 -9
- package/dist/dashboard/home/widgets/money-widgets.js.map +1 -1
- package/dist/dashboard/home/widgets/registry.d.ts +3 -6
- package/dist/dashboard/home/widgets/registry.js +2 -12
- package/dist/dashboard/home/widgets/registry.js.map +1 -1
- package/dist/dashboard/home/widgets/team-widget.js +3 -15
- package/dist/dashboard/home/widgets/team-widget.js.map +1 -1
- package/dist/dashboard/home/widgets/types.d.ts +1 -13
- package/dist/dashboard/home/widgets/types.js.map +1 -1
- package/dist/dashboard/home/widgets/use-home-layout.js +4 -9
- package/dist/dashboard/home/widgets/use-home-layout.js.map +1 -1
- package/dist/dashboard/home/widgets/website-widget.js +1 -9
- package/dist/dashboard/home/widgets/website-widget.js.map +1 -1
- package/dist/dashboard/page-transition.d.ts +0 -11
- package/dist/dashboard/page-transition.js +2 -9
- package/dist/dashboard/page-transition.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-cards-feature.js +518 -656
- package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
- package/dist/features/product-form.js +1 -1
- package/dist/features/product-form.js.map +1 -1
- package/dist/mobile/screens/billing-flows.js +6 -7
- package/dist/mobile/screens/billing-flows.js.map +1 -1
- package/dist/mobile/screens/billing.js +2 -2
- package/dist/mobile/screens/billing.js.map +1 -1
- package/dist/notifications-dropdown.js +1 -11
- package/dist/notifications-dropdown.js.map +1 -1
- package/dist/shell/notification-kinds.d.ts +2 -11
- package/dist/shell/notification-kinds.js +1 -16
- package/dist/shell/notification-kinds.js.map +1 -1
- package/dist/shell/notification-row.d.ts +2 -6
- package/dist/shell/notification-row.js +33 -59
- package/dist/shell/notification-row.js.map +1 -1
- package/dist/shell/product-switcher.js +0 -3
- package/dist/shell/product-switcher.js.map +1 -1
- package/dist/shell/settings-modal-context.d.ts +0 -2
- package/dist/shell/settings-modal-context.js +2 -4
- package/dist/shell/settings-modal-context.js.map +1 -1
- package/dist/shell/settings-panels.js +9 -29
- package/dist/shell/settings-panels.js.map +1 -1
- package/dist/shell/settings-rows.d.ts +2 -7
- package/dist/shell/settings-rows.js +36 -93
- package/dist/shell/settings-rows.js.map +1 -1
- package/dist/shell/shell.css +0 -192
- package/dist/shell/sidebar-footer.js +2 -6
- package/dist/shell/sidebar-footer.js.map +1 -1
- package/dist/shell/unified-settings-modal.d.ts +2 -3
- package/dist/shell/unified-settings-modal.js +74 -151
- package/dist/shell/unified-settings-modal.js.map +1 -1
- package/dist/sidebar/config.js +3 -19
- package/dist/sidebar/config.js.map +1 -1
- package/dist/sidebar/more-group.js +1 -1
- package/dist/sidebar/more-group.js.map +1 -1
- package/dist/sidebar/nav-renderers.js +0 -2
- package/dist/sidebar/nav-renderers.js.map +1 -1
- package/dist/sidebar/nav-section.js +3 -5
- package/dist/sidebar/nav-section.js.map +1 -1
- package/dist/sidebar/setup-progress.d.ts +1 -3
- package/dist/sidebar/setup-progress.js +2 -2
- package/dist/sidebar/setup-progress.js.map +1 -1
- package/dist/sidebar/spine-groups.js +1 -7
- package/dist/sidebar/spine-groups.js.map +1 -1
- package/dist/sidebar.js +5 -5
- package/dist/sidebar.js.map +1 -1
- package/dist/templates/template-live-thumb.js +5 -16
- package/dist/templates/template-live-thumb.js.map +1 -1
- package/package.json +5 -5
- package/dist/dashboard/activity-paid.d.ts +0 -10
- package/dist/dashboard/activity-paid.js +0 -7
- package/dist/dashboard/activity-paid.js.map +0 -1
- package/dist/dashboard/home/host-quick-actions.d.ts +0 -19
- package/dist/dashboard/home/host-quick-actions.js +0 -12
- package/dist/dashboard/home/host-quick-actions.js.map +0 -1
- package/dist/dashboard/home/widgets/host-widgets.d.ts +0 -6
- package/dist/dashboard/home/widgets/host-widgets.js +0 -16
- package/dist/dashboard/home/widgets/host-widgets.js.map +0 -1
- package/dist/dashboard/home/widgets/member-label.d.ts +0 -11
- package/dist/dashboard/home/widgets/member-label.js +0 -10
- package/dist/dashboard/home/widgets/member-label.js.map +0 -1
- package/dist/dashboard/home/widgets/middle-ellipsis.d.ts +0 -5
- package/dist/dashboard/home/widgets/middle-ellipsis.js +0 -11
- package/dist/dashboard/home/widgets/middle-ellipsis.js.map +0 -1
- package/dist/dashboard/home/widgets/revenue-series.d.ts +0 -12
- package/dist/dashboard/home/widgets/revenue-series.js +0 -26
- package/dist/dashboard/home/widgets/revenue-series.js.map +0 -1
- package/dist/dashboard/page-transition-key.d.ts +0 -7
- package/dist/dashboard/page-transition-key.js +0 -11
- package/dist/dashboard/page-transition-key.js.map +0 -1
- package/dist/features/gift-cards/gift-card-logic.d.ts +0 -41
- package/dist/features/gift-cards/gift-card-logic.js +0 -65
- package/dist/features/gift-cards/gift-card-logic.js.map +0 -1
- package/dist/features/gift-cards/hint.d.ts +0 -9
- package/dist/features/gift-cards/hint.js +0 -25
- package/dist/features/gift-cards/hint.js.map +0 -1
- package/dist/shell/notification-actions.d.ts +0 -12
- package/dist/shell/notification-actions.js +0 -18
- package/dist/shell/notification-actions.js.map +0 -1
- package/dist/shell/settings-groups.d.ts +0 -23
- package/dist/shell/settings-groups.js +0 -22
- package/dist/shell/settings-groups.js.map +0 -1
- package/dist/shell/settings-host.d.ts +0 -39
- package/dist/shell/settings-host.js +0 -37
- package/dist/shell/settings-host.js.map +0 -1
- package/dist/shell/sidebar-cards.d.ts +0 -32
- package/dist/shell/sidebar-cards.js +0 -37
- package/dist/shell/sidebar-cards.js.map +0 -1
- package/dist/templates/template-tile-source.d.ts +0 -24
- package/dist/templates/template-tile-source.js +0 -18
- package/dist/templates/template-tile-source.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/home-ask.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState, type RefObject } from 'react';\nimport Link from 'next/link';\nimport { Info, X } from 'lucide-react';\nimport { AutoGrowTextarea } from '@withpotter/ui-kit/auto-grow-textarea';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { AskControls } from './ask-controls';\nimport { AskGlow } from './ask-glow';\nimport { AskModeChip, type AskMode } from './ask-mode-chip';\nimport { AskSendButton } from './ask-send-button';\nimport { AskSupportAttachments } from './ask-support-attachments';\nimport { useAskSupport } from './use-ask-support';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { SUPPORT_NAME } from '@withpotter/lib/support';\nimport { AskResults } from './ask-results';\nimport { SearchTips } from './search-tips';\nimport { HomeQuickActions } from './home-quick-actions';\n\n/**\n * The home's one box: a search, and a way to reach support.\n *\n * SEARCH IS THE DEFAULT. Clicking the field folds the board away and fills the\n * room with what has been happening, then with what matches as you type, and a\n * row opens that record. See `AskResults` and `SearchTips`.\n *\n * SUPPORT IS A MODE, chosen on the pill beside the paperclip or entered by\n * using the mic or the paperclip, neither of which can be searched for. In it\n * the round send button grows into \"Send to support\" and opens a case through\n * the same call the Support page uses, with the message, and the voice note or\n * picture, attached. A voice note can go with no words; a picture cannot, and\n * the card says so under it. The mechanics live in `useAskSupport`.\n *\n * ENTER SENDS, SHIFT+ENTER BREAKS THE LINE. In search it only keeps the caret\n * where it is, because the list under the box is the answer; an IME\n * composition's Enter is neither and is left alone.\n *\n * THE NOTICE STRIP IS DRIVEN BY `usage`, and renders nothing without it. There\n * is no usage figure to read yet, so the home passes none; when there is,\n * handing it in is the whole of the wiring.\n */\nexport function HomeAsk({\n /** Optional handle, so the page header's Box control can focus this. */\n inputRef: externalRef,\n usage,\n onOpenChange,\n}: {\n /** The search opened or closed; the page folds the board away on open. */\n onOpenChange?: (open: boolean) => void;\n inputRef?: RefObject<HTMLTextAreaElement | null>;\n /** Share of the allowance used, and where \"View usage\" goes. */\n usage?: { percent: number; href: string };\n}) {\n const orderLabel = useEntityLabel('order');\n const itemLabel = useEntityLabel('item');\n /* SEARCH WORDING, NOT QUESTIONS. This field finds records, so what it\n suggests is what can be found; `askPlaceholders` are things to ask the\n assistant and no longer belong here. */\n const prompts = useMemo(\n () => [\n `Search ${orderLabel.plural.toLowerCase()} by customer name`,\n 'Search customers by name or email',\n `Search ${itemLabel.plural.toLowerCase()} by name or SKU`,\n `Find an invoice by number`,\n ],\n [orderLabel, itemLabel],\n );\n const focusedPrompt = `Search ${orderLabel.plural.toLowerCase()}, customers, ${itemLabel.plural.toLowerCase()}…`;\n const [i, setI] = useState(0);\n const [value, setValue] = useState('');\n const [focused, setFocused] = useState(false);\n const [noticeDismissed, setNoticeDismissed] = useState(false);\n /* What the box is for. Search unless someone chose Support, or put a voice\n note or a picture on the card, which can only go to support. */\n const [mode, setMode] = useState<AskMode>('search');\n const ownRef = useRef<HTMLTextAreaElement>(null);\n const inputRef = externalRef ?? ownRef;\n const root = useRef<HTMLDivElement>(null);\n\n /* THE SEARCH, NOT A CONVERSATION. Focusing the field opens it: the page\n folds the board away and the room holds what has been happening, then what\n matches as you type. It stays open across a blur (pressing a result blurs\n the field before the click lands) and closes on Escape, a press outside\n this card and its list, or navigating. */\n const [searching, setSearching] = useState(false);\n const setOpen = (next: boolean) => {\n setSearching(next);\n onOpenChange?.(next);\n };\n\n /* After a case goes and the green \"Sent\" has had its moment, the box is\n back to what it normally is: a search. */\n const support = useAskSupport(value, setValue, () => setMode('search'));\n const inSupport = mode === 'support' || support.engaged;\n const recording = support.composer.recordingNow;\n const voice = {\n supported: support.voiceSupported,\n active: recording,\n start: () => {\n setMode('support');\n support.startVoice();\n },\n stop: support.stopVoice,\n /* One clip at a time: remove it to record another. */\n disabled:\n Boolean(support.composer.voice) || support.composer.recorder.isBusy,\n };\n /* The latest of these, for the handlers below that were created on an\n earlier render and would otherwise close over stale ones. */\n const latest = useRef({ mode, engaged: false, value: '' });\n useEffect(() => {\n latest.current = { mode, engaged: support.engaged, value };\n });\n /* CLOSING THE SEARCH ENDS A SUPPORT MODE NOBODY IS USING. Support is sticky\n on purpose while there is something in the box, but a pill left on Support\n after the box was emptied and closed hid the carousel next time with no\n sign of why, and the bottom of the page just sat blank. If there are words\n or an attachment, the mode stays; if not, the box goes back to searching. */\n const closeSearch = () => {\n setOpen(false);\n const l = latest.current;\n if (l.mode === 'support' && !l.engaged && !l.value.trim()) {\n setMode('search');\n }\n };\n useEffect(() => {\n if (!searching) return;\n /* STAYS OPEN ONLY FOR A PRESS ON SOMETHING. The search block is wider and\n taller than anything in it: it holds the empty gap under the box and the\n carousel's own background, and a press there is a press on nothing. So\n \"inside\" is the card itself or a real control in this block (a link, a\n button, a field), plus the Add menu, which is portaled out of it. */\n const onDown = (e: PointerEvent) => {\n const t = e.target as HTMLElement;\n const inside =\n !!root.current?.contains(t) &&\n !!t.closest('[data-ask-card],a,button,input,textarea,[role=\"menu\"]');\n const inMenu = !!t.closest('[role=\"menu\"]');\n if (!inside && !inMenu) closeSearch();\n };\n document.addEventListener('pointerdown', onDown);\n return () => document.removeEventListener('pointerdown', onDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [searching]);\n\n const supportPrompt = recording\n ? 'Listening. What you say appears here…'\n : 'Tell support what you need…';\n\n /* The rotation stops the moment there is a caret in the field: a placeholder\n that keeps changing under one reads as the field arguing with you. */\n useEffect(() => {\n if (focused || value || inSupport) return;\n const t = setInterval(() => setI((n) => (n + 1) % prompts.length), 4200);\n return () => clearInterval(t);\n }, [focused, value, inSupport, prompts.length]);\n\n const canSearchSend = value.trim().length > 0;\n\n /* No hand-off to the ask screen: this field searches. Enter keeps the\n caret where it is and the list under it is the answer. */\n const submit = () => {\n inputRef.current?.focus();\n };\n\n const showNotice = !!usage && !noticeDismissed;\n\n return (\n <div ref={root} className=\"relative isolate w-full\">\n {showNotice && (\n <div className=\"flex h-11 items-center gap-3 rounded-t-[var(--radius-card)] bg-[var(--bg-muted)] pb-3 pl-4 pr-3 text-sm text-[var(--text-primary)]\">\n <Info\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-tertiary)]\"\n />\n <p className=\"min-w-0 flex-1 truncate\">\n You've used {Math.round(usage.percent)}% of your tokens.{' '}\n <Link href={usage.href} className=\"text-brand-500 hover:underline\">\n View usage\n </Link>\n </p>\n <button\n type=\"button\"\n onClick={() => setNoticeDismissed(true)}\n aria-label=\"Dismiss\"\n className=\"grid size-6 shrink-0 place-items-center rounded-md text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n )}\n\n {/* The card overlaps the strip's bottom edge, which is why the strip\n carries a padding-bottom it never shows: the card's top border runs\n through the strip, as drawn. */}\n <div\n data-ask-card\n className={`group relative rounded-[var(--radius-card)] border border-[var(--border-default)] bg-[color-mix(in_srgb,var(--bg-muted)_55%,var(--bg-base))] transition-colors focus-within:border-[color-mix(in_srgb,var(--brand-500)_45%,var(--text-muted))] ${\n showNotice ? '-mt-3' : ''\n }`}\n >\n {/* The rings sit at -z-10 behind this card's OPAQUE fill, which is what\n keeps the centre clear. They bloom on hover and on focus. */}\n <AskGlow />\n <AskSupportAttachments support={support} />\n <div className=\"relative px-4 pt-3\">\n <AutoGrowTextarea\n ref={inputRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => {\n setFocused(true);\n setOpen(true);\n }}\n onBlur={() => setFocused(false)}\n /* Focus alone is not enough: close the search with the box still\n focused (a press on a non-focusable part of the page can leave\n it so) and clicking back in fires no focus event, so nothing\n opened and the bottom stayed blank. */\n onClick={() => {\n if (!searching) setOpen(true);\n }}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n if (inSupport) void support.send();\n else submit();\n }\n if (e.key === 'Escape') {\n /* A message to support is somebody's words: closing the box\n does not throw them away. */\n if (!inSupport) setValue('');\n closeSearch();\n e.currentTarget.blur();\n }\n }}\n aria-label={inSupport ? 'Message to support' : 'Search'}\n className=\"block min-h-[36px] w-full bg-transparent text-md text-[var(--text-primary)] outline-none\"\n />\n {/* Crossfading placeholders, stacked so the card never changes\n height and the rotation cannot reflow it. Rendered beside the\n field rather than as its `placeholder`, which cannot animate. */}\n {!value && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-4 top-3 flex items-start\"\n >\n {(inSupport\n ? [supportPrompt]\n : focused\n ? [focusedPrompt]\n : prompts\n ).map((p, n) => (\n <span\n key={p}\n className={`block truncate text-md text-[var(--text-muted)] transition-opacity duration-[var(--motion-slow)] ${\n focused || inSupport || n === i\n ? 'opacity-100'\n : 'absolute inset-0 opacity-0'\n }`}\n >\n {p}\n </span>\n ))}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 px-3 pb-2 pt-1\">\n {/* The plus that attached a picture is out of this row for now, on\n John's call, 2026-10-09. Sending a picture to support still works\n underneath (`support.composer.pickImage`, and the card's\n attachment frame); put a button back here to reach it. */}\n <AskModeChip\n mode={inSupport ? 'support' : 'search'}\n onChange={setMode}\n locked={support.engaged}\n />\n\n <div className=\"ml-auto flex items-center gap-2\">\n <AskControls\n voice={voice}\n voiceLabels={{\n start: 'Record a voice note for support',\n stop: 'Stop recording',\n }}\n value={value}\n canSend={canSearchSend}\n engaged={false}\n inputRef={inputRef}\n onSubmit={submit}\n onClose={() => inputRef.current?.blur()}\n /* SEARCH HAS NO SEND BUTTON. Results update as you type, so an\n arrow there was a control that did nothing and made the box\n read as a chat input. It appears in Message support, where\n pressing it really sends. */\n send={\n !inSupport ? (\n <></>\n ) : (\n <AskSendButton\n support={inSupport}\n canSend={inSupport ? support.canSend : canSearchSend}\n sending={support.sending}\n sent={support.sent}\n title={\n support.needsCaption\n ? 'Add a few words about the picture first'\n : undefined\n }\n onClick={() => void support.send()}\n />\n )\n }\n />\n </div>\n </div>\n </div>\n {/* THE QUICK ACTIONS STEP ASIDE while the search is open. They are the\n most-used things on the page, so they are there whenever the box is\n closed; once you click in, the room under it belongs to the search.\n `popLayout` takes them out of the flow at once and fades them where\n they stood, so the carousel does not wait for them to finish leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {!searching && (\n <motion.div\n key=\"quick\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n transition={{ duration: 0.2 }}\n >\n <HomeQuickActions />\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* The results rise in from below as the board lifts away, and sink back\n out of the way when you click off, so the page never just blinks\n between states. `popLayout`, not `wait`, so the results start at once\n instead of after the old state has finished leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {searching && inSupport && (\n <motion.div\n key=\"support\"\n className=\"mt-3 px-1 text-center text-sm text-[var(--text-tertiary)]\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n This goes to {SUPPORT_NAME} support. We reply in Support and by email.\n </motion.div>\n )}\n {searching && !inSupport && (\n <motion.div\n key=\"search\"\n className=\"mt-3\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n {/* Before anything is typed: suggestions that move on their own. */}\n {!value.trim() && (\n /* 260px from the card to the top of the first suggestion on a tall\n window. The quick actions are hidden while the search is open,\n so the only thing between is the rail's own 8px of top padding\n and this margin: 252. On a short window the gap gives way\n instead of the page scrolling: 100vh less what sits above the\n rail (about 271px of heading and box) and the rail itself\n (296px) and the container's inset, never below 24px. The list's\n 12px margin above collapses into this one, so it adds nothing. */\n <div\n style={{\n marginTop: 'clamp(24px, calc(100vh - 585px), 252px)',\n }}\n >\n <SearchTips />\n </div>\n )}\n {/* Results only once there is something to look for. With an empty\n box the carousel above is the whole panel: a list of recent\n orders and customers under it read as a second, unrelated\n thing, and it cost three requests on every focus. */}\n {value.trim() && (\n <AskResults\n query={value}\n onNavigate={() => {\n setOpen(false);\n setValue('');\n }}\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/home-ask.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState, type RefObject } from 'react';\nimport Link from 'next/link';\nimport { Info, X } from 'lucide-react';\nimport { AutoGrowTextarea } from '@withpotter/ui-kit/auto-grow-textarea';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { AskControls } from './ask-controls';\nimport { AskGlow } from './ask-glow';\nimport { AskModeChip, type AskMode } from './ask-mode-chip';\nimport { AskSendButton } from './ask-send-button';\nimport { AskSupportAttachments } from './ask-support-attachments';\nimport { useAskSupport } from './use-ask-support';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { SUPPORT_NAME } from '@withpotter/lib/support';\nimport { AskResults } from './ask-results';\nimport { SearchTips } from './search-tips';\nimport { HomeQuickActions } from './home-quick-actions';\n\n/**\n * The home's one box: a search, and a way to reach support.\n *\n * SEARCH IS THE DEFAULT. Clicking the field folds the board away and fills the\n * room with what has been happening, then with what matches as you type, and a\n * row opens that record. See `AskResults` and `SearchTips`.\n *\n * SUPPORT IS A MODE, chosen on the pill beside the paperclip or entered by\n * using the mic or the paperclip, neither of which can be searched for. In it\n * the round send button grows into \"Send to support\" and opens a case through\n * the same call the Support page uses, with the message, and the voice note or\n * picture, attached. A voice note can go with no words; a picture cannot, and\n * the card says so under it. The mechanics live in `useAskSupport`.\n *\n * ENTER SENDS, SHIFT+ENTER BREAKS THE LINE. In search it only keeps the caret\n * where it is, because the list under the box is the answer; an IME\n * composition's Enter is neither and is left alone.\n *\n * THE NOTICE STRIP IS DRIVEN BY `usage`, and renders nothing without it. There\n * is no usage figure to read yet, so the home passes none; when there is,\n * handing it in is the whole of the wiring.\n */\nexport function HomeAsk({\n /** Optional handle, so the page header's Box control can focus this. */\n inputRef: externalRef,\n usage,\n onOpenChange,\n}: {\n /** The search opened or closed; the page folds the board away on open. */\n onOpenChange?: (open: boolean) => void;\n inputRef?: RefObject<HTMLTextAreaElement | null>;\n /** Share of the allowance used, and where \"View usage\" goes. */\n usage?: { percent: number; href: string };\n}) {\n const orderLabel = useEntityLabel('order');\n const itemLabel = useEntityLabel('item');\n /* SEARCH WORDING, NOT QUESTIONS. This field finds records, so what it\n suggests is what can be found; `askPlaceholders` are things to ask the\n assistant and no longer belong here. */\n const prompts = useMemo(\n () => [\n `Search ${orderLabel.plural.toLowerCase()} by customer name`,\n 'Search customers by name or email',\n `Search ${itemLabel.plural.toLowerCase()} by name or SKU`,\n `Find an invoice by number`,\n ],\n [orderLabel, itemLabel],\n );\n const focusedPrompt = `Search ${orderLabel.plural.toLowerCase()}, customers, ${itemLabel.plural.toLowerCase()}…`;\n const [i, setI] = useState(0);\n const [value, setValue] = useState('');\n const [focused, setFocused] = useState(false);\n const [noticeDismissed, setNoticeDismissed] = useState(false);\n /* What the box is for. Search unless someone chose Support, or put a voice\n note or a picture on the card, which can only go to support. */\n const [mode, setMode] = useState<AskMode>('search');\n const ownRef = useRef<HTMLTextAreaElement>(null);\n const inputRef = externalRef ?? ownRef;\n const root = useRef<HTMLDivElement>(null);\n\n /* THE SEARCH, NOT A CONVERSATION. Focusing the field opens it: the page\n folds the board away and the room holds what has been happening, then what\n matches as you type. It stays open across a blur (pressing a result blurs\n the field before the click lands) and closes on Escape, a press outside\n this card and its list, or navigating. */\n const [searching, setSearching] = useState(false);\n const setOpen = (next: boolean) => {\n setSearching(next);\n onOpenChange?.(next);\n };\n\n /* After a case goes and the green \"Sent\" has had its moment, the box is\n back to what it normally is: a search. */\n const support = useAskSupport(value, setValue, () => setMode('search'));\n const inSupport = mode === 'support' || support.engaged;\n const recording = support.composer.recordingNow;\n const voice = {\n supported: support.voiceSupported,\n active: recording,\n start: () => {\n setMode('support');\n support.startVoice();\n },\n stop: support.stopVoice,\n /* One clip at a time: remove it to record another. */\n disabled:\n Boolean(support.composer.voice) || support.composer.recorder.isBusy,\n };\n /* The latest of these, for the handlers below that were created on an\n earlier render and would otherwise close over stale ones. */\n const latest = useRef({ mode, engaged: false, value: '' });\n useEffect(() => {\n latest.current = { mode, engaged: support.engaged, value };\n });\n /* CLOSING THE SEARCH ENDS A SUPPORT MODE NOBODY IS USING. Support is sticky\n on purpose while there is something in the box, but a pill left on Support\n after the box was emptied and closed hid the carousel next time with no\n sign of why, and the bottom of the page just sat blank. If there are words\n or an attachment, the mode stays; if not, the box goes back to searching. */\n const closeSearch = () => {\n setOpen(false);\n const l = latest.current;\n if (l.mode === 'support' && !l.engaged && !l.value.trim()) {\n setMode('search');\n }\n };\n useEffect(() => {\n if (!searching) return;\n /* STAYS OPEN ONLY FOR A PRESS ON SOMETHING. The search block is wider and\n taller than anything in it: it holds the empty gap under the box and the\n carousel's own background, and a press there is a press on nothing. So\n \"inside\" is the card itself or a real control in this block (a link, a\n button, a field), plus the Add menu, which is portaled out of it. */\n const onDown = (e: PointerEvent) => {\n const t = e.target as HTMLElement;\n const inside =\n !!root.current?.contains(t) &&\n !!t.closest('[data-ask-card],a,button,input,textarea,[role=\"menu\"]');\n const inMenu = !!t.closest('[role=\"menu\"]');\n if (!inside && !inMenu) closeSearch();\n };\n document.addEventListener('pointerdown', onDown);\n return () => document.removeEventListener('pointerdown', onDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [searching]);\n\n const supportPrompt = recording\n ? 'Listening. What you say appears here…'\n : 'Tell support what you need…';\n\n /* The rotation stops the moment there is a caret in the field: a placeholder\n that keeps changing under one reads as the field arguing with you. */\n useEffect(() => {\n if (focused || value || inSupport) return;\n const t = setInterval(() => setI((n) => (n + 1) % prompts.length), 4200);\n return () => clearInterval(t);\n }, [focused, value, inSupport, prompts.length]);\n\n const canSearchSend = value.trim().length > 0;\n\n /* No hand-off to the ask screen: this field searches. Enter keeps the\n caret where it is and the list under it is the answer. */\n const submit = () => {\n inputRef.current?.focus();\n };\n\n const showNotice = !!usage && !noticeDismissed;\n\n return (\n <div ref={root} className=\"relative isolate w-full\">\n {showNotice && (\n <div className=\"flex h-11 items-center gap-3 rounded-t-[var(--radius-card)] bg-[var(--bg-muted)] pb-3 pl-4 pr-3 text-sm text-[var(--text-primary)]\">\n <Info\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-tertiary)]\"\n />\n <p className=\"min-w-0 flex-1 truncate\">\n You've used {Math.round(usage.percent)}% of your tokens.{' '}\n <Link href={usage.href} className=\"text-brand-500 hover:underline\">\n View usage\n </Link>\n </p>\n <button\n type=\"button\"\n onClick={() => setNoticeDismissed(true)}\n aria-label=\"Dismiss\"\n className=\"grid size-6 shrink-0 place-items-center rounded-md text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n )}\n\n {/* The card overlaps the strip's bottom edge, which is why the strip\n carries a padding-bottom it never shows: the card's top border runs\n through the strip, as drawn. */}\n <div\n data-ask-card\n className={`group relative rounded-[var(--radius-card)] border border-[var(--border-default)] bg-[color-mix(in_srgb,var(--bg-muted)_55%,var(--bg-base))] transition-colors focus-within:border-[color-mix(in_srgb,var(--brand-500)_45%,var(--text-muted))] ${\n showNotice ? '-mt-3' : ''\n }`}\n >\n {/* The rings sit at -z-10 behind this card's OPAQUE fill, which is what\n keeps the centre clear. They bloom on hover and on focus. */}\n <AskGlow />\n <AskSupportAttachments support={support} />\n <div className=\"relative px-4 pt-3\">\n <AutoGrowTextarea\n ref={inputRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => {\n setFocused(true);\n setOpen(true);\n }}\n onBlur={() => setFocused(false)}\n /* Focus alone is not enough: close the search with the box still\n focused (a press on a non-focusable part of the page can leave\n it so) and clicking back in fires no focus event, so nothing\n opened and the bottom stayed blank. */\n onClick={() => {\n if (!searching) setOpen(true);\n }}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n if (inSupport) void support.send();\n else submit();\n }\n if (e.key === 'Escape') {\n /* A message to support is somebody's words: closing the box\n does not throw them away. */\n if (!inSupport) setValue('');\n closeSearch();\n e.currentTarget.blur();\n }\n }}\n aria-label={inSupport ? 'Message to support' : 'Search'}\n className=\"block min-h-[36px] w-full bg-transparent text-md text-[var(--text-primary)] outline-none\"\n />\n {/* Crossfading placeholders, stacked so the card never changes\n height and the rotation cannot reflow it. Rendered beside the\n field rather than as its `placeholder`, which cannot animate. */}\n {!value && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-4 top-3 flex items-start\"\n >\n {(inSupport\n ? [supportPrompt]\n : focused\n ? [focusedPrompt]\n : prompts\n ).map((p, n) => (\n <span\n key={p}\n className={`block truncate text-md text-[var(--text-muted)] transition-opacity duration-[var(--motion-slow)] ${\n focused || inSupport || n === i\n ? 'opacity-100'\n : 'absolute inset-0 opacity-0'\n }`}\n >\n {p}\n </span>\n ))}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 px-3 pb-2 pt-1\">\n {/* The plus that attached a picture is out of this row for now, on\n John's call, 2026-10-09. Sending a picture to support still works\n underneath (`support.composer.pickImage`, and the card's\n attachment frame); put a button back here to reach it. */}\n <AskModeChip\n mode={inSupport ? 'support' : 'search'}\n onChange={setMode}\n locked={support.engaged}\n />\n\n <div className=\"ml-auto flex items-center gap-2\">\n <AskControls\n voice={voice}\n voiceLabels={{\n start: 'Record a voice note for support',\n stop: 'Stop recording',\n }}\n value={value}\n canSend={canSearchSend}\n engaged={false}\n inputRef={inputRef}\n onSubmit={submit}\n onClose={() => inputRef.current?.blur()}\n send={\n <AskSendButton\n support={inSupport}\n canSend={inSupport ? support.canSend : canSearchSend}\n sending={support.sending}\n sent={support.sent}\n title={\n support.needsCaption\n ? 'Add a few words about the picture first'\n : undefined\n }\n onClick={inSupport ? () => void support.send() : submit}\n />\n }\n />\n </div>\n </div>\n </div>\n {/* THE QUICK ACTIONS STEP ASIDE while the search is open. They are the\n most-used things on the page, so they are there whenever the box is\n closed; once you click in, the room under it belongs to the search.\n `popLayout` takes them out of the flow at once and fades them where\n they stood, so the carousel does not wait for them to finish leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {!searching && (\n <motion.div\n key=\"quick\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n transition={{ duration: 0.2 }}\n >\n <HomeQuickActions />\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* The results rise in from below as the board lifts away, and sink back\n out of the way when you click off, so the page never just blinks\n between states. `popLayout`, not `wait`, so the results start at once\n instead of after the old state has finished leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {searching && inSupport && (\n <motion.div\n key=\"support\"\n className=\"mt-3 px-1 text-center text-sm text-[var(--text-tertiary)]\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n This goes to {SUPPORT_NAME} support. We reply in Support and by email.\n </motion.div>\n )}\n {searching && !inSupport && (\n <motion.div\n key=\"search\"\n className=\"mt-3\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n {/* Before anything is typed: suggestions that move on their own. */}\n {!value.trim() && (\n /* 260px from the card to the top of the first suggestion on a tall\n window. The quick actions are hidden while the search is open,\n so the only thing between is the rail's own 8px of top padding\n and this margin: 252. On a short window the gap gives way\n instead of the page scrolling: 100vh less what sits above the\n rail (about 271px of heading and box) and the rail itself\n (296px) and the container's inset, never below 24px. The list's\n 12px margin above collapses into this one, so it adds nothing. */\n <div\n style={{\n marginTop: 'clamp(24px, calc(100vh - 585px), 252px)',\n }}\n >\n <SearchTips />\n </div>\n )}\n {/* Results only once there is something to look for. With an empty\n box the carousel above is the whole panel: a list of recent\n orders and customers under it read as a second, unrelated\n thing, and it cost three requests on every focus. */}\n {value.trim() && (\n <AskResults\n query={value}\n onNavigate={() => {\n setOpen(false);\n setValue('');\n }}\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -12,7 +12,6 @@ import { useEntityLabel } from "@withpotter/lib/intents/use-entity-label";
|
|
|
12
12
|
import { useIntentConfig } from "@withpotter/lib/intents/use-intent-config";
|
|
13
13
|
import { usePermissions } from "@withpotter/lib/use-permissions";
|
|
14
14
|
import { useTakePayment } from "../take-payment-context";
|
|
15
|
-
import { hostQuickActions } from "./host-quick-actions";
|
|
16
15
|
import { homeAddItemIntent, homeAddItemLinks } from "./home-add-item-links";
|
|
17
16
|
const PILL = "flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]";
|
|
18
17
|
function HomeQuickActions() {
|
|
@@ -45,14 +44,7 @@ function HomeQuickActions() {
|
|
|
45
44
|
Icon: Plus,
|
|
46
45
|
run: () => router.push(links.create)
|
|
47
46
|
}
|
|
48
|
-
] : []
|
|
49
|
-
// Whatever the host registered, after the built-in three.
|
|
50
|
-
...hostQuickActions().filter((a) => !a.allowed || a.allowed((p) => can(p))).map((a) => ({
|
|
51
|
-
id: a.id,
|
|
52
|
-
label: a.label,
|
|
53
|
-
Icon: a.Icon,
|
|
54
|
-
run: () => router.push(a.href)
|
|
55
|
-
}))
|
|
47
|
+
] : []
|
|
56
48
|
];
|
|
57
49
|
if (actions.length === 0) return null;
|
|
58
50
|
return /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/home-quick-actions.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport {\n ChevronDown,\n CreditCard,\n FilePlus2,\n Plus,\n type LucideIcon,\n} from 'lucide-react';\nimport { OverflowMenu, OverflowItem } from '@withpotter/ui-kit/overflow-menu';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { useTakePayment } from '../take-payment-context';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/home-quick-actions.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport {\n ChevronDown,\n CreditCard,\n FilePlus2,\n Plus,\n type LucideIcon,\n} from 'lucide-react';\nimport { OverflowMenu, OverflowItem } from '@withpotter/ui-kit/overflow-menu';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { useTakePayment } from '../take-payment-context';\nimport { homeAddItemIntent, homeAddItemLinks } from './home-add-item-links';\n\nconst PILL =\n 'flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]';\n\n/**\n * Take payment, Send invoice, Add item: the three things done most, as pills\n * under the ask card.\n *\n * They replaced Ask, Write and Organize, which were a look: choosing one moved a\n * highlight and nothing read it. These do something. Take payment opens the same\n * modal the rail used to carry; Send invoice and Add item go to the screens that\n * make them. \"Add item\" says the business's own word (`useEntityLabel`), so a\n * club is told to add a service and a shop a product.\n *\n * The first two write money, so they follow the home's own gate\n * (`canManageFinance`); Add item follows the catalogue's (`canManageProducts`).\n * Add item opens a two-way menu, the same as on the catalogue — typing one or\n * importing many is decided at the button, not on a blank form.\n * Nothing is drawn while permissions resolve, so the row appears whole.\n */\nexport function HomeQuickActions() {\n const router = useRouter();\n const takePayment = useTakePayment();\n const item = useEntityLabel('item');\n const intentConfig = useIntentConfig();\n const { can, loading } = usePermissions();\n if (loading) return null;\n // Named, so the item lands on the list the sidebar opens; see the helper.\n const links = homeAddItemLinks(homeAddItemIntent(intentConfig, item));\n\n const actions: {\n id: string;\n label: string;\n Icon: LucideIcon;\n run: () => void;\n }[] = [\n ...(can('canManageFinance')\n ? [\n {\n id: 'payment',\n label: 'Take payment',\n Icon: CreditCard,\n run: takePayment,\n },\n {\n id: 'invoice',\n label: 'Send invoice',\n Icon: FilePlus2,\n run: () => router.push('/dashboard/invoices/new'),\n },\n ]\n : []),\n ...(can('canManageProducts')\n ? [\n {\n id: 'item',\n label: `Add ${item.singular.toLowerCase()}`,\n Icon: Plus,\n run: () => router.push(links.create),\n },\n ]\n : []),\n ];\n if (actions.length === 0) return null;\n\n return (\n <div\n role=\"group\"\n aria-label=\"Quick actions\"\n className=\"mt-3 flex flex-wrap items-center justify-center gap-3\"\n >\n {actions.map(({ id, label, Icon, run }) =>\n id === 'item' ? (\n <OverflowMenu\n key={id}\n label={label}\n align=\"start\"\n trigger={(props) => (\n <button type=\"button\" {...props} className={PILL}>\n <Icon\n aria-hidden\n className=\"size-4 text-[var(--text-tertiary)]\"\n />\n {label}\n <ChevronDown aria-hidden className=\"-ml-1 size-3.5\" />\n </button>\n )}\n >\n <OverflowItem onClick={run}>Add manually</OverflowItem>\n <OverflowItem\n onClick={() => router.push(links.importSheet)}\n >\n Import from CSV\n </OverflowItem>\n </OverflowMenu>\n ) : (\n <button key={id} type=\"button\" onClick={run} className={PILL}>\n <Icon aria-hidden className=\"size-4 text-[var(--text-tertiary)]\" />\n {label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";AA8Fc,SACE,KADF;AA5Fd,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,cAAc,oBAAoB;AAC3C,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB,wBAAwB;AAEpD,MAAM,OACJ;AAkBK,SAAS,mBAAmB;AACjC,QAAM,SAAS,UAAU;AACzB,QAAM,cAAc,eAAe;AACnC,QAAM,OAAO,eAAe,MAAM;AAClC,QAAM,eAAe,gBAAgB;AACrC,QAAM,EAAE,KAAK,QAAQ,IAAI,eAAe;AACxC,MAAI,QAAS,QAAO;AAEpB,QAAM,QAAQ,iBAAiB,kBAAkB,cAAc,IAAI,CAAC;AAEpE,QAAM,UAKA;AAAA,IACJ,GAAI,IAAI,kBAAkB,IACtB;AAAA,MACE;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK;AAAA,MACP;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK,MAAM,OAAO,KAAK,yBAAyB;AAAA,MAClD;AAAA,IACF,IACA,CAAC;AAAA,IACL,GAAI,IAAI,mBAAmB,IACvB;AAAA,MACE;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,OAAO,KAAK,SAAS,YAAY,CAAC;AAAA,QACzC,MAAM;AAAA,QACN,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM;AAAA,MACrC;AAAA,IACF,IACA,CAAC;AAAA,EACP;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAW;AAAA,MACX,WAAU;AAAA,MAET,kBAAQ;AAAA,QAAI,CAAC,EAAE,IAAI,OAAO,MAAM,IAAI,MACnC,OAAO,SACL;AAAA,UAAC;AAAA;AAAA,YAEC;AAAA,YACA,OAAM;AAAA,YACN,SAAS,CAAC,UACR,qBAAC,YAAO,MAAK,UAAU,GAAG,OAAO,WAAW,MAC1C;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAW;AAAA,kBACX,WAAU;AAAA;AAAA,cACZ;AAAA,cACC;AAAA,cACD,oBAAC,eAAY,eAAW,MAAC,WAAU,kBAAiB;AAAA,eACtD;AAAA,YAGF;AAAA,kCAAC,gBAAa,SAAS,KAAK,0BAAY;AAAA,cACxC;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,MAAM,OAAO,KAAK,MAAM,WAAW;AAAA,kBAC7C;AAAA;AAAA,cAED;AAAA;AAAA;AAAA,UAnBK;AAAA,QAoBP,IAEA,qBAAC,YAAgB,MAAK,UAAS,SAAS,KAAK,WAAW,MACtD;AAAA,8BAAC,QAAK,eAAW,MAAC,WAAU,sCAAqC;AAAA,UAChE;AAAA,aAFU,EAGb;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -109,62 +109,49 @@ function ActivityWidget({
|
|
|
109
109
|
last: i === shown.length - 1
|
|
110
110
|
};
|
|
111
111
|
});
|
|
112
|
-
return /* @__PURE__ */ jsxs(
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
/* @__PURE__ */ jsx(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
a.href ? /* @__PURE__ */ jsx(
|
|
156
|
-
Link,
|
|
157
|
-
{
|
|
158
|
-
href: a.href,
|
|
159
|
-
className: "-mx-2 block rounded-lg px-2 transition-colors hover:bg-[var(--interactive-hover)]",
|
|
160
|
-
children: row
|
|
161
|
-
}
|
|
162
|
-
) : /* @__PURE__ */ jsx("div", { className: "-mx-2 px-2", children: row })
|
|
163
|
-
] }, a.id);
|
|
164
|
-
}) })
|
|
165
|
-
]
|
|
166
|
-
}
|
|
167
|
-
);
|
|
112
|
+
return /* @__PURE__ */ jsxs(Panel, { title: "Recent activity", bodyClassName: "overflow-y-auto pt-1", children: [
|
|
113
|
+
blockedBlock,
|
|
114
|
+
/* @__PURE__ */ jsx("ol", { className: "relative", children: rows.map(({ item: a, day, showDay: newDay, last }) => {
|
|
115
|
+
const { icon: Icon, money } = MARK[a.type] ?? MARK.order_placed;
|
|
116
|
+
const row = /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
|
|
117
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex w-5 shrink-0 justify-center", children: [
|
|
118
|
+
!last && /* @__PURE__ */ jsx(
|
|
119
|
+
"span",
|
|
120
|
+
{
|
|
121
|
+
"aria-hidden": true,
|
|
122
|
+
className: "absolute left-1/2 top-5 h-[calc(100%-8px)] w-px -translate-x-1/2 bg-[var(--border-subtle)]"
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
"span",
|
|
127
|
+
{
|
|
128
|
+
"aria-hidden": true,
|
|
129
|
+
className: `relative z-10 mt-1 grid h-5 w-5 place-items-center rounded-full ${money ? "bg-[var(--success-bg,var(--bg-muted))] text-[var(--success)]" : "bg-[var(--bg-muted)] text-[var(--text-tertiary)]"}`,
|
|
130
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "h-[11px] w-[11px]", strokeWidth: 2 })
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
] }),
|
|
134
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 pb-3", children: [
|
|
135
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-baseline justify-between gap-2", children: [
|
|
136
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-sm font-medium text-[var(--text-primary)]", children: a.title }),
|
|
137
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-2xs tabular-nums text-[var(--text-muted)]", children: timeLabel(a.timestamp) })
|
|
138
|
+
] }),
|
|
139
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm text-[var(--text-tertiary)]", children: a.description })
|
|
140
|
+
] })
|
|
141
|
+
] });
|
|
142
|
+
return /* @__PURE__ */ jsxs("li", { children: [
|
|
143
|
+
newDay && /* @__PURE__ */ jsx("p", { className: "pb-2 pt-1 text-2xs font-medium uppercase tracking-wide text-[var(--text-muted)] first:pt-0", children: day }),
|
|
144
|
+
a.href ? /* @__PURE__ */ jsx(
|
|
145
|
+
Link,
|
|
146
|
+
{
|
|
147
|
+
href: a.href,
|
|
148
|
+
className: "-mx-2 block rounded-lg px-2 transition-colors hover:bg-[var(--interactive-hover)]",
|
|
149
|
+
children: row
|
|
150
|
+
}
|
|
151
|
+
) : /* @__PURE__ */ jsx("div", { className: "-mx-2 px-2", children: row })
|
|
152
|
+
] }, a.id);
|
|
153
|
+
}) })
|
|
154
|
+
] });
|
|
168
155
|
}
|
|
169
156
|
export {
|
|
170
157
|
ActivityWidget
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/dashboard/home/widgets/activity-widget.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport {\n Package,\n PencilLine,\n ShoppingBag,\n CircleCheck,\n UserPlus,\n UserCheck,\n RefreshCw,\n Receipt,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport type { ActivityItem } from '@withpotter/components/dashboard/activity-feed';\nimport type { NextUpItem } from '../next-up-sources';\nimport { Button } from '@withpotter/ui-kit';\nimport { Panel, PanelEmpty } from '../panel';\nimport { Badge } from '@withpotter/ui-kit';\n\n/**\n * What has happened, as a timeline.\n *\n * WHY IT EXISTS ALONGSIDE \"Today\". Those two cards answer different questions\n * and the home needs both: Today is what is WAITING on you, Activity is what\n * already went. Merging them was tried and is the mistake this replaces — a\n * single list with now as the pivot puts \"Tuesday 22:00, Tunde Ogunleye\" and\n * \"invoice paid 20 minutes ago\" in one column under one heading, so the reader\n * has to check each row's tense before they know whether it is a task.\n *\n * WHAT MAKES IT A TIMELINE RATHER THAN A LIST, and this is the depth the old\n * feed never had:\n *\n * A RAIL. A hairline runs behind the icon column and stops at the last node,\n * so the rows read as one sequence rather than as five unrelated rows that\n * happen to be stacked. It is the cheapest possible signal that these are\n * ordered in time and it is the one every activity feed worth copying has.\n *\n * TYPED, COLOURED NODES. `ActivityItem.type` already carries nine kinds and\n * nothing rendered them — the desktop feed dropped the emoji and drew\n * nothing in its place. Money is green, because money arriving is the one\n * event on this list a merchant scans for; everything else is neutral, so the\n * green means something. Colour that appears on every row means nothing.\n *\n * DAY HEADINGS. Cloudflare's Recents and Square's Tasks both fail here —\n * \"3d ago\" on row four with no break above it makes a five-row list look\n * like five things that happened this morning. A heading per day is what\n * turns a stack of timestamps into a history.\n *\n * The rows are links where the object still exists and plain rows where it does\n * not, rather than every row being a link to somewhere approximate.\n *\n * BLOCKERS RIDE ON TOP, and they have to, because this card replaced Today on\n * the default board and Today was carrying them. Losing them would have made\n * the home stop answering the second half of its own question — \"what do I do\n * next, AND is anything wrong?\" — which is the half a merchant cannot find\n * anywhere else.\n *\n * They are Square's ranked task rows: a title, one line of CONSEQUENCE, and a\n * real button with a verb on it. `NextUpItem` already carries all three\n * (`title`, `meta`, `action`), and the treatment is deliberately unlike the\n * timeline below — a blocker is not something that happened, it is something\n * that is still happening to you. Only the first gets a primary button, as on\n * Square: two primaries side by side is two things that are both the most\n * important thing.\n */\n\n/**\n * Nine kinds, nine marks. `emoji` is on the type and is deliberately not used:\n * it exists for the mobile feed, and an emoji at 13px in a dense column reads\n * as a smudge rather than as a symbol.\n */\nconst MARK: Record<\n ActivityItem['type'],\n { icon: LucideIcon; money?: boolean }\n> = {\n product_added: { icon: Package },\n product_updated: { icon: PencilLine },\n order_placed: { icon: ShoppingBag },\n order_completed: { icon: CircleCheck, money: true },\n member_signup: { icon: UserPlus },\n member_converted: { icon: UserCheck },\n subscription_created: { icon: RefreshCw, money: true },\n invoice_paid: { icon: Receipt, money: true },\n invoice_sent: { icon: Send },\n};\n\n/**\n * Which day a timestamp belongs to, in the reader's own words.\n *\n * Deliberately only three answers. \"Wednesday\" for something eight days old is\n * a puzzle, not a heading — past a week the date is the only honest label.\n */\nfunction dayLabel(iso: string): string {\n const d = new Date(iso);\n const today = new Date();\n const startOf = (x: Date) =>\n new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();\n const days = Math.round((startOf(today) - startOf(d)) / 86_400_000);\n if (days <= 0) return 'Today';\n if (days === 1) return 'Yesterday';\n if (days < 7) return d.toLocaleDateString(undefined, { weekday: 'long' });\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\nfunction timeLabel(iso: string): string {\n return new Date(iso).toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n });\n}\n\n/**\n * How overdue something is, in whole days, or null when it has no clock.\n *\n * Only for items carrying a real `at`. A blocker like \"set up payouts\" has no\n * due date and never gets a countdown — Square can say \"2 days until offline\n * payments are automatically allowed\" because a real policy deadline exists\n * behind it, and manufacturing an equivalent here would be inventing urgency.\n */\nfunction overdueLabel(item: NextUpItem): string | null {\n if (!item.overdue || !item.at) return null;\n const days = Math.floor(\n (Date.now() - new Date(item.at).getTime()) / 86_400_000,\n );\n if (days < 1) return 'due today';\n return `${days}d overdue`;\n}\n\n/**\n * The blockers the go-live dialog can actually resolve.\n *\n * Both are \"your store cannot sell\", and both are fixable in two or three\n * fields without leaving the page — so they open the dialog rather than\n * navigating to a settings screen. Anything else keeps its link.\n */\nconst FIXABLE_IN_PLACE = new Set(['alert-delivery', 'alert-payouts']);\n\nexport function ActivityWidget({\n items,\n blockers = [],\n limit,\n onFixBlocker,\n}: {\n items: ActivityItem[];\n /** Things stopping money. Pinned above the timeline; never sorted into it. */\n blockers?: NextUpItem[];\n /** Narrow cards hold fewer rows than wide ones. */\n limit: number;\n /**\n * Opens the go-live checks. Undefined on a surface that cannot host the\n * dialog, in which case every blocker falls back to its own href — which is\n * a real settings screen, not a dead end.\n */\n onFixBlocker?: () => void;\n}) {\n // Two at most. A card that opens with four chores is a to-do list wearing a\n // history's title, and the rest are still on the screens that own them.\n const blocked = blockers.slice(0, 2);\n // Blockers eat rows: the card is a fixed height and cannot grow to fit both.\n const shown = items.slice(0, Math.max(1, limit - blocked.length));\n\n const blockedBlock = blocked.length > 0 && (\n <ul className=\"mb-3 space-y-2 border-b border-[var(--border-subtle)] pb-3\">\n {blocked.map((b, i) => (\n <li key={b.id} className=\"flex items-start justify-between gap-3\">\n <span className=\"min-w-0\">\n <span className=\"flex items-center gap-2\">\n <span className=\"min-w-0 truncate text-sm font-medium text-[var(--text-primary)]\">\n {b.title}\n </span>\n {/* SQUARE'S `Required` PILL, and the flag was already on the data.\n `NextUpItem.required` has been set on the payouts blocker all\n along and nothing has ever rendered it, so the one task that\n genuinely stops money read exactly like the one that does not.\n A blocker list where every row looks equally urgent is a list\n with no ranking, whatever order it is sorted in. */}\n {b.required && (\n <Badge variant=\"error\" className=\"shrink-0\">\n Required\n </Badge>\n )}\n {/* HOW LONG, where the clock is real. Square's tasks carry a\n deadline — \"2 days until offline payments are automatically\n allowed\" — and ours carried none. The honest half of that is\n elapsed time on items that HAVE a date: an invoice knows when\n it was due. Blockers have no date and are given no countdown,\n because inventing one would be the fabrication this avoids. */}\n {overdueLabel(b) && (\n <span className=\"shrink-0 text-2xs font-medium tabular-nums text-[var(--error)]\">\n {overdueLabel(b)}\n </span>\n )}\n </span>\n {b.meta && (\n <span className=\"block truncate text-sm text-[var(--text-tertiary)]\">\n {b.meta}\n </span>\n )}\n </span>\n {onFixBlocker && FIXABLE_IN_PLACE.has(b.id) ? (\n <Button\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n onClick={onFixBlocker}\n >\n {b.action ?? 'Fix'}\n </Button>\n ) : (\n <Button\n asChild\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n >\n <Link href={b.href}>{b.action ?? 'Fix'}</Link>\n </Button>\n )}\n </li>\n ))}\n </ul>\n );\n\n if (shown.length === 0 || items.length === 0) {\n return (\n <Panel title=\"Recent activity\">\n {blockedBlock}\n <PanelEmpty\n title=\"Nothing has happened yet\"\n description=\"Orders, payments and new customers will appear here as they come in.\"\n />\n </Panel>\n );\n }\n\n /**\n * Day headings resolved BEFORE the JSX, by comparing each row to the one\n * above it, rather than by carrying a `lastDay` variable through the map.\n * A mutable binding written inside a render callback is exactly what the\n * immutability rule exists to catch: it survives across renders and makes\n * the output depend on how many times the list has been walked.\n */\n const rows = shown.map((item, i) => {\n const day = dayLabel(item.timestamp);\n return {\n item,\n day,\n showDay: i === 0 || day !== dayLabel(shown[i - 1].timestamp),\n last: i === shown.length - 1,\n };\n });\n\n return (\n <Panel\n title=\"Recent activity\"\n /* The card scrolls. A row cut off at its edge looked like a mistake, so the last 20px fades\n out and the card has that much empty padding: with every row in view the fade falls on\n nothing, and with more below, the half row dissolves instead of ending mid-sentence. */\n bodyClassName=\"overflow-y-auto pt-1 pb-5 [-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)] [mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)]\"\n >\n {blockedBlock}\n <ol className=\"relative\">\n {rows.map(({ item: a, day, showDay: newDay, last }) => {\n const { icon: Icon, money } = MARK[a.type] ?? MARK.order_placed;\n\n const row = (\n <div className=\"flex gap-3\">\n {/* The rail and the node. The line is drawn by the node's own\n ::before via a sibling span rather than as a border on the\n row, so it can stop at the last item instead of running into\n the card's bottom padding and looking cut off. */}\n <div className=\"relative flex w-5 shrink-0 justify-center\">\n {!last && (\n <span\n aria-hidden\n className=\"absolute left-1/2 top-5 h-[calc(100%-8px)] w-px -translate-x-1/2 bg-[var(--border-subtle)]\"\n />\n )}\n <span\n aria-hidden\n className={`relative z-10 mt-1 grid h-5 w-5 place-items-center rounded-full ${\n money\n ? 'bg-[var(--success-bg,var(--bg-muted))] text-[var(--success)]'\n : 'bg-[var(--bg-muted)] text-[var(--text-tertiary)]'\n }`}\n >\n <Icon className=\"h-[11px] w-[11px]\" strokeWidth={2} />\n </span>\n </div>\n\n <div className=\"min-w-0 flex-1 pb-3\">\n <p className=\"flex items-baseline justify-between gap-2\">\n <span\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":[]}
|
|
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":[]}
|
|
@@ -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 { WIDGETS, 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,8 +153,7 @@ 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 =
|
|
157
|
-
if (!def) return null;
|
|
156
|
+
const def = WIDGETS[w.id];
|
|
158
157
|
const zone = dragging && dragging !== w.id && overIndex === index ? /* @__PURE__ */ jsx(
|
|
159
158
|
motion.div,
|
|
160
159
|
{
|
|
@@ -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 { 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":[]}
|
|
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":[]}
|
|
@@ -6,7 +6,6 @@ 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";
|
|
10
9
|
const RANGES = ["7d", "30d", "90d"];
|
|
11
10
|
function Delta({
|
|
12
11
|
dir,
|
|
@@ -29,7 +28,7 @@ function Delta({
|
|
|
29
28
|
}
|
|
30
29
|
function Series({ series }) {
|
|
31
30
|
const max = series.reduce((m, p) => Math.max(m, p.value), 0);
|
|
32
|
-
return /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "mt-auto flex h-
|
|
31
|
+
return /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "mt-auto flex h-16 w-full items-end gap-px pt-3", children: series.map((p) => /* @__PURE__ */ jsx(
|
|
33
32
|
"div",
|
|
34
33
|
{
|
|
35
34
|
style: { height: `${Math.max(2, p.value / max * 100)}%` },
|
|
@@ -59,8 +58,7 @@ function MoneyWidget({
|
|
|
59
58
|
series,
|
|
60
59
|
size,
|
|
61
60
|
showRange,
|
|
62
|
-
empty
|
|
63
|
-
caption
|
|
61
|
+
empty
|
|
64
62
|
}) {
|
|
65
63
|
const { range, pending, failed, retry } = useMoneyBoard();
|
|
66
64
|
const big = size !== "narrow";
|
|
@@ -108,18 +106,16 @@ function MoneyWidget({
|
|
|
108
106
|
),
|
|
109
107
|
delta && /* @__PURE__ */ jsx(Delta, { ...delta })
|
|
110
108
|
] }),
|
|
111
|
-
|
|
112
|
-
size === "wide" && series?.length ? /* @__PURE__ */ jsx(Series, { series: spreadByDay(series, Number.parseInt(range, 10)) }) : null
|
|
109
|
+
size === "wide" && series?.length ? /* @__PURE__ */ jsx(Series, { series }) : null
|
|
113
110
|
] })
|
|
114
111
|
]
|
|
115
112
|
}
|
|
116
113
|
);
|
|
117
114
|
}
|
|
118
115
|
function RevenueWidget({ size }) {
|
|
119
|
-
const { data, prior,
|
|
116
|
+
const { data, prior, formatCurrency } = useMoneyBoard();
|
|
120
117
|
const revenue = data?.sales.totalRevenue ?? 0;
|
|
121
118
|
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;
|
|
123
119
|
return /* @__PURE__ */ jsx(
|
|
124
120
|
MoneyWidget,
|
|
125
121
|
{
|
|
@@ -128,7 +124,6 @@ function RevenueWidget({ size }) {
|
|
|
128
124
|
showRange: true,
|
|
129
125
|
value: formatCurrency(Math.round(revenue)),
|
|
130
126
|
delta: computeDelta(revenue, prior?.sales.totalRevenue ?? 0),
|
|
131
|
-
caption,
|
|
132
127
|
series,
|
|
133
128
|
empty: data != null && revenue === 0 && !series.some((p) => p.value > 0)
|
|
134
129
|
}
|