@burdenoff/microfe-theculturepost 2026.704.2 → 2026.705.2
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/theculturepost/TheCulturePostRoot.js +63 -45
- package/dist/theculturepost/TheCulturePostRoot.js.map +1 -1
- package/dist/theculturepost/TheCulturePostRoutes.js +17 -9
- package/dist/theculturepost/TheCulturePostRoutes.js.map +1 -1
- package/dist/theculturepost/components/AddCitationDialog.js +131 -102
- package/dist/theculturepost/components/AddCitationDialog.js.map +1 -1
- package/dist/theculturepost/components/ArchiveItemCard.js +56 -43
- package/dist/theculturepost/components/ArchiveItemCard.js.map +1 -1
- package/dist/theculturepost/components/AudioPlayer.js +74 -0
- package/dist/theculturepost/components/AudioPlayer.js.map +1 -0
- package/dist/theculturepost/components/CitationList.js +215 -105
- package/dist/theculturepost/components/CitationList.js.map +1 -1
- package/dist/theculturepost/components/CollectionCard.js +32 -32
- package/dist/theculturepost/components/CollectionCard.js.map +1 -1
- package/dist/theculturepost/components/CreateArchiveItemDialog.js +133 -93
- package/dist/theculturepost/components/CreateArchiveItemDialog.js.map +1 -1
- package/dist/theculturepost/components/CreateCollectionDialog.js +91 -70
- package/dist/theculturepost/components/CreateCollectionDialog.js.map +1 -1
- package/dist/theculturepost/components/CreateStoryDialog.js +311 -132
- package/dist/theculturepost/components/CreateStoryDialog.js.map +1 -1
- package/dist/theculturepost/components/DecorativeCover.js +78 -0
- package/dist/theculturepost/components/DecorativeCover.js.map +1 -0
- package/dist/theculturepost/components/DecorativeDivider.js +45 -0
- package/dist/theculturepost/components/DecorativeDivider.js.map +1 -0
- package/dist/theculturepost/components/DiscussionCard.js +75 -60
- package/dist/theculturepost/components/DiscussionCard.js.map +1 -1
- package/dist/theculturepost/components/HistoryTimeline.js +115 -0
- package/dist/theculturepost/components/HistoryTimeline.js.map +1 -0
- package/dist/theculturepost/components/LifecycleStepper.js +33 -0
- package/dist/theculturepost/components/LifecycleStepper.js.map +1 -0
- package/dist/theculturepost/components/OpenDiscussionDialog.js +66 -56
- package/dist/theculturepost/components/OpenDiscussionDialog.js.map +1 -1
- package/dist/theculturepost/components/OralHistoryCard.js +6 -73
- package/dist/theculturepost/components/OverlayBadge.js +22 -0
- package/dist/theculturepost/components/OverlayBadge.js.map +1 -0
- package/dist/theculturepost/components/PageHero.js +62 -0
- package/dist/theculturepost/components/PageHero.js.map +1 -0
- package/dist/theculturepost/components/RecordOralHistoryDialog.js +306 -131
- package/dist/theculturepost/components/RecordOralHistoryDialog.js.map +1 -1
- package/dist/theculturepost/components/ReplyThread.js +268 -139
- package/dist/theculturepost/components/ReplyThread.js.map +1 -1
- package/dist/theculturepost/components/StatusBadge.js +9 -9
- package/dist/theculturepost/components/StatusBadge.js.map +1 -1
- package/dist/theculturepost/components/StoryCard.js +54 -49
- package/dist/theculturepost/components/StoryCard.js.map +1 -1
- package/dist/theculturepost/components/TopicCard.js +60 -41
- package/dist/theculturepost/components/TopicCard.js.map +1 -1
- package/dist/theculturepost/components/Waveform.js +31 -0
- package/dist/theculturepost/components/Waveform.js.map +1 -0
- package/dist/theculturepost/components/categoryMeta.js +20 -0
- package/dist/theculturepost/components/categoryMeta.js.map +1 -0
- package/dist/theculturepost/components/index.js +41 -0
- package/dist/theculturepost/components/viz/BarChart.js +53 -0
- package/dist/theculturepost/components/viz/BarChart.js.map +1 -0
- package/dist/theculturepost/components/viz/Donut.js +58 -0
- package/dist/theculturepost/components/viz/Donut.js.map +1 -0
- package/dist/theculturepost/components/viz/StatTile.js +35 -0
- package/dist/theculturepost/components/viz/StatTile.js.map +1 -0
- package/dist/theculturepost/components/viz/index.js +3 -0
- package/dist/theculturepost/hooks/useFiles.js +1 -1
- package/dist/theculturepost/hooks/useFiles.js.map +1 -1
- package/dist/theculturepost/pages/HomePage.js +247 -124
- package/dist/theculturepost/pages/HomePage.js.map +1 -1
- package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js +319 -127
- package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/archive/ArchivePage.js +265 -180
- package/dist/theculturepost/pages/archive/ArchivePage.js.map +1 -1
- package/dist/theculturepost/pages/archive/CollectionDetailPage.js +158 -143
- package/dist/theculturepost/pages/archive/CollectionDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js +155 -93
- package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/discussions/DiscussionsPage.js +16 -16
- package/dist/theculturepost/pages/discussions/DiscussionsPage.js.map +1 -1
- package/dist/theculturepost/pages/governance/GovernancePage.js +293 -0
- package/dist/theculturepost/pages/governance/GovernancePage.js.map +1 -0
- package/dist/theculturepost/pages/stories/StoriesPage.js +18 -18
- package/dist/theculturepost/pages/stories/StoriesPage.js.map +1 -1
- package/dist/theculturepost/pages/stories/StoryDetailPage.js +603 -285
- package/dist/theculturepost/pages/stories/StoryDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/timeline/TimelinePage.js +162 -0
- package/dist/theculturepost/pages/timeline/TimelinePage.js.map +1 -0
- package/dist/theculturepost/pages/topics/TopicDetailPage.js +239 -163
- package/dist/theculturepost/pages/topics/TopicDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/topics/TopicsPage.js +76 -74
- package/dist/theculturepost/pages/topics/TopicsPage.js.map +1 -1
- package/dist/theculturepost/styles/injectDisplayFont.js +11 -0
- package/dist/theculturepost/styles/injectDisplayFont.js.map +1 -0
- package/dist/theculturepost/utils/formatters.js +6 -1
- package/dist/theculturepost/utils/formatters.js.map +1 -1
- package/package.json +1 -1
- package/dist/theculturepost/components/OralHistoryCard.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddCitationDialog.js","names":[],"sources":["../../../src/theculturepost/components/AddCitationDialog.tsx"],"sourcesContent":["/**\n * AddCitationDialog — attach a verifiable source to a story.\n * @module theculturepost/components\n *\n * Writes `tcpAddCitation`. An optional source scan is uploaded to\n * wspace-files-svc (only the id is stored).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpAddCitation, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpCitation, TcpSourceType } from '../types';\n\nconst SOURCE_TYPES: TcpSourceType[] = [\n 'WEB',\n 'BOOK',\n 'ACADEMIC',\n 'PERIODICAL',\n 'INTERVIEW',\n 'PRIMARY_DOCUMENT',\n 'ARCHIVAL_RECORD',\n 'ARTIFACT',\n];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface AddCitationDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n storyId: string;\n onCreated?: (citation: TcpCitation) => void;\n}\n\nexport function AddCitationDialog({\n open,\n onOpenChange,\n storyId,\n onCreated,\n}: AddCitationDialogProps) {\n const addCitation = useTcpAddCitation();\n const [title, setTitle] = useState('');\n const [author, setAuthor] = useState('');\n const [publisher, setPublisher] = useState('');\n const [publishedOn, setPublishedOn] = useState('');\n const [url, setUrl] = useState('');\n const [excerpt, setExcerpt] = useState('');\n const [provenance, setProvenance] = useState('');\n const [sourceType, setSourceType] = useState<TcpSourceType>('WEB');\n const [sourceFileId, setSourceFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setAuthor('');\n setPublisher('');\n setPublishedOn('');\n setUrl('');\n setExcerpt('');\n setProvenance('');\n setSourceType('WEB');\n setSourceFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A citation title is required.');\n return;\n }\n try {\n const created = await addCitation.mutateAsync({\n storyId,\n title: title.trim(),\n author: author.trim() || null,\n publisher: publisher.trim() || null,\n publishedOn: dateToIso(publishedOn),\n url: url.trim() || null,\n excerpt: excerpt.trim() || null,\n provenance: provenance.trim() || null,\n sourceType,\n sourceFileId,\n });\n notify.success('Citation added', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add a citation</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Ground this story in the record with a verifiable source.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-title\">Title</Label>\n <Input\n id=\"cite-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"Source title\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-author\">Author</Label>\n <Input id=\"cite-author\" value={author} onChange={(e) => setAuthor(e.target.value)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-publisher\">Publisher</Label>\n <Input\n id=\"cite-publisher\"\n value={publisher}\n onChange={(e) => setPublisher(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Source type</Label>\n <Select value={sourceType} onValueChange={(v) => setSourceType(v as TcpSourceType)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SOURCE_TYPES.map((s) => (\n <SelectItem key={s} value={s}>\n {formatEnumLabel(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-published\">Published on</Label>\n <Input\n id=\"cite-published\"\n type=\"date\"\n value={publishedOn}\n onChange={(e) => setPublishedOn(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-url\">URL</Label>\n <Input\n id=\"cite-url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n placeholder=\"https://…\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-excerpt\">Excerpt</Label>\n <Textarea\n id=\"cite-excerpt\"\n value={excerpt}\n onChange={(e) => setExcerpt(e.target.value)}\n placeholder=\"A short quoted passage\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-provenance\">Provenance</Label>\n <Textarea\n id=\"cite-provenance\"\n value={provenance}\n onChange={(e) => setProvenance(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Source scan</Label>\n <FileUploadField\n folder={FILE_FOLDERS.artifacts}\n accept=\"image/*,application/pdf\"\n label=\"Upload source scan\"\n hint=\"A scan of the primary source (optional).\"\n fileId={sourceFileId}\n onUploaded={setSourceFileId}\n onCleared={() => setSourceFileId(null)}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={addCitation.isPending}>\n Add citation\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":";;;;;;;;;;AAgCA,IAAM,IAAgC;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,KAAa,MAAiC;AAClD,KAAI,CAAC,EAAO,QAAO;CACnB,IAAM,IAAI,IAAI,KAAK,EAAM;AACzB,QAAO,OAAO,MAAM,EAAE,SAAS,CAAC,GAAG,OAAO,EAAE,aAAa;;AAU3D,SAAgB,EAAkB,EAChC,SACA,iBACA,YACA,gBACyB;CACzB,IAAM,IAAc,GAAmB,EACjC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAK,KAAU,EAAS,GAAG,EAC5B,CAAC,GAAS,KAAc,EAAS,GAAG,EACpC,CAAC,GAAY,KAAiB,EAAS,GAAG,EAC1C,CAAC,GAAY,KAAiB,EAAwB,MAAM,EAC5D,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAC/D,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,UAAc;AAUlB,EATA,EAAS,GAAG,EACZ,EAAU,GAAG,EACb,EAAa,GAAG,EAChB,EAAe,GAAG,EAClB,EAAO,GAAG,EACV,EAAW,GAAG,EACd,EAAc,GAAG,EACjB,EAAc,MAAM,EACpB,EAAgB,KAAK,EACrB,EAAS,KAAK;;AA8BhB,QACE,kBAAC,GAAD;EAAwB;EAAoB;YAC1C,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAuB,kBAAsC,CAAA,EAC7D,kBAAC,GAAD,EAAA,UAA6B,6DAEC,CAAA,CACP,EAAA,CAAA;IAEzB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAa;QAAa,CAAA,EACzC,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;QACzC,aAAY;QACZ,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAc;SAAc,CAAA,EAC3C,kBAAC,GAAD;SAAO,IAAG;SAAc,OAAO;SAAQ,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;SAAI,CAAA,CACjF;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAiB;SAAiB,CAAA,EACjD,kBAAC,GAAD;SACE,IAAG;SACH,OAAO;SACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;SAC7C,CAAA,CACE;UACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,eAAmB,CAAA,EAC1B,kBAAC,GAAD;SAAQ,OAAO;SAAY,gBAAgB,MAAM,EAAc,EAAmB;mBAAlF,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAe,CAAA,EACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAa,KAAK,MACjB,kBAAC,GAAD;UAAoB,OAAO;oBACxB,EAAgB,EAAE;UACR,EAFI,EAEJ,CACb,EACY,CAAA,CACT;WACL;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAiB;SAAoB,CAAA,EACpD,kBAAC,GAAD;SACE,IAAG;SACH,MAAK;SACL,OAAO;SACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;SAC/C,CAAA,CACE;UACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAW;QAAW,CAAA,EACrC,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAO,EAAE,OAAO,MAAM;QACvC,aAAY;QACZ,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAe;QAAe,CAAA,EAC7C,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAW,EAAE,OAAO,MAAM;QAC3C,aAAY;QACZ,MAAM;QACN,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAkB;QAAkB,CAAA,EACnD,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;QAC9C,MAAM;QACN,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,eAAmB,CAAA,EAC1B,kBAAC,GAAD;QACE,QAAQ,EAAa;QACrB,QAAO;QACP,OAAM;QACN,MAAK;QACL,QAAQ;QACR,YAAY;QACZ,iBAAiB,EAAgB,KAAK;QACtC,CAAA,CACE;;MACL,KAAS,kBAAC,KAAD;OAAG,WAAU;iBAAkC;OAAU,CAAA;MAC/D;;IAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,eAAe,EAAa,GAAM;eAAE;KAErD,CAAA,EACT,kBAAC,GAAD;KAAQ,SAtID,YAAY;AACzB,UAAI,CAAC,EAAM,MAAM,EAAE;AACjB,SAAS,gCAAgC;AACzC;;AAEF,UAAI;OACF,IAAM,IAAU,MAAM,EAAY,YAAY;QAC5C;QACA,OAAO,EAAM,MAAM;QACnB,QAAQ,EAAO,MAAM,IAAI;QACzB,WAAW,EAAU,MAAM,IAAI;QAC/B,aAAa,EAAU,EAAY;QACnC,KAAK,EAAI,MAAM,IAAI;QACnB,SAAS,EAAQ,MAAM,IAAI;QAC3B,YAAY,EAAW,MAAM,IAAI;QACjC;QACA;QACD,CAAC;AAIF,OAHA,EAAO,QAAQ,kBAAkB,EAAQ,MAAM,EAC/C,EAAa,GAAM,EACnB,GAAO,EACP,IAAY,EAAQ;eACb,GAAK;AACZ,SAAS,EAAc,EAAI,CAAC;;;KA+GC,UAAU,EAAY;eAAW;KAEjD,CAAA,CACc,EAAA,CAAA;IACD;;EACT,CAAA"}
|
|
1
|
+
{"version":3,"file":"AddCitationDialog.js","names":[],"sources":["../../../src/theculturepost/components/AddCitationDialog.tsx"],"sourcesContent":["/**\n * AddCitationDialog — attach a verifiable source to a story.\n * @module theculturepost/components\n *\n * Writes `tcpAddCitation`. An optional source scan is uploaded to\n * wspace-files-svc (only the id is stored).\n */\n\nimport { createElement, useState } from 'react';\nimport { ShieldCheck } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { FileUploadField } from './FileUploadField';\nimport { sourceTypeIcon } from './CitationList';\nimport { useTcpAddCitation, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpCitation, TcpSourceType } from '../types';\n\nconst SOURCE_TYPES: TcpSourceType[] = [\n 'WEB',\n 'BOOK',\n 'ACADEMIC',\n 'PERIODICAL',\n 'INTERVIEW',\n 'PRIMARY_DOCUMENT',\n 'ARCHIVAL_RECORD',\n 'ARTIFACT',\n];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface AddCitationDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n storyId: string;\n onCreated?: (citation: TcpCitation) => void;\n}\n\nexport function AddCitationDialog({\n open,\n onOpenChange,\n storyId,\n onCreated,\n}: AddCitationDialogProps) {\n const addCitation = useTcpAddCitation();\n const [title, setTitle] = useState('');\n const [author, setAuthor] = useState('');\n const [publisher, setPublisher] = useState('');\n const [publishedOn, setPublishedOn] = useState('');\n const [url, setUrl] = useState('');\n const [excerpt, setExcerpt] = useState('');\n const [provenance, setProvenance] = useState('');\n const [sourceType, setSourceType] = useState<TcpSourceType>('WEB');\n const [sourceFileId, setSourceFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setAuthor('');\n setPublisher('');\n setPublishedOn('');\n setUrl('');\n setExcerpt('');\n setProvenance('');\n setSourceType('WEB');\n setSourceFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A citation title is required.');\n return;\n }\n try {\n const created = await addCitation.mutateAsync({\n storyId,\n title: title.trim(),\n author: author.trim() || null,\n publisher: publisher.trim() || null,\n publishedOn: dateToIso(publishedOn),\n url: url.trim() || null,\n excerpt: excerpt.trim() || null,\n provenance: provenance.trim() || null,\n sourceType,\n sourceFileId,\n });\n notify.success('Citation added', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add a citation</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Ground this story in the record with a verifiable source.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-5 overflow-y-auto py-2\">\n {/* Source identity */}\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-title\">\n Title <span className=\"text-status-error-text\">*</span>\n </Label>\n <Input\n id=\"cite-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. The Penguin History of Early India\"\n />\n </div>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label>Source type</Label>\n <Select value={sourceType} onValueChange={(v) => setSourceType(v as TcpSourceType)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SOURCE_TYPES.map((s) => (\n <SelectItem key={s} value={s}>\n <span className=\"inline-flex items-center gap-2\">\n {createElement(sourceTypeIcon(s), {\n className: 'h-3.5 w-3.5 text-text-tertiary',\n })}\n {formatEnumLabel(s)}\n </span>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-published\">Published on</Label>\n <Input\n id=\"cite-published\"\n type=\"date\"\n value={publishedOn}\n onChange={(e) => setPublishedOn(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-author\">Author</Label>\n <Input\n id=\"cite-author\"\n value={author}\n onChange={(e) => setAuthor(e.target.value)}\n placeholder=\"e.g. Romila Thapar\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-publisher\">Publisher</Label>\n <Input\n id=\"cite-publisher\"\n value={publisher}\n onChange={(e) => setPublisher(e.target.value)}\n placeholder=\"e.g. Penguin Books\"\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-url\">URL</Label>\n <Input\n id=\"cite-url\"\n type=\"url\"\n inputMode=\"url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n placeholder=\"https://…\"\n />\n </div>\n\n {/* Grounding detail */}\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-excerpt\">Excerpt</Label>\n <Textarea\n id=\"cite-excerpt\"\n value={excerpt}\n onChange={(e) => setExcerpt(e.target.value)}\n placeholder=\"A short quoted passage that supports the story.\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-provenance\">Provenance</Label>\n <Textarea\n id=\"cite-provenance\"\n value={provenance}\n onChange={(e) => setProvenance(e.target.value)}\n placeholder=\"Where the source is held, reference numbers, chain of custody…\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Source scan</Label>\n <FileUploadField\n folder={FILE_FOLDERS.artifacts}\n accept=\"image/*,application/pdf\"\n label=\"Upload source scan\"\n hint=\"A scan of the primary source (optional).\"\n fileId={sourceFileId}\n onUploaded={setSourceFileId}\n onCleared={() => setSourceFileId(null)}\n />\n </div>\n\n <p className=\"flex items-start gap-2 rounded-lg bg-bg-sunken/60 px-3 py-2 text-xs text-text-tertiary\">\n <ShieldCheck className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" />\n New sources start as <span className=\"font-medium text-text-secondary\">\n Unverified\n </span>{' '}\n and are reviewed for accuracy, relevance, and cultural sensitivity before verification.\n </p>\n\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={addCitation.isPending}>\n Add citation\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,68 +1,81 @@
|
|
|
1
1
|
import { CoverImage as e } from "./CoverImage.js";
|
|
2
|
-
import { formatDate as t } from "../utils/formatters.js";
|
|
3
|
-
import { StatusBadge as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { formatDate as t, formatEnumLabel as n } from "../utils/formatters.js";
|
|
3
|
+
import { StatusBadge as r } from "./StatusBadge.js";
|
|
4
|
+
import { OverlayBadge as i } from "./OverlayBadge.js";
|
|
5
|
+
import { KIND_ICON as a } from "./kindIcon.js";
|
|
6
|
+
import { createElement as o } from "react";
|
|
7
|
+
import { CalendarClock as s, Languages as c, MapPin as l } from "lucide-react";
|
|
8
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
9
|
+
import { GlassCard as f } from "@burdenoff/fe-libs/ui";
|
|
9
10
|
//#region src/theculturepost/components/ArchiveItemCard.tsx
|
|
10
|
-
function
|
|
11
|
-
let
|
|
12
|
-
return /* @__PURE__ */
|
|
11
|
+
function p({ item: p, onOpen: m }) {
|
|
12
|
+
let h = a[p.kind], g = p.languages[0];
|
|
13
|
+
return /* @__PURE__ */ d(f, {
|
|
13
14
|
treatment: "plain",
|
|
14
15
|
className: "flex flex-col overflow-hidden",
|
|
15
|
-
children: [/* @__PURE__ */
|
|
16
|
+
children: [/* @__PURE__ */ d("button", {
|
|
16
17
|
type: "button",
|
|
17
|
-
onClick:
|
|
18
|
+
onClick: m,
|
|
18
19
|
className: "group relative block aspect-[4/3] w-full overflow-hidden text-left",
|
|
19
|
-
"aria-label": `Open ${
|
|
20
|
-
children: [/* @__PURE__ */
|
|
21
|
-
fileId:
|
|
22
|
-
alt:
|
|
20
|
+
"aria-label": `Open ${p.title}`,
|
|
21
|
+
children: [/* @__PURE__ */ u(e, {
|
|
22
|
+
fileId: p.file?.id,
|
|
23
|
+
alt: p.title,
|
|
23
24
|
rounded: "rounded-none",
|
|
24
25
|
className: "h-full w-full transition-transform duration-500 group-hover:scale-[1.03]",
|
|
25
|
-
icon: /* @__PURE__ */
|
|
26
|
-
}), /* @__PURE__ */
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
icon: /* @__PURE__ */ u(h, { className: "h-8 w-8" })
|
|
27
|
+
}), /* @__PURE__ */ u(i, {
|
|
28
|
+
position: "top-left",
|
|
29
|
+
icon: o(h, { className: "h-3 w-3" }),
|
|
30
|
+
children: n(p.kind)
|
|
29
31
|
})]
|
|
30
|
-
}), /* @__PURE__ */
|
|
32
|
+
}), /* @__PURE__ */ d("div", {
|
|
31
33
|
className: "flex flex-1 flex-col p-4",
|
|
32
34
|
children: [
|
|
33
|
-
/* @__PURE__ */
|
|
35
|
+
/* @__PURE__ */ u("div", {
|
|
34
36
|
className: "mb-1.5",
|
|
35
|
-
children: /* @__PURE__ */
|
|
37
|
+
children: /* @__PURE__ */ u(r, { status: p.accessLevel })
|
|
36
38
|
}),
|
|
37
|
-
/* @__PURE__ */
|
|
39
|
+
/* @__PURE__ */ u("button", {
|
|
38
40
|
type: "button",
|
|
39
|
-
onClick:
|
|
41
|
+
onClick: m,
|
|
40
42
|
className: "text-left text-sm font-semibold leading-snug text-text-primary hover:underline",
|
|
41
|
-
children:
|
|
43
|
+
children: p.title
|
|
42
44
|
}),
|
|
43
|
-
/* @__PURE__ */
|
|
45
|
+
/* @__PURE__ */ d("div", {
|
|
44
46
|
className: "mt-auto flex flex-col gap-1 pt-3 text-xs text-text-tertiary",
|
|
45
|
-
children: [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
children: [
|
|
48
|
+
p.origin && /* @__PURE__ */ d("span", {
|
|
49
|
+
className: "inline-flex items-center gap-1 truncate",
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ u(l, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
52
|
+
" ",
|
|
53
|
+
p.origin
|
|
54
|
+
]
|
|
55
|
+
}),
|
|
56
|
+
g && /* @__PURE__ */ d("span", {
|
|
57
|
+
className: "inline-flex items-center gap-1",
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ u(c, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
60
|
+
" ",
|
|
61
|
+
n(g)
|
|
62
|
+
]
|
|
63
|
+
}),
|
|
64
|
+
p.datedAt && /* @__PURE__ */ d("span", {
|
|
65
|
+
className: "inline-flex items-center gap-1",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ u(s, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
68
|
+
" ",
|
|
69
|
+
t(p.datedAt)
|
|
70
|
+
]
|
|
71
|
+
})
|
|
72
|
+
]
|
|
60
73
|
})
|
|
61
74
|
]
|
|
62
75
|
})]
|
|
63
76
|
});
|
|
64
77
|
}
|
|
65
78
|
//#endregion
|
|
66
|
-
export {
|
|
79
|
+
export { p as ArchiveItemCard };
|
|
67
80
|
|
|
68
81
|
//# sourceMappingURL=ArchiveItemCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArchiveItemCard.js","names":[],"sources":["../../../src/theculturepost/components/ArchiveItemCard.tsx"],"sourcesContent":["/**\n * ArchiveItemCard — a tile for an archived artifact.\n * @module theculturepost/components\n *\n * Kind
|
|
1
|
+
{"version":3,"file":"ArchiveItemCard.js","names":[],"sources":["../../../src/theculturepost/components/ArchiveItemCard.tsx"],"sourcesContent":["/**\n * ArchiveItemCard — a tile for an archived artifact.\n * @module theculturepost/components\n *\n * Kind-icon badge overlaid on the cover, access-level badge, title, provenance/\n * origin, primary language + dated chips. Image-like kinds resolve a thumbnail\n * from the file id; others fall back to the kind icon. The schema exposes a\n * single `file` (no galleries) — never render more than one. Links to detail.\n */\n\nimport { createElement } from 'react';\nimport { MapPin, CalendarClock, Languages } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { CoverImage } from './CoverImage';\nimport { StatusBadge } from './StatusBadge';\nimport { OverlayBadge } from './OverlayBadge';\nimport { KIND_ICON } from './kindIcon';\nimport { formatDate, formatEnumLabel } from '../utils/formatters';\nimport type { TcpArchiveItem } from '../types';\n\ninterface ArchiveItemCardProps {\n item: TcpArchiveItem;\n onOpen: () => void;\n}\n\nexport function ArchiveItemCard({ item, onOpen }: ArchiveItemCardProps) {\n const Icon = KIND_ICON[item.kind];\n const primaryLanguage = item.languages[0];\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col overflow-hidden\">\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"group relative block aspect-[4/3] w-full overflow-hidden text-left\"\n aria-label={`Open ${item.title}`}\n >\n <CoverImage\n fileId={item.file?.id}\n alt={item.title}\n rounded=\"rounded-none\"\n className=\"h-full w-full transition-transform duration-500 group-hover:scale-[1.03]\"\n icon={<Icon className=\"h-8 w-8\" />}\n />\n <OverlayBadge position=\"top-left\" icon={createElement(Icon, { className: 'h-3 w-3' })}>\n {formatEnumLabel(item.kind)}\n </OverlayBadge>\n </button>\n\n <div className=\"flex flex-1 flex-col p-4\">\n <div className=\"mb-1.5\">\n <StatusBadge status={item.accessLevel} />\n </div>\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"text-left text-sm font-semibold leading-snug text-text-primary hover:underline\"\n >\n {item.title}\n </button>\n\n <div className=\"mt-auto flex flex-col gap-1 pt-3 text-xs text-text-tertiary\">\n {item.origin && (\n <span className=\"inline-flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" /> {item.origin}\n </span>\n )}\n {primaryLanguage && (\n <span className=\"inline-flex items-center gap-1\">\n <Languages className=\"h-3.5 w-3.5 shrink-0\" /> {formatEnumLabel(primaryLanguage)}\n </span>\n )}\n {item.datedAt && (\n <span className=\"inline-flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5 shrink-0\" /> {formatDate(item.datedAt)}\n </span>\n )}\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;AAyBA,SAAgB,EAAgB,EAAE,SAAM,aAAgC;CACtE,IAAM,IAAO,EAAU,EAAK,OACtB,IAAkB,EAAK,UAAU;AAEvC,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC,CACE,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;GACV,cAAY,QAAQ,EAAK;aAJ3B,CAME,kBAAC,GAAD;IACE,QAAQ,EAAK,MAAM;IACnB,KAAK,EAAK;IACV,SAAQ;IACR,WAAU;IACV,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;IAClC,CAAA,EACF,kBAAC,GAAD;IAAc,UAAS;IAAW,MAAM,EAAc,GAAM,EAAE,WAAW,WAAW,CAAC;cAClF,EAAgB,EAAK,KAAK;IACd,CAAA,CACR;MAET,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAa,QAAQ,EAAK,aAAe,CAAA;KACrC,CAAA;IACN,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAET,EAAK;KACC,CAAA;IAET,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAK,UACJ,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAQ,WAAU,wBAAyB,CAAA;;QAAE,EAAK;QAC7C;;MAER,KACC,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAW,WAAU,wBAAyB,CAAA;;QAAE,EAAgB,EAAgB;QAC3E;;MAER,EAAK,WACJ,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAe,WAAU,wBAAyB,CAAA;;QAAE,EAAW,EAAK,QAAQ;QACvE;;MAEL;;IACF;KACI"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { Waveform as t } from "./Waveform.js";
|
|
3
|
+
import { useCallback as n, useRef as r, useState as i } from "react";
|
|
4
|
+
import { Pause as a, Play as o } from "lucide-react";
|
|
5
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
|
+
//#region src/theculturepost/components/AudioPlayer.tsx
|
|
7
|
+
function l(e) {
|
|
8
|
+
if (!Number.isFinite(e) || e < 0) return "0:00";
|
|
9
|
+
let t = Math.floor(e), n = Math.floor(t / 3600), r = Math.floor(t % 3600 / 60), i = t % 60, a = n > 0 ? String(r).padStart(2, "0") : String(r), o = String(i).padStart(2, "0");
|
|
10
|
+
return n > 0 ? `${n}:${a}:${o}` : `${a}:${o}`;
|
|
11
|
+
}
|
|
12
|
+
function u({ src: u, seed: d, durationSecs: f = 0, className: p }) {
|
|
13
|
+
let m = r(null), [h, g] = i(!1), [_, v] = i(0), [y, b] = i(f), [x, S] = i(u);
|
|
14
|
+
x !== u && (S(u), g(!1), v(0), b(f));
|
|
15
|
+
let C = !u, w = y > 0 ? Math.min(1, _ / y) : 0, T = n(() => {
|
|
16
|
+
let e = m.current;
|
|
17
|
+
!e || C || (e.paused ? e.play() : e.pause());
|
|
18
|
+
}, [C]), E = n((e) => {
|
|
19
|
+
let t = m.current;
|
|
20
|
+
!t || !Number.isFinite(t.duration) || t.duration <= 0 || (t.currentTime = Math.max(0, Math.min(1, e)) * t.duration, v(t.currentTime));
|
|
21
|
+
}, []), D = n((e) => {
|
|
22
|
+
let t = e.currentTarget.getBoundingClientRect();
|
|
23
|
+
t.width <= 0 || E((e.clientX - t.left) / t.width);
|
|
24
|
+
}, [E]);
|
|
25
|
+
return /* @__PURE__ */ c("div", {
|
|
26
|
+
className: e("flex items-center gap-3 rounded-xl border border-border-seam bg-bg-surface p-3", p),
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ s("button", {
|
|
29
|
+
type: "button",
|
|
30
|
+
onClick: T,
|
|
31
|
+
disabled: C,
|
|
32
|
+
"aria-label": h ? "Pause recording" : "Play recording",
|
|
33
|
+
className: e("flex h-11 w-11 shrink-0 items-center justify-center rounded-full transition-colors", C ? "cursor-not-allowed bg-bg-sunken text-text-tertiary" : "bg-action-primary-bg text-action-primary-text hover:opacity-90"),
|
|
34
|
+
children: s(h ? a : o, { className: "h-5 w-5 fill-current" })
|
|
35
|
+
}),
|
|
36
|
+
/* @__PURE__ */ c("div", {
|
|
37
|
+
className: "min-w-0 flex-1",
|
|
38
|
+
children: [/* @__PURE__ */ s("button", {
|
|
39
|
+
type: "button",
|
|
40
|
+
onClick: D,
|
|
41
|
+
disabled: C,
|
|
42
|
+
"aria-label": "Seek within recording",
|
|
43
|
+
className: e("block w-full rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]", C ? "cursor-not-allowed opacity-60" : "cursor-pointer"),
|
|
44
|
+
children: /* @__PURE__ */ s(t, {
|
|
45
|
+
seed: d,
|
|
46
|
+
bars: 56,
|
|
47
|
+
progress: w
|
|
48
|
+
})
|
|
49
|
+
}), /* @__PURE__ */ c("div", {
|
|
50
|
+
className: "mt-1 flex items-center justify-between text-xs tabular-nums text-text-tertiary",
|
|
51
|
+
children: [/* @__PURE__ */ s("span", { children: l(_) }), /* @__PURE__ */ s("span", { children: y > 0 ? l(y) : "—" })]
|
|
52
|
+
})]
|
|
53
|
+
}),
|
|
54
|
+
u && /* @__PURE__ */ s("audio", {
|
|
55
|
+
ref: m,
|
|
56
|
+
src: u,
|
|
57
|
+
preload: "metadata",
|
|
58
|
+
onPlay: () => g(!0),
|
|
59
|
+
onPause: () => g(!1),
|
|
60
|
+
onEnded: () => {
|
|
61
|
+
g(!1), v(0);
|
|
62
|
+
},
|
|
63
|
+
onTimeUpdate: (e) => v(e.currentTarget.currentTime),
|
|
64
|
+
onLoadedMetadata: (e) => {
|
|
65
|
+
Number.isFinite(e.currentTarget.duration) && b(e.currentTarget.duration);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { u as AudioPlayer };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=AudioPlayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioPlayer.js","names":[],"sources":["../../../src/theculturepost/components/AudioPlayer.tsx"],"sourcesContent":["/**\n * AudioPlayer — a token-only HTML5 <audio> player for oral-history recordings.\n * @module theculturepost/components\n *\n * fe-libs ships no audio player, so this wraps a native <audio> element with a\n * play/pause control, the deterministic {@link Waveform} as a clickable seek bar\n * (click position → seek), and current / duration timers. Degrades gracefully\n * when `src` is null (renders a muted, disabled shell). No external audio decode\n * — the waveform is decorative and driven by real playback progress.\n */\n\nimport { useCallback, useRef, useState } from 'react';\nimport { Play, Pause } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { Waveform } from './Waveform';\n\n/** Clock timer format (m:ss / h:mm:ss) for the player read-outs. */\nfunction clock(totalSeconds: number): string {\n if (!Number.isFinite(totalSeconds) || totalSeconds < 0) return '0:00';\n const secs = Math.floor(totalSeconds);\n const h = Math.floor(secs / 3600);\n const m = Math.floor((secs % 3600) / 60);\n const s = secs % 60;\n const mm = h > 0 ? String(m).padStart(2, '0') : String(m);\n const ss = String(s).padStart(2, '0');\n return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;\n}\n\ninterface AudioPlayerProps {\n /** Resolved playable URL, or null when the recording is unavailable. */\n src?: string | null;\n /** Stable seed so the waveform shape is stable for a given recording. */\n seed: string;\n /** Fallback duration (secs) shown before metadata loads. */\n durationSecs?: number;\n className?: string;\n}\n\nexport function AudioPlayer({ src, seed, durationSecs = 0, className }: AudioPlayerProps) {\n const audioRef = useRef<HTMLAudioElement | null>(null);\n const [isPlaying, setIsPlaying] = useState(false);\n const [current, setCurrent] = useState(0);\n const [duration, setDuration] = useState(durationSecs);\n\n // Reset playback state when the source changes — React's documented\n // \"store info from previous renders\" pattern (adjust state during render,\n // no setState-in-effect, no ref mutation during render).\n const [trackedSrc, setTrackedSrc] = useState(src);\n if (trackedSrc !== src) {\n setTrackedSrc(src);\n setIsPlaying(false);\n setCurrent(0);\n setDuration(durationSecs);\n }\n\n const disabled = !src;\n const progress = duration > 0 ? Math.min(1, current / duration) : 0;\n\n const toggle = useCallback(() => {\n const el = audioRef.current;\n if (!el || disabled) return;\n if (el.paused) {\n void el.play();\n } else {\n el.pause();\n }\n }, [disabled]);\n\n const seekToFraction = useCallback((fraction: number) => {\n const el = audioRef.current;\n if (!el || !Number.isFinite(el.duration) || el.duration <= 0) return;\n el.currentTime = Math.max(0, Math.min(1, fraction)) * el.duration;\n setCurrent(el.currentTime);\n }, []);\n\n const onWaveClick = useCallback(\n (event: React.MouseEvent<HTMLButtonElement>) => {\n const rect = event.currentTarget.getBoundingClientRect();\n if (rect.width <= 0) return;\n seekToFraction((event.clientX - rect.left) / rect.width);\n },\n [seekToFraction]\n );\n\n return (\n <div\n className={cn(\n 'flex items-center gap-3 rounded-xl border border-border-seam bg-bg-surface p-3',\n className\n )}\n >\n <button\n type=\"button\"\n onClick={toggle}\n disabled={disabled}\n aria-label={isPlaying ? 'Pause recording' : 'Play recording'}\n className={cn(\n 'flex h-11 w-11 shrink-0 items-center justify-center rounded-full transition-colors',\n disabled\n ? 'cursor-not-allowed bg-bg-sunken text-text-tertiary'\n : 'bg-action-primary-bg text-action-primary-text hover:opacity-90'\n )}\n >\n {isPlaying ? (\n <Pause className=\"h-5 w-5 fill-current\" />\n ) : (\n <Play className=\"h-5 w-5 fill-current\" />\n )}\n </button>\n\n <div className=\"min-w-0 flex-1\">\n <button\n type=\"button\"\n onClick={onWaveClick}\n disabled={disabled}\n aria-label=\"Seek within recording\"\n className={cn(\n 'block w-full rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]',\n disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'\n )}\n >\n <Waveform seed={seed} bars={56} progress={progress} />\n </button>\n <div className=\"mt-1 flex items-center justify-between text-xs tabular-nums text-text-tertiary\">\n <span>{clock(current)}</span>\n <span>{duration > 0 ? clock(duration) : '—'}</span>\n </div>\n </div>\n\n {src && (\n <audio\n ref={audioRef}\n src={src}\n preload=\"metadata\"\n onPlay={() => setIsPlaying(true)}\n onPause={() => setIsPlaying(false)}\n onEnded={() => {\n setIsPlaying(false);\n setCurrent(0);\n }}\n onTimeUpdate={(e) => setCurrent(e.currentTarget.currentTime)}\n onLoadedMetadata={(e) => {\n if (Number.isFinite(e.currentTarget.duration)) {\n setDuration(e.currentTarget.duration);\n }\n }}\n />\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAiBA,SAAS,EAAM,GAA8B;AAC3C,KAAI,CAAC,OAAO,SAAS,EAAa,IAAI,IAAe,EAAG,QAAO;CAC/D,IAAM,IAAO,KAAK,MAAM,EAAa,EAC/B,IAAI,KAAK,MAAM,IAAO,KAAK,EAC3B,IAAI,KAAK,MAAO,IAAO,OAAQ,GAAG,EAClC,IAAI,IAAO,IACX,IAAK,IAAI,IAAI,OAAO,EAAE,CAAC,SAAS,GAAG,IAAI,GAAG,OAAO,EAAE,EACnD,IAAK,OAAO,EAAE,CAAC,SAAS,GAAG,IAAI;AACrC,QAAO,IAAI,IAAI,GAAG,EAAE,GAAG,EAAG,GAAG,MAAO,GAAG,EAAG,GAAG;;AAa/C,SAAgB,EAAY,EAAE,QAAK,SAAM,kBAAe,GAAG,gBAA+B;CACxF,IAAM,IAAW,EAAgC,KAAK,EAChD,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAS,KAAc,EAAS,EAAE,EACnC,CAAC,GAAU,KAAe,EAAS,EAAa,EAKhD,CAAC,GAAY,KAAiB,EAAS,EAAI;AACjD,CAAI,MAAe,MACjB,EAAc,EAAI,EAClB,EAAa,GAAM,EACnB,EAAW,EAAE,EACb,EAAY,EAAa;CAG3B,IAAM,IAAW,CAAC,GACZ,IAAW,IAAW,IAAI,KAAK,IAAI,GAAG,IAAU,EAAS,GAAG,GAE5D,IAAS,QAAkB;EAC/B,IAAM,IAAK,EAAS;AAChB,GAAC,KAAM,MACP,EAAG,SACA,EAAG,MAAM,GAEd,EAAG,OAAO;IAEX,CAAC,EAAS,CAAC,EAER,IAAiB,GAAa,MAAqB;EACvD,IAAM,IAAK,EAAS;AAChB,GAAC,KAAM,CAAC,OAAO,SAAS,EAAG,SAAS,IAAI,EAAG,YAAY,MAC3D,EAAG,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,EAAS,CAAC,GAAG,EAAG,UACzD,EAAW,EAAG,YAAY;IACzB,EAAE,CAAC,EAEA,IAAc,GACjB,MAA+C;EAC9C,IAAM,IAAO,EAAM,cAAc,uBAAuB;AACpD,IAAK,SAAS,KAClB,GAAgB,EAAM,UAAU,EAAK,QAAQ,EAAK,MAAM;IAE1D,CAAC,EAAe,CACjB;AAED,QACE,kBAAC,OAAD;EACE,WAAW,EACT,kFACA,EACD;YAJH;GAME,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACC;IACV,cAAY,IAAY,oBAAoB;IAC5C,WAAW,EACT,sFACA,IACI,uDACA,iEACL;cAGC,EADD,IACE,IAEA,GAFD,EAAO,WAAU,wBAAyB,CAED;IAEpC,CAAA;GAET,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACC;KACV,cAAW;KACX,WAAW,EACT,oJACA,IAAW,kCAAkC,iBAC9C;eAED,kBAAC,GAAD;MAAgB;MAAM,MAAM;MAAc;MAAY,CAAA;KAC/C,CAAA,EACT,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD,EAAA,UAAO,EAAM,EAAQ,EAAQ,CAAA,EAC7B,kBAAC,QAAD,EAAA,UAAO,IAAW,IAAI,EAAM,EAAS,GAAG,KAAW,CAAA,CAC/C;OACF;;GAEL,KACC,kBAAC,SAAD;IACE,KAAK;IACA;IACL,SAAQ;IACR,cAAc,EAAa,GAAK;IAChC,eAAe,EAAa,GAAM;IAClC,eAAe;AAEb,KADA,EAAa,GAAM,EACnB,EAAW,EAAE;;IAEf,eAAe,MAAM,EAAW,EAAE,cAAc,YAAY;IAC5D,mBAAmB,MAAM;AACvB,KAAI,OAAO,SAAS,EAAE,cAAc,SAAS,IAC3C,EAAY,EAAE,cAAc,SAAS;;IAGzC,CAAA;GAEA"}
|