@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.
Files changed (91) 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/hooks/useFiles.js +1 -1
  61. package/dist/theculturepost/hooks/useFiles.js.map +1 -1
  62. package/dist/theculturepost/pages/HomePage.js +247 -124
  63. package/dist/theculturepost/pages/HomePage.js.map +1 -1
  64. package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js +319 -127
  65. package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js.map +1 -1
  66. package/dist/theculturepost/pages/archive/ArchivePage.js +265 -180
  67. package/dist/theculturepost/pages/archive/ArchivePage.js.map +1 -1
  68. package/dist/theculturepost/pages/archive/CollectionDetailPage.js +158 -143
  69. package/dist/theculturepost/pages/archive/CollectionDetailPage.js.map +1 -1
  70. package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js +155 -93
  71. package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js.map +1 -1
  72. package/dist/theculturepost/pages/discussions/DiscussionsPage.js +16 -16
  73. package/dist/theculturepost/pages/discussions/DiscussionsPage.js.map +1 -1
  74. package/dist/theculturepost/pages/governance/GovernancePage.js +293 -0
  75. package/dist/theculturepost/pages/governance/GovernancePage.js.map +1 -0
  76. package/dist/theculturepost/pages/stories/StoriesPage.js +18 -18
  77. package/dist/theculturepost/pages/stories/StoriesPage.js.map +1 -1
  78. package/dist/theculturepost/pages/stories/StoryDetailPage.js +603 -285
  79. package/dist/theculturepost/pages/stories/StoryDetailPage.js.map +1 -1
  80. package/dist/theculturepost/pages/timeline/TimelinePage.js +162 -0
  81. package/dist/theculturepost/pages/timeline/TimelinePage.js.map +1 -0
  82. package/dist/theculturepost/pages/topics/TopicDetailPage.js +239 -163
  83. package/dist/theculturepost/pages/topics/TopicDetailPage.js.map +1 -1
  84. package/dist/theculturepost/pages/topics/TopicsPage.js +76 -74
  85. package/dist/theculturepost/pages/topics/TopicsPage.js.map +1 -1
  86. package/dist/theculturepost/styles/injectDisplayFont.js +11 -0
  87. package/dist/theculturepost/styles/injectDisplayFont.js.map +1 -0
  88. package/dist/theculturepost/utils/formatters.js +6 -1
  89. package/dist/theculturepost/utils/formatters.js.map +1 -1
  90. package/package.json +1 -1
  91. package/dist/theculturepost/components/OralHistoryCard.js.map +0 -1
