@graffiticode/l0000-view 0.2.1 → 0.3.1
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/index.d.ts +19 -3
- package/dist/index.js +104 -82
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,11 +4,15 @@ import { JSX as JSX_2 } from 'react/jsx-runtime';
|
|
|
4
4
|
/**
|
|
5
5
|
* The host's Check button and the score it reveals. Exported for hosts that mount a Form without
|
|
6
6
|
* the View (the MCP widget), so every one of our hosts checks the same way.
|
|
7
|
+
*
|
|
8
|
+
* Drawn as player chrome — its own bordered strip, set apart from the question — because it is
|
|
9
|
+
* the host's control, not part of the interaction. It is a toggle (`aria-pressed`), disabled
|
|
10
|
+
* until the response is complete; a check already showing can always be hidden.
|
|
7
11
|
*/
|
|
8
|
-
export declare const CheckBar: ({ result, checked,
|
|
12
|
+
export declare const CheckBar: ({ result, checked, onToggle, }: {
|
|
9
13
|
result: Score;
|
|
10
14
|
checked: boolean;
|
|
11
|
-
|
|
15
|
+
onToggle: () => void;
|
|
12
16
|
}) => JSX_2.Element;
|
|
13
17
|
|
|
14
18
|
export declare interface CompileError {
|
|
@@ -54,6 +58,11 @@ export declare type LanguageScore = (data: any) => Score | undefined;
|
|
|
54
58
|
export declare interface Score {
|
|
55
59
|
score: number;
|
|
56
60
|
max: number;
|
|
61
|
+
/**
|
|
62
|
+
* Whether the learner has answered everything. `false` keeps Check disabled until they have;
|
|
63
|
+
* absent counts as complete, so a language that does not say never blocks the button.
|
|
64
|
+
*/
|
|
65
|
+
complete?: boolean;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
export declare interface StateAction {
|
|
@@ -61,11 +70,18 @@ export declare interface StateAction {
|
|
|
61
70
|
args?: any;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
export declare const View: ({ Form, reduce, formModel, score, }: {
|
|
73
|
+
export declare const View: ({ Form, reduce, formModel, score, id: idProp, accessToken: accessTokenProp, }: {
|
|
65
74
|
Form: FormComponent;
|
|
66
75
|
reduce?: LanguageReducer;
|
|
67
76
|
formModel?: FormModel;
|
|
68
77
|
score?: LanguageScore;
|
|
78
|
+
/**
|
|
79
|
+
* The item to load. A host that mounts the View as a React component passes it here; the
|
|
80
|
+
* embeddable /form bundle leaves it out and the View reads `?id=` from the page URL instead.
|
|
81
|
+
*/
|
|
82
|
+
id?: string;
|
|
83
|
+
/** Credential for a private item. Falls back to `?access_token=`, like `id`. */
|
|
84
|
+
accessToken?: string;
|
|
69
85
|
}) => JSX_2.Element;
|
|
70
86
|
|
|
71
87
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -79,96 +79,96 @@ var S = (e, t) => {
|
|
|
79
79
|
compileSeq: n.compileSeq + +!!h.has(r.type),
|
|
80
80
|
version: n.version + 1
|
|
81
81
|
};
|
|
82
|
-
}, w = ({ Form: u, reduce: d, formModel: f = "live", score: h }) => {
|
|
83
|
-
let [
|
|
82
|
+
}, w = ({ Form: u, reduce: d, formModel: f = "live", score: h, id: g, accessToken: b }) => {
|
|
83
|
+
let [x] = a(() => new URLSearchParams(window.location.search)), S = g ?? x.get("id") ?? void 0, w = b ?? x.get("access_token") ?? void 0, [E] = a(x.get("origin") ?? void 0), [D, O] = a([]), k = n(() => C(d, f), [d, f]), [M, N] = r(k, void 0, () => ({
|
|
84
84
|
data: {},
|
|
85
85
|
renderData: {},
|
|
86
86
|
compileSeq: 0,
|
|
87
87
|
version: 0
|
|
88
|
-
})), { data:
|
|
88
|
+
})), { data: P, renderData: F, compileSeq: I, version: L } = M;
|
|
89
89
|
t(() => {
|
|
90
|
-
let e =
|
|
91
|
-
e &&
|
|
90
|
+
let e = x.get("data");
|
|
91
|
+
e && N({
|
|
92
92
|
type: "init",
|
|
93
93
|
args: JSON.parse(e)
|
|
94
94
|
});
|
|
95
95
|
}, []), t(() => {
|
|
96
|
-
|
|
96
|
+
E && window.parent.postMessage({
|
|
97
97
|
type: "onload",
|
|
98
|
-
data:
|
|
99
|
-
},
|
|
98
|
+
data: P
|
|
99
|
+
}, E);
|
|
100
100
|
}, []), t(() => {
|
|
101
|
-
|
|
101
|
+
E && window.parent.postMessage({
|
|
102
102
|
type: "data-updated",
|
|
103
|
-
data:
|
|
104
|
-
},
|
|
105
|
-
}, [
|
|
106
|
-
let
|
|
107
|
-
accessToken:
|
|
108
|
-
id:
|
|
103
|
+
data: P
|
|
104
|
+
}, E);
|
|
105
|
+
}, [L]);
|
|
106
|
+
let R = o(S ? {
|
|
107
|
+
accessToken: w,
|
|
108
|
+
id: S
|
|
109
109
|
} : null, m, {
|
|
110
110
|
revalidateOnFocus: !1,
|
|
111
111
|
revalidateOnReconnect: !1,
|
|
112
112
|
revalidateIfStale: !1
|
|
113
113
|
});
|
|
114
114
|
t(() => {
|
|
115
|
-
if (
|
|
116
|
-
let e = _(
|
|
117
|
-
|
|
115
|
+
if (R.data === void 0) return;
|
|
116
|
+
let e = _(R.data);
|
|
117
|
+
N({
|
|
118
118
|
type: "loaded",
|
|
119
119
|
args: e.data
|
|
120
|
-
}),
|
|
121
|
-
}, [
|
|
122
|
-
let
|
|
120
|
+
}), O(e.errors);
|
|
121
|
+
}, [R.data]);
|
|
122
|
+
let z = i(0);
|
|
123
123
|
t(() => {
|
|
124
|
-
if (
|
|
125
|
-
let e = ++
|
|
126
|
-
|
|
124
|
+
if (I === 0 || !S) return;
|
|
125
|
+
let e = ++z.current;
|
|
126
|
+
E && window.parent.postMessage({ focus: {
|
|
127
127
|
type: "update",
|
|
128
|
-
value:
|
|
129
|
-
} },
|
|
128
|
+
value: P
|
|
129
|
+
} }, E);
|
|
130
130
|
let t = !1;
|
|
131
131
|
return (async () => {
|
|
132
132
|
try {
|
|
133
133
|
let n = await p({
|
|
134
|
-
accessToken:
|
|
135
|
-
id:
|
|
136
|
-
data:
|
|
134
|
+
accessToken: w,
|
|
135
|
+
id: S,
|
|
136
|
+
data: P
|
|
137
137
|
});
|
|
138
|
-
if (t || e !==
|
|
138
|
+
if (t || e !== z.current) return;
|
|
139
139
|
let r = _(n);
|
|
140
|
-
|
|
140
|
+
N({
|
|
141
141
|
type: "compiled",
|
|
142
142
|
args: r.data
|
|
143
|
-
}),
|
|
143
|
+
}), O(r.errors);
|
|
144
144
|
} catch (n) {
|
|
145
|
-
!t && e ===
|
|
145
|
+
!t && e === z.current && O([{ message: String(n?.message ?? n) }]);
|
|
146
146
|
}
|
|
147
147
|
})(), () => {
|
|
148
148
|
t = !0;
|
|
149
149
|
};
|
|
150
|
-
}, [
|
|
151
|
-
let
|
|
152
|
-
t(() =>
|
|
153
|
-
let
|
|
150
|
+
}, [I]);
|
|
151
|
+
let B = v(F, []) ? F : P, [V, H] = a(!1);
|
|
152
|
+
t(() => H(!1), [I]);
|
|
153
|
+
let U = n(() => h && D.length === 0 ? h(P) : void 0, [
|
|
154
154
|
h,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
]),
|
|
158
|
-
...
|
|
155
|
+
P,
|
|
156
|
+
D
|
|
157
|
+
]), W = V && U && y(B) ? {
|
|
158
|
+
...B,
|
|
159
159
|
showValidationUI: !0
|
|
160
|
-
} :
|
|
161
|
-
data:
|
|
162
|
-
errors:
|
|
163
|
-
apply:
|
|
164
|
-
}), [
|
|
165
|
-
return v(
|
|
166
|
-
result:
|
|
167
|
-
checked:
|
|
168
|
-
|
|
169
|
-
})] }) : /* @__PURE__ */ c(u, { state:
|
|
160
|
+
} : B, G = n(() => ({
|
|
161
|
+
data: W,
|
|
162
|
+
errors: D,
|
|
163
|
+
apply: N
|
|
164
|
+
}), [W, D]), K = e(() => R.mutate(), [R]);
|
|
165
|
+
return v(P, D) ? U ? /* @__PURE__ */ l(s, { children: [/* @__PURE__ */ c(u, { state: G }), /* @__PURE__ */ c(T, {
|
|
166
|
+
result: U,
|
|
167
|
+
checked: V,
|
|
168
|
+
onToggle: () => H((e) => !e)
|
|
169
|
+
})] }) : /* @__PURE__ */ c(u, { state: G }) : R.error ? /* @__PURE__ */ l("div", {
|
|
170
170
|
role: "alert",
|
|
171
|
-
style:
|
|
171
|
+
style: A,
|
|
172
172
|
children: [
|
|
173
173
|
/* @__PURE__ */ c("p", {
|
|
174
174
|
style: {
|
|
@@ -182,53 +182,75 @@ var S = (e, t) => {
|
|
|
182
182
|
margin: "4px 0 12px",
|
|
183
183
|
color: "#555"
|
|
184
184
|
},
|
|
185
|
-
children: String(
|
|
185
|
+
children: String(R.error?.message ?? R.error)
|
|
186
186
|
}),
|
|
187
187
|
/* @__PURE__ */ c("button", {
|
|
188
188
|
type: "button",
|
|
189
|
-
style:
|
|
190
|
-
onClick:
|
|
189
|
+
style: j,
|
|
190
|
+
onClick: K,
|
|
191
191
|
children: "Retry"
|
|
192
192
|
})
|
|
193
193
|
]
|
|
194
|
-
}) :
|
|
194
|
+
}) : R.isLoading ? /* @__PURE__ */ c("div", {
|
|
195
195
|
role: "status",
|
|
196
196
|
"aria-live": "polite",
|
|
197
|
-
style:
|
|
197
|
+
style: A,
|
|
198
198
|
children: "Loading…"
|
|
199
199
|
}) : /* @__PURE__ */ c("div", {});
|
|
200
|
-
}, T = ({ result: e, checked: t,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
200
|
+
}, T = ({ result: e, checked: t, onToggle: n }) => {
|
|
201
|
+
let r = !t && e.complete === !1;
|
|
202
|
+
return /* @__PURE__ */ l("div", {
|
|
203
|
+
role: "group",
|
|
204
|
+
"aria-label": "Check your answers",
|
|
205
|
+
style: E,
|
|
206
|
+
children: [/* @__PURE__ */ c("button", {
|
|
207
|
+
type: "button",
|
|
208
|
+
"aria-pressed": t,
|
|
209
|
+
style: {
|
|
210
|
+
...D,
|
|
211
|
+
...t ? O : {},
|
|
212
|
+
...r ? k : {}
|
|
213
|
+
},
|
|
214
|
+
onClick: n,
|
|
215
|
+
disabled: r,
|
|
216
|
+
children: "Check"
|
|
217
|
+
}), /* @__PURE__ */ c("span", {
|
|
218
|
+
"aria-live": "polite",
|
|
219
|
+
style: {
|
|
220
|
+
color: "#52525b",
|
|
221
|
+
minWidth: "10em"
|
|
222
|
+
},
|
|
223
|
+
children: t ? `${e.score} of ${e.max} points` : r ? "Answer everything to check." : ""
|
|
224
|
+
})]
|
|
225
|
+
});
|
|
226
|
+
}, E = {
|
|
217
227
|
display: "flex",
|
|
218
228
|
alignItems: "center",
|
|
219
229
|
justifyContent: "center",
|
|
220
230
|
gap: "12px",
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
margin: "12px auto 0",
|
|
232
|
+
maxWidth: "64rem",
|
|
233
|
+
padding: "10px 16px",
|
|
234
|
+
border: "1px solid #d4d4d8",
|
|
235
|
+
borderRadius: "8px",
|
|
236
|
+
background: "#f4f4f5",
|
|
237
|
+
font: "14px/1.4 system-ui, sans-serif",
|
|
238
|
+
boxSizing: "border-box"
|
|
223
239
|
}, D = {
|
|
224
240
|
padding: "8px 16px",
|
|
225
|
-
border: "
|
|
241
|
+
border: "1px solid #18181b",
|
|
226
242
|
borderRadius: "6px",
|
|
227
243
|
background: "#18181b",
|
|
228
244
|
color: "#fff",
|
|
229
245
|
fontWeight: 500,
|
|
230
246
|
cursor: "pointer"
|
|
231
247
|
}, O = {
|
|
248
|
+
background: "#fff",
|
|
249
|
+
color: "#18181b"
|
|
250
|
+
}, k = {
|
|
251
|
+
opacity: .45,
|
|
252
|
+
cursor: "default"
|
|
253
|
+
}, A = {
|
|
232
254
|
display: "flex",
|
|
233
255
|
flexDirection: "column",
|
|
234
256
|
alignItems: "center",
|
|
@@ -237,7 +259,7 @@ var S = (e, t) => {
|
|
|
237
259
|
padding: "24px",
|
|
238
260
|
textAlign: "center",
|
|
239
261
|
font: "14px/1.4 system-ui, sans-serif"
|
|
240
|
-
},
|
|
262
|
+
}, j = {
|
|
241
263
|
padding: "6px 14px",
|
|
242
264
|
border: "1px solid #888",
|
|
243
265
|
borderRadius: "4px",
|
|
@@ -246,7 +268,7 @@ var S = (e, t) => {
|
|
|
246
268
|
};
|
|
247
269
|
//#endregion
|
|
248
270
|
//#region src/components/form/Form.tsx
|
|
249
|
-
function
|
|
271
|
+
function M({ errors: e }) {
|
|
250
272
|
return /* @__PURE__ */ c("div", {
|
|
251
273
|
className: "flex flex-col gap-2 font-mono text-xs",
|
|
252
274
|
children: e.map((e, t) => /* @__PURE__ */ l("div", {
|
|
@@ -263,9 +285,9 @@ function A({ errors: e }) {
|
|
|
263
285
|
}, t))
|
|
264
286
|
});
|
|
265
287
|
}
|
|
266
|
-
var
|
|
288
|
+
var N = ({ state: e }) => {
|
|
267
289
|
let t = e.errors ?? [];
|
|
268
|
-
if (t.length > 0) return /* @__PURE__ */ c(
|
|
290
|
+
if (t.length > 0) return /* @__PURE__ */ c(M, { errors: t });
|
|
269
291
|
let n = e.data;
|
|
270
292
|
return /* @__PURE__ */ c("pre", {
|
|
271
293
|
className: "rounded-md bg-zinc-50 text-zinc-900 text-xs font-mono p-4 overflow-auto",
|
|
@@ -273,6 +295,6 @@ var j = ({ state: e }) => {
|
|
|
273
295
|
});
|
|
274
296
|
};
|
|
275
297
|
//#endregion
|
|
276
|
-
export { T as CheckBar,
|
|
298
|
+
export { T as CheckBar, N as Form, w as View };
|
|
277
299
|
|
|
278
300
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/lib/api.ts","../src/swr/fetchers.ts","../src/view.tsx","../src/components/form/Form.tsx"],"sourcesContent":["// SPDX-License-Identifier: MIT\n// Client for the Graffiticode data/compile API. Uses the platform API host derived from\n// the page's location (local console in dev, api.graffiticode.org in prod).\n\nfunction getApiUrl(): string {\n const host = window.document.location.host;\n return host.indexOf(\"localhost\") === 0\n ? \"http://localhost:3100\"\n : \"https://api.graffiticode.org\";\n}\n\nexport const getApiData = async ({\n accessToken,\n id,\n}: {\n accessToken?: string;\n id: string;\n}) => {\n const resp = await fetch(`${getApiUrl()}/data?id=${id}`, {\n headers: { Authorization: accessToken || \"\" },\n });\n const { status, error, data } = await resp.json();\n if (status !== \"success\") {\n throw new Error(`failed to get data ${id}: ${error?.message}`);\n }\n return data;\n};\n\nexport const postApiCompile = async ({\n accessToken,\n id,\n data,\n}: {\n accessToken?: string;\n id: string;\n data: any;\n}) => {\n const resp = await fetch(`${getApiUrl()}/compile`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n authorization: accessToken || \"\",\n \"x-graffiticode-storage-type\": \"persistent\",\n },\n body: JSON.stringify({ id, data }),\n });\n const out = await resp.json();\n if (out.status !== \"success\") {\n throw new Error(`failed to post compile ${id}: ${out.error?.message}`);\n }\n return out.data;\n};\n","// SPDX-License-Identifier: MIT\nimport { postApiCompile, getApiData } from \"../lib/api\";\n\nexport const compile = async ({\n accessToken,\n id,\n data,\n}: {\n accessToken?: string;\n id: string;\n data: any;\n}) => {\n // Empty data → recompile the full id; otherwise recompile with the code id only.\n const index = Object.keys(data).length > 0 ? 1 : 2;\n id = id.split(\"+\").slice(0, index).join(\"+\");\n return await postApiCompile({ accessToken, id, data });\n};\n\nexport const getData = async ({\n accessToken,\n id,\n}: {\n accessToken?: string;\n id: string;\n}) => {\n return await getApiData({ accessToken, id });\n};\n","// SPDX-License-Identifier: MIT\n//\n// View is the SHARED, inherited front-end harness. It manages form state, drives\n// compile/getData, reads its inputs from the URL search params, and posts state to the host\n// via window.parent.postMessage — so it works both embedded in an iframe and standalone. It\n// is parameterized by the language-specific `Form`: child languages import this `View` and\n// inject their own `Form` (the only language-specific UX). L0000 ships the base `Form` (JSON),\n// so its own embed mounts `<View Form={Form} />`.\n//\n// Compile/getData responses use the envelope `{ data, errors }`: successful output in `data`,\n// compile errors in `errors`. The View stores `data` as the form's data model (so recompiles\n// operate on real data, not the envelope) and threads `errors` to the Form alongside it.\n//\n// ── State ──────────────────────────────────────────────────────────────────────────────────\n//\n// The data model lives in React state and every `apply` re-renders. It used to live in an\n// EXTERNAL mutable store (`createState`) that React did not track, which meant a form seeded\n// from the `?data=` search param never appeared at all: the seed mutated the store from an\n// effect, nothing re-rendered, and the harness kept showing the empty `<div/>` it had already\n// committed. Keep state here, and keep the reducer PURE — it is re-run under StrictMode.\n//\n// ── The action protocol ────────────────────────────────────────────────────────────────────\n//\n// `init` replace the model (a fresh load)\n// `compiled` MERGE the compile result over the model. It must not replace: a compile response\n// carries only what the compiler produced, so replacing discards client-side state\n// (focus, in-progress entries) and re-initializes the whole form on every edit.\n// `update` a user edit; merges, and requests a recompile\n// `response` a learner's answer; merges, and requests a recompile\n// `focus` selection tracking; merges under `focus` and does NOT recompile\n//\n// `loaded` the stored model arriving from getData. Merges exactly like `compiled`; it is a\n// separate type only so the harness can tell an EXTERNAL load from the echo of an\n// edit the Form itself just reported (see `formModel` below).\n//\n// `response` and `focus` were previously unhandled, so they fell to the `default:` branch and\n// returned the model UNCHANGED — silently discarding every answer a learner typed into an\n// L0166/L0179 spreadsheet and every response an L0175 item collected.\n//\n// ── formModel: which model the Form is rendered from ───────────────────────────────────────\n//\n// A CONTROLLED Form (the default, `formModel: \"live\"`) renders whatever the model currently\n// says, so it must see every change — including its own edits coming back.\n//\n// An UNCONTROLLED Form owns its own editing state and only SEEDS from the model. L0166's\n// spreadsheet Form — which L0179 injects — is one: its TableEditor builds a ProseMirror\n// document from `interaction.cells` and, whenever that object's IDENTITY changes, rebuilds the\n// whole document and puts the caret back in A1. Feeding such a Form its own reported edits\n// re-seeds it on every keystroke commit: the grid visibly redraws and the selection jumps.\n//\n// L0166 never hits this because its state lives in an untracked closure that re-renders\n// nothing — the editor is seeded once and left alone. `formModel: \"loaded\"` makes that\n// deliberate rather than accidental: the Form renders from the model as last loaded from\n// OUTSIDE it (`init`, `loaded`), while `update`/`response` and the compile results they\n// trigger keep updating the live model for postMessage, recompiles, and reporting.\n//\n// A language whose Form needs different semantics for one of these passes `reduce`. It is\n// consulted FIRST and returns `undefined` for anything it does not claim, so a language adds\n// cases without restating the generic ones. This exists because L0179 injects L0166's\n// spreadsheet Form, whose `update` must merge cell text into `data.interaction.cells` rather\n// than onto the top level — a shape that has no business being hardcoded here.\n//\n// ── score: checking is the host's, not the Form's ──────────────────────────────────────────\n//\n// An assessment language passes `score`, and the View shows a Check button under the Form.\n// Pressing it shows the score and hands the Form `showValidationUI: true` until the learner\n// next changes the model (an `update` or `response` that changes nothing does not count). That flag is the one Learnosity sets through cqt, so a Form draws\n// feedback from it and nothing else, and never needs to know which host it is in. It is laid\n// over what the Form sees only: it is not in the model, so it is never posted or compiled.\n// Whether feedback also shows WITHOUT a check (instant feedback) is the program's to say, and\n// the Form reads that from the model itself.\nimport { useCallback, useEffect, useMemo, useReducer, useRef, useState } from \"react\";\nimport type { ComponentType, CSSProperties } from \"react\";\nimport useSWR from \"swr\";\nimport { compile, getData } from \"./swr/fetchers\";\nimport \"./index.css\";\n\nexport interface CompileError {\n message: string;\n from?: number;\n to?: number;\n}\n\nexport interface StateAction {\n type: string;\n args?: any;\n}\n\n/**\n * A language's extra reducer cases. Return the next data model to claim an action, or\n * `undefined` to fall through to the generic handling below.\n */\nexport type LanguageReducer = (data: any, action: StateAction) => any | undefined;\n\nexport interface FormProps {\n state: {\n data: any;\n errors: CompileError[];\n apply: (action: StateAction) => void;\n };\n}\n\nexport type FormComponent = ComponentType<FormProps>;\n\n/**\n * Which model the Form renders from.\n *\n * `live` — everything, including the Form's own edits coming back (a controlled Form).\n * `loaded` — only the model as last loaded from outside the Form (an uncontrolled Form that\n * seeds itself from the model and owns its editing state thereafter).\n */\nexport type FormModel = \"live\" | \"loaded\";\n\n/** A learner's score, as the Check button reports it. */\nexport interface Score {\n score: number;\n max: number;\n}\n\n/**\n * Score the live model, or return `undefined` when it has nothing to check (no answer key, no\n * blanks). Called on every change, so keep it cheap and never throw.\n */\nexport type LanguageScore = (data: any) => Score | undefined;\n\n/** Actions that represent a user changing the form, and so warrant a recompile. */\nconst RECOMPILE_ON = new Set([\"update\", \"response\"]);\n\n/**\n * Actions that carry a model from OUTSIDE the Form, and so may re-seed an uncontrolled one.\n * `compiled` is absent deliberately: after the initial load every compile is a response to an\n * edit the Form itself reported, and re-seeding from it is the flash this exists to stop.\n */\nconst EXTERNAL_LOAD = new Set([\"init\", \"loaded\"]);\n\n// Normalize a response into the { data, errors } envelope. Accepts a bare value (treated\n// as data with no errors) for backward/forward compatibility.\nfunction asEnvelope(payload: any): { data: any; errors: CompileError[] } {\n if (payload && typeof payload === \"object\" && Array.isArray(payload.errors)) {\n return { data: payload.data ?? null, errors: payload.errors };\n }\n return { data: payload, errors: [] };\n}\n\nfunction hasRenderable(data: any, errors: CompileError[]): boolean {\n if (errors.length > 0) return true;\n if (data === undefined || data === null) return false;\n if (typeof data === \"object\" && !Array.isArray(data) && Object.keys(data).length === 0) {\n return false;\n }\n return true;\n}\n\nconst isPlainObject = (v: any): boolean =>\n v !== null && typeof v === \"object\" && !Array.isArray(v);\n\n// Merge only when both sides are records. A program can compile to an ATOMIC value (`10`, a\n// string, an array); spreading one into an object yields `{}` (or index keys), which the\n// reducer then saw as \"no change\" — leaving the model empty and the View blank. A scalar or\n// array result REPLACES the model instead. A null result (e.g. a compile that produced only\n// errors) leaves the model alone, as the spread always did.\nconst merge = (data: any, args: any): any => {\n if (args === null || args === undefined) return data;\n return isPlainObject(data) && isPlainObject(args) ? { ...data, ...args } : args;\n};\n\nfunction baseReduce(data: any, { type, args }: StateAction): any {\n switch (type) {\n case \"init\":\n return isPlainObject(args) ? { ...args } : args;\n case \"compiled\":\n case \"loaded\":\n case \"update\":\n case \"response\":\n return merge(data, args);\n case \"focus\":\n return { ...data, focus: args };\n default:\n console.error(`Unimplemented action type: ${type}`);\n return data;\n }\n}\n\n/**\n * Structural equality, used to decide whether an action actually changed the model.\n *\n * This replaces `JSON.stringify(a) === JSON.stringify(b)`, which serialized the ENTIRE model\n * twice on every dispatched action. For a spreadsheet of a few hundred cells that is real work\n * on a path that runs on every keystroke, focus change and response — and it is work done to\n * answer a question that usually resolves on the first differing key.\n *\n * Unlike the stringify comparison this is insensitive to key ORDER, which is the intended\n * meaning: a model whose keys were rebuilt in a different order has not changed.\n */\nconst deepEqual = (a: any, b: any): boolean => {\n if (a === b) return true;\n if (typeof a !== typeof b || a === null || b === null || typeof a !== \"object\") return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false;\n return a.every((item, i) => deepEqual(item, b[i]));\n }\n const aKeys = Object.keys(a);\n if (aKeys.length !== Object.keys(b).length) return false;\n return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));\n};\n\ninterface ViewState {\n data: any;\n /**\n * What the Form is rendered from. Under `formModel: \"live\"` it is always `data`; under\n * `\"loaded\"` its IDENTITY only changes on an external load, which is what keeps an\n * uncontrolled Form from re-seeding itself on its own edits.\n */\n renderData: any;\n /** Bumped by each user edit. The compile effect keys off it, never off `data` itself. */\n compileSeq: number;\n /**\n * Bumped whenever `data` changes. Effects that need to run on a real change key off this\n * instead of off a serialization of the whole model.\n */\n version: number;\n}\n\nconst makeReducer =\n (reduce: LanguageReducer | undefined, formModel: FormModel) =>\n (prev: ViewState, action: StateAction): ViewState => {\n const claimed = reduce ? reduce(prev.data, action) : undefined;\n const data = claimed !== undefined ? claimed : baseReduce(prev.data, action);\n if (data === prev.data || deepEqual(data, prev.data)) {\n return prev;\n }\n return {\n data,\n renderData:\n formModel === \"live\" || EXTERNAL_LOAD.has(action.type) ? data : prev.renderData,\n compileSeq: prev.compileSeq + (RECOMPILE_ON.has(action.type) ? 1 : 0),\n version: prev.version + 1,\n };\n };\n\nexport const View = ({\n Form,\n reduce,\n formModel = \"live\",\n score,\n}: {\n Form: FormComponent;\n reduce?: LanguageReducer;\n formModel?: FormModel;\n score?: LanguageScore;\n}) => {\n const [params] = useState(() => new URLSearchParams(window.location.search));\n const [id] = useState<string | undefined>(params.get(\"id\") ?? undefined);\n const [accessToken] = useState<string | undefined>(params.get(\"access_token\") ?? undefined);\n const [targetOrigin] = useState<string | undefined>(params.get(\"origin\") ?? undefined);\n const [errors, setErrors] = useState<CompileError[]>([]);\n\n const reducer = useMemo(() => makeReducer(reduce, formModel), [reduce, formModel]);\n const [state, apply] = useReducer(reducer, undefined, () => ({\n data: {},\n renderData: {},\n compileSeq: 0,\n version: 0,\n }));\n const { data, renderData, compileSeq, version } = state;\n\n // Initialize from a `data` search param on first load.\n useEffect(() => {\n const seed = params.get(\"data\");\n if (seed) apply({ type: \"init\", args: JSON.parse(seed) });\n }, []);\n\n // Announce load to the host.\n useEffect(() => {\n if (targetOrigin) {\n window.parent.postMessage({ type: \"onload\", data }, targetOrigin);\n }\n }, []);\n\n // Post state to the host whenever it changes.\n //\n // Keyed on the reducer's own version counter. It used to be keyed on `JSON.stringify(data)`,\n // which serialized the whole model on EVERY render rather than only when something changed —\n // and the reducer already knows the answer, because it returns `prev` untouched when an action\n // changes nothing.\n useEffect(() => {\n if (targetOrigin) {\n window.parent.postMessage({ type: \"data-updated\", data }, targetOrigin);\n }\n }, [version]);\n\n // Fetch stored data when an id is present.\n //\n // Revalidation is off: this is a one-shot load, and a background refetch on window focus\n // would re-apply the STORED model over whatever the learner has since entered — the form\n // silently reverting when you tab away and back.\n const getDataResp = useSWR(id ? { accessToken, id } : null, getData, {\n revalidateOnFocus: false,\n revalidateOnReconnect: false,\n revalidateIfStale: false,\n });\n\n // Apply the loaded data in an EFFECT. Applying it during render (as this used to) both\n // mutated state mid-render and re-ran on every subsequent render, because the SWR result\n // stays populated — the old code only escaped that loop by flipping its own fetch key to\n // null, which is what let a warm cache entry re-apply a stale model later.\n useEffect(() => {\n if (getDataResp.data === undefined) return;\n const env = asEnvelope(getDataResp.data);\n apply({ type: \"loaded\", args: env.data });\n setErrors(env.errors);\n }, [getDataResp.data]);\n\n // Recompile after a user edit.\n //\n // Deliberately NOT SWR: a compile is a mutation, not a cacheable query. Keying it on the\n // data (as this used to) meant returning a cell to a value it already held replayed an\n // OLDER compiled snapshot straight out of the cache, reverting the sheet. A bare request\n // with last-write-wins ordering has no cache to go stale.\n const latestCompile = useRef(0);\n useEffect(() => {\n if (compileSeq === 0 || !id) return;\n const seq = ++latestCompile.current;\n if (targetOrigin) {\n window.parent.postMessage({ focus: { type: \"update\", value: data } }, targetOrigin);\n }\n let cancelled = false;\n (async () => {\n try {\n const out = await compile({ accessToken, id, data });\n // Drop a response that a newer edit has already superseded, so a slow compile can\n // never overwrite a faster one that started later.\n if (cancelled || seq !== latestCompile.current) return;\n const env = asEnvelope(out);\n apply({ type: \"compiled\", args: env.data });\n setErrors(env.errors);\n } catch (err: any) {\n if (!cancelled && seq === latestCompile.current) {\n setErrors([{ message: String(err?.message ?? err) }]);\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [compileSeq]);\n\n // The Form sees `renderData`; everything else here (postMessage, recompiles) uses the live\n // `data`. They are the same object unless the language asked for `formModel: \"loaded\"`, and\n // the fallback covers the one case where they can disagree about EXISTENCE rather than\n // content: a model that arrived without ever passing through an external load.\n const formData = hasRenderable(renderData, []) ? renderData : data;\n\n // Checking. A change the learner makes hides the result again; a compile does not, since it\n // only echoes a change that already did. Keyed on `compileSeq`, which moves only when an\n // `update`/`response` actually changed the model: L0179 reports both on every caret move, and\n // clearing on the action alone wiped the check the moment the learner clicked a cell.\n const [checked, setChecked] = useState(false);\n useEffect(() => setChecked(false), [compileSeq]);\n // Scored from the live model, which under `formModel: \"loaded\"` is not what the Form renders.\n const result = useMemo(\n () => (score && errors.length === 0 ? score(data) : undefined),\n [score, data, errors],\n );\n const shown =\n checked && result && isPlainObject(formData) ? { ...formData, showValidationUI: true } : formData;\n const formState = useMemo(() => ({ data: shown, errors, apply }), [shown, errors]);\n\n // Render priority: real content first; otherwise surface why there's none.\n // A getData failure (e.g. a stale/expired token 401ing a public read) used to\n // fall through to a blank `<div/>` with no console output — indistinguishable\n // from \"still loading\" and impossible to diagnose. Show the error (and a\n // retry) instead. Once data has loaded, a later revalidation error is ignored\n // so the form isn't replaced by an error screen.\n const retry = useCallback(() => getDataResp.mutate(), [getDataResp]);\n\n if (hasRenderable(data, errors)) {\n if (!result) return <Form state={formState} />;\n return (\n <>\n <Form state={formState} />\n <CheckBar result={result} checked={checked} onCheck={() => setChecked(true)} />\n </>\n );\n }\n if (getDataResp.error) {\n return (\n <div role=\"alert\" style={MESSAGE_STYLE}>\n <p style={{ margin: 0, fontWeight: 600 }}>Couldn’t load this form.</p>\n <p style={{ margin: \"4px 0 12px\", color: \"#555\" }}>\n {String((getDataResp.error as { message?: string })?.message ?? getDataResp.error)}\n </p>\n <button type=\"button\" style={RETRY_STYLE} onClick={retry}>\n Retry\n </button>\n </div>\n );\n }\n if (getDataResp.isLoading) {\n return (\n <div role=\"status\" aria-live=\"polite\" style={MESSAGE_STYLE}>\n Loading…\n </div>\n );\n }\n return <div />;\n};\n\n/**\n * The host's Check button and the score it reveals. Exported for hosts that mount a Form without\n * the View (the MCP widget), so every one of our hosts checks the same way.\n */\nexport const CheckBar = ({\n result,\n checked,\n onCheck,\n}: {\n result: Score;\n checked: boolean;\n onCheck: () => void;\n}) => (\n <div style={CHECK_BAR_STYLE}>\n <button type=\"button\" style={CHECK_BUTTON_STYLE} onClick={onCheck} disabled={checked}>\n Check\n </button>\n <span aria-live=\"polite\" style={{ color: \"#52525b\", minWidth: \"8em\" }}>\n {checked ? `${result.score} of ${result.max} points` : \"\"}\n </span>\n </div>\n);\n\nconst CHECK_BAR_STYLE: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"12px\",\n padding: \"12px\",\n font: \"14px/1.4 system-ui, sans-serif\",\n};\n\nconst CHECK_BUTTON_STYLE: CSSProperties = {\n padding: \"8px 16px\",\n border: \"none\",\n borderRadius: \"6px\",\n background: \"#18181b\",\n color: \"#fff\",\n fontWeight: 500,\n cursor: \"pointer\",\n};\n\nconst MESSAGE_STYLE: CSSProperties = {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: \"100%\",\n padding: \"24px\",\n textAlign: \"center\",\n font: \"14px/1.4 system-ui, sans-serif\",\n};\n\nconst RETRY_STYLE: CSSProperties = {\n padding: \"6px 14px\",\n border: \"1px solid #888\",\n borderRadius: \"4px\",\n background: \"#fff\",\n cursor: \"pointer\",\n};\n","// SPDX-License-Identifier: MIT\n//\n// L0000's base Form: renders compile errors when present, otherwise the compiled data\n// model as stringified JSON. This is the trivial default rendering; child languages\n// supply their own Form and inject it into the shared View instead.\nimport \"../../index.css\";\nimport type { FormProps, CompileError } from \"../../view\";\n\nfunction ErrorList({ errors }: { errors: CompileError[] }) {\n return (\n <div className=\"flex flex-col gap-2 font-mono text-xs\">\n {errors.map((error, i) => (\n <div\n key={i}\n className=\"rounded-md border border-red-200 bg-red-50 p-3 text-red-800\"\n >\n {error.message}\n {typeof error.from === \"number\" && error.from >= 0 && (\n <span className=\"ml-2 text-red-400\">\n [{error.from}\n {typeof error.to === \"number\" && error.to >= 0 ? `–${error.to}` : \"\"}]\n </span>\n )}\n </div>\n ))}\n </div>\n );\n}\n\nexport const Form = ({ state }: FormProps) => {\n const errors = state.errors ?? [];\n if (errors.length > 0) {\n return <ErrorList errors={errors} />;\n }\n const source = state.data;\n return (\n <pre className=\"rounded-md bg-zinc-50 text-zinc-900 text-xs font-mono p-4 overflow-auto\">\n {JSON.stringify(source, null, 2)}\n </pre>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/lib/api.ts","../src/swr/fetchers.ts","../src/view.tsx","../src/components/form/Form.tsx"],"sourcesContent":["// SPDX-License-Identifier: MIT\n// Client for the Graffiticode data/compile API. Uses the platform API host derived from\n// the page's location (local console in dev, api.graffiticode.org in prod).\n\nfunction getApiUrl(): string {\n const host = window.document.location.host;\n return host.indexOf(\"localhost\") === 0\n ? \"http://localhost:3100\"\n : \"https://api.graffiticode.org\";\n}\n\nexport const getApiData = async ({\n accessToken,\n id,\n}: {\n accessToken?: string;\n id: string;\n}) => {\n const resp = await fetch(`${getApiUrl()}/data?id=${id}`, {\n headers: { Authorization: accessToken || \"\" },\n });\n const { status, error, data } = await resp.json();\n if (status !== \"success\") {\n throw new Error(`failed to get data ${id}: ${error?.message}`);\n }\n return data;\n};\n\nexport const postApiCompile = async ({\n accessToken,\n id,\n data,\n}: {\n accessToken?: string;\n id: string;\n data: any;\n}) => {\n const resp = await fetch(`${getApiUrl()}/compile`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n authorization: accessToken || \"\",\n \"x-graffiticode-storage-type\": \"persistent\",\n },\n body: JSON.stringify({ id, data }),\n });\n const out = await resp.json();\n if (out.status !== \"success\") {\n throw new Error(`failed to post compile ${id}: ${out.error?.message}`);\n }\n return out.data;\n};\n","// SPDX-License-Identifier: MIT\nimport { postApiCompile, getApiData } from \"../lib/api\";\n\nexport const compile = async ({\n accessToken,\n id,\n data,\n}: {\n accessToken?: string;\n id: string;\n data: any;\n}) => {\n // Empty data → recompile the full id; otherwise recompile with the code id only.\n const index = Object.keys(data).length > 0 ? 1 : 2;\n id = id.split(\"+\").slice(0, index).join(\"+\");\n return await postApiCompile({ accessToken, id, data });\n};\n\nexport const getData = async ({\n accessToken,\n id,\n}: {\n accessToken?: string;\n id: string;\n}) => {\n return await getApiData({ accessToken, id });\n};\n","// SPDX-License-Identifier: MIT\n//\n// View is the SHARED, inherited front-end harness. It manages form state, drives\n// compile/getData, reads its inputs from the URL search params, and posts state to the host\n// via window.parent.postMessage — so it works both embedded in an iframe and standalone. It\n// is parameterized by the language-specific `Form`: child languages import this `View` and\n// inject their own `Form` (the only language-specific UX). L0000 ships the base `Form` (JSON),\n// so its own embed mounts `<View Form={Form} />`.\n//\n// Compile/getData responses use the envelope `{ data, errors }`: successful output in `data`,\n// compile errors in `errors`. The View stores `data` as the form's data model (so recompiles\n// operate on real data, not the envelope) and threads `errors` to the Form alongside it.\n//\n// ── State ──────────────────────────────────────────────────────────────────────────────────\n//\n// The data model lives in React state and every `apply` re-renders. It used to live in an\n// EXTERNAL mutable store (`createState`) that React did not track, which meant a form seeded\n// from the `?data=` search param never appeared at all: the seed mutated the store from an\n// effect, nothing re-rendered, and the harness kept showing the empty `<div/>` it had already\n// committed. Keep state here, and keep the reducer PURE — it is re-run under StrictMode.\n//\n// ── The action protocol ────────────────────────────────────────────────────────────────────\n//\n// `init` replace the model (a fresh load)\n// `compiled` MERGE the compile result over the model. It must not replace: a compile response\n// carries only what the compiler produced, so replacing discards client-side state\n// (focus, in-progress entries) and re-initializes the whole form on every edit.\n// `update` a user edit; merges, and requests a recompile\n// `response` a learner's answer; merges, and requests a recompile\n// `focus` selection tracking; merges under `focus` and does NOT recompile\n//\n// `loaded` the stored model arriving from getData. Merges exactly like `compiled`; it is a\n// separate type only so the harness can tell an EXTERNAL load from the echo of an\n// edit the Form itself just reported (see `formModel` below).\n//\n// `response` and `focus` were previously unhandled, so they fell to the `default:` branch and\n// returned the model UNCHANGED — silently discarding every answer a learner typed into an\n// L0166/L0179 spreadsheet and every response an L0175 item collected.\n//\n// ── formModel: which model the Form is rendered from ───────────────────────────────────────\n//\n// A CONTROLLED Form (the default, `formModel: \"live\"`) renders whatever the model currently\n// says, so it must see every change — including its own edits coming back.\n//\n// An UNCONTROLLED Form owns its own editing state and only SEEDS from the model. L0166's\n// spreadsheet Form — which L0179 injects — is one: its TableEditor builds a ProseMirror\n// document from `interaction.cells` and, whenever that object's IDENTITY changes, rebuilds the\n// whole document and puts the caret back in A1. Feeding such a Form its own reported edits\n// re-seeds it on every keystroke commit: the grid visibly redraws and the selection jumps.\n//\n// L0166 never hits this because its state lives in an untracked closure that re-renders\n// nothing — the editor is seeded once and left alone. `formModel: \"loaded\"` makes that\n// deliberate rather than accidental: the Form renders from the model as last loaded from\n// OUTSIDE it (`init`, `loaded`), while `update`/`response` and the compile results they\n// trigger keep updating the live model for postMessage, recompiles, and reporting.\n//\n// A language whose Form needs different semantics for one of these passes `reduce`. It is\n// consulted FIRST and returns `undefined` for anything it does not claim, so a language adds\n// cases without restating the generic ones. This exists because L0179 injects L0166's\n// spreadsheet Form, whose `update` must merge cell text into `data.interaction.cells` rather\n// than onto the top level — a shape that has no business being hardcoded here.\n//\n// ── score: checking is the host's, not the Form's ──────────────────────────────────────────\n//\n// An assessment language passes `score`, and the View shows a Check button under the Form.\n// Check is a toggle: pressing it shows the score and hands the Form `showValidationUI: true`,\n// pressing it again hides both, and so does the learner's next change to the model (an\n// `update` or `response` that changes nothing does not count) — feedback returns to whatever\n// the program's instant-feedback says. Check stays disabled until `score` reports the\n// response `complete`. That flag is the one Learnosity sets through cqt, so a Form draws\n// feedback from it and nothing else, and never needs to know which host it is in. It is laid\n// over what the Form sees only: it is not in the model, so it is never posted or compiled.\n// Whether feedback also shows WITHOUT a check (instant feedback) is the program's to say, and\n// the Form reads that from the model itself.\nimport { useCallback, useEffect, useMemo, useReducer, useRef, useState } from \"react\";\nimport type { ComponentType, CSSProperties } from \"react\";\nimport useSWR from \"swr\";\nimport { compile, getData } from \"./swr/fetchers\";\nimport \"./index.css\";\n\nexport interface CompileError {\n message: string;\n from?: number;\n to?: number;\n}\n\nexport interface StateAction {\n type: string;\n args?: any;\n}\n\n/**\n * A language's extra reducer cases. Return the next data model to claim an action, or\n * `undefined` to fall through to the generic handling below.\n */\nexport type LanguageReducer = (data: any, action: StateAction) => any | undefined;\n\nexport interface FormProps {\n state: {\n data: any;\n errors: CompileError[];\n apply: (action: StateAction) => void;\n };\n}\n\nexport type FormComponent = ComponentType<FormProps>;\n\n/**\n * Which model the Form renders from.\n *\n * `live` — everything, including the Form's own edits coming back (a controlled Form).\n * `loaded` — only the model as last loaded from outside the Form (an uncontrolled Form that\n * seeds itself from the model and owns its editing state thereafter).\n */\nexport type FormModel = \"live\" | \"loaded\";\n\n/** A learner's score, as the Check button reports it. */\nexport interface Score {\n score: number;\n max: number;\n /**\n * Whether the learner has answered everything. `false` keeps Check disabled until they have;\n * absent counts as complete, so a language that does not say never blocks the button.\n */\n complete?: boolean;\n}\n\n/**\n * Score the live model, or return `undefined` when it has nothing to check (no answer key, no\n * blanks). Called on every change, so keep it cheap and never throw.\n */\nexport type LanguageScore = (data: any) => Score | undefined;\n\n/** Actions that represent a user changing the form, and so warrant a recompile. */\nconst RECOMPILE_ON = new Set([\"update\", \"response\"]);\n\n/**\n * Actions that carry a model from OUTSIDE the Form, and so may re-seed an uncontrolled one.\n * `compiled` is absent deliberately: after the initial load every compile is a response to an\n * edit the Form itself reported, and re-seeding from it is the flash this exists to stop.\n */\nconst EXTERNAL_LOAD = new Set([\"init\", \"loaded\"]);\n\n// Normalize a response into the { data, errors } envelope. Accepts a bare value (treated\n// as data with no errors) for backward/forward compatibility.\nfunction asEnvelope(payload: any): { data: any; errors: CompileError[] } {\n if (payload && typeof payload === \"object\" && Array.isArray(payload.errors)) {\n return { data: payload.data ?? null, errors: payload.errors };\n }\n return { data: payload, errors: [] };\n}\n\nfunction hasRenderable(data: any, errors: CompileError[]): boolean {\n if (errors.length > 0) return true;\n if (data === undefined || data === null) return false;\n if (typeof data === \"object\" && !Array.isArray(data) && Object.keys(data).length === 0) {\n return false;\n }\n return true;\n}\n\nconst isPlainObject = (v: any): boolean =>\n v !== null && typeof v === \"object\" && !Array.isArray(v);\n\n// Merge only when both sides are records. A program can compile to an ATOMIC value (`10`, a\n// string, an array); spreading one into an object yields `{}` (or index keys), which the\n// reducer then saw as \"no change\" — leaving the model empty and the View blank. A scalar or\n// array result REPLACES the model instead. A null result (e.g. a compile that produced only\n// errors) leaves the model alone, as the spread always did.\nconst merge = (data: any, args: any): any => {\n if (args === null || args === undefined) return data;\n return isPlainObject(data) && isPlainObject(args) ? { ...data, ...args } : args;\n};\n\nfunction baseReduce(data: any, { type, args }: StateAction): any {\n switch (type) {\n case \"init\":\n return isPlainObject(args) ? { ...args } : args;\n case \"compiled\":\n case \"loaded\":\n case \"update\":\n case \"response\":\n return merge(data, args);\n case \"focus\":\n return { ...data, focus: args };\n default:\n console.error(`Unimplemented action type: ${type}`);\n return data;\n }\n}\n\n/**\n * Structural equality, used to decide whether an action actually changed the model.\n *\n * This replaces `JSON.stringify(a) === JSON.stringify(b)`, which serialized the ENTIRE model\n * twice on every dispatched action. For a spreadsheet of a few hundred cells that is real work\n * on a path that runs on every keystroke, focus change and response — and it is work done to\n * answer a question that usually resolves on the first differing key.\n *\n * Unlike the stringify comparison this is insensitive to key ORDER, which is the intended\n * meaning: a model whose keys were rebuilt in a different order has not changed.\n */\nconst deepEqual = (a: any, b: any): boolean => {\n if (a === b) return true;\n if (typeof a !== typeof b || a === null || b === null || typeof a !== \"object\") return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a)) {\n if (a.length !== b.length) return false;\n return a.every((item, i) => deepEqual(item, b[i]));\n }\n const aKeys = Object.keys(a);\n if (aKeys.length !== Object.keys(b).length) return false;\n return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));\n};\n\ninterface ViewState {\n data: any;\n /**\n * What the Form is rendered from. Under `formModel: \"live\"` it is always `data`; under\n * `\"loaded\"` its IDENTITY only changes on an external load, which is what keeps an\n * uncontrolled Form from re-seeding itself on its own edits.\n */\n renderData: any;\n /** Bumped by each user edit. The compile effect keys off it, never off `data` itself. */\n compileSeq: number;\n /**\n * Bumped whenever `data` changes. Effects that need to run on a real change key off this\n * instead of off a serialization of the whole model.\n */\n version: number;\n}\n\nconst makeReducer =\n (reduce: LanguageReducer | undefined, formModel: FormModel) =>\n (prev: ViewState, action: StateAction): ViewState => {\n const claimed = reduce ? reduce(prev.data, action) : undefined;\n const data = claimed !== undefined ? claimed : baseReduce(prev.data, action);\n if (data === prev.data || deepEqual(data, prev.data)) {\n return prev;\n }\n return {\n data,\n renderData:\n formModel === \"live\" || EXTERNAL_LOAD.has(action.type) ? data : prev.renderData,\n compileSeq: prev.compileSeq + (RECOMPILE_ON.has(action.type) ? 1 : 0),\n version: prev.version + 1,\n };\n };\n\nexport const View = ({\n Form,\n reduce,\n formModel = \"live\",\n score,\n id: idProp,\n accessToken: accessTokenProp,\n}: {\n Form: FormComponent;\n reduce?: LanguageReducer;\n formModel?: FormModel;\n score?: LanguageScore;\n /**\n * The item to load. A host that mounts the View as a React component passes it here; the\n * embeddable /form bundle leaves it out and the View reads `?id=` from the page URL instead.\n */\n id?: string;\n /** Credential for a private item. Falls back to `?access_token=`, like `id`. */\n accessToken?: string;\n}) => {\n const [params] = useState(() => new URLSearchParams(window.location.search));\n const id = idProp ?? params.get(\"id\") ?? undefined;\n const accessToken = accessTokenProp ?? params.get(\"access_token\") ?? undefined;\n const [targetOrigin] = useState<string | undefined>(params.get(\"origin\") ?? undefined);\n const [errors, setErrors] = useState<CompileError[]>([]);\n\n const reducer = useMemo(() => makeReducer(reduce, formModel), [reduce, formModel]);\n const [state, apply] = useReducer(reducer, undefined, () => ({\n data: {},\n renderData: {},\n compileSeq: 0,\n version: 0,\n }));\n const { data, renderData, compileSeq, version } = state;\n\n // Initialize from a `data` search param on first load.\n useEffect(() => {\n const seed = params.get(\"data\");\n if (seed) apply({ type: \"init\", args: JSON.parse(seed) });\n }, []);\n\n // Announce load to the host.\n useEffect(() => {\n if (targetOrigin) {\n window.parent.postMessage({ type: \"onload\", data }, targetOrigin);\n }\n }, []);\n\n // Post state to the host whenever it changes.\n //\n // Keyed on the reducer's own version counter. It used to be keyed on `JSON.stringify(data)`,\n // which serialized the whole model on EVERY render rather than only when something changed —\n // and the reducer already knows the answer, because it returns `prev` untouched when an action\n // changes nothing.\n useEffect(() => {\n if (targetOrigin) {\n window.parent.postMessage({ type: \"data-updated\", data }, targetOrigin);\n }\n }, [version]);\n\n // Fetch stored data when an id is present.\n //\n // Revalidation is off: this is a one-shot load, and a background refetch on window focus\n // would re-apply the STORED model over whatever the learner has since entered — the form\n // silently reverting when you tab away and back.\n const getDataResp = useSWR(id ? { accessToken, id } : null, getData, {\n revalidateOnFocus: false,\n revalidateOnReconnect: false,\n revalidateIfStale: false,\n });\n\n // Apply the loaded data in an EFFECT. Applying it during render (as this used to) both\n // mutated state mid-render and re-ran on every subsequent render, because the SWR result\n // stays populated — the old code only escaped that loop by flipping its own fetch key to\n // null, which is what let a warm cache entry re-apply a stale model later.\n useEffect(() => {\n if (getDataResp.data === undefined) return;\n const env = asEnvelope(getDataResp.data);\n apply({ type: \"loaded\", args: env.data });\n setErrors(env.errors);\n }, [getDataResp.data]);\n\n // Recompile after a user edit.\n //\n // Deliberately NOT SWR: a compile is a mutation, not a cacheable query. Keying it on the\n // data (as this used to) meant returning a cell to a value it already held replayed an\n // OLDER compiled snapshot straight out of the cache, reverting the sheet. A bare request\n // with last-write-wins ordering has no cache to go stale.\n const latestCompile = useRef(0);\n useEffect(() => {\n if (compileSeq === 0 || !id) return;\n const seq = ++latestCompile.current;\n if (targetOrigin) {\n window.parent.postMessage({ focus: { type: \"update\", value: data } }, targetOrigin);\n }\n let cancelled = false;\n (async () => {\n try {\n const out = await compile({ accessToken, id, data });\n // Drop a response that a newer edit has already superseded, so a slow compile can\n // never overwrite a faster one that started later.\n if (cancelled || seq !== latestCompile.current) return;\n const env = asEnvelope(out);\n apply({ type: \"compiled\", args: env.data });\n setErrors(env.errors);\n } catch (err: any) {\n if (!cancelled && seq === latestCompile.current) {\n setErrors([{ message: String(err?.message ?? err) }]);\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [compileSeq]);\n\n // The Form sees `renderData`; everything else here (postMessage, recompiles) uses the live\n // `data`. They are the same object unless the language asked for `formModel: \"loaded\"`, and\n // the fallback covers the one case where they can disagree about EXISTENCE rather than\n // content: a model that arrived without ever passing through an external load.\n const formData = hasRenderable(renderData, []) ? renderData : data;\n\n // Checking. A change the learner makes hides the result again; a compile does not, since it\n // only echoes a change that already did. Keyed on `compileSeq`, which moves only when an\n // `update`/`response` actually changed the model: L0179 reports both on every caret move, and\n // clearing on the action alone wiped the check the moment the learner clicked a cell.\n const [checked, setChecked] = useState(false);\n useEffect(() => setChecked(false), [compileSeq]);\n // Scored from the live model, which under `formModel: \"loaded\"` is not what the Form renders.\n const result = useMemo(\n () => (score && errors.length === 0 ? score(data) : undefined),\n [score, data, errors],\n );\n const shown =\n checked && result && isPlainObject(formData) ? { ...formData, showValidationUI: true } : formData;\n const formState = useMemo(() => ({ data: shown, errors, apply }), [shown, errors]);\n\n // Render priority: real content first; otherwise surface why there's none.\n // A getData failure (e.g. a stale/expired token 401ing a public read) used to\n // fall through to a blank `<div/>` with no console output — indistinguishable\n // from \"still loading\" and impossible to diagnose. Show the error (and a\n // retry) instead. Once data has loaded, a later revalidation error is ignored\n // so the form isn't replaced by an error screen.\n const retry = useCallback(() => getDataResp.mutate(), [getDataResp]);\n\n if (hasRenderable(data, errors)) {\n if (!result) return <Form state={formState} />;\n return (\n <>\n <Form state={formState} />\n <CheckBar result={result} checked={checked} onToggle={() => setChecked((c) => !c)} />\n </>\n );\n }\n if (getDataResp.error) {\n return (\n <div role=\"alert\" style={MESSAGE_STYLE}>\n <p style={{ margin: 0, fontWeight: 600 }}>Couldn’t load this form.</p>\n <p style={{ margin: \"4px 0 12px\", color: \"#555\" }}>\n {String((getDataResp.error as { message?: string })?.message ?? getDataResp.error)}\n </p>\n <button type=\"button\" style={RETRY_STYLE} onClick={retry}>\n Retry\n </button>\n </div>\n );\n }\n if (getDataResp.isLoading) {\n return (\n <div role=\"status\" aria-live=\"polite\" style={MESSAGE_STYLE}>\n Loading…\n </div>\n );\n }\n return <div />;\n};\n\n/**\n * The host's Check button and the score it reveals. Exported for hosts that mount a Form without\n * the View (the MCP widget), so every one of our hosts checks the same way.\n *\n * Drawn as player chrome — its own bordered strip, set apart from the question — because it is\n * the host's control, not part of the interaction. It is a toggle (`aria-pressed`), disabled\n * until the response is complete; a check already showing can always be hidden.\n */\nexport const CheckBar = ({\n result,\n checked,\n onToggle,\n}: {\n result: Score;\n checked: boolean;\n onToggle: () => void;\n}) => {\n const waiting = !checked && result.complete === false;\n return (\n <div role=\"group\" aria-label=\"Check your answers\" style={CHECK_BAR_STYLE}>\n <button\n type=\"button\"\n aria-pressed={checked}\n style={{ ...CHECK_BUTTON_STYLE, ...(checked ? CHECK_BUTTON_ON : {}), ...(waiting ? CHECK_BUTTON_OFF : {}) }}\n onClick={onToggle}\n disabled={waiting}\n >\n Check\n </button>\n <span aria-live=\"polite\" style={{ color: \"#52525b\", minWidth: \"10em\" }}>\n {checked\n ? `${result.score} of ${result.max} points`\n : waiting\n ? \"Answer everything to check.\"\n : \"\"}\n </span>\n </div>\n );\n};\n\nconst CHECK_BAR_STYLE: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"12px\",\n margin: \"12px auto 0\",\n maxWidth: \"64rem\",\n padding: \"10px 16px\",\n border: \"1px solid #d4d4d8\",\n borderRadius: \"8px\",\n background: \"#f4f4f5\",\n font: \"14px/1.4 system-ui, sans-serif\",\n boxSizing: \"border-box\",\n};\n\nconst CHECK_BUTTON_STYLE: CSSProperties = {\n padding: \"8px 16px\",\n border: \"1px solid #18181b\",\n borderRadius: \"6px\",\n background: \"#18181b\",\n color: \"#fff\",\n fontWeight: 500,\n cursor: \"pointer\",\n};\n\n/** Pressed: the check is showing, and pressing again hides it. */\nconst CHECK_BUTTON_ON: CSSProperties = {\n background: \"#fff\",\n color: \"#18181b\",\n};\n\nconst CHECK_BUTTON_OFF: CSSProperties = {\n opacity: 0.45,\n cursor: \"default\",\n};\n\nconst MESSAGE_STYLE: CSSProperties = {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: \"100%\",\n padding: \"24px\",\n textAlign: \"center\",\n font: \"14px/1.4 system-ui, sans-serif\",\n};\n\nconst RETRY_STYLE: CSSProperties = {\n padding: \"6px 14px\",\n border: \"1px solid #888\",\n borderRadius: \"4px\",\n background: \"#fff\",\n cursor: \"pointer\",\n};\n","// SPDX-License-Identifier: MIT\n//\n// L0000's base Form: renders compile errors when present, otherwise the compiled data\n// model as stringified JSON. This is the trivial default rendering; child languages\n// supply their own Form and inject it into the shared View instead.\nimport \"../../index.css\";\nimport type { FormProps, CompileError } from \"../../view\";\n\nfunction ErrorList({ errors }: { errors: CompileError[] }) {\n return (\n <div className=\"flex flex-col gap-2 font-mono text-xs\">\n {errors.map((error, i) => (\n <div\n key={i}\n className=\"rounded-md border border-red-200 bg-red-50 p-3 text-red-800\"\n >\n {error.message}\n {typeof error.from === \"number\" && error.from >= 0 && (\n <span className=\"ml-2 text-red-400\">\n [{error.from}\n {typeof error.to === \"number\" && error.to >= 0 ? `–${error.to}` : \"\"}]\n </span>\n )}\n </div>\n ))}\n </div>\n );\n}\n\nexport const Form = ({ state }: FormProps) => {\n const errors = state.errors ?? [];\n if (errors.length > 0) {\n return <ErrorList errors={errors} />;\n }\n const source = state.data;\n return (\n <pre className=\"rounded-md bg-zinc-50 text-zinc-900 text-xs font-mono p-4 overflow-auto\">\n {JSON.stringify(source, null, 2)}\n </pre>\n );\n};\n"],"mappings":"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"}
|
package/dist/style.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.ml-2{margin-left:.5rem}.flex{display:flex}.grid{display:grid}.flex-col{flex-direction:column}.gap-2{gap:.5rem}.overflow-auto{overflow:auto}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-zinc-50{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity,1))}.p-3{padding:.75rem}.p-4{padding:1rem}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-xs{font-size:.75rem;line-height:1rem}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-zinc-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}
|
|
1
|
+
*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.ml-2{margin-left:.5rem}.flex{display:flex}.grid{display:grid}.hidden{display:none}.flex-col{flex-direction:column}.gap-2{gap:.5rem}.overflow-auto{overflow:auto}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-zinc-50{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity,1))}.p-3{padding:.75rem}.p-4{padding:1rem}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-xs{font-size:.75rem;line-height:1rem}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-zinc-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}
|
|
2
2
|
/*$vite$:1*/
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"access": "public"
|
|
4
4
|
},
|
|
5
5
|
"name": "@graffiticode/l0000-view",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.3.1",
|
|
7
7
|
"description": "L0000 view — the shared View harness (inherited by child languages) plus the base JSON Form",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.js",
|