@bygghammar/cms-platform 0.90.0-m7.9 → 0.91.0

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 (88) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/README.md +23 -1
  3. package/docs/GOOGLE-SHEETS.md +145 -0
  4. package/docs/LAYOUT-BLOCKS.md +125 -0
  5. package/docs/MEDIA-FOLDERS.md +90 -0
  6. package/docs/NEW-SITE.md +35 -0
  7. package/docs/PAGES.md +452 -0
  8. package/docs/SCHEDULING.md +130 -0
  9. package/docs/TYPOGRAPHY.md +86 -0
  10. package/migrations/20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql +33 -0
  11. package/migrations/20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql +383 -0
  12. package/migrations/20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql +751 -0
  13. package/package.json +5 -1
  14. package/src/backoffice/DomainsView.tsx +152 -25
  15. package/src/backoffice/FormEditorView.tsx +10 -0
  16. package/src/backoffice/FormSettingsView.tsx +8 -0
  17. package/src/backoffice/MediaLibraryView.tsx +239 -21
  18. package/src/backoffice/NavigationView.tsx +43 -0
  19. package/src/backoffice/PageBuilderView.tsx +193 -36
  20. package/src/backoffice/PageEditorView.tsx +293 -57
  21. package/src/backoffice/PagesListView.tsx +294 -19
  22. package/src/backoffice/ScheduleFields.tsx +151 -0
  23. package/src/backoffice/SectionFormView.tsx +58 -0
  24. package/src/backoffice/StatusBadge.tsx +174 -0
  25. package/src/backoffice/TrackingView.tsx +247 -208
  26. package/src/backoffice/TypographyPanel.tsx +308 -0
  27. package/src/backoffice/forms/GoogleSheetsHelp.tsx +220 -0
  28. package/src/backoffice/inspectors/blocks.tsx +149 -0
  29. package/src/backoffice/inspectors/fields.tsx +81 -0
  30. package/src/backoffice/inspectors/index.tsx +3 -0
  31. package/src/backoffice/media/ConfirmDialog.tsx +64 -4
  32. package/src/backoffice/routes/host.ts +31 -0
  33. package/src/backoffice/routes/index.tsx +140 -15
  34. package/src/backoffice/routes/media-forms.tsx +5 -0
  35. package/src/backoffice/seo/PageSeoPanelView.tsx +31 -6
  36. package/src/backoffice/seo/page-seo-form.ts +11 -1
  37. package/src/backoffice/shell/domain-guard.ts +27 -14
  38. package/src/backoffice/shell/index.ts +5 -0
  39. package/src/backoffice/shell/use-resolved-config.tsx +67 -0
  40. package/src/backoffice/tracking-drafts.ts +150 -0
  41. package/src/blocks/block-children.tsx +40 -0
  42. package/src/blocks/block-tree.ts +378 -0
  43. package/src/blocks/components/IconGridBlock.tsx +76 -51
  44. package/src/blocks/components/LayoutBlock.tsx +100 -0
  45. package/src/blocks/countdown-time.ts +17 -87
  46. package/src/blocks/registry.tsx +21 -0
  47. package/src/blocks/types.ts +40 -0
  48. package/src/cms/page-publication.ts +377 -0
  49. package/src/cms/page-status.ts +152 -0
  50. package/src/cms/timezone.ts +181 -0
  51. package/src/cms/use-publication-boundary.ts +84 -0
  52. package/src/cms/use-schedule-clock.ts +43 -0
  53. package/src/cms/visibility-schedule.ts +267 -0
  54. package/src/config/modules.ts +34 -10
  55. package/src/config/site-config.ts +32 -0
  56. package/src/config/typography.ts +143 -0
  57. package/src/http/cache-policy.ts +49 -2
  58. package/src/media/folders.ts +183 -0
  59. package/src/media/use-media-upload.ts +21 -4
  60. package/src/navigation/index.ts +34 -2
  61. package/src/platform/contract.ts +52 -1
  62. package/src/runtime/server-entry.ts +8 -2
  63. package/src/server/content-sections-core.ts +16 -0
  64. package/src/server/domains-core.ts +124 -107
  65. package/src/server/forms-delivery-core.ts +61 -10
  66. package/src/server/forms-submissions-core.ts +47 -0
  67. package/src/server/google-service-account.ts +325 -0
  68. package/src/server/google-sheets-core.ts +463 -0
  69. package/src/server/http/sitemap.ts +32 -8
  70. package/src/server/injections-core.ts +230 -49
  71. package/src/server/media-library-core.ts +27 -6
  72. package/src/server/pages-core.ts +680 -184
  73. package/src/server/published-cache.ts +25 -4
  74. package/src/server/resolve-globals.ts +6 -1
  75. package/src/server/schema-drift.ts +4 -0
  76. package/src/server/schemas.ts +15 -0
  77. package/src/site/PageRenderer.tsx +45 -12
  78. package/src/site/SiteFooter.tsx +44 -3
  79. package/src/site/SiteHeader.tsx +23 -2
  80. package/src/site/google-fonts.ts +191 -0
  81. package/src/site/host.ts +172 -0
  82. package/src/site/locale-domains.ts +500 -0
  83. package/src/site/routes/public-page.tsx +12 -1
  84. package/src/site/routes.ts +206 -38
  85. package/src/site/site-url.ts +80 -17
  86. package/src/site/typography.tsx +75 -0
  87. package/src/tracking/injections.ts +52 -9
  88. package/src/ui/MobileCarousel.tsx +289 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The backoffice's status badges.
