@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.
Files changed (27) hide show
  1. package/dist/{chunk-VY5V4HBV.js → chunk-H2FENAQH.js} +1 -1
  2. package/dist/{chunk-6VOIZP2O.js → chunk-SK6GGTTO.js} +217 -204
  3. package/dist/components/brandguide/Colors.d.ts.map +1 -1
  4. package/dist/components/brandguide/ColorsEditable.d.ts +20 -0
  5. package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -0
  6. package/dist/components/brandguide/ColorsView.d.ts +9 -0
  7. package/dist/components/brandguide/ColorsView.d.ts.map +1 -0
  8. package/dist/components/brandguide/colors-shared.d.ts +4 -0
  9. package/dist/components/brandguide/colors-shared.d.ts.map +1 -0
  10. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  11. package/dist/components/sections/SectionIsland.d.ts +8 -0
  12. package/dist/components/sections/SectionIsland.d.ts.map +1 -0
  13. package/dist/components/sections/brand-guide.js +1 -1
  14. package/dist/components/sections/register-schemas.js +2 -2
  15. package/dist/components/sections/register.js +2 -2
  16. package/dist/hooks/useCollabSession.d.ts.map +1 -1
  17. package/dist/lib/registry.d.ts +7 -0
  18. package/dist/lib/registry.d.ts.map +1 -1
  19. package/package.json +1 -1
  20. package/src/components/brandguide/Colors.tsx +2 -275
  21. package/src/components/brandguide/ColorsEditable.tsx +155 -0
  22. package/src/components/brandguide/ColorsView.tsx +100 -0
  23. package/src/components/brandguide/colors-shared.ts +17 -0
  24. package/src/components/sections/Colors/index.tsx +1 -0
  25. package/src/components/sections/SectionIsland.tsx +21 -0
  26. package/src/hooks/useCollabSession.ts +27 -0
  27. 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
+ });
@@ -24,6 +24,7 @@ const schema = z.object({
24
24
 
25
25
  export default defineSection({
26
26
  type: "colors",
27
+ interactive: true,
27
28
  category: "brand-guide",
28
29
  label: "Colors",
29
30
  icon: <Palette size={18} />,
@@ -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;
@@ -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>(