@burdenoff/microfe-theculturepost 2026.704.2 → 2026.705.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/theculturepost/TheCulturePostRoot.js +63 -45
  2. package/dist/theculturepost/TheCulturePostRoot.js.map +1 -1
  3. package/dist/theculturepost/TheCulturePostRoutes.js +17 -9
  4. package/dist/theculturepost/TheCulturePostRoutes.js.map +1 -1
  5. package/dist/theculturepost/components/AddCitationDialog.js +131 -102
  6. package/dist/theculturepost/components/AddCitationDialog.js.map +1 -1
  7. package/dist/theculturepost/components/ArchiveItemCard.js +56 -43
  8. package/dist/theculturepost/components/ArchiveItemCard.js.map +1 -1
  9. package/dist/theculturepost/components/AudioPlayer.js +74 -0
  10. package/dist/theculturepost/components/AudioPlayer.js.map +1 -0
  11. package/dist/theculturepost/components/CitationList.js +215 -105
  12. package/dist/theculturepost/components/CitationList.js.map +1 -1
  13. package/dist/theculturepost/components/CollectionCard.js +32 -32
  14. package/dist/theculturepost/components/CollectionCard.js.map +1 -1
  15. package/dist/theculturepost/components/CreateArchiveItemDialog.js +133 -93
  16. package/dist/theculturepost/components/CreateArchiveItemDialog.js.map +1 -1
  17. package/dist/theculturepost/components/CreateCollectionDialog.js +91 -70
  18. package/dist/theculturepost/components/CreateCollectionDialog.js.map +1 -1
  19. package/dist/theculturepost/components/CreateStoryDialog.js +311 -132
  20. package/dist/theculturepost/components/CreateStoryDialog.js.map +1 -1
  21. package/dist/theculturepost/components/DecorativeCover.js +78 -0
  22. package/dist/theculturepost/components/DecorativeCover.js.map +1 -0
  23. package/dist/theculturepost/components/DecorativeDivider.js +45 -0
  24. package/dist/theculturepost/components/DecorativeDivider.js.map +1 -0
  25. package/dist/theculturepost/components/DiscussionCard.js +75 -60
  26. package/dist/theculturepost/components/DiscussionCard.js.map +1 -1
  27. package/dist/theculturepost/components/HistoryTimeline.js +115 -0
  28. package/dist/theculturepost/components/HistoryTimeline.js.map +1 -0
  29. package/dist/theculturepost/components/LifecycleStepper.js +33 -0
  30. package/dist/theculturepost/components/LifecycleStepper.js.map +1 -0
  31. package/dist/theculturepost/components/OpenDiscussionDialog.js +66 -56
  32. package/dist/theculturepost/components/OpenDiscussionDialog.js.map +1 -1
  33. package/dist/theculturepost/components/OralHistoryCard.js +6 -73
  34. package/dist/theculturepost/components/OverlayBadge.js +22 -0
  35. package/dist/theculturepost/components/OverlayBadge.js.map +1 -0
  36. package/dist/theculturepost/components/PageHero.js +62 -0
  37. package/dist/theculturepost/components/PageHero.js.map +1 -0
  38. package/dist/theculturepost/components/RecordOralHistoryDialog.js +306 -131
  39. package/dist/theculturepost/components/RecordOralHistoryDialog.js.map +1 -1
  40. package/dist/theculturepost/components/ReplyThread.js +268 -139
  41. package/dist/theculturepost/components/ReplyThread.js.map +1 -1
  42. package/dist/theculturepost/components/StatusBadge.js +9 -9
  43. package/dist/theculturepost/components/StatusBadge.js.map +1 -1
  44. package/dist/theculturepost/components/StoryCard.js +54 -49
  45. package/dist/theculturepost/components/StoryCard.js.map +1 -1
  46. package/dist/theculturepost/components/TopicCard.js +60 -41
  47. package/dist/theculturepost/components/TopicCard.js.map +1 -1
  48. package/dist/theculturepost/components/Waveform.js +31 -0
  49. package/dist/theculturepost/components/Waveform.js.map +1 -0
  50. package/dist/theculturepost/components/categoryMeta.js +20 -0
  51. package/dist/theculturepost/components/categoryMeta.js.map +1 -0
  52. package/dist/theculturepost/components/index.js +41 -0
  53. package/dist/theculturepost/components/viz/BarChart.js +53 -0
  54. package/dist/theculturepost/components/viz/BarChart.js.map +1 -0
  55. package/dist/theculturepost/components/viz/Donut.js +58 -0
  56. package/dist/theculturepost/components/viz/Donut.js.map +1 -0
  57. package/dist/theculturepost/components/viz/StatTile.js +35 -0
  58. package/dist/theculturepost/components/viz/StatTile.js.map +1 -0
  59. package/dist/theculturepost/components/viz/index.js +3 -0
  60. package/dist/theculturepost/pages/HomePage.js +247 -124
  61. package/dist/theculturepost/pages/HomePage.js.map +1 -1
  62. package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js +319 -127
  63. package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js.map +1 -1
  64. package/dist/theculturepost/pages/archive/ArchivePage.js +265 -180
  65. package/dist/theculturepost/pages/archive/ArchivePage.js.map +1 -1
  66. package/dist/theculturepost/pages/archive/CollectionDetailPage.js +158 -143
  67. package/dist/theculturepost/pages/archive/CollectionDetailPage.js.map +1 -1
  68. package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js +155 -93
  69. package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js.map +1 -1
  70. package/dist/theculturepost/pages/discussions/DiscussionsPage.js +16 -16
  71. package/dist/theculturepost/pages/discussions/DiscussionsPage.js.map +1 -1
  72. package/dist/theculturepost/pages/governance/GovernancePage.js +293 -0
  73. package/dist/theculturepost/pages/governance/GovernancePage.js.map +1 -0
  74. package/dist/theculturepost/pages/stories/StoriesPage.js +18 -18
  75. package/dist/theculturepost/pages/stories/StoriesPage.js.map +1 -1
  76. package/dist/theculturepost/pages/stories/StoryDetailPage.js +603 -285
  77. package/dist/theculturepost/pages/stories/StoryDetailPage.js.map +1 -1
  78. package/dist/theculturepost/pages/timeline/TimelinePage.js +162 -0
  79. package/dist/theculturepost/pages/timeline/TimelinePage.js.map +1 -0
  80. package/dist/theculturepost/pages/topics/TopicDetailPage.js +239 -163
  81. package/dist/theculturepost/pages/topics/TopicDetailPage.js.map +1 -1
  82. package/dist/theculturepost/pages/topics/TopicsPage.js +76 -74
  83. package/dist/theculturepost/pages/topics/TopicsPage.js.map +1 -1
  84. package/dist/theculturepost/styles/injectDisplayFont.js +11 -0
  85. package/dist/theculturepost/styles/injectDisplayFont.js.map +1 -0
  86. package/dist/theculturepost/utils/formatters.js +6 -1
  87. package/dist/theculturepost/utils/formatters.js.map +1 -1
  88. package/package.json +1 -1
  89. package/dist/theculturepost/components/OralHistoryCard.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { useTheCulturePost as e } from "../../context/TheCulturePostContext.js";
