@drawnagency/primitives 0.1.94 → 0.1.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-UFOWXOG6.js → chunk-ABV7OZB3.js} +6 -0
- package/dist/{chunk-677NTHAZ.js → chunk-PTOLUMGV.js} +3 -0
- package/dist/{chunk-GDLH3LVH.js → chunk-QQBBYQYD.js} +3 -2
- package/dist/{chunk-XYFZ57AS.js → chunk-YUUUCDXN.js} +59 -5
- package/dist/components/brandguide/Colors.d.ts +2 -2
- package/dist/components/brandguide/Colors.d.ts.map +1 -1
- package/dist/components/brandguide/ColorsEditable.d.ts +2 -2
- package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/editor/index.js +4 -3
- package/dist/components/sections/Container/Container.d.ts +2 -2
- package/dist/components/sections/Container/Container.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.js +2 -2
- package/dist/components/sections/register.js +2 -2
- package/dist/components/shared/HistoryChangesModal.d.ts +39 -0
- package/dist/components/shared/HistoryChangesModal.d.ts.map +1 -0
- package/dist/components/shared/HistoryPopover.d.ts +3 -1
- package/dist/components/shared/HistoryPopover.d.ts.map +1 -1
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shared/Popover.d.ts +10 -1
- package/dist/components/shared/Popover.d.ts.map +1 -1
- package/dist/components/shell/EditorModal.d.ts +6 -1
- package/dist/components/shell/EditorModal.d.ts.map +1 -1
- package/dist/components/shell/EditorModalContext.d.ts +3 -2
- package/dist/components/shell/EditorModalContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/hooks/useEditorPublish.d.ts +5 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/lib/events.d.ts +18 -0
- package/dist/lib/events.d.ts.map +1 -1
- package/dist/lib/history-buckets.d.ts +30 -0
- package/dist/lib/history-buckets.d.ts.map +1 -0
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +4 -2
- package/dist/lib/media-folders.d.ts +13 -0
- package/dist/lib/media-folders.d.ts.map +1 -1
- package/dist/lib/media-folders.js +172 -0
- package/dist/lib/registry.d.ts +7 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/theme-vars.d.ts +19 -4
- package/dist/lib/theme-vars.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/brandguide/Colors.tsx +2 -1
- package/src/components/brandguide/ColorsEditable.tsx +2 -1
- package/src/components/editor/SectionWrapper.tsx +1 -0
- package/src/components/editor/SettingsForm.tsx +5 -2
- package/src/components/sections/Container/Container.tsx +5 -4
- package/src/components/shared/HistoryChangesModal.tsx +256 -0
- package/src/components/shared/HistoryPopover.tsx +255 -76
- package/src/components/shared/Navigation.tsx +30 -9
- package/src/components/shared/Popover.tsx +95 -2
- package/src/components/shell/EditorModal.tsx +26 -4
- package/src/components/shell/EditorModalContext.tsx +5 -4
- package/src/components/shell/EditorShell.tsx +51 -2
- package/src/components/shell/SiteSettingsModal.tsx +1 -1
- package/src/hooks/useEditorPublish.ts +15 -6
- package/src/lib/events.ts +11 -0
- package/src/lib/history-buckets.ts +190 -0
- package/src/lib/index.ts +1 -1
- package/src/lib/media-folders.ts +16 -0
- package/src/lib/registry.ts +9 -1
- package/src/lib/theme-vars.ts +25 -4
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export interface HistoryCommit {
|
|
2
|
+
sha: string;
|
|
3
|
+
date: string;
|
|
4
|
+
/** First parent's sha; null for the repo's root commit. */
|
|
5
|
+
parent: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type NodeKind = "commit" | "day" | "week" | "month" | "year";
|
|
9
|
+
|
|
10
|
+
export interface HistoryNode {
|
|
11
|
+
/** Stable React key, unique within the tree. */
|
|
12
|
+
id: string;
|
|
13
|
+
kind: NodeKind;
|
|
14
|
+
label: string;
|
|
15
|
+
sublabel: string | null;
|
|
16
|
+
count: number;
|
|
17
|
+
/** Newest commit in this node — what "browse" opens. */
|
|
18
|
+
newestSha: string | null;
|
|
19
|
+
/** Compare base: parent of this node's oldest commit. Null at repo root. */
|
|
20
|
+
baseSha: string | null;
|
|
21
|
+
children: HistoryNode[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type GroupId =
|
|
25
|
+
| "today" | "this-week" | "previous-weeks" | "earlier-months" | "previous-years";
|
|
26
|
+
|
|
27
|
+
export interface HistoryGroup {
|
|
28
|
+
id: GroupId;
|
|
29
|
+
label: string;
|
|
30
|
+
nodes: HistoryNode[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const GROUPS: { id: GroupId; label: string }[] = [
|
|
34
|
+
{ id: "today", label: "Today" },
|
|
35
|
+
{ id: "this-week", label: "Earlier this week" },
|
|
36
|
+
{ id: "previous-weeks", label: "Previous weeks" },
|
|
37
|
+
{ id: "earlier-months", label: "Earlier months" },
|
|
38
|
+
{ id: "previous-years", label: "Previous years" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Calendar-day arithmetic (not fixed-ms) so DST transitions don't shift boundaries.
|
|
42
|
+
const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
43
|
+
const addDays = (d: Date, n: number) =>
|
|
44
|
+
new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
45
|
+
const startOfYear = (d: Date) => new Date(d.getFullYear(), 0, 1);
|
|
46
|
+
|
|
47
|
+
const WEEKDAY = new Intl.DateTimeFormat(undefined, { weekday: "long" });
|
|
48
|
+
const MONTH = new Intl.DateTimeFormat(undefined, { month: "long" });
|
|
49
|
+
const MON_DAY = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" });
|
|
50
|
+
const TIME = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" });
|
|
51
|
+
|
|
52
|
+
interface Dated extends HistoryCommit {
|
|
53
|
+
at: Date;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Oldest-first list → node metadata shared by every non-commit node. */
|
|
57
|
+
function rollup(sorted: Dated[]): Pick<HistoryNode, "count" | "newestSha" | "baseSha"> {
|
|
58
|
+
return {
|
|
59
|
+
count: sorted.length,
|
|
60
|
+
newestSha: sorted.length ? sorted[sorted.length - 1].sha : null,
|
|
61
|
+
baseSha: sorted.length ? sorted[0].parent : null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function commitNode(c: Dated): HistoryNode {
|
|
66
|
+
return {
|
|
67
|
+
id: `c:${c.sha}`,
|
|
68
|
+
kind: "commit",
|
|
69
|
+
label: TIME.format(c.at),
|
|
70
|
+
sublabel: null,
|
|
71
|
+
count: 1,
|
|
72
|
+
newestSha: c.sha,
|
|
73
|
+
baseSha: c.parent,
|
|
74
|
+
children: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Bucket by a key derived from each commit's date, newest bucket first. */
|
|
79
|
+
function bucketBy(sorted: Dated[], key: (d: Date) => string): Map<string, Dated[]> {
|
|
80
|
+
const out = new Map<string, Dated[]>();
|
|
81
|
+
for (const c of sorted) {
|
|
82
|
+
const k = key(c.at);
|
|
83
|
+
const list = out.get(k);
|
|
84
|
+
if (list) list.push(c);
|
|
85
|
+
else out.set(k, [c]);
|
|
86
|
+
}
|
|
87
|
+
// sorted is oldest-first, so reverse the insertion order for newest-first display.
|
|
88
|
+
return new Map([...out.entries()].reverse());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const dayKey = (d: Date) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
92
|
+
const monthKey = (d: Date) => `${d.getFullYear()}-${d.getMonth()}`;
|
|
93
|
+
const yearKey = (d: Date) => `${d.getFullYear()}`;
|
|
94
|
+
|
|
95
|
+
/** Day nodes whose children are commit nodes. `weekday` picks the label style. */
|
|
96
|
+
function dayNodes(sorted: Dated[], weekday: boolean): HistoryNode[] {
|
|
97
|
+
return [...bucketBy(sorted, dayKey).entries()].map(([k, list]) => ({
|
|
98
|
+
id: `d:${k}`,
|
|
99
|
+
kind: "day" as const,
|
|
100
|
+
label: weekday ? WEEKDAY.format(list[0].at) : MON_DAY.format(list[0].at),
|
|
101
|
+
sublabel: weekday ? MON_DAY.format(list[0].at) : null,
|
|
102
|
+
...rollup(list),
|
|
103
|
+
children: [...list].reverse().map(commitNode),
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function monthNodes(sorted: Dated[], withYear: boolean): HistoryNode[] {
|
|
108
|
+
return [...bucketBy(sorted, monthKey).entries()].map(([k, list]) => ({
|
|
109
|
+
id: `m:${k}`,
|
|
110
|
+
kind: "month" as const,
|
|
111
|
+
label: MONTH.format(list[0].at),
|
|
112
|
+
sublabel: withYear ? String(list[0].at.getFullYear()) : null,
|
|
113
|
+
...rollup(list),
|
|
114
|
+
children: dayNodes(list, false),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function yearNodes(sorted: Dated[]): HistoryNode[] {
|
|
119
|
+
return [...bucketBy(sorted, yearKey).entries()].map(([k, list]) => ({
|
|
120
|
+
id: `y:${k}`,
|
|
121
|
+
kind: "year" as const,
|
|
122
|
+
label: k,
|
|
123
|
+
sublabel: null,
|
|
124
|
+
...rollup(list),
|
|
125
|
+
children: monthNodes(list, false),
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Up to three 7-day windows, newest first. Windows with no commits are dropped
|
|
131
|
+
* rather than rendered empty — an editor scanning history wants the edits, not
|
|
132
|
+
* a fixed-width calendar.
|
|
133
|
+
*/
|
|
134
|
+
function weekNodes(sorted: Dated[], windowStart: Date): HistoryNode[] {
|
|
135
|
+
const out: HistoryNode[] = [];
|
|
136
|
+
for (let i = 1; i <= 3; i++) {
|
|
137
|
+
const start = addDays(windowStart, -7 * i);
|
|
138
|
+
const end = addDays(windowStart, -7 * (i - 1));
|
|
139
|
+
const list = sorted.filter((c) => c.at >= start && c.at < end);
|
|
140
|
+
if (list.length === 0) continue;
|
|
141
|
+
out.push({
|
|
142
|
+
id: `w:${dayKey(start)}`,
|
|
143
|
+
kind: "week",
|
|
144
|
+
label: `${MON_DAY.format(start)} – ${MON_DAY.format(addDays(end, -1))}`,
|
|
145
|
+
sublabel: null,
|
|
146
|
+
...rollup(list),
|
|
147
|
+
children: dayNodes(list, false),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildHistoryTree(commits: HistoryCommit[], now: Date): HistoryGroup[] {
|
|
154
|
+
// Oldest-first throughout; each builder reverses for display.
|
|
155
|
+
const sorted: Dated[] = commits
|
|
156
|
+
.map((c) => ({ ...c, at: new Date(c.date) }))
|
|
157
|
+
.filter((c) => !Number.isNaN(c.at.getTime()))
|
|
158
|
+
.sort((a, b) => a.at.getTime() - b.at.getTime());
|
|
159
|
+
|
|
160
|
+
const t0 = startOfDay(now); // today begins
|
|
161
|
+
const t1 = addDays(t0, -6); // "earlier this week" window begins
|
|
162
|
+
const t2 = addDays(t1, -21); // "previous weeks" window begins
|
|
163
|
+
const y0 = startOfYear(now); // current year begins
|
|
164
|
+
const monthsEnd = t2; // earlier-months covers [y0, t2)
|
|
165
|
+
const yearsEnd = t2 < y0 ? t2 : y0; // previous-years covers [-inf, yearsEnd)
|
|
166
|
+
|
|
167
|
+
const inRange = (lo: Date | null, hi: Date) =>
|
|
168
|
+
sorted.filter((c) => (lo === null || c.at >= lo) && c.at < hi);
|
|
169
|
+
|
|
170
|
+
const byId: Record<GroupId, HistoryNode[]> = {
|
|
171
|
+
"today": [...sorted.filter((c) => c.at >= t0)].reverse().map(commitNode),
|
|
172
|
+
"this-week": dayNodes(inRange(t1, t0), true),
|
|
173
|
+
"previous-weeks": weekNodes(sorted, t1),
|
|
174
|
+
"earlier-months": monthsEnd > y0 ? monthNodes(inRange(y0, monthsEnd), true) : [],
|
|
175
|
+
"previous-years": yearNodes(inRange(null, yearsEnd)),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return GROUPS.map((g) => ({ ...g, nodes: byId[g.id] }));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Largest node count anywhere in the tree; never 0, so callers can divide by it. */
|
|
182
|
+
export function maxNodeCount(groups: HistoryGroup[]): number {
|
|
183
|
+
let max = 1;
|
|
184
|
+
const walk = (n: HistoryNode) => {
|
|
185
|
+
if (n.count > max) max = n.count;
|
|
186
|
+
n.children.forEach(walk);
|
|
187
|
+
};
|
|
188
|
+
groups.forEach((g) => g.nodes.forEach(walk));
|
|
189
|
+
return max;
|
|
190
|
+
}
|
package/src/lib/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export {
|
|
|
12
12
|
suggestAccessibleOn,
|
|
13
13
|
type WcagLevel,
|
|
14
14
|
} from "./contrast";
|
|
15
|
-
export { primaryVarValue, accentVarValue } from "./theme-vars";
|
|
15
|
+
export { primaryVarValue, accentVarValue, onPrimaryVarValue } from "./theme-vars";
|
|
16
16
|
export { sanitizeHtml, ensureSanitizer } from "./sanitize";
|
|
17
17
|
export { gridColsClass } from "./grid";
|
|
18
18
|
export { getIcon, curatedIcons, type IconEntry } from "./icons";
|
package/src/lib/media-folders.ts
CHANGED
|
@@ -178,6 +178,22 @@ export function healMediaManifest(manifest: MediaManifest): MediaManifest {
|
|
|
178
178
|
return { ...manifest, images, folders };
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Rebase a local manifest onto a freshly-fetched remote one, so media another
|
|
183
|
+
* writer added is in our copy before the next save assembles from it. Without
|
|
184
|
+
* this a rebased session still writes its pre-conflict manifest, which silently
|
|
185
|
+
* drops their entries and orphans the files they committed.
|
|
186
|
+
*
|
|
187
|
+
* Images union, ours winning, so local alt/folder-move edits survive; a pending
|
|
188
|
+
* local deletion isn't removed from the manifest until save, so the union can't
|
|
189
|
+
* resurrect one. Folders stay ours verbatim — deleting a folder removes it from
|
|
190
|
+
* our map with no wire signal, so unioning would undo the delete. Mirrors the
|
|
191
|
+
* server-side merge in core's content-ops.
|
|
192
|
+
*/
|
|
193
|
+
export function rebaseMediaManifest(local: MediaManifest, remote: MediaManifest): MediaManifest {
|
|
194
|
+
return { ...local, images: { ...remote.images, ...local.images } };
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
/**
|
|
182
198
|
* Save-time manifest assembly: merge uploaded pending items, apply deletions,
|
|
183
199
|
* preserve every other manifest key (folders!), then heal.
|
package/src/lib/registry.ts
CHANGED
|
@@ -77,12 +77,20 @@ export type Thumbnail =
|
|
|
77
77
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
78
|
export type CustomSettingsFormProps = Record<string, any>;
|
|
79
79
|
|
|
80
|
+
export interface ModalOptions {
|
|
81
|
+
/** Content is a tab set — lock the modal height so tab switches don't resize it. */
|
|
82
|
+
tabbed?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Signature of the editor's modal opener as threaded down to sections. */
|
|
86
|
+
export type OpenModal = (title: string, content: ReactNode, options?: ModalOptions) => void;
|
|
87
|
+
|
|
80
88
|
export interface SectionProps<T = unknown> {
|
|
81
89
|
content: T;
|
|
82
90
|
options?: Record<string, unknown>;
|
|
83
91
|
onChange?: (content: T) => void;
|
|
84
92
|
isEditMode: boolean;
|
|
85
|
-
openModal?:
|
|
93
|
+
openModal?: OpenModal;
|
|
86
94
|
/** Delete this whole section. Used e.g. when a colors section's last swatch
|
|
87
95
|
* is dragged into another section, leaving it empty. */
|
|
88
96
|
onDelete?: () => void;
|
package/src/lib/theme-vars.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { SiteConfig } from "../schemas/site-config";
|
|
2
|
+
import { suggestAccessibleOn } from "./contrast";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Computes the CSS value for the brand `--color-primary` / `--color-primary-contrast`
|
|
5
|
-
* custom properties. When darkMode is "optional" AND a dark
|
|
6
|
-
* value is a CSS `light-dark(<light>, <dark>)` expression — the
|
|
7
|
-
* it per the element's `color-scheme` (the `.dark` class sets
|
|
8
|
-
* `:root` sets `color-scheme: light`). Otherwise it's the
|
|
6
|
+
* / `--color-on-primary` custom properties. When darkMode is "optional" AND a dark
|
|
7
|
+
* variant is set, the value is a CSS `light-dark(<light>, <dark>)` expression — the
|
|
8
|
+
* browser resolves it per the element's `color-scheme` (the `.dark` class sets
|
|
9
|
+
* `color-scheme: dark`; `:root` sets `color-scheme: light`). Otherwise it's the
|
|
10
|
+
* plain light hex.
|
|
9
11
|
*
|
|
10
12
|
* Single source of truth for every injection site: Layout.astro / LoginLayout.astro
|
|
11
13
|
* (SSR inline `<html style>`) and EditorShell (live setProperty + localStorage).
|
|
@@ -29,3 +31,22 @@ export function primaryVarValue(c?: Partial<ThemeColorConfig> | null): string {
|
|
|
29
31
|
export function accentVarValue(c?: Partial<ThemeColorConfig> | null): string {
|
|
30
32
|
return lightDark(c?.primaryContrast ?? DEFAULT_CONTRAST, c?.primaryContrastDark, c?.darkMode === "optional");
|
|
31
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Foreground ink for text drawn ON a `--color-primary`-filled surface (branded
|
|
37
|
+
* plates, primary cards). Derived from `primaryColor` with the same
|
|
38
|
+
* black/white WCAG pick the editor's accent "Suggest" button uses, so a
|
|
39
|
+
* primary-filled surface is legible on every brand without configuration.
|
|
40
|
+
*
|
|
41
|
+
* This exists because `--color-primary-contrast` cannot serve the role: it is
|
|
42
|
+
* the *accent* — brands legitimately repoint it at a decorative colour (Siete's
|
|
43
|
+
* gold on aubergine) that reads as a highlight but cannot carry body copy.
|
|
44
|
+
* Sections should consume `var(--color-on-primary)` for text on primary and
|
|
45
|
+
* keep `--color-primary-contrast` for accents, and derive softer inks by mixing
|
|
46
|
+
* the two brand tokens: `color-mix(in srgb, var(--color-on-primary) 70%, var(--color-primary))`.
|
|
47
|
+
*/
|
|
48
|
+
export function onPrimaryVarValue(c?: Partial<ThemeColorConfig> | null): string {
|
|
49
|
+
const light = suggestAccessibleOn(c?.primaryColor ?? DEFAULT_PRIMARY);
|
|
50
|
+
const dark = c?.primaryColorDark ? suggestAccessibleOn(c.primaryColorDark) : null;
|
|
51
|
+
return lightDark(light, dark, c?.darkMode === "optional");
|
|
52
|
+
}
|