@@ -0,0 +1,78 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { categoryIcon as t } from "./categoryMeta.js";
3
+ import { createElement as n, useMemo as r } from "react";
4
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
5
+ //#region src/theculturepost/components/DecorativeCover.tsx
6
+ function o(e) {
7
+ let t = 2166136261;
8
+ for (let n = 0; n < e.length; n++) t ^= e.charCodeAt(n), t = Math.imul(t, 16777619);
9
+ return t >>> 0;
10
+ }
11
+ var s = [
12
+ "bg-gradient-to-br",
13
+ "bg-gradient-to-tr",
14
+ "bg-gradient-to-r",
15
+ "bg-gradient-to-bl"
16
+ ];
17
+ function c({ seed: c, icon: l, category: u, className: d, aspect: f }) {
18
+ let p = l ?? t(u), { dir: m, overlayOpacity: h, ringCx: g, ringCy: _, iconOpacity: v } = r(() => {
19
+ let e = o(c);
20
+ return {
21
+ dir: s[e % s.length],
22
+ overlayOpacity: .35 + (e >>> 3) % 30 / 100,
23
+ iconOpacity: .14 + (e >>> 7) % 8 / 100,
24
+ ringCx: 20 + (e >>> 11) % 60,
25
+ ringCy: 20 + (e >>> 17) % 50
26
+ };
27
+ }, [c]);
28
+ return /* @__PURE__ */ a("div", {
29
+ className: e("relative isolate overflow-hidden bg-bg-sunken text-text-tertiary", f, d),
30
+ "aria-hidden": "true",
31
+ children: [
32
+ /* @__PURE__ */ i("div", {
33
+ className: e("absolute inset-0", m, "from-[var(--color-accent-soft)] to-bg-sunken"),
34
+ style: { opacity: h }
35
+ }),
36
+ /* @__PURE__ */ i("svg", {
37
+ className: "absolute inset-0 h-full w-full",
38
+ viewBox: "0 0 100 100",
39
+ preserveAspectRatio: "xMidYMid slice",
40
+ children: /* @__PURE__ */ a("g", {
41
+ fill: "none",
42
+ stroke: "currentColor",
43
+ strokeWidth: "0.4",
44
+ opacity: "0.25",
45
+ children: [
46
+ /* @__PURE__ */ i("circle", {
47
+ cx: g,
48
+ cy: _,
49
+ r: "14"
50
+ }),
51
+ /* @__PURE__ */ i("circle", {
52
+ cx: g,
53
+ cy: _,
54
+ r: "24"
55
+ }),
56
+ /* @__PURE__ */ i("circle", {
57
+ cx: g,
58
+ cy: _,
59
+ r: "34"
60
+ })
61
+ ]
62
+ })
63
+ }),
64
+ /* @__PURE__ */ i("div", {
65
+ className: "absolute inset-0 flex items-center justify-center",
66
+ children: n(p, {
67
+ className: "h-1/2 w-1/2 max-h-24 max-w-24 text-text-primary",
68
+ style: { opacity: v },
69
+ strokeWidth: 1.25
70
+ })
71
+ })
72
+ ]
73
+ });
74
+ }
75
+ //#endregion
76
+ export { c as DecorativeCover };
77
+
78
+ //# sourceMappingURL=DecorativeCover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DecorativeCover.js","names":[],"sources":["../../../src/theculturepost/components/DecorativeCover.tsx"],"sourcesContent":["/**\n * DecorativeCover — a deterministic, token-only cover backdrop for entities that\n * have no image (topics, discussions, missing story/collection covers).\n * @module theculturepost/components\n *\n * The schema exposes no imagery for these entities, so instead of fabricating a\n * photo we draw a warm, editorial motif: a single accent-soft → sunken gradient\n * plus a faint lucide motif icon and concentric rings. Variants are distinguished\n * ONLY by the motif icon and by subtle opacity/placement derived from a seed —\n * never by a different raw hue (the gradient tokens are constant). A given seed\n * always renders the same shape (stable across renders / SSR).\n */\n\nimport { createElement, useMemo } from 'react';\nimport type { LucideIcon } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { categoryIcon } from './categoryMeta';\nimport type { TcpTopicCategory } from '../types';\n\ninterface DecorativeCoverProps {\n /** Stable seed (id / slug / name) — same seed → same backdrop. */\n seed: string;\n /** Explicit motif icon; overrides the category icon. */\n icon?: LucideIcon;\n /** Category to derive the motif icon from when no explicit icon is given. */\n category?: TcpTopicCategory | null;\n className?: string;\n /** Tailwind aspect-ratio utility (e.g. 'aspect-[16/9]'). Omit if the parent sizes it. */\n aspect?: string;\n}\n\n/** Cheap deterministic hash → unsigned int from a seed string. */\nfunction hashSeed(seed: string): number {\n let h = 2166136261;\n for (let i = 0; i < seed.length; i++) {\n h ^= seed.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return h >>> 0;\n}\n\nconst GRADIENT_DIR = [\n 'bg-gradient-to-br',\n 'bg-gradient-to-tr',\n 'bg-gradient-to-r',\n 'bg-gradient-to-bl',\n] as const;\n\nexport function DecorativeCover({ seed, icon, category, className, aspect }: DecorativeCoverProps) {\n const resolvedIcon: LucideIcon = icon ?? categoryIcon(category);\n\n const { dir, overlayOpacity, ringCx, ringCy, iconOpacity } = useMemo(() => {\n const h = hashSeed(seed);\n return {\n dir: GRADIENT_DIR[h % GRADIENT_DIR.length],\n // subtle opacity variance only — hue is constant (token-driven)\n overlayOpacity: 0.35 + ((h >>> 3) % 30) / 100, // 0.35–0.65\n iconOpacity: 0.14 + ((h >>> 7) % 8) / 100, // 0.14–0.22\n ringCx: 20 + ((h >>> 11) % 60), // 20–80 (%)\n ringCy: 20 + ((h >>> 17) % 50), // 20–70 (%)\n };\n }, [seed]);\n\n return (\n <div\n className={cn(\n 'relative isolate overflow-hidden bg-bg-sunken text-text-tertiary',\n aspect,\n className\n )}\n aria-hidden=\"true\"\n >\n {/* Constant token gradient; only opacity varies per seed */}\n <div\n className={cn('absolute inset-0', dir, 'from-[var(--color-accent-soft)] to-bg-sunken')}\n style={{ opacity: overlayOpacity }}\n />\n {/* Faint concentric rings, placed by seed, drawn in currentColor (tertiary) */}\n <svg\n className=\"absolute inset-0 h-full w-full\"\n viewBox=\"0 0 100 100\"\n preserveAspectRatio=\"xMidYMid slice\"\n >\n <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"0.4\" opacity=\"0.25\">\n <circle cx={ringCx} cy={ringCy} r=\"14\" />\n <circle cx={ringCx} cy={ringCy} r=\"24\" />\n <circle cx={ringCx} cy={ringCy} r=\"34\" />\n </g>\n </svg>\n {/* Centred motif icon */}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n {createElement(resolvedIcon, {\n className: 'h-1/2 w-1/2 max-h-24 max-w-24 text-text-primary',\n style: { opacity: iconOpacity },\n strokeWidth: 1.25,\n })}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAgCA,SAAS,EAAS,GAAsB;CACtC,IAAI,IAAI;AACR,MAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,IAE/B,CADA,KAAK,EAAK,WAAW,EAAE,EACvB,IAAI,KAAK,KAAK,GAAG,SAAS;AAE5B,QAAO,MAAM;;AAGf,IAAM,IAAe;CACnB;CACA;CACA;CACA;CACD;AAED,SAAgB,EAAgB,EAAE,SAAM,SAAM,aAAU,cAAW,aAAgC;CACjG,IAAM,IAA2B,KAAQ,EAAa,EAAS,EAEzD,EAAE,QAAK,mBAAgB,WAAQ,WAAQ,mBAAgB,QAAc;EACzE,IAAM,IAAI,EAAS,EAAK;AACxB,SAAO;GACL,KAAK,EAAa,IAAI,EAAa;GAEnC,gBAAgB,OAAS,MAAM,KAAK,KAAM;GAC1C,aAAa,OAAS,MAAM,KAAK,IAAK;GACtC,QAAQ,MAAO,MAAM,MAAM;GAC3B,QAAQ,MAAO,MAAM,MAAM;GAC5B;IACA,CAAC,EAAK,CAAC;AAEV,QACE,kBAAC,OAAD;EACE,WAAW,EACT,oEACA,GACA,EACD;EACD,eAAY;YANd;GASE,kBAAC,OAAD;IACE,WAAW,EAAG,oBAAoB,GAAK,+CAA+C;IACtF,OAAO,EAAE,SAAS,GAAgB;IAClC,CAAA;GAEF,kBAAC,OAAD;IACE,WAAU;IACV,SAAQ;IACR,qBAAoB;cAEpB,kBAAC,KAAD;KAAG,MAAK;KAAO,QAAO;KAAe,aAAY;KAAM,SAAQ;eAA/D;MACE,kBAAC,UAAD;OAAQ,IAAI;OAAQ,IAAI;OAAQ,GAAE;OAAO,CAAA;MACzC,kBAAC,UAAD;OAAQ,IAAI;OAAQ,IAAI;OAAQ,GAAE;OAAO,CAAA;MACzC,kBAAC,UAAD;OAAQ,IAAI;OAAQ,IAAI;OAAQ,GAAE;OAAO,CAAA;MACvC;;IACA,CAAA;GAEN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAc,GAAc;KAC3B,WAAW;KACX,OAAO,EAAE,SAAS,GAAa;KAC/B,aAAa;KACd,CAAC;IACE,CAAA;GACF"}
@@ -0,0 +1,45 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
+ //#region src/theculturepost/components/DecorativeDivider.tsx
4
+ function r({ className: r }) {
5
+ return /* @__PURE__ */ n("div", {
6
+ className: e("flex items-center justify-center gap-3 py-6 text-text-tertiary", r),
7
+ role: "presentation",
8
+ "aria-hidden": "true",
9
+ children: [
10
+ /* @__PURE__ */ t("span", { className: "h-px w-16 max-w-[20vw] bg-gradient-to-r from-transparent to-border-seam" }),
11
+ /* @__PURE__ */ n("svg", {
12
+ width: "34",
13
+ height: "12",
14
+ viewBox: "0 0 34 12",
15
+ fill: "none",
16
+ className: "shrink-0",
17
+ children: [
18
+ /* @__PURE__ */ t("path", {
19
+ d: "M17 1 L21 6 L17 11 L13 6 Z",
20
+ stroke: "currentColor",
21
+ strokeWidth: "1",
22
+ fill: "var(--color-accent-soft)"
23
+ }),
24
+ /* @__PURE__ */ t("circle", {
25
+ cx: "4",
26
+ cy: "6",
27
+ r: "1.4",
28
+ fill: "currentColor"
29
+ }),
30
+ /* @__PURE__ */ t("circle", {
31
+ cx: "30",
32
+ cy: "6",
33
+ r: "1.4",
34
+ fill: "currentColor"
35
+ })
36
+ ]
37
+ }),
38
+ /* @__PURE__ */ t("span", { className: "h-px w-16 max-w-[20vw] bg-gradient-to-l from-transparent to-border-seam" })
39
+ ]
40
+ });
41
+ }
42
+ //#endregion
43
+ export { r as DecorativeDivider };
44
+
45
+ //# sourceMappingURL=DecorativeDivider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DecorativeDivider.js","names":[],"sources":["../../../src/theculturepost/components/DecorativeDivider.tsx"],"sourcesContent":["/**\n * DecorativeDivider — a small ornamental section break for reading views.\n * @module theculturepost/components\n *\n * A token-only SVG flourish (a centered diamond/knot between two hairlines),\n * used between passages in story/essay reading surfaces for an editorial feel.\n * Purely decorative — hidden from assistive tech.\n */\n\nimport { cn } from '../utils/cn';\n\ninterface DecorativeDividerProps {\n className?: string;\n}\n\nexport function DecorativeDivider({ className }: DecorativeDividerProps) {\n return (\n <div\n className={cn('flex items-center justify-center gap-3 py-6 text-text-tertiary', className)}\n role=\"presentation\"\n aria-hidden=\"true\"\n >\n <span className=\"h-px w-16 max-w-[20vw] bg-gradient-to-r from-transparent to-border-seam\" />\n <svg width=\"34\" height=\"12\" viewBox=\"0 0 34 12\" fill=\"none\" className=\"shrink-0\">\n <path\n d=\"M17 1 L21 6 L17 11 L13 6 Z\"\n stroke=\"currentColor\"\n strokeWidth=\"1\"\n fill=\"var(--color-accent-soft)\"\n />\n <circle cx=\"4\" cy=\"6\" r=\"1.4\" fill=\"currentColor\" />\n <circle cx=\"30\" cy=\"6\" r=\"1.4\" fill=\"currentColor\" />\n </svg>\n <span className=\"h-px w-16 max-w-[20vw] bg-gradient-to-l from-transparent to-border-seam\" />\n </div>\n );\n}\n"],"mappings":";;;AAeA,SAAgB,EAAkB,EAAE,gBAAqC;AACvE,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,kEAAkE,EAAU;EAC1F,MAAK;EACL,eAAY;YAHd;GAKE,kBAAC,QAAD,EAAM,WAAU,2EAA4E,CAAA;GAC5F,kBAAC,OAAD;IAAK,OAAM;IAAK,QAAO;IAAK,SAAQ;IAAY,MAAK;IAAO,WAAU;cAAtE;KACE,kBAAC,QAAD;MACE,GAAE;MACF,QAAO;MACP,aAAY;MACZ,MAAK;MACL,CAAA;KACF,kBAAC,UAAD;MAAQ,IAAG;MAAI,IAAG;MAAI,GAAE;MAAM,MAAK;MAAiB,CAAA;KACpD,kBAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAI,GAAE;MAAM,MAAK;MAAiB,CAAA;KACjD;;GACN,kBAAC,QAAD,EAAM,WAAU,2EAA4E,CAAA;GACxF"}
@@ -1,71 +1,86 @@
1
1
  import { formatNumber as e, formatRelativeTime as t } from "../utils/formatters.js";
