@polderlabs/bizar 4.4.7 → 4.4.8
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/bizar-dash/dist/assets/main-CEazNxxy.js +347 -0
- package/bizar-dash/dist/assets/main-CEazNxxy.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-CQTXbuHq.js → mobile-DSb-t42Y.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-CQTXbuHq.js.map → mobile-DSb-t42Y.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-CaZ0gEeJ.js → mobile-Dl1q7Cyq.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-CaZ0gEeJ.js.map → mobile-Dl1q7Cyq.js.map} +1 -1
- package/bizar-dash/dist/index.html +2 -2
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +13 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +240 -9
- package/config/skills/glyph/SKILL.md +126 -25
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-C4Dq4wTz.js +0 -347
- package/bizar-dash/dist/assets/main-C4Dq4wTz.js.map +0 -1
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
}
|
|
30
30
|
})();
|
|
31
31
|
</script>
|
|
32
|
-
<script type="module" crossorigin src="/assets/main-
|
|
33
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-
|
|
32
|
+
<script type="module" crossorigin src="/assets/main-CEazNxxy.js"></script>
|
|
33
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-Dl1q7Cyq.js">
|
|
34
34
|
<link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
|
|
35
35
|
<link rel="stylesheet" crossorigin href="/assets/main-Nq8Dq3VR.css">
|
|
36
36
|
</head>
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
13
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
14
14
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
15
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-
|
|
15
|
+
<script type="module" crossorigin src="/assets/mobile-DSb-t42Y.js"></script>
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-Dl1q7Cyq.js">
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
@@ -199,6 +199,19 @@ function parseJsxValue(src) {
|
|
|
199
199
|
}
|
|
200
200
|
while (i < len) {
|
|
201
201
|
skipWs();
|
|
202
|
+
// v4.4.8 — After consuming a comma + whitespace, check for the
|
|
203
|
+
// closer before recursing. Without this, an array like `[a, b, ]`
|
|
204
|
+
// would call parseValue on `]`, which falls through to the
|
|
205
|
+
// bareword-identifier branch and returns `{__ident: ''}` as a
|
|
206
|
+
// phantom element. The trailing-comma-then-closer case was the
|
|
207
|
+
// user-visible bug in the v3-to-v4-consolidation glyph: the
|
|
208
|
+
// FileTree's last entry was the bareword phantom, which then
|
|
209
|
+
// crashed the React renderer (components.tsx:609 tried to read
|
|
210
|
+
// `t.bg` on undefined).
|
|
211
|
+
if (arr && src[i] === closer) {
|
|
212
|
+
i += 1;
|
|
213
|
+
return items;
|
|
214
|
+
}
|
|
202
215
|
if (arr) {
|
|
203
216
|
items.push(parseValue());
|
|
204
217
|
} else {
|
|
@@ -49,6 +49,142 @@ import {
|
|
|
49
49
|
Mockup,
|
|
50
50
|
} from './components';
|
|
51
51
|
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// BlockErrorBoundary — v4.4.8
|
|
54
|
+
//
|
|
55
|
+
// A single broken block (e.g. a FileTree whose `data.entries` ended up
|
|
56
|
+
// not being an array because the parser silently produced a phantom
|
|
57
|
+
// bareword entry) used to throw inside React, which propagated up and
|
|
58
|
+
// blanked the whole canvas. The boundary catches throws per-block and
|
|
59
|
+
// renders an inline error card instead. The boundary also exposes its
|
|
60
|
+
// errors via a callback so we can surface them in the top-of-page
|
|
61
|
+
// "render errors" banner.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
interface BlockErrorBoundaryProps {
|
|
65
|
+
blockId: string;
|
|
66
|
+
blockType: string;
|
|
67
|
+
children: React.ReactNode;
|
|
68
|
+
onError: (blockId: string, blockType: string, err: Error, info: React.ErrorInfo) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface BlockErrorBoundaryState {
|
|
72
|
+
err: Error | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class BlockErrorBoundary extends React.Component<BlockErrorBoundaryProps, BlockErrorBoundaryState> {
|
|
76
|
+
state: BlockErrorBoundaryState = { err: null };
|
|
77
|
+
static getDerivedStateFromError(err: Error): BlockErrorBoundaryState {
|
|
78
|
+
return { err };
|
|
79
|
+
}
|
|
80
|
+
componentDidCatch(err: Error, info: React.ErrorInfo): void {
|
|
81
|
+
try {
|
|
82
|
+
this.props.onError(this.props.blockId, this.props.blockType, err, info);
|
|
83
|
+
} catch {
|
|
84
|
+
/* ignore — the boundary itself must not throw */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
render(): React.ReactNode {
|
|
88
|
+
if (this.state.err) {
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className="glyph-block-error"
|
|
92
|
+
role="alert"
|
|
93
|
+
style={{
|
|
94
|
+
border: '1px solid var(--error, #f85149)',
|
|
95
|
+
background: 'rgba(248, 81, 73, 0.06)',
|
|
96
|
+
borderRadius: 8,
|
|
97
|
+
padding: '12px 14px',
|
|
98
|
+
margin: '12px 0',
|
|
99
|
+
color: 'var(--text)',
|
|
100
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
101
|
+
fontSize: 12,
|
|
102
|
+
lineHeight: 1.55,
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<strong style={{ color: 'var(--error, #f85149)', fontFamily: 'var(--font-sans, system-ui, sans-serif)', display: 'block', marginBottom: 4 }}>
|
|
106
|
+
{this.props.blockType} block crashed
|
|
107
|
+
</strong>
|
|
108
|
+
<div style={{ color: 'var(--text-muted)', marginBottom: 6 }}>
|
|
109
|
+
block id: <code>{this.props.blockId}</code>
|
|
110
|
+
</div>
|
|
111
|
+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--text)' }}>
|
|
112
|
+
{this.state.err.message}
|
|
113
|
+
</pre>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return this.props.children;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Block validation — v4.4.8
|
|
123
|
+
//
|
|
124
|
+
// Before handing block data to the component switch, verify that the
|
|
125
|
+
// shape matches the prop contract. Returns `{ ok: true }` when the
|
|
126
|
+
// block can be rendered, or `{ ok: false, error }` when a field is
|
|
127
|
+
// missing or wrong type. The renderer uses the error to skip the
|
|
128
|
+
// component and surface an inline message instead of throwing.
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
function validateBlock(b: Block): { ok: true } | { ok: false; error: string } {
|
|
132
|
+
const data = (b.data ?? {}) as Record<string, unknown>;
|
|
133
|
+
switch (b.type) {
|
|
134
|
+
case 'RichText':
|
|
135
|
+
return { ok: true };
|
|
136
|
+
case 'Callout':
|
|
137
|
+
return { ok: true };
|
|
138
|
+
case 'Checklist':
|
|
139
|
+
if (!Array.isArray(data.items)) return { ok: false, error: 'Checklist requires data.items to be an array' };
|
|
140
|
+
return { ok: true };
|
|
141
|
+
case 'Table':
|
|
142
|
+
if (!Array.isArray(data.columns)) return { ok: false, error: 'Table requires data.columns to be an array' };
|
|
143
|
+
if (!Array.isArray(data.rows)) return { ok: false, error: 'Table requires data.rows to be an array' };
|
|
144
|
+
if (data.rows.length > 0 && !Array.isArray(data.rows[0])) return { ok: false, error: 'Table.data.rows[0] must be an array (cell array)' };
|
|
145
|
+
return { ok: true };
|
|
146
|
+
case 'CodeTabs':
|
|
147
|
+
if (!Array.isArray(data.tabs)) return { ok: false, error: 'CodeTabs requires data.tabs to be an array' };
|
|
148
|
+
return { ok: true };
|
|
149
|
+
case 'Decision':
|
|
150
|
+
if (!Array.isArray(data.options)) return { ok: false, error: 'Decision requires data.options to be an array' };
|
|
151
|
+
return { ok: true };
|
|
152
|
+
case 'OpenQuestions':
|
|
153
|
+
if (!Array.isArray(data.questions)) return { ok: false, error: 'OpenQuestions requires data.questions to be an array' };
|
|
154
|
+
return { ok: true };
|
|
155
|
+
case 'FileTree':
|
|
156
|
+
if (!Array.isArray(data.entries)) return { ok: false, error: 'FileTree requires data.entries to be an array' };
|
|
157
|
+
// v4.4.8 — also catch the phantom bareword entry the parser used
|
|
158
|
+
// to emit (the v3-to-v4-consolidation glyph crash). An entry that
|
|
159
|
+
// is not an object means the parser miscounted braces.
|
|
160
|
+
for (const e of data.entries) {
|
|
161
|
+
if (e === null || typeof e !== 'object' || Array.isArray(e)) {
|
|
162
|
+
return { ok: false, error: `FileTree contains a malformed entry: ${JSON.stringify(e)}` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { ok: true };
|
|
166
|
+
case 'Diff':
|
|
167
|
+
if (typeof data.before !== 'string') return { ok: false, error: 'Diff requires data.before to be a string' };
|
|
168
|
+
if (typeof data.after !== 'string') return { ok: false, error: 'Diff requires data.after to be a string' };
|
|
169
|
+
return { ok: true };
|
|
170
|
+
case 'Stat':
|
|
171
|
+
if (data.label === undefined || data.label === null) return { ok: false, error: 'Stat requires data.label' };
|
|
172
|
+
if (data.value === undefined || data.value === null) return { ok: false, error: 'Stat requires data.value' };
|
|
173
|
+
return { ok: true };
|
|
174
|
+
case 'Workflow':
|
|
175
|
+
if (!Array.isArray(data.steps)) return { ok: false, error: 'Workflow requires data.steps to be an array' };
|
|
176
|
+
return { ok: true };
|
|
177
|
+
case 'Mockup':
|
|
178
|
+
if (typeof data.html !== 'string') return { ok: false, error: 'Mockup requires data.html to be a string' };
|
|
179
|
+
return { ok: true };
|
|
180
|
+
case 'Diagram':
|
|
181
|
+
if (typeof data.dataHtml !== 'string') return { ok: false, error: 'Diagram requires data.dataHtml to be a string' };
|
|
182
|
+
return { ok: true };
|
|
183
|
+
default:
|
|
184
|
+
return { ok: false, error: `Unknown block type: ${(b as { type: string }).type}` };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
52
188
|
// ---------------------------------------------------------------------------
|
|
53
189
|
// Types — match the server compiler output
|
|
54
190
|
// ---------------------------------------------------------------------------
|
|
@@ -72,7 +208,7 @@ interface CompiledGlyph {
|
|
|
72
208
|
slug: string;
|
|
73
209
|
frontmatter: Record<string, unknown>;
|
|
74
210
|
blocks: Block[];
|
|
75
|
-
errors: string
|
|
211
|
+
errors: Array<{ line?: number; message: string }>;
|
|
76
212
|
compiledAt: string;
|
|
77
213
|
}
|
|
78
214
|
|
|
@@ -162,6 +298,25 @@ export function GlyphRenderer({ slug, onClose, onCommentAdded }: Props) {
|
|
|
162
298
|
// Active pin (expanded thread view)
|
|
163
299
|
const [activePin, setActivePin] = useState<string | null>(null);
|
|
164
300
|
|
|
301
|
+
// v4.4.8 — Per-block render errors caught by BlockErrorBoundary. The
|
|
302
|
+
// boundary calls onError(err) which pushes into this array. We render
|
|
303
|
+
// the array at the top of the canvas in a prominent red banner so the
|
|
304
|
+
// user isn't left staring at a blank screen wondering what happened.
|
|
305
|
+
const [blockErrors, setBlockErrors] = useState<
|
|
306
|
+
Array<{ blockId: string; blockType: string; message: string }>
|
|
307
|
+
>([]);
|
|
308
|
+
const reportBlockError = React.useCallback(
|
|
309
|
+
(blockId: string, blockType: string, err: Error) => {
|
|
310
|
+
setBlockErrors((prev) => {
|
|
311
|
+
// dedupe by blockId so the same block crashing twice doesn't
|
|
312
|
+
// duplicate the entry
|
|
313
|
+
if (prev.some((e) => e.blockId === blockId)) return prev;
|
|
314
|
+
return [...prev, { blockId, blockType, message: err.message || String(err) }];
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
[],
|
|
318
|
+
);
|
|
319
|
+
|
|
165
320
|
const canvasRef = useRef<HTMLDivElement>(null);
|
|
166
321
|
|
|
167
322
|
// Section grouping — memoized so we don't re-walk the block array on every render
|
|
@@ -289,10 +444,58 @@ export function GlyphRenderer({ slug, onClose, onCommentAdded }: Props) {
|
|
|
289
444
|
setFullscreen((v) => !v);
|
|
290
445
|
}
|
|
291
446
|
|
|
292
|
-
// Render a single block
|
|
447
|
+
// Render a single block — wrapped in a BlockErrorBoundary so a single
|
|
448
|
+
// broken block (e.g. wrong data shape from a parser bug) shows an inline
|
|
449
|
+
// error card instead of blanking the whole canvas.
|
|
293
450
|
const renderBlock = (b: Block) => {
|
|
294
451
|
const id = b.id;
|
|
295
452
|
const data = (b.data ?? {}) as Record<string, unknown>;
|
|
453
|
+
// Validate data shape BEFORE handing to the component switch. A
|
|
454
|
+
// validation failure shows an inline error card immediately, no
|
|
455
|
+
// need for the boundary to catch it.
|
|
456
|
+
const validation = validateBlock(b);
|
|
457
|
+
if (!validation.ok) {
|
|
458
|
+
return (
|
|
459
|
+
<div
|
|
460
|
+
key={id}
|
|
461
|
+
id={id}
|
|
462
|
+
className="glyph-block-error"
|
|
463
|
+
role="alert"
|
|
464
|
+
style={{
|
|
465
|
+
border: '1px solid var(--error, #f85149)',
|
|
466
|
+
background: 'rgba(248, 81, 73, 0.06)',
|
|
467
|
+
borderRadius: 8,
|
|
468
|
+
padding: '12px 14px',
|
|
469
|
+
margin: '12px 0',
|
|
470
|
+
color: 'var(--text)',
|
|
471
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
472
|
+
fontSize: 12,
|
|
473
|
+
lineHeight: 1.55,
|
|
474
|
+
}}
|
|
475
|
+
>
|
|
476
|
+
<strong style={{ color: 'var(--error, #f85149)', fontFamily: 'var(--font-sans, system-ui, sans-serif)', display: 'block', marginBottom: 4 }}>
|
|
477
|
+
{b.type} block invalid
|
|
478
|
+
</strong>
|
|
479
|
+
<div style={{ color: 'var(--text-muted)', marginBottom: 6 }}>
|
|
480
|
+
block id: <code>{id}</code>
|
|
481
|
+
</div>
|
|
482
|
+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{validation.error}</pre>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
// Wrap the actual render in an error boundary so even runtime
|
|
487
|
+
// errors (bad component props, undefined data, etc.) don't crash
|
|
488
|
+
// the whole canvas.
|
|
489
|
+
return (
|
|
490
|
+
<BlockErrorBoundary key={id} blockId={id} blockType={b.type} onError={reportBlockError}>
|
|
491
|
+
{renderBlockInner(b, id, data)}
|
|
492
|
+
</BlockErrorBoundary>
|
|
493
|
+
);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// Inner renderer — split out so the outer renderBlock can validate +
|
|
497
|
+
// wrap in an error boundary without nesting the switch in the JSX tree.
|
|
498
|
+
const renderBlockInner = (b: Block, id: string, data: Record<string, unknown>) => {
|
|
296
499
|
switch (b.type) {
|
|
297
500
|
case 'RichText':
|
|
298
501
|
return <RichText key={id} id={id}>{b.childrenMarkdown ?? ''}</RichText>;
|
|
@@ -506,13 +709,41 @@ export function GlyphRenderer({ slug, onClose, onCommentAdded }: Props) {
|
|
|
506
709
|
</div>
|
|
507
710
|
</header>
|
|
508
711
|
|
|
509
|
-
{/*
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
712
|
+
{/* v4.4.8 — Render errors banner. Two layers:
|
|
713
|
+
1. Compiler warnings from the MDX parser (e.g. unknown
|
|
714
|
+
tag, unclosed block). Lightweight, yellow border.
|
|
715
|
+
2. Block render errors caught by BlockErrorBoundary. These
|
|
716
|
+
are the "one block crashed but the rest still rendered"
|
|
717
|
+
cases. Red border, prominent, listed first so the user
|
|
718
|
+
sees them before scrolling. */}
|
|
719
|
+
{(blockErrors.length > 0 || compiled.errors?.length > 0) && (
|
|
720
|
+
<div
|
|
721
|
+
className="glyph-render-errors"
|
|
722
|
+
role="alert"
|
|
723
|
+
style={{
|
|
724
|
+
border: '1px solid var(--error, #f85149)',
|
|
725
|
+
background: 'rgba(248, 81, 73, 0.08)',
|
|
726
|
+
borderRadius: 8,
|
|
727
|
+
padding: '12px 16px',
|
|
728
|
+
margin: '0 0 16px 0',
|
|
729
|
+
color: 'var(--text)',
|
|
730
|
+
}}
|
|
731
|
+
>
|
|
732
|
+
<strong style={{ color: 'var(--error, #f85149)', display: 'block', marginBottom: 8 }}>
|
|
733
|
+
{blockErrors.length > 0
|
|
734
|
+
? `${blockErrors.length} block${blockErrors.length === 1 ? '' : 's'} failed to render`
|
|
735
|
+
: 'Compiler warnings'}
|
|
736
|
+
</strong>
|
|
737
|
+
<ul style={{ margin: 0, paddingLeft: 20, fontFamily: 'var(--font-mono, ui-monospace, monospace)', fontSize: 12, lineHeight: 1.6 }}>
|
|
738
|
+
{blockErrors.map((e, i) => (
|
|
739
|
+
<li key={`be-${i}`}>
|
|
740
|
+
<strong>{e.blockType}</strong> (<code>{e.blockId}</code>): {e.message}
|
|
741
|
+
</li>
|
|
742
|
+
))}
|
|
743
|
+
{compiled.errors?.map((e, i) => (
|
|
744
|
+
<li key={`ce-${i}`}>
|
|
745
|
+
{e.line ? `line ${e.line}: ` : ''}{e.message}
|
|
746
|
+
</li>
|
|
516
747
|
))}
|
|
517
748
|
</ul>
|
|
518
749
|
</div>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: glyph
|
|
3
3
|
description: Create and consume Bizar glyphs — visual plan/recap artifacts in `artifacts/<slug>/`. Glyphs are MDX files with frontmatter and a block vocabulary. Use for design proposals, decision recaps, and postmortems.
|
|
4
|
-
version:
|
|
4
|
+
version: 2
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Glyphs — Visual Plan Artifacts
|
|
8
8
|
|
|
9
|
-
A **glyph** is a visual artifact used for plans, recaps,
|
|
9
|
+
A **glyph** is a visual artifact used for plans, recaps, design proposals, and postmortems. It's an MDX file at `artifacts/<slug>/artifact.mdx` (or `~/.config/opencode/artifacts/<slug>/` for global glyphs) that the dashboard renders with comment-pin overlay and right-click context menu.
|
|
10
10
|
|
|
11
11
|
## When to create a glyph
|
|
12
12
|
|
|
@@ -22,12 +22,16 @@ If it's a one-line fix, don't make a glyph. If it's a 5-minute edit, don't make
|
|
|
22
22
|
|
|
23
23
|
```
|
|
24
24
|
artifacts/<slug>/
|
|
25
|
-
├── meta.json # title, slug, status, author, created
|
|
25
|
+
├── meta.json # title, slug, status, author, created, lastEdited
|
|
26
26
|
├── artifact.mdx # ← source of truth (git-tracked, diff-friendly)
|
|
27
|
-
└── comments.json # ← free-placed pins (mutable)
|
|
27
|
+
└── comments.json # ← free-placed pins (mutable, gitignored usually)
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Two locations are scanned:
|
|
31
|
+
1. **Per-project:** `<projectRoot>/.bizar/artifacts/<slug>/`
|
|
32
|
+
2. **Global:** `~/.config/opencode/artifacts/<slug>/`
|
|
33
|
+
|
|
34
|
+
The dashboard uses whichever exists. The project's `artifacts/<slug>/` is preferred when running inside a worktree.
|
|
31
35
|
|
|
32
36
|
## Frontmatter
|
|
33
37
|
|
|
@@ -40,6 +44,8 @@ kind: plan | postmortem | recap | design
|
|
|
40
44
|
---
|
|
41
45
|
```
|
|
42
46
|
|
|
47
|
+
The frontmatter is parsed once and exposed to the React renderer. Unknown keys are passed through. Status controls the badge color + whether the glyph is read-only.
|
|
48
|
+
|
|
43
49
|
## Block vocabulary
|
|
44
50
|
|
|
45
51
|
```mdx
|
|
@@ -119,31 +125,109 @@ kind: plan | postmortem | recap | design
|
|
|
119
125
|
/>
|
|
120
126
|
```
|
|
121
127
|
|
|
128
|
+
## What data shape each block expects
|
|
129
|
+
|
|
130
|
+
The dashboard validates every block's `data` shape BEFORE rendering. If a field is missing or the wrong type, the block renders an inline error card instead of crashing the whole canvas (v4.4.8+). The same thing happens if a block throws during render — a per-block `ErrorBoundary` catches it.
|
|
131
|
+
|
|
132
|
+
| Block | Required `data` fields |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `RichText` | none (children are markdown) |
|
|
135
|
+
| `Callout` | none; optional `tone: 'info' \| 'warn' \| 'success' \| 'danger'` (default: info) |
|
|
136
|
+
| `Stat` | `label: string`, `value: string \| number`; optional `trend`, `hint` |
|
|
137
|
+
| `Checklist` | `items: Array<{ id, label, checked }>` |
|
|
138
|
+
| `Table` | `columns: string[]`, `rows: string[][]` (each row is a cell array) |
|
|
139
|
+
| `CodeTabs` | `tabs: Array<{ id, label, language, code, caption? }>` |
|
|
140
|
+
| `Decision` | `options: Array<{ id, label, detail, recommended? }>`; optional `title`, `question` |
|
|
141
|
+
| `OpenQuestions` | `questions: Array<{ id, label, kind, options? }>` |
|
|
142
|
+
| `FileTree` | `entries: Array<{ path, change, note? }>` where `change ∈ {added, modified, removed, renamed}` |
|
|
143
|
+
| `Diff` | `before: string`, `after: string`; optional `filename`, `language`, `mode` |
|
|
144
|
+
| `Workflow` | `steps: Array<{ id, label, type }>`; optional `connections` |
|
|
145
|
+
| `Mockup` | `html: string`; optional `title`, `x`, `y`, `w`, `h` |
|
|
146
|
+
| `Diagram` | `dataHtml: string`; optional `title`, `dataCss` |
|
|
147
|
+
|
|
122
148
|
## Common pitfalls
|
|
123
149
|
|
|
124
|
-
|
|
150
|
+
These are the bugs we've actually hit. Read this section before writing a glyph.
|
|
125
151
|
|
|
126
|
-
|
|
152
|
+
### 1. Backticks inside attribute strings break the parser
|
|
153
|
+
|
|
154
|
+
The MDX parser scans for `` ` `` to delimit template literals in JSX. A literal backtick inside an attribute value — even inside a double-quoted string — will close the surrounding context and produce a parse error.
|
|
127
155
|
|
|
128
156
|
```mdx
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
157
|
+
<!-- WRONG: \` inside a string value kills the parser -->
|
|
158
|
+
<RichText id="x">Run \`bizar install\` to bootstrap.</RichText>
|
|
159
|
+
|
|
160
|
+
<!-- RIGHT: use straight quotes or rephrase -->
|
|
161
|
+
<RichText id="x">Run 'bizar install' to bootstrap.</RichText>
|
|
162
|
+
<RichText id="x">Run the installer to bootstrap.</RichText>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The parser's JSX-attribute walker does NOT track string literals when counting braces — so any `{` or `}` inside a string value also throws the parser off. Quote all strings that contain braces.
|
|
166
|
+
|
|
167
|
+
### 2. The parser counts braces naively (does NOT track strings)
|
|
168
|
+
|
|
169
|
+
The `parseJsxValue` function uses a brace-depth counter that doesn't know about string literals. If you have something like:
|
|
170
|
+
|
|
171
|
+
```mdx
|
|
172
|
+
<FileTree entries={[{ path: "x", change: "added", note: "this {weird} note" }]} />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
…the inner `{` and `}` inside the string still increment the brace depth. With deeply-nested or brace-laden strings the walker terminates at the wrong closing brace, leaving a truncated value for `parseJsxValue`.
|
|
176
|
+
|
|
177
|
+
**Rule of thumb:** keep string values brace-free. If you must use `{...}` in a note, escape or rephrase.
|
|
178
|
+
|
|
179
|
+
### 3. Trailing commas in arrays used to be a phantom-element bug
|
|
180
|
+
|
|
181
|
+
Before v4.4.8, an array ending with a trailing comma — e.g. `[a, b,]` — caused the parser to call `parseValue` on the closing `]`, which fell through to the bareword-identifier fallback and returned `{__ident: ''}` as a phantom element. The FileTree then had one extra broken entry that crashed the React renderer (`t.bg` on undefined).
|
|
182
|
+
|
|
183
|
+
v4.4.8 fixed this in the parser. But: don't rely on trailing commas, even where supported — keep the array clean.
|
|
184
|
+
|
|
185
|
+
### 4. `FileTree` `change` field must be exact
|
|
186
|
+
|
|
187
|
+
`change` must be exactly one of: `added`, `modified`, `removed`, `renamed`. Anything else (including the empty string or `null`) makes `FILE_CHANGE[e.change]` return `undefined` and the renderer crashes with "can't access property 'bg' of undefined".
|
|
188
|
+
|
|
189
|
+
```mdx
|
|
190
|
+
<!-- WRONG -->
|
|
191
|
+
{ path: "src/foo.ts", change: null, note: "..." }
|
|
192
|
+
{ path: "src/foo.ts", change: "", note: "..." }
|
|
193
|
+
|
|
194
|
+
<!-- RIGHT -->
|
|
195
|
+
{ path: "src/foo.ts", change: "modified", note: "..." }
|
|
196
|
+
{ path: "src/foo.ts", change: "added", note: "..." }
|
|
134
197
|
```
|
|
135
198
|
|
|
136
|
-
|
|
199
|
+
If a file's status is genuinely unclear, use `modified` with a `note` that explains why.
|
|
137
200
|
|
|
138
|
-
|
|
201
|
+
### 5. MDX heading with `<N`
|
|
139
202
|
|
|
140
|
-
|
|
203
|
+
A heading like `## Mobile (under 768px)` is fine. But `## Mobile <768px` breaks the MDX validator because `<7` looks like a malformed JSX tag. Always escape or rephrase:
|
|
141
204
|
|
|
142
|
-
|
|
205
|
+
```mdx
|
|
206
|
+
## Mobile (under 768px) <!-- fine -->
|
|
207
|
+
## Mobile: under 768px <!-- fine -->
|
|
208
|
+
## Mobile < 768px <!-- fine but ugly -->
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 6. Block IDs must be unique within a glyph
|
|
212
|
+
|
|
213
|
+
If you copy a `<RichText>` block, change its `id`. The compiler may warn but won't fail. Two blocks with the same id will collide on the comment-pin overlay.
|
|
143
214
|
|
|
144
|
-
### Children vs self-closing
|
|
215
|
+
### 7. Children vs self-closing
|
|
145
216
|
|
|
146
|
-
Blocks with content (RichText
|
|
217
|
+
Blocks with content (`RichText`, `Callout`) use open/close tags with children. Blocks with only data (`Stat`, `Table`, `FileTree`, `Decision`, etc.) are self-closing — no children, no closing tag.
|
|
218
|
+
|
|
219
|
+
```mdx
|
|
220
|
+
<!-- self-closing -->
|
|
221
|
+
<Stat id="x" label="..." value="..." />
|
|
222
|
+
|
|
223
|
+
<!-- with children -->
|
|
224
|
+
<RichText id="x">markdown content here</RichText>
|
|
225
|
+
<Callout id="x" tone="warn">important message</Callout>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 8. Em-dashes and special characters
|
|
229
|
+
|
|
230
|
+
The parser's JSON-like value reader handles `\\`, `\"`, `\'`, `\n`, `\t`, `\r` inside string literals. Em-dashes (`—`), curly quotes (`""`), and other Unicode characters are fine. Just avoid `\``.
|
|
147
231
|
|
|
148
232
|
## meta.json example
|
|
149
233
|
|
|
@@ -158,17 +242,34 @@ Blocks with content (RichText, Callout) use open/close tags with children. Block
|
|
|
158
242
|
}
|
|
159
243
|
```
|
|
160
244
|
|
|
245
|
+
`lastEdited` is updated whenever the dashboard re-saves the glyph via the toolbar.
|
|
246
|
+
|
|
247
|
+
## Validation workflow
|
|
248
|
+
|
|
249
|
+
Before committing a glyph:
|
|
250
|
+
|
|
251
|
+
1. Run the compiler against your MDX:
|
|
252
|
+
```bash
|
|
253
|
+
node -e "import('./bizar-dash/src/server/glyphs/mdx-compiler.mjs').then(m => { const fs = require('node:fs'); const src = fs.readFileSync('artifacts/<slug>/artifact.mdx', 'utf8'); const out = m.compileGlyphMdxSync(src); console.log('blocks:', out.blocks.length, 'errors:', out.errors); out.errors.forEach(e => console.error(' line', e.line, ':', e.message)); })"
|
|
254
|
+
```
|
|
255
|
+
This catches parser bugs (missing tags, malformed attrs, etc.) before they hit the dashboard.
|
|
256
|
+
|
|
257
|
+
2. Visually inspect the rendered output by hitting `GET /api/artifacts/<slug>/render`. The response is the compiled JSON the React renderer consumes. Look for blocks with missing data fields — those will show up as `data: {}` or `data: []`.
|
|
258
|
+
|
|
259
|
+
3. Open the glyph in the dashboard. v4.4.8+ shows a red error banner at the top of the canvas if any block failed to render. The banner lists every block error AND every compiler warning.
|
|
260
|
+
|
|
161
261
|
## How to view in the dashboard
|
|
162
262
|
|
|
163
263
|
Glyphs are rendered at `/artifacts/<slug>`. The dashboard shows them with:
|
|
164
|
-
- Section grouping
|
|
165
|
-
- Comment pin overlay
|
|
166
|
-
- Floating toolbar
|
|
167
|
-
-
|
|
264
|
+
- **Section grouping** — by block id prefix (e.g. all blocks with ids starting `overview_` go into the "Overview" section)
|
|
265
|
+
- **Comment pin overlay** — right-click anywhere to add a comment
|
|
266
|
+
- **Floating toolbar** — Send to agent, share, fullscreen
|
|
267
|
+
- **Error banner** — v4.4.8+: red banner at the top with block render errors and compiler warnings. The page is no longer blank when something fails — you'll see exactly which block crashed and why.
|
|
268
|
+
- **Status badge** — `draft | review | shipped | archived`. `shipped` locks editing.
|
|
168
269
|
|
|
169
270
|
## See also
|
|
170
271
|
|
|
171
272
|
- `bizar-dash/src/web/views/glyphs/components.tsx` — block component implementations
|
|
172
|
-
- `bizar-dash/src/server/glyphs/mdx-compiler.mjs` — MDX parser
|
|
173
|
-
- `
|
|
174
|
-
-
|
|
273
|
+
- `bizar-dash/src/server/glyphs/mdx-compiler.mjs` — MDX parser (custom, no @mdx-js dependency at runtime)
|
|
274
|
+
- `bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx` — React renderer (v4.4.8+ has the error banner + ErrorBoundary)
|
|
275
|
+
- `artifacts/sample-plan-login/` — a complete worked example
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.8",
|
|
4
4
|
"description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, opencode plugin, and typed SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|