2
- import { PageLayout as t } from "../../components/PageLayout.js";
3
- import { ErrorView as n, LoadingView as r } from "../../components/StateViews.js";
4
2
  import "../../context/index.js";
3
+ import { PageHero as t } from "../../components/PageHero.js";
4
+ import { ErrorView as n, LoadingView as r } from "../../components/StateViews.js";
5
5
  import { formatEnumLabel as i } from "../../utils/formatters.js";
6
6
  import { TopicCard as a } from "../../components/TopicCard.js";
7
7
  import { EmptyState as o } from "../../components/EmptyState.js";
@@ -9,13 +9,15 @@ import { useTcpTopics as s } from "../../hooks/useTcpApi.js";
9
9
  import { useCursorPager as c } from "../../hooks/useCursorPager.js";
10
10
  import "../../hooks/index.js";
11
11
  import { Pagination as l } from "../../components/Pagination.js";
12
- import { CreateTopicDialog as u } from "../../components/CreateTopicDialog.js";
13
- import { useState as d } from "react";
14
- import { Compass as f, Plus as p } from "lucide-react";
15
- import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
16
- import { Button as _, Select as v, SelectContent as y, SelectItem as b, SelectTrigger as x, SelectValue as S } from "@burdenoff/fe-libs/ui";
12
+ import { FilterTabs as u } from "../../components/FilterTabs.js";
13
+ import { CreateTopicDialog as d } from "../../components/CreateTopicDialog.js";
14
+ import "../../components/index.js";
15
+ import { useState as f } from "react";
16
+ import { Compass as p, Plus as m } from "lucide-react";
17
+ import { jsx as h, jsxs as g } from "react/jsx-runtime";
18
+ import { Button as _ } from "@burdenoff/fe-libs/ui";
17
19
  //#region src/theculturepost/pages/topics/TopicsPage.tsx