2
2
  import { StatusBadge as n } from "./StatusBadge.js";
3
3
  import { AuthorLabel as r } from "./AuthorLabel.js";
4
- import { HeartHandshake as i, Lock as a, MessagesSquare as o, Tag as s } from "lucide-react";
5
- import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
- import { GlassCard as u } from "@burdenoff/fe-libs/ui";
4
+ import { DecorativeCover as i } from "./DecorativeCover.js";
5
+ import { HeartHandshake as a, Lock as o, MessagesSquare as s, Tag as c } from "lucide-react";
6
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
7
+ import { GlassCard as d } from "@burdenoff/fe-libs/ui";
7
8
  //#region src/theculturepost/components/DiscussionCard.tsx
8
- function d({ discussion: d, onOpen: f }) {
9
- return /* @__PURE__ */ l(u, {
9
+ function f({ discussion: f, onOpen: p }) {
10
+ return /* @__PURE__ */ u(d, {
10
11
  treatment: "plain",
11
- className: "flex flex-col p-5",
12
- children: [
13
- /* @__PURE__ */ l("div", {
14
- className: "mb-2 flex flex-wrap items-center gap-1.5",
15
- children: [
16
- /* @__PURE__ */ c(n, { status: d.status }),
17
- d.isInterfaith && /* @__PURE__ */ l("span", {
18
- className: "inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary",
19
- children: [/* @__PURE__ */ c(i, { className: "h-3 w-3" }), " Interfaith"]
20
- }),
21
- d.isLocked && /* @__PURE__ */ l("span", {
22
- className: "inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary",
23
- children: [/* @__PURE__ */ c(a, { className: "h-3 w-3" }), " Locked"]
24
- })
25
- ]
26
- }),
27
- /* @__PURE__ */ c("button", {
28
- type: "button",
29
- onClick: f,
30
- className: "text-left text-base font-semibold leading-snug text-text-primary hover:underline",
31
- children: d.title
32
- }),
33
- d.prompt && /* @__PURE__ */ c("p", {
34
- className: "mt-1.5 line-clamp-2 text-sm text-text-secondary",
35
- children: d.prompt
36
- }),
37
- d.tags.length > 0 && /* @__PURE__ */ l("div", {
38
- className: "mt-3 flex flex-wrap items-center gap-1.5",
39
- children: [/* @__PURE__ */ c(s, {
40
- className: "h-3.5 w-3.5 text-text-tertiary",
41
- "aria-hidden": "true"
42
- }), d.tags.slice(0, 4).map((e) => /* @__PURE__ */ l("span", {
43
- className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary",
44
- children: ["#", e]
45
- }, e))]
46
- }),
47
- /* @__PURE__ */ l("div", {
48
- className: "mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3 text-xs text-text-tertiary",
49
- children: [/* @__PURE__ */ l("span", {
50
- className: "inline-flex items-center gap-3",
51
- children: [/* @__PURE__ */ l("span", {
52
- className: "inline-flex items-center gap-1",
53
- children: [
54
- /* @__PURE__ */ c(o, { className: "h-3.5 w-3.5" }),
55
- " ",
56
- e(d.replyCount),
57
- " replies"
58
- ]
59
- }), /* @__PURE__ */ c(r, {
60
- actorId: d.openedById,
61
- prefix: "Opened by"
62
- })]
63
- }), /* @__PURE__ */ c("span", { children: t(d.updatedAt) })]
12
+ className: "flex gap-4 overflow-hidden p-4",
13
+ children: [/* @__PURE__ */ l("button", {
14
+ type: "button",
15
+ onClick: p,
16
+ "aria-label": `Open ${f.title}`,
17
+ className: "hidden shrink-0 overflow-hidden rounded-xl sm:block",
18
+ children: /* @__PURE__ */ l(i, {
19
+ seed: f.id || f.title,
20
+ icon: s,
21
+ className: "h-full w-24"
64
22
  })
65
- ]
23
+ }), /* @__PURE__ */ u("div", {
24
+ className: "min-w-0 flex-1",
25
+ children: [
26
+ /* @__PURE__ */ u("div", {
27
+ className: "mb-2 flex flex-wrap items-center gap-1.5",
28
+ children: [
29
+ /* @__PURE__ */ l(n, { status: f.status }),
30
+ f.isInterfaith && /* @__PURE__ */ u("span", {
31
+ className: "inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary",
32
+ children: [/* @__PURE__ */ l(a, { className: "h-3 w-3" }), " Interfaith"]
33
+ }),
34
+ f.isLocked && /* @__PURE__ */ u("span", {
35
+ className: "inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary",
36
+ children: [/* @__PURE__ */ l(o, { className: "h-3 w-3" }), " Locked"]
37
+ })
38
+ ]
39
+ }),
40
+ /* @__PURE__ */ l("button", {
41
+ type: "button",
42
+ onClick: p,
43
+ className: "font-display text-left text-base font-semibold leading-snug text-text-primary hover:underline",
44
+ children: f.title
45
+ }),
46
+ f.prompt && /* @__PURE__ */ l("p", {
47
+ className: "mt-1.5 line-clamp-2 text-sm text-text-secondary",
48
+ children: f.prompt
49
+ }),
50
+ f.tags.length > 0 && /* @__PURE__ */ u("div", {
51
+ className: "mt-3 flex flex-wrap items-center gap-1.5",
52
+ children: [/* @__PURE__ */ l(c, {
53
+ className: "h-3.5 w-3.5 text-text-tertiary",
54
+ "aria-hidden": "true"
55
+ }), f.tags.slice(0, 4).map((e) => /* @__PURE__ */ u("span", {
56
+ className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary",
57
+ children: ["#", e]
58
+ }, e))]
59
+ }),
60
+ /* @__PURE__ */ u("div", {
61
+ className: "mt-3 flex flex-wrap items-center justify-between gap-x-3 gap-y-1 border-t border-border-seam pt-3 text-xs text-text-tertiary",
62
+ children: [/* @__PURE__ */ u("span", {
63
+ className: "inline-flex flex-wrap items-center gap-x-3 gap-y-1",
64
+ children: [/* @__PURE__ */ u("span", {
65
+ className: "inline-flex items-center gap-1",
66
+ children: [
67
+ /* @__PURE__ */ l(s, { className: "h-3.5 w-3.5" }),
68
+ " ",
69
+ e(f.replyCount),
70
+ " ",
71
+ f.replyCount === 1 ? "reply" : "replies"
72
+ ]
73
+ }), /* @__PURE__ */ l(r, {
74
+ actorId: f.openedById,
75
+ prefix: "Opened by"
76
+ })]
77
+ }), /* @__PURE__ */ l("span", { children: t(f.updatedAt) })]
78
+ })
79
+ ]
80
+ })]
66
81
  });
67
82
  }
68
83
  //#endregion
69
- export { d as DiscussionCard };
84
+ export { f as DiscussionCard };
70
85
 
71
86
  //# sourceMappingURL=DiscussionCard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DiscussionCard.js","names":[],"sources":["../../../src/theculturepost/components/DiscussionCard.tsx"],"sourcesContent":["/**\n * DiscussionCard — a tile for a moderated community discussion.\n * @module theculturepost/components\n *\n * Title, prompt, interfaith marker, lock state, moderation status, reply count.\n * Links to the discussion detail page.\n */\n\nimport { MessagesSquare, Lock, HeartHandshake, Tag } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { AuthorLabel } from './AuthorLabel';\nimport { formatNumber, formatRelativeTime } from '../utils/formatters';\nimport type { TcpDiscussion } from '../types';\n\ninterface DiscussionCardProps {\n discussion: TcpDiscussion;\n onOpen: () => void;\n}\n\nexport function DiscussionCard({ discussion, onOpen }: DiscussionCardProps) {\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"mb-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={discussion.status} />\n {discussion.isInterfaith && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <HeartHandshake className=\"h-3 w-3\" /> Interfaith\n </span>\n )}\n {discussion.isLocked && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <Lock className=\"h-3 w-3\" /> Locked\n </span>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"text-left text-base font-semibold leading-snug text-text-primary hover:underline\"\n >\n {discussion.title}\n </button>\n\n {discussion.prompt && (\n <p className=\"mt-1.5 line-clamp-2 text-sm text-text-secondary\">{discussion.prompt}</p>\n )}\n\n {discussion.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap items-center gap-1.5\">\n <Tag className=\"h-3.5 w-3.5 text-text-tertiary\" aria-hidden=\"true\" />\n {discussion.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex items-center gap-3\">\n <span className=\"inline-flex items-center gap-1\">\n <MessagesSquare className=\"h-3.5 w-3.5\" /> {formatNumber(discussion.replyCount)} replies\n </span>\n <AuthorLabel actorId={discussion.openedById} prefix=\"Opened by\" />\n </span>\n <span>{formatRelativeTime(discussion.updatedAt)}</span>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;AAoBA,SAAgB,EAAe,EAAE,eAAY,aAA+B;AAC1E,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAa,QAAQ,EAAW,QAAU,CAAA;KACzC,EAAW,gBACV,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAgB,WAAU,WAAY,CAAA,EAAA,cACjC;;KAER,EAAW,YACV,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,UACvB;;KAEL;;GAEN,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAET,EAAW;IACL,CAAA;GAER,EAAW,UACV,kBAAC,KAAD;IAAG,WAAU;cAAmD,EAAW;IAAW,CAAA;GAGvF,EAAW,KAAK,SAAS,KACxB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAK,WAAU;KAAiC,eAAY;KAAS,CAAA,EACpE,EAAW,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAChC,kBAAC,QAAD;KAEE,WAAU;eAFZ,CAGC,KACG,EACG;OAJA,EAIA,CACP,CACE;;GAGR,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAgB,WAAU,eAAgB,CAAA;;OAAE,EAAa,EAAW,WAAW;OAAC;OAC3E;SACP,kBAAC,GAAD;MAAa,SAAS,EAAW;MAAY,QAAO;MAAc,CAAA,CAC7D;QACP,kBAAC,QAAD,EAAA,UAAO,EAAmB,EAAW,UAAU,EAAQ,CAAA,CACnD;;GACI"}
1
+ {"version":3,"file":"DiscussionCard.js","names":[],"sources":["../../../src/theculturepost/components/DiscussionCard.tsx"],"sourcesContent":["/**\n * DiscussionCard — a list row for a moderated community discussion.\n * @module theculturepost/components\n *\n * Row layout: a token-only {@link DecorativeCover} thumb (no discussion imagery\n * exists in the schema) on the left; title, prompt, interfaith / lock / status\n * badges, tag chips, reply count, opener + time-ago on the right. Links to the\n * discussion detail page. Collapses to a stacked layout at 375px.\n */\n\nimport { MessagesSquare, Lock, HeartHandshake, Tag } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { DecorativeCover } from './DecorativeCover';\nimport { AuthorLabel } from './AuthorLabel';\nimport { formatNumber, formatRelativeTime } from '../utils/formatters';\nimport type { TcpDiscussion } from '../types';\n\ninterface DiscussionCardProps {\n discussion: TcpDiscussion;\n onOpen: () => void;\n}\n\nexport function DiscussionCard({ discussion, onOpen }: DiscussionCardProps) {\n return (\n <GlassCard treatment=\"plain\" className=\"flex gap-4 overflow-hidden p-4\">\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Open ${discussion.title}`}\n className=\"hidden shrink-0 overflow-hidden rounded-xl sm:block\"\n >\n <DecorativeCover\n seed={discussion.id || discussion.title}\n icon={MessagesSquare}\n className=\"h-full w-24\"\n />\n </button>\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"mb-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={discussion.status} />\n {discussion.isInterfaith && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <HeartHandshake className=\"h-3 w-3\" /> Interfaith\n </span>\n )}\n {discussion.isLocked && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <Lock className=\"h-3 w-3\" /> Locked\n </span>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"font-display text-left text-base font-semibold leading-snug text-text-primary hover:underline\"\n >\n {discussion.title}\n </button>\n\n {discussion.prompt && (\n <p className=\"mt-1.5 line-clamp-2 text-sm text-text-secondary\">{discussion.prompt}</p>\n )}\n\n {discussion.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap items-center gap-1.5\">\n <Tag className=\"h-3.5 w-3.5 text-text-tertiary\" aria-hidden=\"true\" />\n {discussion.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-x-3 gap-y-1 border-t border-border-seam pt-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex flex-wrap items-center gap-x-3 gap-y-1\">\n <span className=\"inline-flex items-center gap-1\">\n <MessagesSquare className=\"h-3.5 w-3.5\" /> {formatNumber(discussion.replyCount)}{' '}\n {discussion.replyCount === 1 ? 'reply' : 'replies'}\n </span>\n <AuthorLabel actorId={discussion.openedById} prefix=\"Opened by\" />\n </span>\n <span>{formatRelativeTime(discussion.updatedAt)}</span>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;AAuBA,SAAgB,EAAe,EAAE,eAAY,aAA+B;AAC1E,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC,CACE,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,cAAY,QAAQ,EAAW;GAC/B,WAAU;aAEV,kBAAC,GAAD;IACE,MAAM,EAAW,MAAM,EAAW;IAClC,MAAM;IACN,WAAU;IACV,CAAA;GACK,CAAA,EAET,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAa,QAAQ,EAAW,QAAU,CAAA;MACzC,EAAW,gBACV,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACE,kBAAC,GAAD,EAAgB,WAAU,WAAY,CAAA,EAAA,cACjC;;MAER,EAAW,YACV,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,UACvB;;MAEL;;IAEN,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAET,EAAW;KACL,CAAA;IAER,EAAW,UACV,kBAAC,KAAD;KAAG,WAAU;eAAmD,EAAW;KAAW,CAAA;IAGvF,EAAW,KAAK,SAAS,KACxB,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAK,WAAU;MAAiC,eAAY;MAAS,CAAA,EACpE,EAAW,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAChC,kBAAC,QAAD;MAEE,WAAU;gBAFZ,CAGC,KACG,EACG;QAJA,EAIA,CACP,CACE;;IAGR,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAgB,WAAU,eAAgB,CAAA;;QAAE,EAAa,EAAW,WAAW;QAAE;QAChF,EAAW,eAAe,IAAI,UAAU;QACpC;UACP,kBAAC,GAAD;OAAa,SAAS,EAAW;OAAY,QAAO;OAAc,CAAA,CAC7D;SACP,kBAAC,QAAD,EAAA,UAAO,EAAmB,EAAW,UAAU,EAAQ,CAAA,CACnD;;IACF;KACI"}
@@ -0,0 +1,115 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { useId as t } from "react";
3
+ import { Milestone as n } from "lucide-react";
4
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
+ //#region src/theculturepost/components/HistoryTimeline.tsx
6
+ function a({ entry: t, above: a }) {
7
+ let o = t.icon ?? n, s = !!t.onOpen;
8
+ return /* @__PURE__ */ i(s ? "button" : "div", {
9
+ ...s ? {
10
+ type: "button",
11
+ onClick: t.onOpen,
12
+ "aria-label": `Open ${t.title}`
13
+ } : {},
14
+ className: e("flex w-full flex-col gap-1 rounded-xl border border-border-seam bg-bg-surface p-4 text-left shadow-sm", s && "transition-shadow hover:shadow-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]", a ? "md:self-end" : "md:self-start"),
15
+ children: [
16
+ /* @__PURE__ */ r("span", {
17
+ className: "text-xs font-semibold uppercase tracking-wide text-accent",
18
+ children: t.date
19
+ }),
20
+ /* @__PURE__ */ r("span", {
21
+ className: "font-display text-base font-semibold leading-snug text-text-primary",
22
+ children: t.title
23
+ }),
24
+ t.subtitle && /* @__PURE__ */ r("span", {
25
+ className: "line-clamp-2 text-sm text-text-secondary",
26
+ children: t.subtitle
27
+ }),
28
+ t.kind && /* @__PURE__ */ i("span", {
29
+ className: "mt-1 inline-flex w-fit items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary",
30
+ children: [
31
+ /* @__PURE__ */ r(o, {
32
+ className: "h-3 w-3",
33
+ "aria-hidden": "true"
34
+ }),
35
+ " ",
36
+ t.kind
37
+ ]
38
+ })
39
+ ]
40
+ });
41
+ }
42
+ function o({ entries: n, className: o }) {
43
+ let s = t();
44
+ return n.length === 0 ? /* @__PURE__ */ r("p", {
45
+ className: e("text-sm text-text-tertiary", o),
46
+ children: "No timeline entries yet."
47
+ }) : /* @__PURE__ */ i("section", {
48
+ "aria-labelledby": s,
49
+ className: e("w-full", o),
50
+ children: [
51
+ /* @__PURE__ */ r("h2", {
52
+ id: s,
53
+ className: "sr-only",
54
+ children: "Timeline"
55
+ }),
56
+ /* @__PURE__ */ r("ol", {
57
+ className: "relative space-y-6 border-l border-border-seam pl-6 md:hidden",
58
+ children: n.map((e) => /* @__PURE__ */ i("li", {
59
+ className: "relative",
60
+ children: [/* @__PURE__ */ r("span", {
61
+ className: "absolute -left-[1.6rem] top-1.5 h-3 w-3 rounded-full border-2 border-bg-canvas bg-accent",
62
+ "aria-hidden": "true"
63
+ }), /* @__PURE__ */ r(a, {
64
+ entry: e,
65
+ above: !1
66
+ })]
67
+ }, e.id))
68
+ }),
69
+ /* @__PURE__ */ r("div", {
70
+ className: "hidden overflow-x-auto pb-2 md:block",
71
+ children: /* @__PURE__ */ r("ol", {
72
+ className: "flex min-w-full items-stretch gap-6",
73
+ children: n.map((e, t) => {
74
+ let o = t % 2 == 0;
75
+ return /* @__PURE__ */ i("li", {
76
+ className: "flex w-64 shrink-0 flex-col justify-center",
77
+ "aria-setsize": n.length,
78
+ "aria-posinset": t + 1,
79
+ children: [
80
+ /* @__PURE__ */ r("div", {
81
+ className: "flex min-h-[9rem] items-end",
82
+ children: o && /* @__PURE__ */ r(a, {
83
+ entry: e,
84
+ above: !0
85
+ })
86
+ }),
87
+ /* @__PURE__ */ i("div", {
88
+ className: "relative flex items-center py-3",
89
+ children: [/* @__PURE__ */ r("span", {
90
+ className: "h-px w-full bg-border-seam",
91
+ "aria-hidden": "true"
92
+ }), /* @__PURE__ */ r("span", {
93
+ className: "absolute left-1/2 h-3.5 w-3.5 -translate-x-1/2 rounded-full border-2 border-bg-canvas bg-accent",
94
+ "aria-hidden": "true"
95
+ })]
96
+ }),
97
+ /* @__PURE__ */ r("div", {
98
+ className: "flex min-h-[9rem] items-start",
99
+ children: !o && /* @__PURE__ */ r(a, {
100
+ entry: e,
101
+ above: !1
102
+ })
103
+ })
104
+ ]
105
+ }, e.id);
106
+ })
107
+ })
108
+ })
109
+ ]
110
+ });
111
+ }
112
+ //#endregion
113
+ export { o as HistoryTimeline };
114
+
115
+ //# sourceMappingURL=HistoryTimeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HistoryTimeline.js","names":[],"sources":["../../../src/theculturepost/components/HistoryTimeline.tsx"],"sourcesContent":["/**\n * HistoryTimeline — a responsive, alternating-axis timeline of dated entries.\n * @module theculturepost/components\n *\n * Desktop (md+): a horizontal spine with cards alternating above / below each\n * node, scrolling horizontally when the entries overflow. Mobile: a vertical\n * spine with all cards stacked on one side. Token-only. Cards are optionally\n * clickable via `onOpen`. Dates are shown verbatim (the schema stores free-text\n * era/date strings) — never reformatted or fabricated.\n */\n\nimport { useId } from 'react';\nimport type { LucideIcon } from 'lucide-react';\nimport { Milestone } from 'lucide-react';\nimport { cn } from '../utils/cn';\n\nexport interface TimelineEntry {\n id: string;\n /** Verbatim date/era label (e.g. \"1054 CE\", \"c. 2000 BCE\"). */\n date: string;\n title: string;\n subtitle?: string;\n /** Small kind/category chip shown on the card. */\n kind?: string;\n icon?: LucideIcon;\n onOpen?: () => void;\n}\n\ninterface HistoryTimelineProps {\n entries: TimelineEntry[];\n className?: string;\n}\n\nfunction EntryCard({ entry, above }: { entry: TimelineEntry; above: boolean }) {\n const Icon = entry.icon ?? Milestone;\n const interactive = Boolean(entry.onOpen);\n const Wrapper = interactive ? 'button' : 'div';\n\n return (\n <Wrapper\n {...(interactive\n ? { type: 'button' as const, onClick: entry.onOpen, 'aria-label': `Open ${entry.title}` }\n : {})}\n className={cn(\n 'flex w-full flex-col gap-1 rounded-xl border border-border-seam bg-bg-surface p-4 text-left shadow-sm',\n interactive &&\n 'transition-shadow hover:shadow-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]',\n // point the card toward the spine\n above ? 'md:self-end' : 'md:self-start'\n )}\n >\n <span className=\"text-xs font-semibold uppercase tracking-wide text-accent\">\n {entry.date}\n </span>\n <span className=\"font-display text-base font-semibold leading-snug text-text-primary\">\n {entry.title}\n </span>\n {entry.subtitle && (\n <span className=\"line-clamp-2 text-sm text-text-secondary\">{entry.subtitle}</span>\n )}\n {entry.kind && (\n <span className=\"mt-1 inline-flex w-fit items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <Icon className=\"h-3 w-3\" aria-hidden=\"true\" /> {entry.kind}\n </span>\n )}\n </Wrapper>\n );\n}\n\nexport function HistoryTimeline({ entries, className }: HistoryTimelineProps) {\n const labelId = useId();\n\n if (entries.length === 0) {\n return <p className={cn('text-sm text-text-tertiary', className)}>No timeline entries yet.</p>;\n }\n\n return (\n <section aria-labelledby={labelId} className={cn('w-full', className)}>\n <h2 id={labelId} className=\"sr-only\">\n Timeline\n </h2>\n\n {/* Mobile: vertical spine */}\n <ol className=\"relative space-y-6 border-l border-border-seam pl-6 md:hidden\">\n {entries.map((entry) => (\n <li key={entry.id} className=\"relative\">\n <span\n className=\"absolute -left-[1.6rem] top-1.5 h-3 w-3 rounded-full border-2 border-bg-canvas bg-accent\"\n aria-hidden=\"true\"\n />\n <EntryCard entry={entry} above={false} />\n </li>\n ))}\n </ol>\n\n {/* Desktop: horizontal alternating spine, scrolls on overflow */}\n <div className=\"hidden overflow-x-auto pb-2 md:block\">\n <ol className=\"flex min-w-full items-stretch gap-6\">\n {entries.map((entry, i) => {\n const above = i % 2 === 0;\n return (\n <li\n key={entry.id}\n className=\"flex w-64 shrink-0 flex-col justify-center\"\n aria-setsize={entries.length}\n aria-posinset={i + 1}\n >\n <div className=\"flex min-h-[9rem] items-end\">\n {above && <EntryCard entry={entry} above />}\n </div>\n {/* spine node */}\n <div className=\"relative flex items-center py-3\">\n <span className=\"h-px w-full bg-border-seam\" aria-hidden=\"true\" />\n <span\n className=\"absolute left-1/2 h-3.5 w-3.5 -translate-x-1/2 rounded-full border-2 border-bg-canvas bg-accent\"\n aria-hidden=\"true\"\n />\n </div>\n <div className=\"flex min-h-[9rem] items-start\">\n {!above && <EntryCard entry={entry} above={false} />}\n </div>\n </li>\n );\n })}\n </ol>\n </div>\n </section>\n );\n}\n"],"mappings":";;;;;AAiCA,SAAS,EAAU,EAAE,UAAO,YAAmD;CAC7E,IAAM,IAAO,EAAM,QAAQ,GACrB,IAAc,EAAQ,EAAM;AAGlC,QACE,kBAHc,IAAc,WAAW,OAGvC;EACE,GAAK,IACD;GAAE,MAAM;GAAmB,SAAS,EAAM;GAAQ,cAAc,QAAQ,EAAM;GAAS,GACvF,EAAE;EACN,WAAW,EACT,yGACA,KACE,8JAEF,IAAQ,gBAAgB,gBACzB;YAVH;GAYE,kBAAC,QAAD;IAAM,WAAU;cACb,EAAM;IACF,CAAA;GACP,kBAAC,QAAD;IAAM,WAAU;cACb,EAAM;IACF,CAAA;GACN,EAAM,YACL,kBAAC,QAAD;IAAM,WAAU;cAA4C,EAAM;IAAgB,CAAA;GAEnF,EAAM,QACL,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAM,WAAU;MAAU,eAAY;MAAS,CAAA;;KAAE,EAAM;KAClD;;GAED;;;AAId,SAAgB,EAAgB,EAAE,YAAS,gBAAmC;CAC5E,IAAM,IAAU,GAAO;AAMvB,QAJI,EAAQ,WAAW,IACd,kBAAC,KAAD;EAAG,WAAW,EAAG,8BAA8B,EAAU;YAAE;EAA4B,CAAA,GAI9F,kBAAC,WAAD;EAAS,mBAAiB;EAAS,WAAW,EAAG,UAAU,EAAU;YAArE;GACE,kBAAC,MAAD;IAAI,IAAI;IAAS,WAAU;cAAU;IAEhC,CAAA;GAGL,kBAAC,MAAD;IAAI,WAAU;cACX,EAAQ,KAAK,MACZ,kBAAC,MAAD;KAAmB,WAAU;eAA7B,CACE,kBAAC,QAAD;MACE,WAAU;MACV,eAAY;MACZ,CAAA,EACF,kBAAC,GAAD;MAAkB;MAAO,OAAO;MAAS,CAAA,CACtC;OANI,EAAM,GAMV,CACL;IACC,CAAA;GAGL,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,GAAO,MAAM;MACzB,IAAM,IAAQ,IAAI,KAAM;AACxB,aACE,kBAAC,MAAD;OAEE,WAAU;OACV,gBAAc,EAAQ;OACtB,iBAAe,IAAI;iBAJrB;QAME,kBAAC,OAAD;SAAK,WAAU;mBACZ,KAAS,kBAAC,GAAD;UAAkB;UAAO,OAAA;UAAQ,CAAA;SACvC,CAAA;QAEN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;UAA6B,eAAY;UAAS,CAAA,EAClE,kBAAC,QAAD;UACE,WAAU;UACV,eAAY;UACZ,CAAA,CACE;;QACN,kBAAC,OAAD;SAAK,WAAU;mBACZ,CAAC,KAAS,kBAAC,GAAD;UAAkB;UAAO,OAAO;UAAS,CAAA;SAChD,CAAA;QACH;SAnBE,EAAM,GAmBR;OAEP;KACC,CAAA;IACD,CAAA;GACE"}
@@ -0,0 +1,33 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { formatEnumLabel as t } from "../utils/formatters.js";
3
+ import { Archive as n } from "lucide-react";
4
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
+ import { Stepper as a } from "@burdenoff/fe-libs/ui";
6
+ //#region src/theculturepost/components/LifecycleStepper.tsx
7
+ var o = [
8
+ "IDEA",
9
+ "DRAFT",
10
+ "IN_REVIEW",
11
+ "PUBLISHED"
12
+ ], s = o.map((e) => ({
13
+ key: e,
14
+ label: t(e)
15
+ }));
16
+ function c({ status: t, className: c, onStepClick: l }) {
17
+ return t === "ARCHIVED" ? /* @__PURE__ */ i("div", {
18
+ className: e("inline-flex items-center gap-2 rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm font-medium text-text-secondary", c),
19
+ children: [/* @__PURE__ */ r(n, {
20
+ className: "h-4 w-4",
21
+ "aria-hidden": "true"
22
+ }), "Archived"]
23
+ }) : /* @__PURE__ */ r(a, {
24
+ steps: s,
25
+ active: Math.max(0, o.indexOf(t)),
26
+ onStepClick: l,
27
+ className: c
28
+ });
29
+ }
30
+ //#endregion
31
+ export { c as LifecycleStepper };
32
+
33
+ //# sourceMappingURL=LifecycleStepper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LifecycleStepper.js","names":[],"sources":["../../../src/theculturepost/components/LifecycleStepper.tsx"],"sourcesContent":["/**\n * LifecycleStepper — maps a story's status to an ordered editorial stepper.\n * @module theculturepost/components\n *\n * Idea → Draft → In Review → Published (with Archived as a terminal off-path\n * state). Wraps the fe-libs {@link Stepper} (which fe-libs ships) so we stay on\n * the shared design system; we only translate the domain enum → step index.\n * Read-only by default (no onStepClick) since status transitions are driven by\n * dedicated mutations, not by clicking the indicator.\n */\n\nimport { Stepper, type StepperStep } from '@burdenoff/fe-libs/ui';\nimport { Archive } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\nimport type { TcpStoryStatus } from '../types';\n\ninterface LifecycleStepperProps {\n status: TcpStoryStatus;\n className?: string;\n /** Optional click handler (index into the ordered steps). Read-only if omitted. */\n onStepClick?: (index: number) => void;\n}\n\n/** The linear editorial path. ARCHIVED is terminal / off-path (handled below). */\nconst FLOW: TcpStoryStatus[] = ['IDEA', 'DRAFT', 'IN_REVIEW', 'PUBLISHED'];\n\nconst STEPS: StepperStep[] = FLOW.map((s) => ({ key: s, label: formatEnumLabel(s) }));\n\nexport function LifecycleStepper({ status, className, onStepClick }: LifecycleStepperProps) {\n if (status === 'ARCHIVED') {\n return (\n <div\n className={cn(\n 'inline-flex items-center gap-2 rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm font-medium text-text-secondary',\n className\n )}\n >\n <Archive className=\"h-4 w-4\" aria-hidden=\"true\" />\n Archived\n </div>\n );\n }\n\n const active = Math.max(0, FLOW.indexOf(status));\n\n return <Stepper steps={STEPS} active={active} onStepClick={onStepClick} className={className} />;\n}\n"],"mappings":";;;;;;AAyBA,IAAM,IAAyB;CAAC;CAAQ;CAAS;CAAa;CAAY,EAEpE,IAAuB,EAAK,KAAK,OAAO;CAAE,KAAK;CAAG,OAAO,EAAgB,EAAE;CAAE,EAAE;AAErF,SAAgB,EAAiB,EAAE,WAAQ,cAAW,kBAAsC;AAiB1F,QAhBI,MAAW,aAEX,kBAAC,OAAD;EACE,WAAW,EACT,wIACA,EACD;YAJH,CAME,kBAAC,GAAD;GAAS,WAAU;GAAU,eAAY;GAAS,CAAA,EAAA,WAE9C;MAMH,kBAAC,GAAD;EAAS,OAAO;EAAe,QAFvB,KAAK,IAAI,GAAG,EAAK,QAAQ,EAAO,CAAC;EAEW;EAAwB;EAAa,CAAA"}