@drawnagency/primitives 0.1.87 → 0.1.88
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-VY5V4HBV.js → chunk-H2FENAQH.js} +1 -1
- package/dist/{chunk-6VOIZP2O.js → chunk-SK6GGTTO.js} +217 -204
- package/dist/components/brandguide/Colors.d.ts.map +1 -1
- package/dist/components/brandguide/ColorsEditable.d.ts +20 -0
- package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -0
- package/dist/components/brandguide/ColorsView.d.ts +9 -0
- package/dist/components/brandguide/ColorsView.d.ts.map +1 -0
- package/dist/components/brandguide/colors-shared.d.ts +4 -0
- package/dist/components/brandguide/colors-shared.d.ts.map +1 -0
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/SectionIsland.d.ts +8 -0
- package/dist/components/sections/SectionIsland.d.ts.map +1 -0
- package/dist/components/sections/brand-guide.js +1 -1
- package/dist/components/sections/register-schemas.js +2 -2
- package/dist/components/sections/register.js +2 -2
- package/dist/hooks/useCollabSession.d.ts.map +1 -1
- package/dist/lib/registry.d.ts +7 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/brandguide/Colors.tsx +2 -275
- package/src/components/brandguide/ColorsEditable.tsx +155 -0
- package/src/components/brandguide/ColorsView.tsx +100 -0
- package/src/components/brandguide/colors-shared.ts +17 -0
- package/src/components/sections/Colors/index.tsx +1 -0
- package/src/components/sections/SectionIsland.tsx +21 -0
- package/src/hooks/useCollabSession.ts +27 -0
- package/src/lib/registry.ts +7 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { containerGridClass } from "../../lib/container-grid";
|
|
5
|
+
import { getContrastClass } from "./colors-shared";
|
|
6
|
+
import type { ColorItem } from "../sections/Colors/schema";
|
|
7
|
+
|
|
8
|
+
export default function ColorsView({
|
|
9
|
+
colors,
|
|
10
|
+
columns,
|
|
11
|
+
label,
|
|
12
|
+
collapsing,
|
|
13
|
+
showLabel,
|
|
14
|
+
}: {
|
|
15
|
+
colors: ColorItem[];
|
|
16
|
+
columns: number;
|
|
17
|
+
label?: string;
|
|
18
|
+
collapsing?: boolean;
|
|
19
|
+
showLabel?: boolean;
|
|
20
|
+
}) {
|
|
21
|
+
const [expanded, setExpanded] = useState(!collapsing);
|
|
22
|
+
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
|
23
|
+
|
|
24
|
+
const handleCopy = (value: string, colorIndex: number) => {
|
|
25
|
+
if (navigator.clipboard?.writeText) {
|
|
26
|
+
navigator.clipboard.writeText(value);
|
|
27
|
+
} else {
|
|
28
|
+
const ta = document.createElement("textarea");
|
|
29
|
+
ta.value = value;
|
|
30
|
+
ta.style.position = "fixed";
|
|
31
|
+
ta.style.opacity = "0";
|
|
32
|
+
document.body.appendChild(ta);
|
|
33
|
+
ta.select();
|
|
34
|
+
document.execCommand("copy");
|
|
35
|
+
document.body.removeChild(ta);
|
|
36
|
+
}
|
|
37
|
+
setCopiedIndex(colorIndex);
|
|
38
|
+
setTimeout(() => setCopiedIndex(null), 1500);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
{(showLabel && !!label || collapsing) && (
|
|
44
|
+
<div className="mb-4 flex items-center gap-2">
|
|
45
|
+
{showLabel && !!label && (
|
|
46
|
+
<span className="text-lg font-bold">{label}</span>
|
|
47
|
+
)}
|
|
48
|
+
{collapsing && (
|
|
49
|
+
<button
|
|
50
|
+
onClick={() => setExpanded(!expanded)}
|
|
51
|
+
className="cursor-pointer ml-auto flex items-center gap-1.5 text-sm text-base-contrast-light hover:text-base-contrast"
|
|
52
|
+
>
|
|
53
|
+
<span>{expanded ? "Hide Details" : "Show Details"}</span>
|
|
54
|
+
{expanded ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<div className="@container">
|
|
61
|
+
<div className={cn("grid gap-4", containerGridClass[columns] || containerGridClass[3])}>
|
|
62
|
+
{colors.map((color, i) => {
|
|
63
|
+
const hex = color.spaces[0]?.hex;
|
|
64
|
+
const contrast = getContrastClass(hex);
|
|
65
|
+
return (
|
|
66
|
+
<div key={i} className="flex h-full flex-col overflow-hidden rounded-outer border border-base-200">
|
|
67
|
+
<div className={cn("relative flex min-h-[80px] items-end p-3", contrast)} style={{ backgroundColor: hex || "#ccc" }}>
|
|
68
|
+
{color.name && <span className="text-sm font-bold">{color.name}</span>}
|
|
69
|
+
{copiedIndex === i && (
|
|
70
|
+
<span className={cn(
|
|
71
|
+
"absolute top-2 right-2 rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
72
|
+
getContrastClass(hex) === "text-black" ? "bg-black/15 text-black" : "bg-white/25 text-white",
|
|
73
|
+
)}>
|
|
74
|
+
Copied!
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{expanded && (
|
|
79
|
+
// flex-1 so every swatch's label block grows to the tallest in its row.
|
|
80
|
+
<div className="flex-1 space-y-1 bg-base-accent p-3 text-sm">
|
|
81
|
+
{color.spaces.map((space, j) =>
|
|
82
|
+
Object.entries(space).map(([key, value]) =>
|
|
83
|
+
value ? (
|
|
84
|
+
<button key={`${j}-${key}`} onClick={() => handleCopy(value, i)} className="cursor-pointer flex w-full justify-between hover:text-primary">
|
|
85
|
+
<span className="font-medium uppercase">{key}</span>
|
|
86
|
+
<span>{value}</span>
|
|
87
|
+
</button>
|
|
88
|
+
) : null
|
|
89
|
+
)
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ColorItem } from "../sections/Colors/schema";
|
|
2
|
+
|
|
3
|
+
export function getContrastClass(hex: string | undefined): string {
|
|
4
|
+
if (!hex) return "text-white";
|
|
5
|
+
const h = hex.replace("#", "");
|
|
6
|
+
if (h.length < 6) return "text-white";
|
|
7
|
+
const r = parseInt(h.substring(0, 2), 16);
|
|
8
|
+
const g = parseInt(h.substring(2, 4), 16);
|
|
9
|
+
const b = parseInt(h.substring(4, 6), 16);
|
|
10
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
11
|
+
return yiq >= 128 ? "text-black" : "text-white";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const createColorItem = (): ColorItem => ({
|
|
15
|
+
name: "",
|
|
16
|
+
spaces: [{ hex: "#cccccc" }],
|
|
17
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ensureSectionsRegistered } from "./register";
|
|
2
|
+
import { getSection } from "../../lib/registry";
|
|
3
|
+
|
|
4
|
+
// Registering on import means the registry is populated both during SSR of the
|
|
5
|
+
// island and after it hydrates client-side (register runs again in the browser
|
|
6
|
+
// bundle; the Symbol.for-keyed singleton dedupes).
|
|
7
|
+
ensureSectionsRegistered();
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
type: string;
|
|
11
|
+
content: unknown;
|
|
12
|
+
options?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function SectionIsland({ type, content, options }: Props) {
|
|
16
|
+
const def = getSection(type);
|
|
17
|
+
if (!def) return null;
|
|
18
|
+
const Component = def.component;
|
|
19
|
+
// Viewer mode: no onChange / openModal callbacks -> the view (not editable) path.
|
|
20
|
+
return <Component content={content} options={options} isEditMode={false} />;
|
|
21
|
+
}
|
|
@@ -5,6 +5,13 @@ import type {
|
|
|
5
5
|
} from "../collab/types";
|
|
6
6
|
|
|
7
7
|
const REJOIN_DELAYS_MS = [5_000, 15_000, 30_000, 60_000];
|
|
8
|
+
// Circuit breaker: after this many consecutive failed rejoins (~3.5 min of
|
|
9
|
+
// backoff), stop the auto-rejoin loop and stay in solo mode rather than retry
|
|
10
|
+
// forever — an unbounded reconnect loop is itself a way to hammer the backend.
|
|
11
|
+
// Refocusing the tab (visibilitychange → visible) resets the breaker and
|
|
12
|
+
// retries, so a user returning to a long-idle/offline tab reconnects without a
|
|
13
|
+
// manual reload.
|
|
14
|
+
const MAX_REJOIN_ATTEMPTS = 6;
|
|
8
15
|
|
|
9
16
|
export interface UseCollabSessionArgs {
|
|
10
17
|
provider: CollabProvider | null;
|
|
@@ -129,14 +136,34 @@ export function useCollabSession(args: UseCollabSessionArgs): UseCollabSessionRe
|
|
|
129
136
|
// race two concurrent joins into two sessions, and the orphaned timer
|
|
130
137
|
// would survive unmount (the cleanup only clears the tracked one).
|
|
131
138
|
if (timer) clearTimeout(timer);
|
|
139
|
+
// Circuit breaker: give up after a capped number of consecutive failures
|
|
140
|
+
// rather than rejoin forever. onVisible() below rearms it on refocus.
|
|
141
|
+
if (attempt >= MAX_REJOIN_ATTEMPTS) {
|
|
142
|
+
console.warn(`Collab: stopped auto-rejoining after ${attempt} consecutive failures; staying solo until the tab is refocused.`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
132
145
|
const delay = REJOIN_DELAYS_MS[Math.min(attempt, REJOIN_DELAYS_MS.length - 1)];
|
|
133
146
|
attempt += 1;
|
|
134
147
|
timer = setTimeout(() => { timer = null; void join(); }, delay);
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
// Rearm the breaker (and reconnect immediately) when the user tabs back to
|
|
151
|
+
// a disconnected editor — the common recovery from a laptop-sleep / network
|
|
152
|
+
// blip that exhausted the rejoin budget. No-op while already live/connecting.
|
|
153
|
+
function onVisible() {
|
|
154
|
+
if (cancelled) return;
|
|
155
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
|
|
156
|
+
if (sessionRef.current || joining) return;
|
|
157
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
158
|
+
attempt = 0;
|
|
159
|
+
void join();
|
|
160
|
+
}
|
|
161
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
|
|
162
|
+
|
|
137
163
|
void join();
|
|
138
164
|
return () => {
|
|
139
165
|
cancelled = true;
|
|
166
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
|
|
140
167
|
if (timer) clearTimeout(timer);
|
|
141
168
|
void sessionRef.current?.disconnect().catch(() => {});
|
|
142
169
|
sessionRef.current = null;
|
package/src/lib/registry.ts
CHANGED
|
@@ -176,6 +176,12 @@ export interface SectionDefinition<T = unknown> {
|
|
|
176
176
|
* top-level `description`. Presentation/metadata only — not validated.
|
|
177
177
|
*/
|
|
178
178
|
authoringNote?: string;
|
|
179
|
+
/**
|
|
180
|
+
* When true, this section must run client-side on the viewer page: the viewer
|
|
181
|
+
* renderer hydrates it as an Astro island (`client:visible`) instead of
|
|
182
|
+
* emitting static HTML. Opt-in per type; default (absent/false) stays zero-JS.
|
|
183
|
+
*/
|
|
184
|
+
interactive?: boolean;
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
// --- defineSection ---
|
|
@@ -204,6 +210,7 @@ type DefineSectionInput<S extends ZodType> = {
|
|
|
204
210
|
settingsTabs?: { label: string; fields: string[] }[];
|
|
205
211
|
presets?: SectionPreset[];
|
|
206
212
|
authoringNote?: string;
|
|
213
|
+
interactive?: boolean;
|
|
207
214
|
};
|
|
208
215
|
|
|
209
216
|
export function defineSection<S extends ZodType>(
|