18
- var C = 12, w = [
20
+ var v = 12, y = [
19
21
  "CULTURE_AND_IDENTITY",
20
22
  "RELIGION_AND_FAITH",
21
23
  "HISTORY",
@@ -25,77 +27,77 @@ var C = 12, w = [
25
27
  "ORAL_HISTORIES",
26
28
  "PHILOSOPHY_AND_THOUGHT",
27
29
  "VALUES_AND_ETHICS"
28
- ];
29
- function T() {
30
- let { navigateTo: T } = e(), E = c(), [D, O] = d("ALL"), [k, A] = d(!1), { data: j, isLoading: M, isError: N, refetch: P } = s({
31
- category: D === "ALL" ? void 0 : D,
30
+ ], b = (e) => i(e).replace(/ And /g, " & "), x = [{
31
+ value: "ALL",
32
+ label: "All topics"
33
+ }, ...y.map((e) => ({
34
+ value: e,
35
+ label: b(e)
36
+ }))];
37
+ function S() {
38
+ let { navigateTo: i } = e(), y = c(), [b, S] = f("ALL"), [C, w] = f(!1), { data: T, isLoading: E, isError: D, refetch: O } = s({
39
+ category: b === "ALL" ? void 0 : b,
32
40
  pagination: {
33
- limit: C,
34
- cursor: E.cursor
41
+ limit: v,
42
+ cursor: y.cursor
35
43
  }
36
- }), F = j?.items ?? [];
37
- return /* @__PURE__ */ g(t, {
38
- title: "Topics",
39
- description: "Curated subjects spanning culture, faith, history, heritage and thought.",
40
- actions: /* @__PURE__ */ g(_, {
41
- onClick: () => A(!0),
42
- children: [/* @__PURE__ */ h(p, { className: "mr-1.5 h-4 w-4" }), " New topic"]
43
- }),
44
- children: [
45
- /* @__PURE__ */ h("div", {
46
- className: "mb-5 flex flex-wrap items-center gap-3",
47
- children: /* @__PURE__ */ g(v, {
48
- value: D,
49
- onValueChange: (e) => {
50
- O(e), E.reset();
51
- },
52
- children: [/* @__PURE__ */ h(x, {
53
- className: "w-[16rem]",
54
- "aria-label": "Filter by category",
55
- children: /* @__PURE__ */ h(S, { placeholder: "All categories" })
56
- }), /* @__PURE__ */ g(y, { children: [/* @__PURE__ */ h(b, {
57
- value: "ALL",
58
- children: "All categories"
59
- }), w.map((e) => /* @__PURE__ */ h(b, {
60
- value: e,
61
- children: i(e)
62
- }, e))] })]
63
- })
64
- }),
65
- M ? /* @__PURE__ */ h(r, { label: "Loading topics…" }) : N ? /* @__PURE__ */ h(n, {
66
- message: "We couldn't load topics.",
67
- onRetry: () => P()
68
- }) : F.length === 0 && E.page === 0 ? /* @__PURE__ */ h(o, {
69
- illustration: "empty-search",
70
- icon: /* @__PURE__ */ h(f, { className: "h-10 w-10" }),
71
- title: D === "ALL" ? "No topics yet" : "No topics in this category",
72
- description: D === "ALL" ? "Curate the first topic to organise stories around a shared subject." : "Try another category, or curate a new topic.",
73
- action: /* @__PURE__ */ g(_, {
74
- onClick: () => A(!0),
75
- children: [/* @__PURE__ */ h(p, { className: "mr-1.5 h-4 w-4" }), " New topic"]
44
+ }), k = T?.items ?? [], A = (e) => {
45
+ S(e), y.reset();
46
+ }, j = /* @__PURE__ */ g(_, {
47
+ onClick: () => w(!0),
48
+ children: [/* @__PURE__ */ h(m, { className: "mr-1.5 h-4 w-4" }), " New topic"]
49
+ });
50
+ return /* @__PURE__ */ g("div", {
51
+ className: "h-full overflow-y-auto",
52
+ children: [/* @__PURE__ */ g("div", {
53
+ className: "mx-auto flex max-w-6xl flex-col gap-6 p-4 sm:p-6",
54
+ children: [
55
+ /* @__PURE__ */ h(t, {
56
+ eyebrow: "Explore",
57
+ title: "Topics",
58
+ description: "Curated subjects spanning culture, faith, history, heritage and thought.",
59
+ actions: j
60
+ }),
61
+ /* @__PURE__ */ h(u, {
62
+ options: x,
63
+ value: b,
64
+ onChange: A,
65
+ className: "max-w-full"
66
+ }),
67
+ E ? /* @__PURE__ */ h(r, { label: "Loading topics…" }) : D ? /* @__PURE__ */ h(n, {
68
+ message: "We couldn't load topics.",
69
+ onRetry: () => O()
70
+ }) : k.length === 0 && y.page === 0 ? /* @__PURE__ */ h(o, {
71
+ illustration: "empty-search",
72
+ icon: /* @__PURE__ */ h(p, { className: "h-10 w-10" }),
73
+ title: b === "ALL" ? "No topics yet" : "No topics in this category",
74
+ description: b === "ALL" ? "Curate the first topic to organise stories around a shared subject." : "Try another category, or curate a new topic.",
75
+ action: j
76
+ }) : /* @__PURE__ */ g("div", {
77
+ className: "flex flex-col gap-6",
78
+ children: [/* @__PURE__ */ h("div", {
79
+ className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3",
80
+ children: k.map((e) => /* @__PURE__ */ h(a, {
81
+ topic: e,
82
+ onOpen: () => i(`topics/${e.id}`)
83
+ }, e.id))
84
+ }), /* @__PURE__ */ h(l, {
85
+ page: y.page,
86
+ pageSize: v,
87
+ itemCount: k.length,
88
+ hasMore: T?.pageInfo.hasMore ?? !1,
89
+ onPageChange: (e) => y.onPageChange(e, T?.pageInfo.cursor)
90
+ })]
76
91
  })
77
- }) : /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h("div", {
78
- className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3",
79
- children: F.map((e) => /* @__PURE__ */ h(a, {
80
- topic: e,
81
- onOpen: () => T(`topics/${e.id}`)
82
- }, e.id))
83
- }), /* @__PURE__ */ h(l, {
84
- page: E.page,
85
- pageSize: C,
86
- itemCount: F.length,
87
- hasMore: j?.pageInfo.hasMore ?? !1,
88
- onPageChange: (e) => E.onPageChange(e, j?.pageInfo.cursor)
89
- })] }),
90
- /* @__PURE__ */ h(u, {
91
- open: k,
92
- onOpenChange: A,
93
- onCreated: (e) => T(`topics/${e.id}`)
94
- })
95
- ]
92
+ ]
93
+ }), /* @__PURE__ */ h(d, {
94
+ open: C,
95
+ onOpenChange: w,
96
+ onCreated: (e) => i(`topics/${e.id}`)
97
+ })]
96
98
  });
97
99
  }
98
100
  //#endregion
99
- export { T as TopicsPage };
101
+ export { S as TopicsPage };
100
102
 
101
103
  //# sourceMappingURL=TopicsPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TopicsPage.js","names":[],"sources":["../../../../src/theculturepost/pages/topics/TopicsPage.tsx"],"sourcesContent":["/**\n * TopicsPage — browse curated topics.\n * @module theculturepost/pages/topics\n *\n * Reads `tcpListTopics` (optionally filtered by category). \"New Topic\" opens\n * {@link CreateTopicDialog}. Cursor-paginated.\n */\n\nimport { useState } from 'react';\nimport { Compass, Plus } from 'lucide-react';\nimport {\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { TopicCard } from '../../components/TopicCard';\nimport { CreateTopicDialog } from '../../components/CreateTopicDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport { useTcpTopics, useCursorPager } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { TcpTopicCategory } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst CATEGORIES: TcpTopicCategory[] = [\n 'CULTURE_AND_IDENTITY',\n 'RELIGION_AND_FAITH',\n 'HISTORY',\n 'HERITAGE_AND_ARCHIVES',\n 'FESTIVALS_AND_RITUALS',\n 'MIGRATION_AND_DIASPORA',\n 'ORAL_HISTORIES',\n 'PHILOSOPHY_AND_THOUGHT',\n 'VALUES_AND_ETHICS',\n];\n\nexport function TopicsPage() {\n const { navigateTo } = useTheCulturePost();\n const pager = useCursorPager();\n const [category, setCategory] = useState<string>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n\n const { data, isLoading, isError, refetch } = useTcpTopics({\n category: category === 'ALL' ? undefined : (category as TcpTopicCategory),\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const topics = data?.items ?? [];\n\n const changeCategory = (next: string) => {\n setCategory(next);\n pager.reset();\n };\n\n return (\n <PageLayout\n title=\"Topics\"\n description=\"Curated subjects spanning culture, faith, history, heritage and thought.\"\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New topic\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select value={category} onValueChange={changeCategory}>\n <SelectTrigger className=\"w-[16rem]\" aria-label=\"Filter by category\">\n <SelectValue placeholder=\"All categories\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All categories</SelectItem>\n {CATEGORIES.map((c) => (\n <SelectItem key={c} value={c}>\n {formatEnumLabel(c)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading topics…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load topics.\" onRetry={() => refetch()} />\n ) : topics.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Compass className=\"h-10 w-10\" />}\n title={category === 'ALL' ? 'No topics yet' : 'No topics in this category'}\n description={\n category === 'ALL'\n ? 'Curate the first topic to organise stories around a shared subject.'\n : 'Try another category, or curate a new topic.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New topic\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {topics.map((topic) => (\n <TopicCard\n key={topic.id}\n topic={topic}\n onOpen={() => navigateTo(`topics/${topic.id}`)}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={topics.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </>\n )}\n\n <CreateTopicDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(topic) => navigateTo(`topics/${topic.id}`)}\n />\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA6BA,IAAM,IAAY,IAEZ,IAAiC;CACrC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,SAAgB,IAAa;CAC3B,IAAM,EAAE,kBAAe,GAAmB,EACpC,IAAQ,GAAgB,EACxB,CAAC,GAAU,KAAe,EAAiB,MAAM,EACjD,CAAC,GAAY,KAAiB,EAAS,GAAM,EAE7C,EAAE,SAAM,cAAW,YAAS,eAAY,EAAa;EACzD,UAAU,MAAa,QAAQ,KAAA,IAAa;EAC5C,YAAY;GAAE,OAAO;GAAW,QAAQ,EAAM;GAAQ;EACvD,CAAC,EAEI,IAAS,GAAM,SAAS,EAAE;AAOhC,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD;GAAQ,eAAe,EAAc,GAAK;aAA1C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,aAC5B;;YANb;GASE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAQ,OAAO;KAAU,gBAhBP,MAAiB;AAEvC,MADA,EAAY,EAAK,EACjB,EAAM,OAAO;;eAcT,CACE,kBAAC,GAAD;MAAe,WAAU;MAAY,cAAW;gBAC9C,kBAAC,GAAD,EAAa,aAAY,kBAAmB,CAAA;MAC9B,CAAA,EAChB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAY,OAAM;gBAAM;MAA2B,CAAA,EAClD,EAAW,KAAK,MACf,kBAAC,GAAD;MAAoB,OAAO;gBACxB,EAAgB,EAAE;MACR,EAFI,EAEJ,CACb,CACY,EAAA,CAAA,CACT;;IACL,CAAA;GAEL,IACC,kBAAC,GAAD,EAAa,OAAM,mBAAoB,CAAA,GACrC,IACF,kBAAC,GAAD;IAAW,SAAQ;IAA2B,eAAe,GAAS;IAAI,CAAA,GACxE,EAAO,WAAW,KAAK,EAAM,SAAS,IACxC,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAS,WAAU,aAAc,CAAA;IACvC,OAAO,MAAa,QAAQ,kBAAkB;IAC9C,aACE,MAAa,QACT,wEACA;IAEN,QACE,kBAAC,GAAD;KAAQ,eAAe,EAAc,GAAK;eAA1C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,aAC5B;;IAEX,CAAA,GAEF,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAO,KAAK,MACX,kBAAC,GAAD;KAES;KACP,cAAc,EAAW,UAAU,EAAM,KAAK;KAC9C,EAHK,EAAM,GAGX,CACF;IACE,CAAA,EACN,kBAAC,GAAD;IACE,MAAM,EAAM;IACZ,UAAU;IACV,WAAW,EAAO;IAClB,SAAS,GAAM,SAAS,WAAW;IACnC,eAAe,MAAW,EAAM,aAAa,GAAQ,GAAM,SAAS,OAAO;IAC3E,CAAA,CACD,EAAA,CAAA;GAGL,kBAAC,GAAD;IACE,MAAM;IACN,cAAc;IACd,YAAY,MAAU,EAAW,UAAU,EAAM,KAAK;IACtD,CAAA;GACS"}
1
+ {"version":3,"file":"TopicsPage.js","names":[],"sources":["../../../../src/theculturepost/pages/topics/TopicsPage.tsx"],"sourcesContent":["/**\n * TopicsPage — the topics hub: browse curated subjects.\n * @module theculturepost/pages/topics\n *\n * An editorial {@link PageHero} header, a category pill-filter bar (every\n * {@link TcpTopicCategory}, plus \"All\"), and the image-forward {@link TopicCard}\n * grid. Reads `tcpListTopics` (optionally filtered by category), cursor-paginated.\n * The schema exposes no per-topic imagery or list story counts, so cards use the\n * token-only DecorativeCover — never a fabricated photo or count.\n */\n\nimport { useState } from 'react';\nimport { Compass, Plus } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n PageHero,\n FilterTabs,\n type FilterOption,\n TopicCard,\n CreateTopicDialog,\n EmptyState,\n LoadingView,\n ErrorView,\n Pagination,\n} from '../../components';\nimport { useTcpTopics, useCursorPager } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { TcpTopicCategory } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst CATEGORIES: TcpTopicCategory[] = [\n 'CULTURE_AND_IDENTITY',\n 'RELIGION_AND_FAITH',\n 'HISTORY',\n 'HERITAGE_AND_ARCHIVES',\n 'FESTIVALS_AND_RITUALS',\n 'MIGRATION_AND_DIASPORA',\n 'ORAL_HISTORIES',\n 'PHILOSOPHY_AND_THOUGHT',\n 'VALUES_AND_ETHICS',\n];\n\ntype CategoryFilter = 'ALL' | TcpTopicCategory;\n\n/** Nicer pill label: \"Culture And Identity\" → \"Culture & Identity\". */\nconst pillLabel = (category: TcpTopicCategory): string =>\n formatEnumLabel(category).replace(/ And /g, ' & ');\n\nconst CATEGORY_FILTERS: FilterOption<CategoryFilter>[] = [\n { value: 'ALL', label: 'All topics' },\n ...CATEGORIES.map((c) => ({ value: c, label: pillLabel(c) })),\n];\n\nexport function TopicsPage() {\n const { navigateTo } = useTheCulturePost();\n const pager = useCursorPager();\n const [category, setCategory] = useState<CategoryFilter>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n\n const { data, isLoading, isError, refetch } = useTcpTopics({\n category: category === 'ALL' ? undefined : category,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const topics = data?.items ?? [];\n\n const changeCategory = (next: CategoryFilter) => {\n setCategory(next);\n pager.reset();\n };\n\n const newTopicButton = (\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New topic\n </Button>\n );\n\n return (\n <div className=\"h-full overflow-y-auto\">\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6 p-4 sm:p-6\">\n <PageHero\n eyebrow=\"Explore\"\n title=\"Topics\"\n description=\"Curated subjects spanning culture, faith, history, heritage and thought.\"\n actions={newTopicButton}\n />\n\n <FilterTabs\n options={CATEGORY_FILTERS}\n value={category}\n onChange={changeCategory}\n className=\"max-w-full\"\n />\n\n {isLoading ? (\n <LoadingView label=\"Loading topics…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load topics.\" onRetry={() => refetch()} />\n ) : topics.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Compass className=\"h-10 w-10\" />}\n title={category === 'ALL' ? 'No topics yet' : 'No topics in this category'}\n description={\n category === 'ALL'\n ? 'Curate the first topic to organise stories around a shared subject.'\n : 'Try another category, or curate a new topic.'\n }\n action={newTopicButton}\n />\n ) : (\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {topics.map((topic) => (\n <TopicCard\n key={topic.id}\n topic={topic}\n onOpen={() => navigateTo(`topics/${topic.id}`)}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={topics.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </div>\n )}\n </div>\n\n <CreateTopicDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(topic) => navigateTo(`topics/${topic.id}`)}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,IAAM,IAAY,IAEZ,IAAiC;CACrC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EAKK,KAAa,MACjB,EAAgB,EAAS,CAAC,QAAQ,UAAU,MAAM,EAE9C,IAAmD,CACvD;CAAE,OAAO;CAAO,OAAO;CAAc,EACrC,GAAG,EAAW,KAAK,OAAO;CAAE,OAAO;CAAG,OAAO,EAAU,EAAE;CAAE,EAAE,CAC9D;AAED,SAAgB,IAAa;CAC3B,IAAM,EAAE,kBAAe,GAAmB,EACpC,IAAQ,GAAgB,EACxB,CAAC,GAAU,KAAe,EAAyB,MAAM,EACzD,CAAC,GAAY,KAAiB,EAAS,GAAM,EAE7C,EAAE,SAAM,cAAW,YAAS,eAAY,EAAa;EACzD,UAAU,MAAa,QAAQ,KAAA,IAAY;EAC3C,YAAY;GAAE,OAAO;GAAW,QAAQ,EAAM;GAAQ;EACvD,CAAC,EAEI,IAAS,GAAM,SAAS,EAAE,EAE1B,KAAkB,MAAyB;AAE/C,EADA,EAAY,EAAK,EACjB,EAAM,OAAO;IAGT,IACJ,kBAAC,GAAD;EAAQ,eAAe,EAAc,GAAK;YAA1C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,aAC5B;;AAGX,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,SAAQ;KACR,OAAM;KACN,aAAY;KACZ,SAAS;KACT,CAAA;IAEF,kBAAC,GAAD;KACE,SAAS;KACT,OAAO;KACP,UAAU;KACV,WAAU;KACV,CAAA;IAED,IACC,kBAAC,GAAD,EAAa,OAAM,mBAAoB,CAAA,GACrC,IACF,kBAAC,GAAD;KAAW,SAAQ;KAA2B,eAAe,GAAS;KAAI,CAAA,GACxE,EAAO,WAAW,KAAK,EAAM,SAAS,IACxC,kBAAC,GAAD;KACE,cAAa;KACb,MAAM,kBAAC,GAAD,EAAS,WAAU,aAAc,CAAA;KACvC,OAAO,MAAa,QAAQ,kBAAkB;KAC9C,aACE,MAAa,QACT,wEACA;KAEN,QAAQ;KACR,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAO,KAAK,MACX,kBAAC,GAAD;OAES;OACP,cAAc,EAAW,UAAU,EAAM,KAAK;OAC9C,EAHK,EAAM,GAGX,CACF;MACE,CAAA,EACN,kBAAC,GAAD;MACE,MAAM,EAAM;MACZ,UAAU;MACV,WAAW,EAAO;MAClB,SAAS,GAAM,SAAS,WAAW;MACnC,eAAe,MAAW,EAAM,aAAa,GAAQ,GAAM,SAAS,OAAO;MAC3E,CAAA,CACE;;IAEJ;MAEN,kBAAC,GAAD;GACE,MAAM;GACN,cAAc;GACd,YAAY,MAAU,EAAW,UAAU,EAAM,KAAK;GACtD,CAAA,CACE"}
@@ -0,0 +1,11 @@
1
+ //#region src/theculturepost/styles/injectDisplayFont.ts
2
+ var e = "tcp-display-font", t = ".font-display{font-family:Georgia,'Iowan Old Style','Palatino Linotype',Palatino,'Times New Roman',serif;font-feature-settings:'liga' 1,'kern' 1;letter-spacing:-0.01em;}";
3
+ function n() {
4
+ if (typeof document > "u" || document.getElementById(e)) return;
5
+ let n = document.createElement("style");
6
+ n.id = e, n.textContent = t, document.head.appendChild(n);
7
+ }
8
+ //#endregion
9
+ export { n as injectDisplayFont };
10
+
11
+ //# sourceMappingURL=injectDisplayFont.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectDisplayFont.js","names":[],"sources":["../../../src/theculturepost/styles/injectDisplayFont.ts"],"sourcesContent":["/**\n * injectDisplayFont — guarantee the `.font-display` utility exists in the built\n * MFE, regardless of the host shell's Tailwind content globs or CSS pipeline.\n * @module theculturepost/styles\n *\n * MFEs ship no component CSS to the shell (the shell owns Tailwind + tokens), and\n * `.font-display` is a custom class Tailwind will not emit. Vite's lib build also\n * strips plain CSS imports out of the JS. So instead of relying on a bundled\n * stylesheet, we inject a single, idempotent <style> rule at runtime the first\n * time the MFE mounts. It sets ONLY the font family (colour stays token-driven on\n * each element). Import for side-effect from the MFE root.\n */\n\nconst STYLE_ID = 'tcp-display-font';\n\nconst CSS = `.font-display{font-family:Georgia,'Iowan Old Style','Palatino Linotype',Palatino,'Times New Roman',serif;font-feature-settings:'liga' 1,'kern' 1;letter-spacing:-0.01em;}`;\n\nexport function injectDisplayFont(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById(STYLE_ID)) return;\n const style = document.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n document.head.appendChild(style);\n}\n"],"mappings":";AAaA,IAAM,IAAW,oBAEX,IAAM;AAEZ,SAAgB,IAA0B;AAExC,KADI,OAAO,WAAa,OACpB,SAAS,eAAe,EAAS,CAAE;CACvC,IAAM,IAAQ,SAAS,cAAc,QAAQ;AAG7C,CAFA,EAAM,KAAK,GACX,EAAM,cAAc,GACpB,SAAS,KAAK,YAAY,EAAM"}
@@ -42,7 +42,12 @@ function i(e) {
42
42
  function a(e) {
43
43
  return e ? e.toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (e) => e.toUpperCase()) : "—";
44
44
  }
45
+ function o(e) {
46
+ if (!e) return "?";
47
+ let t = e.trim().split(/\s+/).filter(Boolean);
48
+ return t.length === 0 ? "?" : t.length === 1 ? t[0].slice(0, 2).toUpperCase() : (t[0][0] + t[t.length - 1][0]).toUpperCase();
49
+ }
45
50
  //#endregion
46
- export { e as formatDate, t as formatDateTime, i as formatDuration, a as formatEnumLabel, r as formatNumber, n as formatRelativeTime };
51
+ export { e as formatDate, t as formatDateTime, i as formatDuration, a as formatEnumLabel, r as formatNumber, n as formatRelativeTime, o as initialsOf };
47
52
 
48
53
  //# sourceMappingURL=formatters.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatters.js","names":[],"sources":["../../../src/theculturepost/utils/formatters.ts"],"sourcesContent":["/**\n * Formatting utilities for the TheCulturePost microfrontend\n * @module theculturepost/utils/formatters\n */\n\n/** Format a date string or Date into a human-readable date. */\nexport function formatDate(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n ...options,\n }).format(d);\n}\n\n/** Format a date-time into a date + time string. */\nexport function formatDateTime(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n ...options,\n }).format(d);\n}\n\n/** Format a date into a relative time string (e.g. \"2 hours ago\", \"in 3 days\"). */\nexport function formatRelativeTime(date: string | Date | null | undefined): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n\n const now = new Date();\n const diffMs = d.getTime() - now.getTime();\n const future = diffMs >= 0;\n const absSeconds = Math.floor(Math.abs(diffMs) / 1000);\n const minutes = Math.floor(absSeconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n const weeks = Math.floor(days / 7);\n const months = Math.floor(days / 30);\n const years = Math.floor(days / 365);\n\n const phrase = (value: number, unit: string): string => {\n const label = `${value} ${unit}${value !== 1 ? 's' : ''}`;\n return future ? `in ${label}` : `${label} ago`;\n };\n\n if (absSeconds < 60) return future ? 'soon' : 'just now';\n if (minutes < 60) return phrase(minutes, 'min');\n if (hours < 24) return phrase(hours, 'hour');\n if (days < 7) return phrase(days, 'day');\n if (weeks < 4) return phrase(weeks, 'week');\n if (months < 12) return phrase(months, 'month');\n return phrase(years, 'year');\n}\n\n/** Format a number with locale grouping. */\nexport function formatNumber(\n value: number | null | undefined,\n options?: Partial<Intl.NumberFormatOptions>\n): string {\n if (value == null || Number.isNaN(value)) return '—';\n return new Intl.NumberFormat(undefined, options).format(value);\n}\n\n/** Format a duration in seconds into a compact \"h m s\" / \"m s\" string. */\nexport function formatDuration(totalSeconds: number | null | undefined): string {\n if (totalSeconds == null || Number.isNaN(totalSeconds) || totalSeconds <= 0) return '—';\n const secs = Math.floor(totalSeconds);\n const hours = Math.floor(secs / 3600);\n const minutes = Math.floor((secs % 3600) / 60);\n const seconds = secs % 60;\n if (hours > 0) return `${hours}h ${minutes}m`;\n if (minutes > 0) return `${minutes}m ${seconds}s`;\n return `${seconds}s`;\n}\n\n/** Turn an ENUM_LIKE_TOKEN into a human \"Enum Like Token\" label. */\nexport function formatEnumLabel(value: string | null | undefined): string {\n if (!value) return '—';\n return value\n .toLowerCase()\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n"],"mappings":";AAMA,SAAgB,EACd,GACA,GACQ;AACR,KAAI,CAAC,EAAM,QAAO;CAClB,IAAM,IAAI,OAAO,KAAS,WAAW,IAAI,KAAK,EAAK,GAAG;AAEtD,QADI,OAAO,MAAM,EAAE,SAAS,CAAC,GAAS,MAC/B,IAAI,KAAK,eAAe,KAAA,GAAW;EACxC,KAAK;EACL,OAAO;EACP,MAAM;EACN,GAAG;EACJ,CAAC,CAAC,OAAO,EAAE;;AAId,SAAgB,EACd,GACA,GACQ;AACR,KAAI,CAAC,EAAM,QAAO;CAClB,IAAM,IAAI,OAAO,KAAS,WAAW,IAAI,KAAK,EAAK,GAAG;AAEtD,QADI,OAAO,MAAM,EAAE,SAAS,CAAC,GAAS,MAC/B,IAAI,KAAK,eAAe,KAAA,GAAW;EACxC,KAAK;EACL,OAAO;EACP,MAAM;EACN,MAAM;EACN,QAAQ;EACR,GAAG;EACJ,CAAC,CAAC,OAAO,EAAE;;AAId,SAAgB,EAAmB,GAAgD;AACjF,KAAI,CAAC,EAAM,QAAO;CAClB,IAAM,IAAI,OAAO,KAAS,WAAW,IAAI,KAAK,EAAK,GAAG;AACtD,KAAI,OAAO,MAAM,EAAE,SAAS,CAAC,CAAE,QAAO;CAEtC,IAAM,oBAAM,IAAI,MAAM,EAChB,IAAS,EAAE,SAAS,GAAG,EAAI,SAAS,EACpC,IAAS,KAAU,GACnB,IAAa,KAAK,MAAM,KAAK,IAAI,EAAO,GAAG,IAAK,EAChD,IAAU,KAAK,MAAM,IAAa,GAAG,EACrC,IAAQ,KAAK,MAAM,IAAU,GAAG,EAChC,IAAO,KAAK,MAAM,IAAQ,GAAG,EAC7B,IAAQ,KAAK,MAAM,IAAO,EAAE,EAC5B,IAAS,KAAK,MAAM,IAAO,GAAG,EAC9B,IAAQ,KAAK,MAAM,IAAO,IAAI,EAE9B,KAAU,GAAe,MAAyB;EACtD,IAAM,IAAQ,GAAG,EAAM,GAAG,IAAO,MAAU,IAAU,KAAN;AAC/C,SAAO,IAAS,MAAM,MAAU,GAAG,EAAM;;AAS3C,QANI,IAAa,KAAW,IAAS,SAAS,aAC1C,IAAU,KAAW,EAAO,GAAS,MAAM,GAC3C,IAAQ,KAAW,EAAO,GAAO,OAAO,GACxC,IAAO,IAAU,EAAO,GAAM,MAAM,GACpC,IAAQ,IAAU,EAAO,GAAO,OAAO,GACvC,IAAS,KAAW,EAAO,GAAQ,QAAQ,GACxC,EAAO,GAAO,OAAO;;AAI9B,SAAgB,EACd,GACA,GACQ;AAER,QADI,KAAS,QAAQ,OAAO,MAAM,EAAM,GAAS,MAC1C,IAAI,KAAK,aAAa,KAAA,GAAW,EAAQ,CAAC,OAAO,EAAM;;AAIhE,SAAgB,EAAe,GAAiD;AAC9E,KAAI,KAAgB,QAAQ,OAAO,MAAM,EAAa,IAAI,KAAgB,EAAG,QAAO;CACpF,IAAM,IAAO,KAAK,MAAM,EAAa,EAC/B,IAAQ,KAAK,MAAM,IAAO,KAAK,EAC/B,IAAU,KAAK,MAAO,IAAO,OAAQ,GAAG,EACxC,IAAU,IAAO;AAGvB,QAFI,IAAQ,IAAU,GAAG,EAAM,IAAI,EAAQ,KACvC,IAAU,IAAU,GAAG,EAAQ,IAAI,EAAQ,KACxC,GAAG,EAAQ;;AAIpB,SAAgB,EAAgB,GAA0C;AAExE,QADK,IACE,EACJ,aAAa,CACb,QAAQ,MAAM,IAAI,CAClB,QAAQ,UAAU,MAAS,EAAK,aAAa,CAAC,GAJ9B"}
1
+ {"version":3,"file":"formatters.js","names":[],"sources":["../../../src/theculturepost/utils/formatters.ts"],"sourcesContent":["/**\n * Formatting utilities for the TheCulturePost microfrontend\n * @module theculturepost/utils/formatters\n */\n\n/** Format a date string or Date into a human-readable date. */\nexport function formatDate(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n ...options,\n }).format(d);\n}\n\n/** Format a date-time into a date + time string. */\nexport function formatDateTime(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n ...options,\n }).format(d);\n}\n\n/** Format a date into a relative time string (e.g. \"2 hours ago\", \"in 3 days\"). */\nexport function formatRelativeTime(date: string | Date | null | undefined): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n\n const now = new Date();\n const diffMs = d.getTime() - now.getTime();\n const future = diffMs >= 0;\n const absSeconds = Math.floor(Math.abs(diffMs) / 1000);\n const minutes = Math.floor(absSeconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n const weeks = Math.floor(days / 7);\n const months = Math.floor(days / 30);\n const years = Math.floor(days / 365);\n\n const phrase = (value: number, unit: string): string => {\n const label = `${value} ${unit}${value !== 1 ? 's' : ''}`;\n return future ? `in ${label}` : `${label} ago`;\n };\n\n if (absSeconds < 60) return future ? 'soon' : 'just now';\n if (minutes < 60) return phrase(minutes, 'min');\n if (hours < 24) return phrase(hours, 'hour');\n if (days < 7) return phrase(days, 'day');\n if (weeks < 4) return phrase(weeks, 'week');\n if (months < 12) return phrase(months, 'month');\n return phrase(years, 'year');\n}\n\n/** Format a number with locale grouping. */\nexport function formatNumber(\n value: number | null | undefined,\n options?: Partial<Intl.NumberFormatOptions>\n): string {\n if (value == null || Number.isNaN(value)) return '—';\n return new Intl.NumberFormat(undefined, options).format(value);\n}\n\n/** Format a duration in seconds into a compact \"h m s\" / \"m s\" string. */\nexport function formatDuration(totalSeconds: number | null | undefined): string {\n if (totalSeconds == null || Number.isNaN(totalSeconds) || totalSeconds <= 0) return '—';\n const secs = Math.floor(totalSeconds);\n const hours = Math.floor(secs / 3600);\n const minutes = Math.floor((secs % 3600) / 60);\n const seconds = secs % 60;\n if (hours > 0) return `${hours}h ${minutes}m`;\n if (minutes > 0) return `${minutes}m ${seconds}s`;\n return `${seconds}s`;\n}\n\n/** Turn an ENUM_LIKE_TOKEN into a human \"Enum Like Token\" label. */\nexport function formatEnumLabel(value: string | null | undefined): string {\n if (!value) return '—';\n return value\n .toLowerCase()\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n\n/** Two-letter monogram from a name (e.g. \"Amma Lakshmi\" -> \"AL\"). */\nexport function initialsOf(name: string | null | undefined): string {\n if (!name) return '?';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();\n return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase();\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-theculturepost",
3
- "version": "2026.704.2",
3
+ "version": "2026.705.1",
4
4
  "description": "Burdenoff TheCulturePost Microfrontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -1 +0,0 @@
1
- {"version":3,"file":"OralHistoryCard.js","names":[],"sources":["../../../src/theculturepost/components/OralHistoryCard.tsx"],"sourcesContent":["/**\n * OralHistoryCard — a row for a recorded oral history.\n * @module theculturepost/components\n *\n * Narrator (a plain String — used directly), language, access level, duration.\n * A resolvable recording exposes a \"Listen / download\" link via the file id.\n */\n\nimport { AudioLines, Mic, Clock, Languages, Download } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { useFileUrl } from '../hooks/useFiles';\nimport { formatDuration, formatEnumLabel } from '../utils/formatters';\nimport type { TcpOralHistory } from '../types';\n\ninterface OralHistoryCardProps {\n oralHistory: TcpOralHistory;\n}\n\nexport function OralHistoryCard({ oralHistory }: OralHistoryCardProps) {\n const { data: recordingUrl } = useFileUrl(oralHistory.recording?.id);\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex items-start gap-4 p-4\">\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <AudioLines className=\"h-5 w-5\" />\n </div>\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{oralHistory.title}</h3>\n <StatusBadge status={oralHistory.accessLevel} />\n </div>\n\n {oralHistory.summary && (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-secondary\">{oralHistory.summary}</p>\n )}\n\n <div className=\"mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-tertiary\">\n {oralHistory.narratorName && (\n <span className=\"inline-flex items-center gap-1\">\n <Mic className=\"h-3.5 w-3.5\" /> {oralHistory.narratorName}\n </span>\n )}\n {oralHistory.durationSecs > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(oralHistory.durationSecs)}\n </span>\n )}\n {oralHistory.language && (\n <span className=\"inline-flex items-center gap-1\">\n <Languages className=\"h-3.5 w-3.5\" /> {formatEnumLabel(oralHistory.language)}\n </span>\n )}\n {recordingUrl && (\n <a\n href={recordingUrl}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1 font-medium text-accent hover:underline\"\n >\n <Download className=\"h-3.5 w-3.5\" /> Listen\n </a>\n )}\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;AAmBA,SAAgB,EAAgB,EAAE,kBAAqC;CACrE,IAAM,EAAE,MAAM,MAAiB,EAAW,EAAY,WAAW,GAAG;AAEpE,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC,CACE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;GAC9B,CAAA,EAEN,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAoD,EAAY;MAAW,CAAA,EACzF,kBAAC,GAAD,EAAa,QAAQ,EAAY,aAAe,CAAA,CAC5C;;IAEL,EAAY,WACX,kBAAC,KAAD;KAAG,WAAU;eAAiD,EAAY;KAAY,CAAA;IAGxF,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAY,gBACX,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAK,WAAU,eAAgB,CAAA;;QAAE,EAAY;QACxC;;MAER,EAAY,eAAe,KAC1B,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;QAAE,EAAe,EAAY,aAAa;QACtE;;MAER,EAAY,YACX,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA;;QAAE,EAAgB,EAAY,SAAS;QACvE;;MAER,KACC,kBAAC,KAAD;OACE,MAAM;OACN,QAAO;OACP,KAAI;OACJ,WAAU;iBAJZ,CAME,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA,EAAA,UAClC;;MAEF;;IACF;KACI"}