@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.
- package/CHANGELOG.md +241 -0
- package/README.md +23 -1
- package/docs/GOOGLE-SHEETS.md +145 -0
- package/docs/LAYOUT-BLOCKS.md +125 -0
- package/docs/MEDIA-FOLDERS.md +90 -0
- package/docs/NEW-SITE.md +35 -0
- package/docs/PAGES.md +452 -0
- package/docs/SCHEDULING.md +130 -0
- package/docs/TYPOGRAPHY.md +86 -0
- package/migrations/20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql +33 -0
- package/migrations/20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql +383 -0
- package/migrations/20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql +751 -0
- package/package.json +5 -1
- package/src/backoffice/DomainsView.tsx +152 -25
- package/src/backoffice/FormEditorView.tsx +10 -0
- package/src/backoffice/FormSettingsView.tsx +8 -0
- package/src/backoffice/MediaLibraryView.tsx +239 -21
- package/src/backoffice/NavigationView.tsx +43 -0
- package/src/backoffice/PageBuilderView.tsx +193 -36
- package/src/backoffice/PageEditorView.tsx +293 -57
- package/src/backoffice/PagesListView.tsx +294 -19
- package/src/backoffice/ScheduleFields.tsx +151 -0
- package/src/backoffice/SectionFormView.tsx +58 -0
- package/src/backoffice/StatusBadge.tsx +174 -0
- package/src/backoffice/TrackingView.tsx +247 -208
- package/src/backoffice/TypographyPanel.tsx +308 -0
- package/src/backoffice/forms/GoogleSheetsHelp.tsx +220 -0
- package/src/backoffice/inspectors/blocks.tsx +149 -0
- package/src/backoffice/inspectors/fields.tsx +81 -0
- package/src/backoffice/inspectors/index.tsx +3 -0
- package/src/backoffice/media/ConfirmDialog.tsx +64 -4
- package/src/backoffice/routes/host.ts +31 -0
- package/src/backoffice/routes/index.tsx +140 -15
- package/src/backoffice/routes/media-forms.tsx +5 -0
- package/src/backoffice/seo/PageSeoPanelView.tsx +31 -6
- package/src/backoffice/seo/page-seo-form.ts +11 -1
- package/src/backoffice/shell/domain-guard.ts +27 -14
- package/src/backoffice/shell/index.ts +5 -0
- package/src/backoffice/shell/use-resolved-config.tsx +67 -0
- package/src/backoffice/tracking-drafts.ts +150 -0
- package/src/blocks/block-children.tsx +40 -0
- package/src/blocks/block-tree.ts +378 -0
- package/src/blocks/components/IconGridBlock.tsx +76 -51
- package/src/blocks/components/LayoutBlock.tsx +100 -0
- package/src/blocks/countdown-time.ts +17 -87
- package/src/blocks/registry.tsx +21 -0
- package/src/blocks/types.ts +40 -0
- package/src/cms/page-publication.ts +377 -0
- package/src/cms/page-status.ts +152 -0
- package/src/cms/timezone.ts +181 -0
- package/src/cms/use-publication-boundary.ts +84 -0
- package/src/cms/use-schedule-clock.ts +43 -0
- package/src/cms/visibility-schedule.ts +267 -0
- package/src/config/modules.ts +34 -10
- package/src/config/site-config.ts +32 -0
- package/src/config/typography.ts +143 -0
- package/src/http/cache-policy.ts +49 -2
- package/src/media/folders.ts +183 -0
- package/src/media/use-media-upload.ts +21 -4
- package/src/navigation/index.ts +34 -2
- package/src/platform/contract.ts +52 -1
- package/src/runtime/server-entry.ts +8 -2
- package/src/server/content-sections-core.ts +16 -0
- package/src/server/domains-core.ts +124 -107
- package/src/server/forms-delivery-core.ts +61 -10
- package/src/server/forms-submissions-core.ts +47 -0
- package/src/server/google-service-account.ts +325 -0
- package/src/server/google-sheets-core.ts +463 -0
- package/src/server/http/sitemap.ts +32 -8
- package/src/server/injections-core.ts +230 -49
- package/src/server/media-library-core.ts +27 -6
- package/src/server/pages-core.ts +680 -184
- package/src/server/published-cache.ts +25 -4
- package/src/server/resolve-globals.ts +6 -1
- package/src/server/schema-drift.ts +4 -0
- package/src/server/schemas.ts +15 -0
- package/src/site/PageRenderer.tsx +45 -12
- package/src/site/SiteFooter.tsx +44 -3
- package/src/site/SiteHeader.tsx +23 -2
- package/src/site/google-fonts.ts +191 -0
- package/src/site/host.ts +172 -0
- package/src/site/locale-domains.ts +500 -0
- package/src/site/routes/public-page.tsx +12 -1
- package/src/site/routes.ts +206 -38
- package/src/site/site-url.ts +80 -17
- package/src/site/typography.tsx +75 -0
- package/src/tracking/injections.ts +52 -9
- 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
|
+
}
|