@withpotter/components 0.13.0-next.84 → 0.13.0-next.86
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 +4 -3
- package/dist/billing/overview-tab.js.map +1 -1
- package/dist/billing/plans-tab.js +6 -12
- package/dist/billing/plans-tab.js.map +1 -1
- package/dist/dashboard/activity-feed.d.ts +2 -0
- package/dist/dashboard/activity-feed.js +3 -1
- package/dist/dashboard/activity-feed.js.map +1 -1
- package/dist/dashboard/activity-paid.d.ts +10 -0
- package/dist/dashboard/activity-paid.js +7 -0
- package/dist/dashboard/activity-paid.js.map +1 -0
- package/dist/dashboard/home/ask-controls.js +1 -0
- package/dist/dashboard/home/ask-controls.js.map +1 -1
- package/dist/dashboard/home/ask-mode-chip.js +4 -4
- package/dist/dashboard/home/ask-mode-chip.js.map +1 -1
- package/dist/dashboard/home/home-ask.js +3 -3
- package/dist/dashboard/home/home-ask.js.map +1 -1
- package/dist/dashboard/home/home-quick-actions.js +9 -1
- package/dist/dashboard/home/home-quick-actions.js.map +1 -1
- package/dist/dashboard/home/host-quick-actions.d.ts +19 -0
- package/dist/dashboard/home/host-quick-actions.js +12 -0
- package/dist/dashboard/home/host-quick-actions.js.map +1 -0
- package/dist/dashboard/home/widgets/activity-widget.js +7 -1
- package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
- package/dist/features/gift-card-artwork.js +1 -1
- package/dist/features/gift-card-artwork.js.map +1 -1
- package/dist/features/gift-cards/gift-card-logic.d.ts +41 -0
- package/dist/features/gift-cards/gift-card-logic.js +65 -0
- package/dist/features/gift-cards/gift-card-logic.js.map +1 -0
- package/dist/features/gift-cards/gift-cards-feature.js +656 -518
- package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
- package/dist/features/gift-cards/hint.d.ts +9 -0
- package/dist/features/gift-cards/hint.js +25 -0
- package/dist/features/gift-cards/hint.js.map +1 -0
- package/dist/features/product-form.js +1 -1
- package/dist/features/product-form.js.map +1 -1
- package/dist/mobile/screens/billing-flows.js +7 -6
- 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/sidebar/nav-renderers.js +2 -0
- package/dist/sidebar/nav-renderers.js.map +1 -1
- package/dist/sidebar/nav-section.js +5 -3
- package/dist/sidebar/nav-section.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/home-ask.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState, type RefObject } from 'react';\nimport Link from 'next/link';\nimport { Info, X } from 'lucide-react';\nimport { AutoGrowTextarea } from '@withpotter/ui-kit/auto-grow-textarea';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { AskControls } from './ask-controls';\nimport { AskGlow } from './ask-glow';\nimport { AskModeChip, type AskMode } from './ask-mode-chip';\nimport { AskSendButton } from './ask-send-button';\nimport { AskSupportAttachments } from './ask-support-attachments';\nimport { useAskSupport } from './use-ask-support';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { SUPPORT_NAME } from '@withpotter/lib/support';\nimport { AskResults } from './ask-results';\nimport { SearchTips } from './search-tips';\nimport { HomeQuickActions } from './home-quick-actions';\n\n/**\n * The home's one box: a search, and a way to reach support.\n *\n * SEARCH IS THE DEFAULT. Clicking the field folds the board away and fills the\n * room with what has been happening, then with what matches as you type, and a\n * row opens that record. See `AskResults` and `SearchTips`.\n *\n * SUPPORT IS A MODE, chosen on the pill beside the paperclip or entered by\n * using the mic or the paperclip, neither of which can be searched for. In it\n * the round send button grows into \"Send to support\" and opens a case through\n * the same call the Support page uses, with the message, and the voice note or\n * picture, attached. A voice note can go with no words; a picture cannot, and\n * the card says so under it. The mechanics live in `useAskSupport`.\n *\n * ENTER SENDS, SHIFT+ENTER BREAKS THE LINE. In search it only keeps the caret\n * where it is, because the list under the box is the answer; an IME\n * composition's Enter is neither and is left alone.\n *\n * THE NOTICE STRIP IS DRIVEN BY `usage`, and renders nothing without it. There\n * is no usage figure to read yet, so the home passes none; when there is,\n * handing it in is the whole of the wiring.\n */\nexport function HomeAsk({\n /** Optional handle, so the page header's Box control can focus this. */\n inputRef: externalRef,\n usage,\n onOpenChange,\n}: {\n /** The search opened or closed; the page folds the board away on open. */\n onOpenChange?: (open: boolean) => void;\n inputRef?: RefObject<HTMLTextAreaElement | null>;\n /** Share of the allowance used, and where \"View usage\" goes. */\n usage?: { percent: number; href: string };\n}) {\n const orderLabel = useEntityLabel('order');\n const itemLabel = useEntityLabel('item');\n /* SEARCH WORDING, NOT QUESTIONS. This field finds records, so what it\n suggests is what can be found; `askPlaceholders` are things to ask the\n assistant and no longer belong here. */\n const prompts = useMemo(\n () => [\n `Search ${orderLabel.plural.toLowerCase()} by customer name`,\n 'Search customers by name or email',\n `Search ${itemLabel.plural.toLowerCase()} by name or SKU`,\n `Find an invoice by number`,\n ],\n [orderLabel, itemLabel],\n );\n const focusedPrompt = `Search ${orderLabel.plural.toLowerCase()}, customers, ${itemLabel.plural.toLowerCase()}…`;\n const [i, setI] = useState(0);\n const [value, setValue] = useState('');\n const [focused, setFocused] = useState(false);\n const [noticeDismissed, setNoticeDismissed] = useState(false);\n /* What the box is for. Search unless someone chose Support, or put a voice\n note or a picture on the card, which can only go to support. */\n const [mode, setMode] = useState<AskMode>('search');\n const ownRef = useRef<HTMLTextAreaElement>(null);\n const inputRef = externalRef ?? ownRef;\n const root = useRef<HTMLDivElement>(null);\n\n /* THE SEARCH, NOT A CONVERSATION. Focusing the field opens it: the page\n folds the board away and the room holds what has been happening, then what\n matches as you type. It stays open across a blur (pressing a result blurs\n the field before the click lands) and closes on Escape, a press outside\n this card and its list, or navigating. */\n const [searching, setSearching] = useState(false);\n const setOpen = (next: boolean) => {\n setSearching(next);\n onOpenChange?.(next);\n };\n\n /* After a case goes and the green \"Sent\" has had its moment, the box is\n back to what it normally is: a search. */\n const support = useAskSupport(value, setValue, () => setMode('search'));\n const inSupport = mode === 'support' || support.engaged;\n const recording = support.composer.recordingNow;\n const voice = {\n supported: support.voiceSupported,\n active: recording,\n start: () => {\n setMode('support');\n support.startVoice();\n },\n stop: support.stopVoice,\n /* One clip at a time: remove it to record another. */\n disabled:\n Boolean(support.composer.voice) || support.composer.recorder.isBusy,\n };\n /* The latest of these, for the handlers below that were created on an\n earlier render and would otherwise close over stale ones. */\n const latest = useRef({ mode, engaged: false, value: '' });\n useEffect(() => {\n latest.current = { mode, engaged: support.engaged, value };\n });\n /* CLOSING THE SEARCH ENDS A SUPPORT MODE NOBODY IS USING. Support is sticky\n on purpose while there is something in the box, but a pill left on Support\n after the box was emptied and closed hid the carousel next time with no\n sign of why, and the bottom of the page just sat blank. If there are words\n or an attachment, the mode stays; if not, the box goes back to searching. */\n const closeSearch = () => {\n setOpen(false);\n const l = latest.current;\n if (l.mode === 'support' && !l.engaged && !l.value.trim()) {\n setMode('search');\n }\n };\n useEffect(() => {\n if (!searching) return;\n /* STAYS OPEN ONLY FOR A PRESS ON SOMETHING. The search block is wider and\n taller than anything in it: it holds the empty gap under the box and the\n carousel's own background, and a press there is a press on nothing. So\n \"inside\" is the card itself or a real control in this block (a link, a\n button, a field), plus the Add menu, which is portaled out of it. */\n const onDown = (e: PointerEvent) => {\n const t = e.target as HTMLElement;\n const inside =\n !!root.current?.contains(t) &&\n !!t.closest('[data-ask-card],a,button,input,textarea,[role=\"menu\"]');\n const inMenu = !!t.closest('[role=\"menu\"]');\n if (!inside && !inMenu) closeSearch();\n };\n document.addEventListener('pointerdown', onDown);\n return () => document.removeEventListener('pointerdown', onDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [searching]);\n\n const supportPrompt = recording\n ? 'Listening. What you say appears here…'\n : 'Tell support what you need…';\n\n /* The rotation stops the moment there is a caret in the field: a placeholder\n that keeps changing under one reads as the field arguing with you. */\n useEffect(() => {\n if (focused || value || inSupport) return;\n const t = setInterval(() => setI((n) => (n + 1) % prompts.length), 4200);\n return () => clearInterval(t);\n }, [focused, value, inSupport, prompts.length]);\n\n const canSearchSend = value.trim().length > 0;\n\n /* No hand-off to the ask screen: this field searches. Enter keeps the\n caret where it is and the list under it is the answer. */\n const submit = () => {\n inputRef.current?.focus();\n };\n\n const showNotice = !!usage && !noticeDismissed;\n\n return (\n <div ref={root} className=\"relative isolate w-full\">\n {showNotice && (\n <div className=\"flex h-11 items-center gap-3 rounded-t-[var(--radius-card)] bg-[var(--bg-muted)] pb-3 pl-4 pr-3 text-sm text-[var(--text-primary)]\">\n <Info\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-tertiary)]\"\n />\n <p className=\"min-w-0 flex-1 truncate\">\n You've used {Math.round(usage.percent)}% of your tokens.{' '}\n <Link href={usage.href} className=\"text-brand-500 hover:underline\">\n View usage\n </Link>\n </p>\n <button\n type=\"button\"\n onClick={() => setNoticeDismissed(true)}\n aria-label=\"Dismiss\"\n className=\"grid size-6 shrink-0 place-items-center rounded-md text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n )}\n\n {/* The card overlaps the strip's bottom edge, which is why the strip\n carries a padding-bottom it never shows: the card's top border runs\n through the strip, as drawn. */}\n <div\n data-ask-card\n className={`group relative rounded-[var(--radius-card)] border border-[var(--border-default)] bg-[color-mix(in_srgb,var(--bg-muted)_55%,var(--bg-base))] transition-colors focus-within:border-[color-mix(in_srgb,var(--brand-500)_45%,var(--text-muted))] ${\n showNotice ? '-mt-3' : ''\n }`}\n >\n {/* The rings sit at -z-10 behind this card's OPAQUE fill, which is what\n keeps the centre clear. They bloom on hover and on focus. */}\n <AskGlow />\n <AskSupportAttachments support={support} />\n <div className=\"relative px-4 pt-3\">\n <AutoGrowTextarea\n ref={inputRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => {\n setFocused(true);\n setOpen(true);\n }}\n onBlur={() => setFocused(false)}\n /* Focus alone is not enough: close the search with the box still\n focused (a press on a non-focusable part of the page can leave\n it so) and clicking back in fires no focus event, so nothing\n opened and the bottom stayed blank. */\n onClick={() => {\n if (!searching) setOpen(true);\n }}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n if (inSupport) void support.send();\n else submit();\n }\n if (e.key === 'Escape') {\n /* A message to support is somebody's words: closing the box\n does not throw them away. */\n if (!inSupport) setValue('');\n closeSearch();\n e.currentTarget.blur();\n }\n }}\n aria-label={inSupport ? 'Message to support' : 'Search'}\n className=\"block min-h-[36px] w-full bg-transparent text-md text-[var(--text-primary)] outline-none\"\n />\n {/* Crossfading placeholders, stacked so the card never changes\n height and the rotation cannot reflow it. Rendered beside the\n field rather than as its `placeholder`, which cannot animate. */}\n {!value && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-4 top-3 flex items-start\"\n >\n {(inSupport\n ? [supportPrompt]\n : focused\n ? [focusedPrompt]\n : prompts\n ).map((p, n) => (\n <span\n key={p}\n className={`block truncate text-md text-[var(--text-muted)] transition-opacity duration-[var(--motion-slow)] ${\n focused || inSupport || n === i\n ? 'opacity-100'\n : 'absolute inset-0 opacity-0'\n }`}\n >\n {p}\n </span>\n ))}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 px-3 pb-2 pt-1\">\n {/* The plus that attached a picture is out of this row for now, on\n John's call, 2026-10-09. Sending a picture to support still works\n underneath (`support.composer.pickImage`, and the card's\n attachment frame); put a button back here to reach it. */}\n <AskModeChip\n mode={inSupport ? 'support' : 'search'}\n onChange={setMode}\n locked={support.engaged}\n />\n\n <div className=\"ml-auto flex items-center gap-2\">\n <AskControls\n voice={voice}\n voiceLabels={{\n start: 'Record a voice note for support',\n stop: 'Stop recording',\n }}\n value={value}\n canSend={canSearchSend}\n engaged={false}\n inputRef={inputRef}\n onSubmit={submit}\n onClose={() => inputRef.current?.blur()}\n send={\n <AskSendButton\n support={inSupport}\n canSend={inSupport ? support.canSend : canSearchSend}\n sending={support.sending}\n sent={support.sent}\n title={\n support.needsCaption\n ? 'Add a few words about the picture first'\n : undefined\n }\n onClick={inSupport ? () => void support.send() : submit}\n />\n }\n />\n </div>\n </div>\n </div>\n {/* THE QUICK ACTIONS STEP ASIDE while the search is open. They are the\n most-used things on the page, so they are there whenever the box is\n closed; once you click in, the room under it belongs to the search.\n `popLayout` takes them out of the flow at once and fades them where\n they stood, so the carousel does not wait for them to finish leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {!searching && (\n <motion.div\n key=\"quick\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n transition={{ duration: 0.2 }}\n >\n <HomeQuickActions />\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* The results rise in from below as the board lifts away, and sink back\n out of the way when you click off, so the page never just blinks\n between states. `popLayout`, not `wait`, so the results start at once\n instead of after the old state has finished leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {searching && inSupport && (\n <motion.div\n key=\"support\"\n className=\"mt-3 px-1 text-center text-sm text-[var(--text-tertiary)]\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n This goes to {SUPPORT_NAME} support. We reply in Support and by email.\n </motion.div>\n )}\n {searching && !inSupport && (\n <motion.div\n key=\"search\"\n className=\"mt-3\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n {/* Before anything is typed: suggestions that move on their own. */}\n {!value.trim() && (\n /* 260px from the card to the top of the first suggestion on a tall\n window. The quick actions are hidden while the search is open,\n so the only thing between is the rail's own 8px of top padding\n and this margin: 252. On a short window the gap gives way\n instead of the page scrolling: 100vh less what sits above the\n rail (about 271px of heading and box) and the rail itself\n (296px) and the container's inset, never below 24px. The list's\n 12px margin above collapses into this one, so it adds nothing. */\n <div\n style={{\n marginTop: 'clamp(24px, calc(100vh - 585px), 252px)',\n }}\n >\n <SearchTips />\n </div>\n )}\n {/* Results only once there is something to look for. With an empty\n box the carousel above is the whole panel: a list of recent\n orders and customers under it read as a second, unrelated\n thing, and it cost three requests on every focus. */}\n {value.trim() && (\n <AskResults\n query={value}\n onNavigate={() => {\n setOpen(false);\n setValue('');\n }}\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/home-ask.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState, type RefObject } from 'react';\nimport Link from 'next/link';\nimport { Info, X } from 'lucide-react';\nimport { AutoGrowTextarea } from '@withpotter/ui-kit/auto-grow-textarea';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { AskControls } from './ask-controls';\nimport { AskGlow } from './ask-glow';\nimport { AskModeChip, type AskMode } from './ask-mode-chip';\nimport { AskSendButton } from './ask-send-button';\nimport { AskSupportAttachments } from './ask-support-attachments';\nimport { useAskSupport } from './use-ask-support';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { springs } from '@withpotter/lib/motion';\nimport { SUPPORT_NAME } from '@withpotter/lib/support';\nimport { AskResults } from './ask-results';\nimport { SearchTips } from './search-tips';\nimport { HomeQuickActions } from './home-quick-actions';\n\n/**\n * The home's one box: a search, and a way to reach support.\n *\n * SEARCH IS THE DEFAULT. Clicking the field folds the board away and fills the\n * room with what has been happening, then with what matches as you type, and a\n * row opens that record. See `AskResults` and `SearchTips`.\n *\n * SUPPORT IS A MODE, chosen on the pill beside the paperclip or entered by\n * using the mic or the paperclip, neither of which can be searched for. In it\n * the round send button grows into \"Send to support\" and opens a case through\n * the same call the Support page uses, with the message, and the voice note or\n * picture, attached. A voice note can go with no words; a picture cannot, and\n * the card says so under it. The mechanics live in `useAskSupport`.\n *\n * ENTER SENDS, SHIFT+ENTER BREAKS THE LINE. In search it only keeps the caret\n * where it is, because the list under the box is the answer; an IME\n * composition's Enter is neither and is left alone.\n *\n * THE NOTICE STRIP IS DRIVEN BY `usage`, and renders nothing without it. There\n * is no usage figure to read yet, so the home passes none; when there is,\n * handing it in is the whole of the wiring.\n */\nexport function HomeAsk({\n /** Optional handle, so the page header's Box control can focus this. */\n inputRef: externalRef,\n usage,\n onOpenChange,\n}: {\n /** The search opened or closed; the page folds the board away on open. */\n onOpenChange?: (open: boolean) => void;\n inputRef?: RefObject<HTMLTextAreaElement | null>;\n /** Share of the allowance used, and where \"View usage\" goes. */\n usage?: { percent: number; href: string };\n}) {\n const orderLabel = useEntityLabel('order');\n const itemLabel = useEntityLabel('item');\n /* SEARCH WORDING, NOT QUESTIONS. This field finds records, so what it\n suggests is what can be found; `askPlaceholders` are things to ask the\n assistant and no longer belong here. */\n const prompts = useMemo(\n () => [\n `Search ${orderLabel.plural.toLowerCase()} by customer name`,\n 'Search customers by name or email',\n `Search ${itemLabel.plural.toLowerCase()} by name or SKU`,\n `Find an invoice by number`,\n ],\n [orderLabel, itemLabel],\n );\n const focusedPrompt = `Search ${orderLabel.plural.toLowerCase()}, customers, ${itemLabel.plural.toLowerCase()}…`;\n const [i, setI] = useState(0);\n const [value, setValue] = useState('');\n const [focused, setFocused] = useState(false);\n const [noticeDismissed, setNoticeDismissed] = useState(false);\n /* What the box is for. Search unless someone chose Support, or put a voice\n note or a picture on the card, which can only go to support. */\n const [mode, setMode] = useState<AskMode>('search');\n const ownRef = useRef<HTMLTextAreaElement>(null);\n const inputRef = externalRef ?? ownRef;\n const root = useRef<HTMLDivElement>(null);\n\n /* THE SEARCH, NOT A CONVERSATION. Focusing the field opens it: the page\n folds the board away and the room holds what has been happening, then what\n matches as you type. It stays open across a blur (pressing a result blurs\n the field before the click lands) and closes on Escape, a press outside\n this card and its list, or navigating. */\n const [searching, setSearching] = useState(false);\n const setOpen = (next: boolean) => {\n setSearching(next);\n onOpenChange?.(next);\n };\n\n /* After a case goes and the green \"Sent\" has had its moment, the box is\n back to what it normally is: a search. */\n const support = useAskSupport(value, setValue, () => setMode('search'));\n const inSupport = mode === 'support' || support.engaged;\n const recording = support.composer.recordingNow;\n const voice = {\n supported: support.voiceSupported,\n active: recording,\n start: () => {\n setMode('support');\n support.startVoice();\n },\n stop: support.stopVoice,\n /* One clip at a time: remove it to record another. */\n disabled:\n Boolean(support.composer.voice) || support.composer.recorder.isBusy,\n };\n /* The latest of these, for the handlers below that were created on an\n earlier render and would otherwise close over stale ones. */\n const latest = useRef({ mode, engaged: false, value: '' });\n useEffect(() => {\n latest.current = { mode, engaged: support.engaged, value };\n });\n /* CLOSING THE SEARCH ENDS A SUPPORT MODE NOBODY IS USING. Support is sticky\n on purpose while there is something in the box, but a pill left on Support\n after the box was emptied and closed hid the carousel next time with no\n sign of why, and the bottom of the page just sat blank. If there are words\n or an attachment, the mode stays; if not, the box goes back to searching. */\n const closeSearch = () => {\n setOpen(false);\n const l = latest.current;\n if (l.mode === 'support' && !l.engaged && !l.value.trim()) {\n setMode('search');\n }\n };\n useEffect(() => {\n if (!searching) return;\n /* STAYS OPEN ONLY FOR A PRESS ON SOMETHING. The search block is wider and\n taller than anything in it: it holds the empty gap under the box and the\n carousel's own background, and a press there is a press on nothing. So\n \"inside\" is the card itself or a real control in this block (a link, a\n button, a field), plus the Add menu, which is portaled out of it. */\n const onDown = (e: PointerEvent) => {\n const t = e.target as HTMLElement;\n const inside =\n !!root.current?.contains(t) &&\n !!t.closest('[data-ask-card],a,button,input,textarea,[role=\"menu\"]');\n const inMenu = !!t.closest('[role=\"menu\"]');\n if (!inside && !inMenu) closeSearch();\n };\n document.addEventListener('pointerdown', onDown);\n return () => document.removeEventListener('pointerdown', onDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [searching]);\n\n const supportPrompt = recording\n ? 'Listening. What you say appears here…'\n : 'Tell support what you need…';\n\n /* The rotation stops the moment there is a caret in the field: a placeholder\n that keeps changing under one reads as the field arguing with you. */\n useEffect(() => {\n if (focused || value || inSupport) return;\n const t = setInterval(() => setI((n) => (n + 1) % prompts.length), 4200);\n return () => clearInterval(t);\n }, [focused, value, inSupport, prompts.length]);\n\n const canSearchSend = value.trim().length > 0;\n\n /* No hand-off to the ask screen: this field searches. Enter keeps the\n caret where it is and the list under it is the answer. */\n const submit = () => {\n inputRef.current?.focus();\n };\n\n const showNotice = !!usage && !noticeDismissed;\n\n return (\n <div ref={root} className=\"relative isolate w-full\">\n {showNotice && (\n <div className=\"flex h-11 items-center gap-3 rounded-t-[var(--radius-card)] bg-[var(--bg-muted)] pb-3 pl-4 pr-3 text-sm text-[var(--text-primary)]\">\n <Info\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-tertiary)]\"\n />\n <p className=\"min-w-0 flex-1 truncate\">\n You've used {Math.round(usage.percent)}% of your tokens.{' '}\n <Link href={usage.href} className=\"text-brand-500 hover:underline\">\n View usage\n </Link>\n </p>\n <button\n type=\"button\"\n onClick={() => setNoticeDismissed(true)}\n aria-label=\"Dismiss\"\n className=\"grid size-6 shrink-0 place-items-center rounded-md text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n )}\n\n {/* The card overlaps the strip's bottom edge, which is why the strip\n carries a padding-bottom it never shows: the card's top border runs\n through the strip, as drawn. */}\n <div\n data-ask-card\n className={`group relative rounded-[var(--radius-card)] border border-[var(--border-default)] bg-[color-mix(in_srgb,var(--bg-muted)_55%,var(--bg-base))] transition-colors focus-within:border-[color-mix(in_srgb,var(--brand-500)_45%,var(--text-muted))] ${\n showNotice ? '-mt-3' : ''\n }`}\n >\n {/* The rings sit at -z-10 behind this card's OPAQUE fill, which is what\n keeps the centre clear. They bloom on hover and on focus. */}\n <AskGlow />\n <AskSupportAttachments support={support} />\n <div className=\"relative px-4 pt-3\">\n <AutoGrowTextarea\n ref={inputRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => {\n setFocused(true);\n setOpen(true);\n }}\n onBlur={() => setFocused(false)}\n /* Focus alone is not enough: close the search with the box still\n focused (a press on a non-focusable part of the page can leave\n it so) and clicking back in fires no focus event, so nothing\n opened and the bottom stayed blank. */\n onClick={() => {\n if (!searching) setOpen(true);\n }}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n if (inSupport) void support.send();\n else submit();\n }\n if (e.key === 'Escape') {\n /* A message to support is somebody's words: closing the box\n does not throw them away. */\n if (!inSupport) setValue('');\n closeSearch();\n e.currentTarget.blur();\n }\n }}\n aria-label={inSupport ? 'Message to support' : 'Search'}\n className=\"block min-h-[36px] w-full bg-transparent text-md text-[var(--text-primary)] outline-none\"\n />\n {/* Crossfading placeholders, stacked so the card never changes\n height and the rotation cannot reflow it. Rendered beside the\n field rather than as its `placeholder`, which cannot animate. */}\n {!value && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-4 top-3 flex items-start\"\n >\n {(inSupport\n ? [supportPrompt]\n : focused\n ? [focusedPrompt]\n : prompts\n ).map((p, n) => (\n <span\n key={p}\n className={`block truncate text-md text-[var(--text-muted)] transition-opacity duration-[var(--motion-slow)] ${\n focused || inSupport || n === i\n ? 'opacity-100'\n : 'absolute inset-0 opacity-0'\n }`}\n >\n {p}\n </span>\n ))}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 px-3 pb-2 pt-1\">\n {/* The plus that attached a picture is out of this row for now, on\n John's call, 2026-10-09. Sending a picture to support still works\n underneath (`support.composer.pickImage`, and the card's\n attachment frame); put a button back here to reach it. */}\n <AskModeChip\n mode={inSupport ? 'support' : 'search'}\n onChange={setMode}\n locked={support.engaged}\n />\n\n <div className=\"ml-auto flex items-center gap-2\">\n <AskControls\n voice={voice}\n voiceLabels={{\n start: 'Record a voice note for support',\n stop: 'Stop recording',\n }}\n value={value}\n canSend={canSearchSend}\n engaged={false}\n inputRef={inputRef}\n onSubmit={submit}\n onClose={() => inputRef.current?.blur()}\n /* SEARCH HAS NO SEND BUTTON. Results update as you type, so an\n arrow there was a control that did nothing and made the box\n read as a chat input. It appears in Message support, where\n pressing it really sends. */\n send={\n !inSupport ? (\n <></>\n ) : (\n <AskSendButton\n support={inSupport}\n canSend={inSupport ? support.canSend : canSearchSend}\n sending={support.sending}\n sent={support.sent}\n title={\n support.needsCaption\n ? 'Add a few words about the picture first'\n : undefined\n }\n onClick={() => void support.send()}\n />\n )\n }\n />\n </div>\n </div>\n </div>\n {/* THE QUICK ACTIONS STEP ASIDE while the search is open. They are the\n most-used things on the page, so they are there whenever the box is\n closed; once you click in, the room under it belongs to the search.\n `popLayout` takes them out of the flow at once and fades them where\n they stood, so the carousel does not wait for them to finish leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {!searching && (\n <motion.div\n key=\"quick\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n transition={{ duration: 0.2 }}\n >\n <HomeQuickActions />\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* The results rise in from below as the board lifts away, and sink back\n out of the way when you click off, so the page never just blinks\n between states. `popLayout`, not `wait`, so the results start at once\n instead of after the old state has finished leaving. */}\n <AnimatePresence initial={false} mode=\"popLayout\">\n {searching && inSupport && (\n <motion.div\n key=\"support\"\n className=\"mt-3 px-1 text-center text-sm text-[var(--text-tertiary)]\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n This goes to {SUPPORT_NAME} support. We reply in Support and by email.\n </motion.div>\n )}\n {searching && !inSupport && (\n <motion.div\n key=\"search\"\n className=\"mt-3\"\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 8, transition: { duration: 0.12 } }}\n transition={springs.gentle}\n >\n {/* Before anything is typed: suggestions that move on their own. */}\n {!value.trim() && (\n /* 260px from the card to the top of the first suggestion on a tall\n window. The quick actions are hidden while the search is open,\n so the only thing between is the rail's own 8px of top padding\n and this margin: 252. On a short window the gap gives way\n instead of the page scrolling: 100vh less what sits above the\n rail (about 271px of heading and box) and the rail itself\n (296px) and the container's inset, never below 24px. The list's\n 12px margin above collapses into this one, so it adds nothing. */\n <div\n style={{\n marginTop: 'clamp(24px, calc(100vh - 585px), 252px)',\n }}\n >\n <SearchTips />\n </div>\n )}\n {/* Results only once there is something to look for. With an empty\n box the carousel above is the whole panel: a list of recent\n orders and customers under it read as a second, unrelated\n thing, and it cost three requests on every focus. */}\n {value.trim() && (\n <AskResults\n query={value}\n onNavigate={() => {\n setOpen(false);\n setValue('');\n }}\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -12,6 +12,7 @@ import { useEntityLabel } from "@withpotter/lib/intents/use-entity-label";
|
|
|
12
12
|
import { useIntentConfig } from "@withpotter/lib/intents/use-intent-config";
|
|
13
13
|
import { usePermissions } from "@withpotter/lib/use-permissions";
|
|
14
14
|
import { useTakePayment } from "../take-payment-context";
|
|
15
|
+
import { hostQuickActions } from "./host-quick-actions";
|
|
15
16
|
import { homeAddItemIntent, homeAddItemLinks } from "./home-add-item-links";
|
|
16
17
|
const PILL = "flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]";
|
|
17
18
|
function HomeQuickActions() {
|
|
@@ -44,7 +45,14 @@ function HomeQuickActions() {
|
|
|
44
45
|
Icon: Plus,
|
|
45
46
|
run: () => router.push(links.create)
|
|
46
47
|
}
|
|
47
|
-
] : []
|
|
48
|
+
] : [],
|
|
49
|
+
// Whatever the host registered, after the built-in three.
|
|
50
|
+
...hostQuickActions().filter((a) => !a.allowed || a.allowed((p) => can(p))).map((a) => ({
|
|
51
|
+
id: a.id,
|
|
52
|
+
label: a.label,
|
|
53
|
+
Icon: a.Icon,
|
|
54
|
+
run: () => router.push(a.href)
|
|
55
|
+
}))
|
|
48
56
|
];
|
|
49
57
|
if (actions.length === 0) return null;
|
|
50
58
|
return /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/home/home-quick-actions.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport {\n ChevronDown,\n CreditCard,\n FilePlus2,\n Plus,\n type LucideIcon,\n} from 'lucide-react';\nimport { OverflowMenu, OverflowItem } from '@withpotter/ui-kit/overflow-menu';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { useTakePayment } from '../take-payment-context';\nimport { homeAddItemIntent, homeAddItemLinks } from './home-add-item-links';\n\nconst PILL =\n 'flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]';\n\n/**\n * Take payment, Send invoice, Add item: the three things done most, as pills\n * under the ask card.\n *\n * They replaced Ask, Write and Organize, which were a look: choosing one moved a\n * highlight and nothing read it. These do something. Take payment opens the same\n * modal the rail used to carry; Send invoice and Add item go to the screens that\n * make them. \"Add item\" says the business's own word (`useEntityLabel`), so a\n * club is told to add a service and a shop a product.\n *\n * The first two write money, so they follow the home's own gate\n * (`canManageFinance`); Add item follows the catalogue's (`canManageProducts`).\n * Add item opens a two-way menu, the same as on the catalogue — typing one or\n * importing many is decided at the button, not on a blank form.\n * Nothing is drawn while permissions resolve, so the row appears whole.\n */\nexport function HomeQuickActions() {\n const router = useRouter();\n const takePayment = useTakePayment();\n const item = useEntityLabel('item');\n const intentConfig = useIntentConfig();\n const { can, loading } = usePermissions();\n if (loading) return null;\n // Named, so the item lands on the list the sidebar opens; see the helper.\n const links = homeAddItemLinks(homeAddItemIntent(intentConfig, item));\n\n const actions: {\n id: string;\n label: string;\n Icon: LucideIcon;\n run: () => void;\n }[] = [\n ...(can('canManageFinance')\n ? [\n {\n id: 'payment',\n label: 'Take payment',\n Icon: CreditCard,\n run: takePayment,\n },\n {\n id: 'invoice',\n label: 'Send invoice',\n Icon: FilePlus2,\n run: () => router.push('/dashboard/invoices/new'),\n },\n ]\n : []),\n ...(can('canManageProducts')\n ? [\n {\n id: 'item',\n label: `Add ${item.singular.toLowerCase()}`,\n Icon: Plus,\n run: () => router.push(links.create),\n },\n ]\n : []),\n ];\n if (actions.length === 0) return null;\n\n return (\n <div\n role=\"group\"\n aria-label=\"Quick actions\"\n className=\"mt-3 flex flex-wrap items-center justify-center gap-3\"\n >\n {actions.map(({ id, label, Icon, run }) =>\n id === 'item' ? (\n <OverflowMenu\n key={id}\n label={label}\n align=\"start\"\n trigger={(props) => (\n <button type=\"button\" {...props} className={PILL}>\n <Icon\n aria-hidden\n className=\"size-4 text-[var(--text-tertiary)]\"\n />\n {label}\n <ChevronDown aria-hidden className=\"-ml-1 size-3.5\" />\n </button>\n )}\n >\n <OverflowItem onClick={run}>Add manually</OverflowItem>\n <OverflowItem\n onClick={() => router.push(links.importSheet)}\n >\n Import from CSV\n </OverflowItem>\n </OverflowMenu>\n ) : (\n <button key={id} type=\"button\" onClick={run} className={PILL}>\n <Icon aria-hidden className=\"size-4 text-[var(--text-tertiary)]\" />\n {label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/home-quick-actions.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport {\n ChevronDown,\n CreditCard,\n FilePlus2,\n Plus,\n type LucideIcon,\n} from 'lucide-react';\nimport { OverflowMenu, OverflowItem } from '@withpotter/ui-kit/overflow-menu';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { useTakePayment } from '../take-payment-context';\nimport { hostQuickActions } from './host-quick-actions';\nimport { homeAddItemIntent, homeAddItemLinks } from './home-add-item-links';\n\nconst PILL =\n 'flex h-8 cursor-pointer items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-muted)] px-3 text-sm text-[var(--text-secondary)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--interactive-active)] hover:text-[var(--text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]';\n\n/**\n * Take payment, Send invoice, Add item: the three things done most, as pills\n * under the ask card.\n *\n * They replaced Ask, Write and Organize, which were a look: choosing one moved a\n * highlight and nothing read it. These do something. Take payment opens the same\n * modal the rail used to carry; Send invoice and Add item go to the screens that\n * make them. \"Add item\" says the business's own word (`useEntityLabel`), so a\n * club is told to add a service and a shop a product.\n *\n * The first two write money, so they follow the home's own gate\n * (`canManageFinance`); Add item follows the catalogue's (`canManageProducts`).\n * Add item opens a two-way menu, the same as on the catalogue — typing one or\n * importing many is decided at the button, not on a blank form.\n * Nothing is drawn while permissions resolve, so the row appears whole.\n */\nexport function HomeQuickActions() {\n const router = useRouter();\n const takePayment = useTakePayment();\n const item = useEntityLabel('item');\n const intentConfig = useIntentConfig();\n const { can, loading } = usePermissions();\n if (loading) return null;\n // Named, so the item lands on the list the sidebar opens; see the helper.\n const links = homeAddItemLinks(homeAddItemIntent(intentConfig, item));\n\n const actions: {\n id: string;\n label: string;\n Icon: LucideIcon;\n run: () => void;\n }[] = [\n ...(can('canManageFinance')\n ? [\n {\n id: 'payment',\n label: 'Take payment',\n Icon: CreditCard,\n run: takePayment,\n },\n {\n id: 'invoice',\n label: 'Send invoice',\n Icon: FilePlus2,\n run: () => router.push('/dashboard/invoices/new'),\n },\n ]\n : []),\n ...(can('canManageProducts')\n ? [\n {\n id: 'item',\n label: `Add ${item.singular.toLowerCase()}`,\n Icon: Plus,\n run: () => router.push(links.create),\n },\n ]\n : []),\n // Whatever the host registered, after the built-in three.\n ...hostQuickActions()\n .filter((a) => !a.allowed || a.allowed((p) => can(p as never)))\n .map((a) => ({\n id: a.id,\n label: a.label,\n Icon: a.Icon,\n run: () => router.push(a.href),\n })),\n ];\n if (actions.length === 0) return null;\n\n return (\n <div\n role=\"group\"\n aria-label=\"Quick actions\"\n className=\"mt-3 flex flex-wrap items-center justify-center gap-3\"\n >\n {actions.map(({ id, label, Icon, run }) =>\n id === 'item' ? (\n <OverflowMenu\n key={id}\n label={label}\n align=\"start\"\n trigger={(props) => (\n <button type=\"button\" {...props} className={PILL}>\n <Icon\n aria-hidden\n className=\"size-4 text-[var(--text-tertiary)]\"\n />\n {label}\n <ChevronDown aria-hidden className=\"-ml-1 size-3.5\" />\n </button>\n )}\n >\n <OverflowItem onClick={run}>Add manually</OverflowItem>\n <OverflowItem\n onClick={() => router.push(links.importSheet)}\n >\n Import from CSV\n </OverflowItem>\n </OverflowMenu>\n ) : (\n <button key={id} type=\"button\" onClick={run} className={PILL}>\n <Icon aria-hidden className=\"size-4 text-[var(--text-tertiary)]\" />\n {label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";AAwGc,SACE,KADF;AAtGd,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,cAAc,oBAAoB;AAC3C,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,mBAAmB,wBAAwB;AAEpD,MAAM,OACJ;AAkBK,SAAS,mBAAmB;AACjC,QAAM,SAAS,UAAU;AACzB,QAAM,cAAc,eAAe;AACnC,QAAM,OAAO,eAAe,MAAM;AAClC,QAAM,eAAe,gBAAgB;AACrC,QAAM,EAAE,KAAK,QAAQ,IAAI,eAAe;AACxC,MAAI,QAAS,QAAO;AAEpB,QAAM,QAAQ,iBAAiB,kBAAkB,cAAc,IAAI,CAAC;AAEpE,QAAM,UAKA;AAAA,IACJ,GAAI,IAAI,kBAAkB,IACtB;AAAA,MACE;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK;AAAA,MACP;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK,MAAM,OAAO,KAAK,yBAAyB;AAAA,MAClD;AAAA,IACF,IACA,CAAC;AAAA,IACL,GAAI,IAAI,mBAAmB,IACvB;AAAA,MACE;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,OAAO,KAAK,SAAS,YAAY,CAAC;AAAA,QACzC,MAAM;AAAA,QACN,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM;AAAA,MACrC;AAAA,IACF,IACA,CAAC;AAAA;AAAA,IAEL,GAAG,iBAAiB,EACjB,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,MAAM,IAAI,CAAU,CAAC,CAAC,EAC7D,IAAI,CAAC,OAAO;AAAA,MACX,IAAI,EAAE;AAAA,MACN,OAAO,EAAE;AAAA,MACT,MAAM,EAAE;AAAA,MACR,KAAK,MAAM,OAAO,KAAK,EAAE,IAAI;AAAA,IAC/B,EAAE;AAAA,EACN;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAW;AAAA,MACX,WAAU;AAAA,MAET,kBAAQ;AAAA,QAAI,CAAC,EAAE,IAAI,OAAO,MAAM,IAAI,MACnC,OAAO,SACL;AAAA,UAAC;AAAA;AAAA,YAEC;AAAA,YACA,OAAM;AAAA,YACN,SAAS,CAAC,UACR,qBAAC,YAAO,MAAK,UAAU,GAAG,OAAO,WAAW,MAC1C;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAW;AAAA,kBACX,WAAU;AAAA;AAAA,cACZ;AAAA,cACC;AAAA,cACD,oBAAC,eAAY,eAAW,MAAC,WAAU,kBAAiB;AAAA,eACtD;AAAA,YAGF;AAAA,kCAAC,gBAAa,SAAS,KAAK,0BAAY;AAAA,cACxC;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,MAAM,OAAO,KAAK,MAAM,WAAW;AAAA,kBAC7C;AAAA;AAAA,cAED;AAAA;AAAA;AAAA,UAnBK;AAAA,QAoBP,IAEA,qBAAC,YAAgB,MAAK,UAAS,SAAS,KAAK,WAAW,MACtD;AAAA,8BAAC,QAAK,eAAW,MAAC,WAAU,sCAAqC;AAAA,UAChE;AAAA,aAFU,EAGb;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
/**
|
|
3
|
+
* A quick action a host adds under the Home search box, such as "Send money"
|
|
4
|
+
* from an engine this package knows nothing of. Registered once, where the
|
|
5
|
+
* host builds its dashboard layout, like `registerHostWidget`. Registering the
|
|
6
|
+
* same id again replaces it.
|
|
7
|
+
*/
|
|
8
|
+
export interface HostQuickAction {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
Icon: LucideIcon;
|
|
12
|
+
/** Where it goes. */
|
|
13
|
+
href: string;
|
|
14
|
+
/** Hide it from members who may not use it. Shown to everyone when absent. */
|
|
15
|
+
allowed?: (can: (permission: string) => boolean) => boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function registerHostQuickAction(action: HostQuickAction): void;
|
|
18
|
+
/** The host's quick actions, in the order they were registered. */
|
|
19
|
+
export declare function hostQuickActions(): HostQuickAction[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const HOST_ACTIONS = /* @__PURE__ */ new Map();
|
|
2
|
+
function registerHostQuickAction(action) {
|
|
3
|
+
HOST_ACTIONS.set(action.id, action);
|
|
4
|
+
}
|
|
5
|
+
function hostQuickActions() {
|
|
6
|
+
return [...HOST_ACTIONS.values()];
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
hostQuickActions,
|
|
10
|
+
registerHostQuickAction
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=host-quick-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/home/host-quick-actions.ts"],"sourcesContent":["import type { LucideIcon } from 'lucide-react';\n\n/**\n * A quick action a host adds under the Home search box, such as \"Send money\"\n * from an engine this package knows nothing of. Registered once, where the\n * host builds its dashboard layout, like `registerHostWidget`. Registering the\n * same id again replaces it.\n */\nexport interface HostQuickAction {\n id: string;\n label: string;\n Icon: LucideIcon;\n /** Where it goes. */\n href: string;\n /** Hide it from members who may not use it. Shown to everyone when absent. */\n allowed?: (can: (permission: string) => boolean) => boolean;\n}\n\nconst HOST_ACTIONS = new Map<string, HostQuickAction>();\n\nexport function registerHostQuickAction(action: HostQuickAction): void {\n HOST_ACTIONS.set(action.id, action);\n}\n\n/** The host's quick actions, in the order they were registered. */\nexport function hostQuickActions(): HostQuickAction[] {\n return [...HOST_ACTIONS.values()];\n}\n"],"mappings":"AAkBA,MAAM,eAAe,oBAAI,IAA6B;AAE/C,SAAS,wBAAwB,QAA+B;AACrE,eAAa,IAAI,OAAO,IAAI,MAAM;AACpC;AAGO,SAAS,mBAAsC;AACpD,SAAO,CAAC,GAAG,aAAa,OAAO,CAAC;AAClC;","names":[]}
|
|
@@ -138,7 +138,13 @@ function ActivityWidget({
|
|
|
138
138
|
] }),
|
|
139
139
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 pb-3", children: [
|
|
140
140
|
/* @__PURE__ */ jsxs("p", { className: "flex items-baseline justify-between gap-2", children: [
|
|
141
|
-
/* @__PURE__ */ jsx(
|
|
141
|
+
/* @__PURE__ */ jsx(
|
|
142
|
+
"span",
|
|
143
|
+
{
|
|
144
|
+
className: `min-w-0 truncate text-sm ${a.unpaid ? "text-[var(--text-tertiary)]" : "font-medium text-[var(--text-primary)]"}`,
|
|
145
|
+
children: a.title
|
|
146
|
+
}
|
|
147
|
+
),
|
|
142
148
|
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-2xs tabular-nums text-[var(--text-muted)]", children: timeLabel(a.timestamp) })
|
|
143
149
|
] }),
|
|
144
150
|
/* @__PURE__ */ jsx("p", { className: "truncate text-sm text-[var(--text-tertiary)]", children: a.description })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/dashboard/home/widgets/activity-widget.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport {\n Package,\n PencilLine,\n ShoppingBag,\n CircleCheck,\n UserPlus,\n UserCheck,\n RefreshCw,\n Receipt,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport type { ActivityItem } from '@withpotter/components/dashboard/activity-feed';\nimport type { NextUpItem } from '../next-up-sources';\nimport { Button } from '@withpotter/ui-kit';\nimport { Panel, PanelEmpty } from '../panel';\nimport { Badge } from '@withpotter/ui-kit';\n\n/**\n * What has happened, as a timeline.\n *\n * WHY IT EXISTS ALONGSIDE \"Today\". Those two cards answer different questions\n * and the home needs both: Today is what is WAITING on you, Activity is what\n * already went. Merging them was tried and is the mistake this replaces — a\n * single list with now as the pivot puts \"Tuesday 22:00, Tunde Ogunleye\" and\n * \"invoice paid 20 minutes ago\" in one column under one heading, so the reader\n * has to check each row's tense before they know whether it is a task.\n *\n * WHAT MAKES IT A TIMELINE RATHER THAN A LIST, and this is the depth the old\n * feed never had:\n *\n * A RAIL. A hairline runs behind the icon column and stops at the last node,\n * so the rows read as one sequence rather than as five unrelated rows that\n * happen to be stacked. It is the cheapest possible signal that these are\n * ordered in time and it is the one every activity feed worth copying has.\n *\n * TYPED, COLOURED NODES. `ActivityItem.type` already carries nine kinds and\n * nothing rendered them — the desktop feed dropped the emoji and drew\n * nothing in its place. Money is green, because money arriving is the one\n * event on this list a merchant scans for; everything else is neutral, so the\n * green means something. Colour that appears on every row means nothing.\n *\n * DAY HEADINGS. Cloudflare's Recents and Square's Tasks both fail here —\n * \"3d ago\" on row four with no break above it makes a five-row list look\n * like five things that happened this morning. A heading per day is what\n * turns a stack of timestamps into a history.\n *\n * The rows are links where the object still exists and plain rows where it does\n * not, rather than every row being a link to somewhere approximate.\n *\n * BLOCKERS RIDE ON TOP, and they have to, because this card replaced Today on\n * the default board and Today was carrying them. Losing them would have made\n * the home stop answering the second half of its own question — \"what do I do\n * next, AND is anything wrong?\" — which is the half a merchant cannot find\n * anywhere else.\n *\n * They are Square's ranked task rows: a title, one line of CONSEQUENCE, and a\n * real button with a verb on it. `NextUpItem` already carries all three\n * (`title`, `meta`, `action`), and the treatment is deliberately unlike the\n * timeline below — a blocker is not something that happened, it is something\n * that is still happening to you. Only the first gets a primary button, as on\n * Square: two primaries side by side is two things that are both the most\n * important thing.\n */\n\n/**\n * Nine kinds, nine marks. `emoji` is on the type and is deliberately not used:\n * it exists for the mobile feed, and an emoji at 13px in a dense column reads\n * as a smudge rather than as a symbol.\n */\nconst MARK: Record<\n ActivityItem['type'],\n { icon: LucideIcon; money?: boolean }\n> = {\n product_added: { icon: Package },\n product_updated: { icon: PencilLine },\n order_placed: { icon: ShoppingBag },\n order_completed: { icon: CircleCheck, money: true },\n member_signup: { icon: UserPlus },\n member_converted: { icon: UserCheck },\n subscription_created: { icon: RefreshCw, money: true },\n invoice_paid: { icon: Receipt, money: true },\n invoice_sent: { icon: Send },\n};\n\n/**\n * Which day a timestamp belongs to, in the reader's own words.\n *\n * Deliberately only three answers. \"Wednesday\" for something eight days old is\n * a puzzle, not a heading — past a week the date is the only honest label.\n */\nfunction dayLabel(iso: string): string {\n const d = new Date(iso);\n const today = new Date();\n const startOf = (x: Date) =>\n new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();\n const days = Math.round((startOf(today) - startOf(d)) / 86_400_000);\n if (days <= 0) return 'Today';\n if (days === 1) return 'Yesterday';\n if (days < 7) return d.toLocaleDateString(undefined, { weekday: 'long' });\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\nfunction timeLabel(iso: string): string {\n return new Date(iso).toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n });\n}\n\n/**\n * How overdue something is, in whole days, or null when it has no clock.\n *\n * Only for items carrying a real `at`. A blocker like \"set up payouts\" has no\n * due date and never gets a countdown — Square can say \"2 days until offline\n * payments are automatically allowed\" because a real policy deadline exists\n * behind it, and manufacturing an equivalent here would be inventing urgency.\n */\nfunction overdueLabel(item: NextUpItem): string | null {\n if (!item.overdue || !item.at) return null;\n const days = Math.floor(\n (Date.now() - new Date(item.at).getTime()) / 86_400_000,\n );\n if (days < 1) return 'due today';\n return `${days}d overdue`;\n}\n\n/**\n * The blockers the go-live dialog can actually resolve.\n *\n * Both are \"your store cannot sell\", and both are fixable in two or three\n * fields without leaving the page — so they open the dialog rather than\n * navigating to a settings screen. Anything else keeps its link.\n */\nconst FIXABLE_IN_PLACE = new Set(['alert-delivery', 'alert-payouts']);\n\nexport function ActivityWidget({\n items,\n blockers = [],\n limit,\n onFixBlocker,\n}: {\n items: ActivityItem[];\n /** Things stopping money. Pinned above the timeline; never sorted into it. */\n blockers?: NextUpItem[];\n /** Narrow cards hold fewer rows than wide ones. */\n limit: number;\n /**\n * Opens the go-live checks. Undefined on a surface that cannot host the\n * dialog, in which case every blocker falls back to its own href — which is\n * a real settings screen, not a dead end.\n */\n onFixBlocker?: () => void;\n}) {\n // Two at most. A card that opens with four chores is a to-do list wearing a\n // history's title, and the rest are still on the screens that own them.\n const blocked = blockers.slice(0, 2);\n // Blockers eat rows: the card is a fixed height and cannot grow to fit both.\n const shown = items.slice(0, Math.max(1, limit - blocked.length));\n\n const blockedBlock = blocked.length > 0 && (\n <ul className=\"mb-3 space-y-2 border-b border-[var(--border-subtle)] pb-3\">\n {blocked.map((b, i) => (\n <li key={b.id} className=\"flex items-start justify-between gap-3\">\n <span className=\"min-w-0\">\n <span className=\"flex items-center gap-2\">\n <span className=\"min-w-0 truncate text-sm font-medium text-[var(--text-primary)]\">\n {b.title}\n </span>\n {/* SQUARE'S `Required` PILL, and the flag was already on the data.\n `NextUpItem.required` has been set on the payouts blocker all\n along and nothing has ever rendered it, so the one task that\n genuinely stops money read exactly like the one that does not.\n A blocker list where every row looks equally urgent is a list\n with no ranking, whatever order it is sorted in. */}\n {b.required && (\n <Badge variant=\"error\" className=\"shrink-0\">\n Required\n </Badge>\n )}\n {/* HOW LONG, where the clock is real. Square's tasks carry a\n deadline — \"2 days until offline payments are automatically\n allowed\" — and ours carried none. The honest half of that is\n elapsed time on items that HAVE a date: an invoice knows when\n it was due. Blockers have no date and are given no countdown,\n because inventing one would be the fabrication this avoids. */}\n {overdueLabel(b) && (\n <span className=\"shrink-0 text-2xs font-medium tabular-nums text-[var(--error)]\">\n {overdueLabel(b)}\n </span>\n )}\n </span>\n {b.meta && (\n <span className=\"block truncate text-sm text-[var(--text-tertiary)]\">\n {b.meta}\n </span>\n )}\n </span>\n {onFixBlocker && FIXABLE_IN_PLACE.has(b.id) ? (\n <Button\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n onClick={onFixBlocker}\n >\n {b.action ?? 'Fix'}\n </Button>\n ) : (\n <Button\n asChild\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n >\n <Link href={b.href}>{b.action ?? 'Fix'}</Link>\n </Button>\n )}\n </li>\n ))}\n </ul>\n );\n\n if (shown.length === 0 || items.length === 0) {\n return (\n <Panel title=\"Recent activity\">\n {blockedBlock}\n <PanelEmpty\n title=\"Nothing has happened yet\"\n description=\"Orders, payments and new customers will appear here as they come in.\"\n />\n </Panel>\n );\n }\n\n /**\n * Day headings resolved BEFORE the JSX, by comparing each row to the one\n * above it, rather than by carrying a `lastDay` variable through the map.\n * A mutable binding written inside a render callback is exactly what the\n * immutability rule exists to catch: it survives across renders and makes\n * the output depend on how many times the list has been walked.\n */\n const rows = shown.map((item, i) => {\n const day = dayLabel(item.timestamp);\n return {\n item,\n day,\n showDay: i === 0 || day !== dayLabel(shown[i - 1].timestamp),\n last: i === shown.length - 1,\n };\n });\n\n return (\n <Panel\n title=\"Recent activity\"\n /* The card scrolls. A row cut off at its edge looked like a mistake, so the last 20px fades\n out and the card has that much empty padding: with every row in view the fade falls on\n nothing, and with more below, the half row dissolves instead of ending mid-sentence. */\n bodyClassName=\"overflow-y-auto pt-1 pb-5 [-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)] [mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)]\"\n >\n {blockedBlock}\n <ol className=\"relative\">\n {rows.map(({ item: a, day, showDay: newDay, last }) => {\n const { icon: Icon, money } = MARK[a.type] ?? MARK.order_placed;\n\n const row = (\n <div className=\"flex gap-3\">\n {/* The rail and the node. The line is drawn by the node's own\n ::before via a sibling span rather than as a border on the\n row, so it can stop at the last item instead of running into\n the card's bottom padding and looking cut off. */}\n <div className=\"relative flex w-5 shrink-0 justify-center\">\n {!last && (\n <span\n aria-hidden\n className=\"absolute left-1/2 top-5 h-[calc(100%-8px)] w-px -translate-x-1/2 bg-[var(--border-subtle)]\"\n />\n )}\n <span\n aria-hidden\n className={`relative z-10 mt-1 grid h-5 w-5 place-items-center rounded-full ${\n money\n ? 'bg-[var(--success-bg,var(--bg-muted))] text-[var(--success)]'\n : 'bg-[var(--bg-muted)] text-[var(--text-tertiary)]'\n }`}\n >\n <Icon className=\"h-[11px] w-[11px]\" strokeWidth={2} />\n </span>\n </div>\n\n <div className=\"min-w-0 flex-1 pb-3\">\n <p className=\"flex items-baseline justify-between gap-2\">\n <span className=\"min-w-0 truncate text-sm font-medium text-[var(--text-primary)]\">\n {a.title}\n </span>\n <span className=\"shrink-0 text-2xs tabular-nums text-[var(--text-muted)]\">\n {timeLabel(a.timestamp)}\n </span>\n </p>\n <p className=\"truncate text-sm text-[var(--text-tertiary)]\">\n {a.description}\n </p>\n </div>\n </div>\n );\n\n return (\n <li key={a.id}>\n {newDay && (\n <p className=\"pb-2 pt-1 text-2xs font-medium uppercase tracking-wide text-[var(--text-muted)] first:pt-0\">\n {day}\n </p>\n )}\n {a.href ? (\n <Link\n href={a.href}\n className=\"-mx-2 block rounded-lg px-2 transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {row}\n </Link>\n ) : (\n <div className=\"-mx-2 px-2\">{row}</div>\n )}\n </li>\n );\n })}\n </ol>\n </Panel>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/home/widgets/activity-widget.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport {\n Package,\n PencilLine,\n ShoppingBag,\n CircleCheck,\n UserPlus,\n UserCheck,\n RefreshCw,\n Receipt,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport type { ActivityItem } from '@withpotter/components/dashboard/activity-feed';\nimport type { NextUpItem } from '../next-up-sources';\nimport { Button } from '@withpotter/ui-kit';\nimport { Panel, PanelEmpty } from '../panel';\nimport { Badge } from '@withpotter/ui-kit';\n\n/**\n * What has happened, as a timeline.\n *\n * WHY IT EXISTS ALONGSIDE \"Today\". Those two cards answer different questions\n * and the home needs both: Today is what is WAITING on you, Activity is what\n * already went. Merging them was tried and is the mistake this replaces — a\n * single list with now as the pivot puts \"Tuesday 22:00, Tunde Ogunleye\" and\n * \"invoice paid 20 minutes ago\" in one column under one heading, so the reader\n * has to check each row's tense before they know whether it is a task.\n *\n * WHAT MAKES IT A TIMELINE RATHER THAN A LIST, and this is the depth the old\n * feed never had:\n *\n * A RAIL. A hairline runs behind the icon column and stops at the last node,\n * so the rows read as one sequence rather than as five unrelated rows that\n * happen to be stacked. It is the cheapest possible signal that these are\n * ordered in time and it is the one every activity feed worth copying has.\n *\n * TYPED, COLOURED NODES. `ActivityItem.type` already carries nine kinds and\n * nothing rendered them — the desktop feed dropped the emoji and drew\n * nothing in its place. Money is green, because money arriving is the one\n * event on this list a merchant scans for; everything else is neutral, so the\n * green means something. Colour that appears on every row means nothing.\n *\n * DAY HEADINGS. Cloudflare's Recents and Square's Tasks both fail here —\n * \"3d ago\" on row four with no break above it makes a five-row list look\n * like five things that happened this morning. A heading per day is what\n * turns a stack of timestamps into a history.\n *\n * The rows are links where the object still exists and plain rows where it does\n * not, rather than every row being a link to somewhere approximate.\n *\n * BLOCKERS RIDE ON TOP, and they have to, because this card replaced Today on\n * the default board and Today was carrying them. Losing them would have made\n * the home stop answering the second half of its own question — \"what do I do\n * next, AND is anything wrong?\" — which is the half a merchant cannot find\n * anywhere else.\n *\n * They are Square's ranked task rows: a title, one line of CONSEQUENCE, and a\n * real button with a verb on it. `NextUpItem` already carries all three\n * (`title`, `meta`, `action`), and the treatment is deliberately unlike the\n * timeline below — a blocker is not something that happened, it is something\n * that is still happening to you. Only the first gets a primary button, as on\n * Square: two primaries side by side is two things that are both the most\n * important thing.\n */\n\n/**\n * Nine kinds, nine marks. `emoji` is on the type and is deliberately not used:\n * it exists for the mobile feed, and an emoji at 13px in a dense column reads\n * as a smudge rather than as a symbol.\n */\nconst MARK: Record<\n ActivityItem['type'],\n { icon: LucideIcon; money?: boolean }\n> = {\n product_added: { icon: Package },\n product_updated: { icon: PencilLine },\n order_placed: { icon: ShoppingBag },\n order_completed: { icon: CircleCheck, money: true },\n member_signup: { icon: UserPlus },\n member_converted: { icon: UserCheck },\n subscription_created: { icon: RefreshCw, money: true },\n invoice_paid: { icon: Receipt, money: true },\n invoice_sent: { icon: Send },\n};\n\n/**\n * Which day a timestamp belongs to, in the reader's own words.\n *\n * Deliberately only three answers. \"Wednesday\" for something eight days old is\n * a puzzle, not a heading — past a week the date is the only honest label.\n */\nfunction dayLabel(iso: string): string {\n const d = new Date(iso);\n const today = new Date();\n const startOf = (x: Date) =>\n new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();\n const days = Math.round((startOf(today) - startOf(d)) / 86_400_000);\n if (days <= 0) return 'Today';\n if (days === 1) return 'Yesterday';\n if (days < 7) return d.toLocaleDateString(undefined, { weekday: 'long' });\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\nfunction timeLabel(iso: string): string {\n return new Date(iso).toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n });\n}\n\n/**\n * How overdue something is, in whole days, or null when it has no clock.\n *\n * Only for items carrying a real `at`. A blocker like \"set up payouts\" has no\n * due date and never gets a countdown — Square can say \"2 days until offline\n * payments are automatically allowed\" because a real policy deadline exists\n * behind it, and manufacturing an equivalent here would be inventing urgency.\n */\nfunction overdueLabel(item: NextUpItem): string | null {\n if (!item.overdue || !item.at) return null;\n const days = Math.floor(\n (Date.now() - new Date(item.at).getTime()) / 86_400_000,\n );\n if (days < 1) return 'due today';\n return `${days}d overdue`;\n}\n\n/**\n * The blockers the go-live dialog can actually resolve.\n *\n * Both are \"your store cannot sell\", and both are fixable in two or three\n * fields without leaving the page — so they open the dialog rather than\n * navigating to a settings screen. Anything else keeps its link.\n */\nconst FIXABLE_IN_PLACE = new Set(['alert-delivery', 'alert-payouts']);\n\nexport function ActivityWidget({\n items,\n blockers = [],\n limit,\n onFixBlocker,\n}: {\n items: ActivityItem[];\n /** Things stopping money. Pinned above the timeline; never sorted into it. */\n blockers?: NextUpItem[];\n /** Narrow cards hold fewer rows than wide ones. */\n limit: number;\n /**\n * Opens the go-live checks. Undefined on a surface that cannot host the\n * dialog, in which case every blocker falls back to its own href — which is\n * a real settings screen, not a dead end.\n */\n onFixBlocker?: () => void;\n}) {\n // Two at most. A card that opens with four chores is a to-do list wearing a\n // history's title, and the rest are still on the screens that own them.\n const blocked = blockers.slice(0, 2);\n // Blockers eat rows: the card is a fixed height and cannot grow to fit both.\n const shown = items.slice(0, Math.max(1, limit - blocked.length));\n\n const blockedBlock = blocked.length > 0 && (\n <ul className=\"mb-3 space-y-2 border-b border-[var(--border-subtle)] pb-3\">\n {blocked.map((b, i) => (\n <li key={b.id} className=\"flex items-start justify-between gap-3\">\n <span className=\"min-w-0\">\n <span className=\"flex items-center gap-2\">\n <span className=\"min-w-0 truncate text-sm font-medium text-[var(--text-primary)]\">\n {b.title}\n </span>\n {/* SQUARE'S `Required` PILL, and the flag was already on the data.\n `NextUpItem.required` has been set on the payouts blocker all\n along and nothing has ever rendered it, so the one task that\n genuinely stops money read exactly like the one that does not.\n A blocker list where every row looks equally urgent is a list\n with no ranking, whatever order it is sorted in. */}\n {b.required && (\n <Badge variant=\"error\" className=\"shrink-0\">\n Required\n </Badge>\n )}\n {/* HOW LONG, where the clock is real. Square's tasks carry a\n deadline — \"2 days until offline payments are automatically\n allowed\" — and ours carried none. The honest half of that is\n elapsed time on items that HAVE a date: an invoice knows when\n it was due. Blockers have no date and are given no countdown,\n because inventing one would be the fabrication this avoids. */}\n {overdueLabel(b) && (\n <span className=\"shrink-0 text-2xs font-medium tabular-nums text-[var(--error)]\">\n {overdueLabel(b)}\n </span>\n )}\n </span>\n {b.meta && (\n <span className=\"block truncate text-sm text-[var(--text-tertiary)]\">\n {b.meta}\n </span>\n )}\n </span>\n {onFixBlocker && FIXABLE_IN_PLACE.has(b.id) ? (\n <Button\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n onClick={onFixBlocker}\n >\n {b.action ?? 'Fix'}\n </Button>\n ) : (\n <Button\n asChild\n size=\"sm\"\n variant={i === 0 ? 'primary' : 'secondary'}\n className=\"shrink-0\"\n >\n <Link href={b.href}>{b.action ?? 'Fix'}</Link>\n </Button>\n )}\n </li>\n ))}\n </ul>\n );\n\n if (shown.length === 0 || items.length === 0) {\n return (\n <Panel title=\"Recent activity\">\n {blockedBlock}\n <PanelEmpty\n title=\"Nothing has happened yet\"\n description=\"Orders, payments and new customers will appear here as they come in.\"\n />\n </Panel>\n );\n }\n\n /**\n * Day headings resolved BEFORE the JSX, by comparing each row to the one\n * above it, rather than by carrying a `lastDay` variable through the map.\n * A mutable binding written inside a render callback is exactly what the\n * immutability rule exists to catch: it survives across renders and makes\n * the output depend on how many times the list has been walked.\n */\n const rows = shown.map((item, i) => {\n const day = dayLabel(item.timestamp);\n return {\n item,\n day,\n showDay: i === 0 || day !== dayLabel(shown[i - 1].timestamp),\n last: i === shown.length - 1,\n };\n });\n\n return (\n <Panel\n title=\"Recent activity\"\n /* The card scrolls. A row cut off at its edge looked like a mistake, so the last 20px fades\n out and the card has that much empty padding: with every row in view the fade falls on\n nothing, and with more below, the half row dissolves instead of ending mid-sentence. */\n bodyClassName=\"overflow-y-auto pt-1 pb-5 [-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)] [mask-image:linear-gradient(to_bottom,#000_calc(100%_-_20px),transparent)]\"\n >\n {blockedBlock}\n <ol className=\"relative\">\n {rows.map(({ item: a, day, showDay: newDay, last }) => {\n const { icon: Icon, money } = MARK[a.type] ?? MARK.order_placed;\n\n const row = (\n <div className=\"flex gap-3\">\n {/* The rail and the node. The line is drawn by the node's own\n ::before via a sibling span rather than as a border on the\n row, so it can stop at the last item instead of running into\n the card's bottom padding and looking cut off. */}\n <div className=\"relative flex w-5 shrink-0 justify-center\">\n {!last && (\n <span\n aria-hidden\n className=\"absolute left-1/2 top-5 h-[calc(100%-8px)] w-px -translate-x-1/2 bg-[var(--border-subtle)]\"\n />\n )}\n <span\n aria-hidden\n className={`relative z-10 mt-1 grid h-5 w-5 place-items-center rounded-full ${\n money\n ? 'bg-[var(--success-bg,var(--bg-muted))] text-[var(--success)]'\n : 'bg-[var(--bg-muted)] text-[var(--text-tertiary)]'\n }`}\n >\n <Icon className=\"h-[11px] w-[11px]\" strokeWidth={2} />\n </span>\n </div>\n\n <div className=\"min-w-0 flex-1 pb-3\">\n <p className=\"flex items-baseline justify-between gap-2\">\n <span\n className={`min-w-0 truncate text-sm ${\n a.unpaid\n ? 'text-[var(--text-tertiary)]'\n : 'font-medium text-[var(--text-primary)]'\n }`}\n >\n {a.title}\n </span>\n <span className=\"shrink-0 text-2xs tabular-nums text-[var(--text-muted)]\">\n {timeLabel(a.timestamp)}\n </span>\n </p>\n <p className=\"truncate text-sm text-[var(--text-tertiary)]\">\n {a.description}\n </p>\n </div>\n </div>\n );\n\n return (\n <li key={a.id}>\n {newDay && (\n <p className=\"pb-2 pt-1 text-2xs font-medium uppercase tracking-wide text-[var(--text-muted)] first:pt-0\">\n {day}\n </p>\n )}\n {a.href ? (\n <Link\n href={a.href}\n className=\"-mx-2 block rounded-lg px-2 transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {row}\n </Link>\n ) : (\n <div className=\"-mx-2 px-2\">{row}</div>\n )}\n </li>\n );\n })}\n </ol>\n </Panel>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/features/gift-card-artwork.tsx"],"sourcesContent":["import { Gift } from 'lucide-react';\nimport type { GiftCardDesignSnapshot } from '@withpotter/lib/api';\n\nexport const GIFT_CARD_PRESETS: GiftCardDesignSnapshot[] = [\n {\n designId: 'gold',\n name: 'Gold',\n imageUrl: null,\n palette: {\n background: '#713f12',\n foreground: '#fffbeb',\n accent: '#fbbf24',\n },\n occasion: 'general',\n },\n {\n designId: 'platinum',\n name: 'Platinum',\n imageUrl: null,\n palette: {\n background: '#334155',\n foreground: '#f8fafc',\n accent: '#cbd5e1',\n },\n occasion: 'general',\n },\n {\n designId: 'bronze',\n name: 'Bronze',\n imageUrl: null,\n palette: {\n background: '#7c2d12',\n foreground: '#fff7ed',\n accent: '#fb923c',\n },\n occasion: 'general',\n },\n {\n designId: 'silver',\n name: 'Silver',\n imageUrl: null,\n palette: {\n background: '#475569',\n foreground: '#f8fafc',\n accent: '#e2e8f0',\n },\n occasion: 'general',\n },\n];\n\ninterface GiftCardArtworkProps {\n design: GiftCardDesignSnapshot;\n merchantName: string;\n amount?: string;\n recipientName?: string;\n senderName?: string;\n code?: string;\n className?: string;\n}\n\nexport interface GiftCardArtworkData\n extends Omit<GiftCardArtworkProps, 'className'> {}\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''');\n}\n\nexport function createGiftCardSvg({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n}: GiftCardArtworkData): string {\n const foreground = design.palette.foreground;\n const background = design.palette.background;\n const accent = design.palette.accent;\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"756\" viewBox=\"0 0 1200 756\">\n <defs><pattern id=\"lines\" width=\"72\" height=\"72\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\"><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"72\" stroke=\"${accent}\" stroke-opacity=\"0.14\" stroke-width=\"3\"/></pattern></defs>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"${background}\"/>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"url(#lines)\"/>\n <rect width=\"1200\" height=\"12\" rx=\"6\" fill=\"${accent}\"/>\n <circle cx=\"78\" cy=\"82\" r=\"28\" fill=\"${accent}\"/>\n <text x=\"126\" y=\"72\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"30\" font-weight=\"700\">${escapeXml(merchantName)}</text>\n <text x=\"126\" y=\"104\" fill=\"${foreground}\" fill-opacity=\"0.7\" font-family=\"Arial, sans-serif\" font-size=\"20\">GIFT CARD</text>\n ${amount ? `<text x=\"1120\" y=\"82\" text-anchor=\"end\" fill=\"${foreground}\" fill-opacity=\"0.82\" font-family=\"Arial, sans-serif\" font-size=\"26\" font-weight=\"600\">${escapeXml(amount)}</text>` : ''}\n <text x=\"72\" y=\"330\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"58\" font-weight=\"700\">${escapeXml(design.name)}</text>\n <text x=\"72\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">TO</text>\n <text x=\"72\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(recipientName || 'Someone special')}</text>\n <text x=\"600\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">FROM</text>\n <text x=\"600\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(senderName || merchantName)}</text>\n ${code ? `<text x=\"72\" y=\"664\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">REDEEM WITH</text><text x=\"72\" y=\"704\" fill=\"${foreground}\" font-family=\"monospace\" font-size=\"30\" font-weight=\"700\">${escapeXml(code)}</text>` : ''}\n</svg>`;\n}\n\nexport async function downloadGiftCardArtwork(\n data: GiftCardArtworkData,\n): Promise<void> {\n const svgBlob = new Blob([createGiftCardSvg(data)], {\n type: 'image/svg+xml;charset=utf-8',\n });\n const svgUrl = URL.createObjectURL(svgBlob);\n const image = new Image();\n await new Promise<void>((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = () => reject(new Error('Unable to render gift card image'));\n image.src = svgUrl;\n });\n\n const canvas = document.createElement('canvas');\n canvas.width = 1200;\n canvas.height = 756;\n const context = canvas.getContext('2d');\n if (!context) {\n URL.revokeObjectURL(svgUrl);\n throw new Error('Unable to create gift card image');\n }\n context.drawImage(image, 0, 0, canvas.width, canvas.height);\n URL.revokeObjectURL(svgUrl);\n\n const pngBlob = await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) =>\n blob\n ? resolve(blob)\n : reject(new Error('Unable to encode gift card image')),\n 'image/png',\n );\n });\n const url = URL.createObjectURL(pngBlob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `gift-card-${data.code || 'preview'}.png`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n}\n\nexport function createGiftCardEmailHtml(data: GiftCardArtworkData): string {\n const { design, merchantName, amount, recipientName, senderName, code } = data;\n return `<div style=\"margin:0;padding:32px 16px;background:#f4f4f5;font-family:Arial,sans-serif;color:#18181b\">\n <div style=\"max-width:620px;margin:0 auto\">\n <p style=\"margin:0 0 16px;font-size:16px\">${escapeXml(senderName || merchantName)} sent you a gift card.</p>\n <div style=\"padding:28px;border-radius:12px;background:${design.palette.background};color:${design.palette.foreground};border-top:6px solid ${design.palette.accent}\">\n <div style=\"font-size:20px;font-weight:700\">${escapeXml(merchantName)}</div>\n <div style=\"margin-top:4px;font-size:12px;opacity:.7\">GIFT CARD</div>\n <div style=\"margin-top:56px;font-size:30px;font-weight:700\">${escapeXml(design.name)}</div>\n <table role=\"presentation\" style=\"width:100%;margin-top:40px;color:inherit\"><tr>\n <td><div style=\"font-size:11px;opacity:.6\">TO</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(recipientName || 'Someone special')}</div></td>\n <td><div style=\"font-size:11px;opacity:.6\">FROM</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(senderName || merchantName)}</div></td>\n ${amount ? `<td style=\"text-align:right;font-size:16px;font-weight:600\">${escapeXml(amount)}</td>` : ''}\n </tr></table>\n ${code ? `<div style=\"margin-top:36px;font-size:11px;opacity:.6\">REDEEM WITH</div><div style=\"margin-top:6px;font-family:monospace;font-size:20px;font-weight:700\">${escapeXml(code)}</div>` : ''}\n </div>\n </div>\n</div>`;\n}\n\nexport function GiftCardArtwork({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n className = '',\n}: GiftCardArtworkProps) {\n return (\n <div\n className={`relative aspect-[1.586/1] w-full overflow-hidden rounded-md p-5 ${className}`}\n style={{\n color: design.palette.foreground,\n backgroundColor: design.palette.background,\n backgroundImage: `repeating-linear-gradient(135deg, transparent 0 18px, ${design.palette.accent}22 18px 19px)`,\n }}\n >\n {design.imageUrl && (\n <img\n src={design.imageUrl}\n alt=\"\"\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n )}\n <div\n className=\"absolute inset-x-0 top-0 h-1\"\n style={{ backgroundColor: design.palette.accent }}\n />\n <div className=\"relative flex h-full flex-col justify-between\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span\n className=\"flex h-7 w-7 items-center justify-center rounded-full\"\n style={{\n color: design.palette.background,\n backgroundColor: design.palette.accent,\n }}\n >\n <Gift className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold\">{merchantName}</p>\n <p className=\"text-[10px] font-medium uppercase opacity-65\">\n Gift card\n </p>\n </div>\n </div>\n {amount && (\n <span className=\"shrink-0 text-sm font-medium opacity-80\">\n {amount}\n </span>\n )}\n </div>\n <div>\n <p className=\"text-lg font-semibold\">{design.name}</p>\n <div className=\"mt-3 grid grid-cols-2 gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">To</p>\n <p className=\"truncate text-sm font-medium\">\n {recipientName || 'Someone special'}\n </p>\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">\n From\n </p>\n <p className=\"truncate text-sm font-medium\">\n {senderName || merchantName}\n </p>\n </div>\n </div>\n {code && <p className=\"mt-2 truncate font-mono text-xs opacity-70\">{code}</p>}\n </div>\n </div>\n </div>\n );\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/features/gift-card-artwork.tsx"],"sourcesContent":["import { Gift } from 'lucide-react';\nimport type { GiftCardDesignSnapshot } from '@withpotter/lib/api';\n\nexport const GIFT_CARD_PRESETS: GiftCardDesignSnapshot[] = [\n {\n designId: 'gold',\n name: 'Gold',\n imageUrl: null,\n palette: {\n background: '#a16207',\n foreground: '#fffbeb',\n accent: '#fbbf24',\n },\n occasion: 'general',\n },\n {\n designId: 'platinum',\n name: 'Platinum',\n imageUrl: null,\n palette: {\n background: '#334155',\n foreground: '#f8fafc',\n accent: '#cbd5e1',\n },\n occasion: 'general',\n },\n {\n designId: 'bronze',\n name: 'Bronze',\n imageUrl: null,\n palette: {\n background: '#7c2d12',\n foreground: '#fff7ed',\n accent: '#fb923c',\n },\n occasion: 'general',\n },\n {\n designId: 'silver',\n name: 'Silver',\n imageUrl: null,\n palette: {\n background: '#475569',\n foreground: '#f8fafc',\n accent: '#e2e8f0',\n },\n occasion: 'general',\n },\n];\n\ninterface GiftCardArtworkProps {\n design: GiftCardDesignSnapshot;\n merchantName: string;\n amount?: string;\n recipientName?: string;\n senderName?: string;\n code?: string;\n className?: string;\n}\n\nexport interface GiftCardArtworkData\n extends Omit<GiftCardArtworkProps, 'className'> {}\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''');\n}\n\nexport function createGiftCardSvg({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n}: GiftCardArtworkData): string {\n const foreground = design.palette.foreground;\n const background = design.palette.background;\n const accent = design.palette.accent;\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"756\" viewBox=\"0 0 1200 756\">\n <defs><pattern id=\"lines\" width=\"72\" height=\"72\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\"><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"72\" stroke=\"${accent}\" stroke-opacity=\"0.14\" stroke-width=\"3\"/></pattern></defs>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"${background}\"/>\n <rect width=\"1200\" height=\"756\" rx=\"32\" fill=\"url(#lines)\"/>\n <rect width=\"1200\" height=\"12\" rx=\"6\" fill=\"${accent}\"/>\n <circle cx=\"78\" cy=\"82\" r=\"28\" fill=\"${accent}\"/>\n <text x=\"126\" y=\"72\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"30\" font-weight=\"700\">${escapeXml(merchantName)}</text>\n <text x=\"126\" y=\"104\" fill=\"${foreground}\" fill-opacity=\"0.7\" font-family=\"Arial, sans-serif\" font-size=\"20\">GIFT CARD</text>\n ${amount ? `<text x=\"1120\" y=\"82\" text-anchor=\"end\" fill=\"${foreground}\" fill-opacity=\"0.82\" font-family=\"Arial, sans-serif\" font-size=\"26\" font-weight=\"600\">${escapeXml(amount)}</text>` : ''}\n <text x=\"72\" y=\"330\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"58\" font-weight=\"700\">${escapeXml(design.name)}</text>\n <text x=\"72\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">TO</text>\n <text x=\"72\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(recipientName || 'Someone special')}</text>\n <text x=\"600\" y=\"470\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">FROM</text>\n <text x=\"600\" y=\"515\" fill=\"${foreground}\" font-family=\"Arial, sans-serif\" font-size=\"34\" font-weight=\"600\">${escapeXml(senderName || merchantName)}</text>\n ${code ? `<text x=\"72\" y=\"664\" fill=\"${foreground}\" fill-opacity=\"0.58\" font-family=\"Arial, sans-serif\" font-size=\"18\" font-weight=\"700\">REDEEM WITH</text><text x=\"72\" y=\"704\" fill=\"${foreground}\" font-family=\"monospace\" font-size=\"30\" font-weight=\"700\">${escapeXml(code)}</text>` : ''}\n</svg>`;\n}\n\nexport async function downloadGiftCardArtwork(\n data: GiftCardArtworkData,\n): Promise<void> {\n const svgBlob = new Blob([createGiftCardSvg(data)], {\n type: 'image/svg+xml;charset=utf-8',\n });\n const svgUrl = URL.createObjectURL(svgBlob);\n const image = new Image();\n await new Promise<void>((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = () => reject(new Error('Unable to render gift card image'));\n image.src = svgUrl;\n });\n\n const canvas = document.createElement('canvas');\n canvas.width = 1200;\n canvas.height = 756;\n const context = canvas.getContext('2d');\n if (!context) {\n URL.revokeObjectURL(svgUrl);\n throw new Error('Unable to create gift card image');\n }\n context.drawImage(image, 0, 0, canvas.width, canvas.height);\n URL.revokeObjectURL(svgUrl);\n\n const pngBlob = await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) =>\n blob\n ? resolve(blob)\n : reject(new Error('Unable to encode gift card image')),\n 'image/png',\n );\n });\n const url = URL.createObjectURL(pngBlob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `gift-card-${data.code || 'preview'}.png`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n}\n\nexport function createGiftCardEmailHtml(data: GiftCardArtworkData): string {\n const { design, merchantName, amount, recipientName, senderName, code } = data;\n return `<div style=\"margin:0;padding:32px 16px;background:#f4f4f5;font-family:Arial,sans-serif;color:#18181b\">\n <div style=\"max-width:620px;margin:0 auto\">\n <p style=\"margin:0 0 16px;font-size:16px\">${escapeXml(senderName || merchantName)} sent you a gift card.</p>\n <div style=\"padding:28px;border-radius:12px;background:${design.palette.background};color:${design.palette.foreground};border-top:6px solid ${design.palette.accent}\">\n <div style=\"font-size:20px;font-weight:700\">${escapeXml(merchantName)}</div>\n <div style=\"margin-top:4px;font-size:12px;opacity:.7\">GIFT CARD</div>\n <div style=\"margin-top:56px;font-size:30px;font-weight:700\">${escapeXml(design.name)}</div>\n <table role=\"presentation\" style=\"width:100%;margin-top:40px;color:inherit\"><tr>\n <td><div style=\"font-size:11px;opacity:.6\">TO</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(recipientName || 'Someone special')}</div></td>\n <td><div style=\"font-size:11px;opacity:.6\">FROM</div><div style=\"margin-top:6px;font-size:18px;font-weight:600\">${escapeXml(senderName || merchantName)}</div></td>\n ${amount ? `<td style=\"text-align:right;font-size:16px;font-weight:600\">${escapeXml(amount)}</td>` : ''}\n </tr></table>\n ${code ? `<div style=\"margin-top:36px;font-size:11px;opacity:.6\">REDEEM WITH</div><div style=\"margin-top:6px;font-family:monospace;font-size:20px;font-weight:700\">${escapeXml(code)}</div>` : ''}\n </div>\n </div>\n</div>`;\n}\n\nexport function GiftCardArtwork({\n design,\n merchantName,\n amount,\n recipientName,\n senderName,\n code,\n className = '',\n}: GiftCardArtworkProps) {\n return (\n <div\n className={`relative aspect-[1.586/1] w-full overflow-hidden rounded-md p-5 ${className}`}\n style={{\n color: design.palette.foreground,\n backgroundColor: design.palette.background,\n backgroundImage: `repeating-linear-gradient(135deg, transparent 0 18px, ${design.palette.accent}22 18px 19px)`,\n }}\n >\n {design.imageUrl && (\n <img\n src={design.imageUrl}\n alt=\"\"\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n )}\n <div\n className=\"absolute inset-x-0 top-0 h-1\"\n style={{ backgroundColor: design.palette.accent }}\n />\n <div className=\"relative flex h-full flex-col justify-between\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span\n className=\"flex h-7 w-7 items-center justify-center rounded-full\"\n style={{\n color: design.palette.background,\n backgroundColor: design.palette.accent,\n }}\n >\n <Gift className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold\">{merchantName}</p>\n <p className=\"text-[10px] font-medium uppercase opacity-65\">\n Gift card\n </p>\n </div>\n </div>\n {amount && (\n <span className=\"shrink-0 text-sm font-medium opacity-80\">\n {amount}\n </span>\n )}\n </div>\n <div>\n <p className=\"text-lg font-semibold\">{design.name}</p>\n <div className=\"mt-3 grid grid-cols-2 gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">To</p>\n <p className=\"truncate text-sm font-medium\">\n {recipientName || 'Someone special'}\n </p>\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-[10px] font-semibold uppercase opacity-55\">\n From\n </p>\n <p className=\"truncate text-sm font-medium\">\n {senderName || merchantName}\n </p>\n </div>\n </div>\n {code && <p className=\"mt-2 truncate font-mono text-xs opacity-70\">{code}</p>}\n </div>\n </div>\n </div>\n );\n}"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { GiftCardStatus } from '@withpotter/lib/api';
|
|
2
|
+
/**
|
|
3
|
+
* One vocabulary for a card's state, used by the filter chips, the status badge
|
|
4
|
+
* in the list and the hints. "Used" and "Redeemed" used to describe two
|
|
5
|
+
* different things with nearly the same word: a card whose balance is all
|
|
6
|
+
* spent, and the amount spent across every card. The state is now "Fully
|
|
7
|
+
* spent" and the amount is "Spent".
|
|
8
|
+
*/
|
|
9
|
+
export declare const STATUS_LABEL: Record<GiftCardStatus, string>;
|
|
10
|
+
export declare const STATUS_HINT: Record<GiftCardStatus, string>;
|
|
11
|
+
export declare const ALL_HINT = "Every card, whatever its state.";
|
|
12
|
+
export interface StatusFilter {
|
|
13
|
+
value: GiftCardStatus | '';
|
|
14
|
+
label: string;
|
|
15
|
+
hint: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const STATUS_FILTERS: StatusFilter[];
|
|
18
|
+
/** The two lines behind the info icon beside the page title. */
|
|
19
|
+
export declare const PAGE_HINT_LINES: string[];
|
|
20
|
+
/**
|
|
21
|
+
* True when there is nothing to show and nothing narrowing the list: no cards
|
|
22
|
+
* have ever been issued. A filtered list that comes back empty is not first
|
|
23
|
+
* run, and neither is a page still loading.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isFirstRun(input: {
|
|
26
|
+
loading: boolean;
|
|
27
|
+
search: string;
|
|
28
|
+
status: GiftCardStatus | '';
|
|
29
|
+
count: number;
|
|
30
|
+
totalIssued: number | null | undefined;
|
|
31
|
+
}): boolean;
|
|
32
|
+
/** The symbol a currency is written with, falling back to its code. */
|
|
33
|
+
export declare function currencySymbol(currency: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Shares one request between callers asking for the same thing at the same
|
|
36
|
+
* time. A key is forgotten the moment its request settles, so a later call
|
|
37
|
+
* (a reload, a poll) still goes to the network.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createInFlightDeduper(): {
|
|
40
|
+
run<T>(key: string, start: () => Promise<T>): Promise<T>;
|
|
41
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const STATUS_LABEL = {
|
|
2
|
+
active: "Active",
|
|
3
|
+
used: "Fully spent",
|
|
4
|
+
expired: "Expired",
|
|
5
|
+
cancelled: "Cancelled"
|
|
6
|
+
};
|
|
7
|
+
const STATUS_HINT = {
|
|
8
|
+
active: "Can still be spent.",
|
|
9
|
+
used: "The whole balance has been spent.",
|
|
10
|
+
expired: "Passed its expiry date.",
|
|
11
|
+
cancelled: "Voided by you. The record is kept."
|
|
12
|
+
};
|
|
13
|
+
const ALL_HINT = "Every card, whatever its state.";
|
|
14
|
+
const STATUS_FILTERS = [
|
|
15
|
+
{ value: "", label: "All", hint: ALL_HINT },
|
|
16
|
+
...["active", "used", "expired", "cancelled"].map((value) => ({
|
|
17
|
+
value,
|
|
18
|
+
label: STATUS_LABEL[value],
|
|
19
|
+
hint: STATUS_HINT[value]
|
|
20
|
+
}))
|
|
21
|
+
];
|
|
22
|
+
const PAGE_HINT_LINES = [
|
|
23
|
+
"Credit a customer pays for now and spends later. Share the code with them.",
|
|
24
|
+
"Open a card to email it or redeem some of its balance. Check balance looks up a code."
|
|
25
|
+
];
|
|
26
|
+
function isFirstRun(input) {
|
|
27
|
+
return !input.loading && !input.search.trim() && !input.status && input.count === 0 && (input.totalIssued ?? 0) === 0;
|
|
28
|
+
}
|
|
29
|
+
function currencySymbol(currency) {
|
|
30
|
+
try {
|
|
31
|
+
const part = new Intl.NumberFormat("en", {
|
|
32
|
+
style: "currency",
|
|
33
|
+
currency,
|
|
34
|
+
currencyDisplay: "narrowSymbol"
|
|
35
|
+
}).formatToParts(0).find((p) => p.type === "currency");
|
|
36
|
+
return part?.value ?? currency;
|
|
37
|
+
} catch {
|
|
38
|
+
return currency;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function createInFlightDeduper() {
|
|
42
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
43
|
+
return {
|
|
44
|
+
run(key, start) {
|
|
45
|
+
const existing = inFlight.get(key);
|
|
46
|
+
if (existing) return existing;
|
|
47
|
+
const promise = start().finally(() => {
|
|
48
|
+
if (inFlight.get(key) === promise) inFlight.delete(key);
|
|
49
|
+
});
|
|
50
|
+
inFlight.set(key, promise);
|
|
51
|
+
return promise;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
ALL_HINT,
|
|
57
|
+
PAGE_HINT_LINES,
|
|
58
|
+
STATUS_FILTERS,
|
|
59
|
+
STATUS_HINT,
|
|
60
|
+
STATUS_LABEL,
|
|
61
|
+
createInFlightDeduper,
|
|
62
|
+
currencySymbol,
|
|
63
|
+
isFirstRun
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=gift-card-logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/features/gift-cards/gift-card-logic.ts"],"sourcesContent":["import type { GiftCardStatus } from '@withpotter/lib/api';\n\n/**\n * One vocabulary for a card's state, used by the filter chips, the status badge\n * in the list and the hints. \"Used\" and \"Redeemed\" used to describe two\n * different things with nearly the same word: a card whose balance is all\n * spent, and the amount spent across every card. The state is now \"Fully\n * spent\" and the amount is \"Spent\".\n */\nexport const STATUS_LABEL: Record<GiftCardStatus, string> = {\n active: 'Active',\n used: 'Fully spent',\n expired: 'Expired',\n cancelled: 'Cancelled',\n};\n\nexport const STATUS_HINT: Record<GiftCardStatus, string> = {\n active: 'Can still be spent.',\n used: 'The whole balance has been spent.',\n expired: 'Passed its expiry date.',\n cancelled: 'Voided by you. The record is kept.',\n};\n\nexport const ALL_HINT = 'Every card, whatever its state.';\n\nexport interface StatusFilter {\n value: GiftCardStatus | '';\n label: string;\n hint: string;\n}\n\nexport const STATUS_FILTERS: StatusFilter[] = [\n { value: '', label: 'All', hint: ALL_HINT },\n ...(['active', 'used', 'expired', 'cancelled'] as const).map((value) => ({\n value,\n label: STATUS_LABEL[value],\n hint: STATUS_HINT[value],\n })),\n];\n\n/** The two lines behind the info icon beside the page title. */\nexport const PAGE_HINT_LINES = [\n 'Credit a customer pays for now and spends later. Share the code with them.',\n 'Open a card to email it or redeem some of its balance. Check balance looks up a code.',\n];\n\n/**\n * True when there is nothing to show and nothing narrowing the list: no cards\n * have ever been issued. A filtered list that comes back empty is not first\n * run, and neither is a page still loading.\n */\nexport function isFirstRun(input: {\n loading: boolean;\n search: string;\n status: GiftCardStatus | '';\n count: number;\n totalIssued: number | null | undefined;\n}): boolean {\n return (\n !input.loading &&\n !input.search.trim() &&\n !input.status &&\n input.count === 0 &&\n (input.totalIssued ?? 0) === 0\n );\n}\n\n/** The symbol a currency is written with, falling back to its code. */\nexport function currencySymbol(currency: string): string {\n try {\n const part = new Intl.NumberFormat('en', {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n })\n .formatToParts(0)\n .find((p) => p.type === 'currency');\n return part?.value ?? currency;\n } catch {\n return currency;\n }\n}\n\n/**\n * Shares one request between callers asking for the same thing at the same\n * time. A key is forgotten the moment its request settles, so a later call\n * (a reload, a poll) still goes to the network.\n */\nexport function createInFlightDeduper() {\n const inFlight = new Map<string, Promise<unknown>>();\n return {\n run<T>(key: string, start: () => Promise<T>): Promise<T> {\n const existing = inFlight.get(key);\n if (existing) return existing as Promise<T>;\n const promise = start().finally(() => {\n if (inFlight.get(key) === promise) inFlight.delete(key);\n });\n inFlight.set(key, promise);\n return promise;\n },\n };\n}\n"],"mappings":"AASO,MAAM,eAA+C;AAAA,EAC1D,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AAAA,EACT,WAAW;AACb;AAEO,MAAM,cAA8C;AAAA,EACzD,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AAAA,EACT,WAAW;AACb;AAEO,MAAM,WAAW;AAQjB,MAAM,iBAAiC;AAAA,EAC5C,EAAE,OAAO,IAAI,OAAO,OAAO,MAAM,SAAS;AAAA,EAC1C,GAAI,CAAC,UAAU,QAAQ,WAAW,WAAW,EAAY,IAAI,CAAC,WAAW;AAAA,IACvE;AAAA,IACA,OAAO,aAAa,KAAK;AAAA,IACzB,MAAM,YAAY,KAAK;AAAA,EACzB,EAAE;AACJ;AAGO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;AAOO,SAAS,WAAW,OAMf;AACV,SACE,CAAC,MAAM,WACP,CAAC,MAAM,OAAO,KAAK,KACnB,CAAC,MAAM,UACP,MAAM,UAAU,MACf,MAAM,eAAe,OAAO;AAEjC;AAGO,SAAS,eAAe,UAA0B;AACvD,MAAI;AACF,UAAM,OAAO,IAAI,KAAK,aAAa,MAAM;AAAA,MACvC,OAAO;AAAA,MACP;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC,EACE,cAAc,CAAC,EACf,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU;AACpC,WAAO,MAAM,SAAS;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,wBAAwB;AACtC,QAAM,WAAW,oBAAI,IAA8B;AACnD,SAAO;AAAA,IACL,IAAO,KAAa,OAAqC;AACvD,YAAM,WAAW,SAAS,IAAI,GAAG;AACjC,UAAI,SAAU,QAAO;AACrB,YAAM,UAAU,MAAM,EAAE,QAAQ,MAAM;AACpC,YAAI,SAAS,IAAI,GAAG,MAAM,QAAS,UAAS,OAAO,GAAG;AAAA,MACxD,CAAC;AACD,eAAS,IAAI,KAAK,OAAO;AACzB,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}
|