3
+ *
4
+ * One vocabulary for every screen that shows whether something is live: pages,
5
+ * menu items and footer items all read the same. Each badge carries a word as
6
+ * well as a colour, and an icon shape as well as a hue, so status is never
7
+ * communicated by colour alone.
8
+ *
9
+ * Two different questions get two different badges. {@link PublicationBadge}
10
+ * answers "can a visitor open this right now", which is what "live" means and
11
+ * what publication rows decide. {@link PageStatusBadge} shows the editorial
12
+ * status an editor last set, says so on its face, and never claims a colour it
13
+ * has not earned — those two answers legitimately disagree while a page is
14
+ * scheduled or expired.
15
+ */
16
+
17
+ import type { ReactNode } from "react";
18
+ import { CalendarClock, CircleDot, CircleSlash, Eye, EyeOff, FileEdit } from "lucide-react";
19
+ import { PAGE_STATUS_DESCRIPTIONS, PAGE_STATUS_LABELS, type PageStatus } from "../cms/page-status";
20
+ import {
21
+ publicationSummary,
22
+ type PublicationState,
23
+ type PublicationTransitionKind,
24
+ } from "../cms/page-publication";
25
+ import {
26
+ VISIBILITY_STATE_DESCRIPTIONS,
27
+ VISIBILITY_STATE_LABELS,
28
+ type VisibilityState,
29
+ } from "../cms/visibility-schedule";
30
+
31
+ export type StatusTone = "draft" | "live" | "hidden" | "scheduled";
32
+
33
+ const TONE_CLASS: Record<StatusTone, string> = {
34
+ draft: "border-ink/25 bg-ink/[0.04] text-ink/70",
35
+ live: "border-sea/40 bg-ice/60 text-ink",
36
+ hidden: "border-ink/20 bg-ink/[0.02] text-ink/50",
37
+ scheduled: "border-brass/50 bg-brass/10 text-ink/80",
38
+ };
39
+
40
+ const TONE_ICON: Record<StatusTone, (props: { className?: string }) => ReactNode> = {
41
+ draft: FileEdit,
42
+ live: CircleDot,
43
+ hidden: CircleSlash,
44
+ scheduled: CalendarClock,
45
+ };
46
+
47
+ const PUBLICATION_TONE: Record<PublicationState, StatusTone> = {
48
+ draft: "hidden",
49
+ scheduled: "scheduled",
50
+ published: "live",
51
+ ending: "scheduled",
52
+ expired: "hidden",
53
+ };
54
+
55
+ const VISIBILITY_TONE: Record<VisibilityState, StatusTone> = {
56
+ visible: "live",
57
+ hidden: "hidden",
58
+ "scheduled-to-publish": "scheduled",
59
+ "scheduled-to-hide": "scheduled",
60
+ expired: "hidden",
61
+ invalid: "draft",
62
+ };
63
+
64
+ /** A single badge. `title` becomes the tooltip and the accessible name. */
65
+ export function StatusBadge({
66
+ label,
67
+ tone,
68
+ title,
69
+ }: {
70
+ label: string;
71
+ tone: StatusTone;
72
+ title?: string;
73
+ }) {
74
+ const Icon = TONE_ICON[tone];
75
+ return (
76
+ <span
77
+ title={title}
78
+ className={[
79
+ "inline-flex items-center gap-1.5 border px-2 py-0.5 text-[0.65rem] uppercase tracking-[0.14em] whitespace-nowrap",
80
+ TONE_CLASS[tone],
81
+ ].join(" ")}
82
+ >
83
+ <Icon className="size-3" aria-hidden="true" />
84
+ {label}
85
+ </span>
86
+ );
87
+ }
88
+
89
+ /**
90
+ * What a visitor gets, from an already evaluated {@link PublicationState} so
91
+ * the badge never re-implements the rules.
92
+ *
93
+ * `at` is the instant the answer changes next and `transition` is what happens
94
+ * then. Both are needed: a date on its own said "Scheduled to hide" over the
95
+ * moment a page swapped to a newer version and stayed exactly where it was.
96
+ * The wording comes from {@link publicationSummary} so this badge, the pages
97
+ * list and any host screen say the same thing about the same rows.
98
+ */
99
+ export function PublicationBadge({
100
+ state,
101
+ at,
102
+ transition,
103
+ formatAt = defaultFormatAt,
104
+ }: {
105
+ state: PublicationState;
106
+ at?: string | null;
107
+ transition?: PublicationTransitionKind | null;
108
+ formatAt?: (at: string) => string;
109
+ }) {
110
+ const when = at ? formatAt(at) : null;
111
+ const summary = publicationSummary(state, transition);
112
+ return (
113
+ <span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
114
+ <StatusBadge
115
+ label={summary.label}
116
+ tone={PUBLICATION_TONE[state]}
117
+ title={summary.description}
118
+ />
119
+ {when && summary.showsAt ? (
120
+ <span className="text-xs text-ink/55 whitespace-nowrap">{when}</span>
121
+ ) : null}
122
+ </span>
123
+ );
124
+ }
125
+
126
+ function defaultFormatAt(at: string): string {
127
+ const parsed = new Date(at);
128
+ if (Number.isNaN(parsed.getTime())) return "";
129
+ return parsed.toLocaleString(undefined, {
130
+ dateStyle: "medium",
131
+ timeStyle: "short",
132
+ });
133
+ }
134
+
135
+ /**
136
+ * The editorial status an editor last set, marked as such.
137
+ *
138
+ * It is deliberately quiet and deliberately labelled: `pages.status` is not
139
+ * public visibility, so this badge gets the same neutral tone either way and
140
+ * never renders "Published" in the colour that means "visitors can see it".
141
+ * Show it beside a {@link PublicationBadge}, never instead of one.
142
+ */
143
+ export function PageStatusBadge({ status }: { status: PageStatus }) {
144
+ return (
145
+ <StatusBadge
146
+ label={`Editorial: ${PAGE_STATUS_LABELS[status]}`}
147
+ tone="draft"
148
+ title={PAGE_STATUS_DESCRIPTIONS[status]}
149
+ />
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Visible / Hidden / Scheduled to publish / Scheduled to hide, from an already
155
+ * evaluated {@link VisibilityState} so the badge never re-implements the rules.
156
+ */
157
+ export function VisibilityBadge({ state }: { state: VisibilityState }) {
158
+ return (
159
+ <StatusBadge
160
+ label={VISIBILITY_STATE_LABELS[state]}
161
+ tone={VISIBILITY_TONE[state]}
162
+ title={VISIBILITY_STATE_DESCRIPTIONS[state]}
163
+ />
164
+ );
165
+ }
166
+
167
+ /** The eye used next to a visibility toggle, matching the badge vocabulary. */
168
+ export function VisibilityIcon({ visible }: { visible: boolean }) {
169
+ return visible ? (
170
+ <Eye className="size-4" aria-hidden="true" />
171
+ ) : (
172
+ <EyeOff className="size-4" aria-hidden="true" />
173
+ );
174
+ }