@graffiticode/l0000-view 0.1.5 β†’ 0.2.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 CHANGED
@@ -1,6 +1,16 @@
1
1
  import { ComponentType } from 'react';
2
2
  import { JSX as JSX_2 } from 'react/jsx-runtime';
3
3
 
4
+ /**
5
+ * The host's Check button and the score it reveals. Exported for hosts that mount a Form without
6
+ * the View (the MCP widget), so every one of our hosts checks the same way.
7
+ */
8
+ export declare const CheckBar: ({ result, checked, onCheck, }: {
9
+ result: Score;
10
+ checked: boolean;
11
+ onCheck: () => void;
12
+ }) => JSX_2.Element;
13
+
4
14
  export declare interface CompileError {
5
15
  message: string;
6
16
  from?: number;
@@ -34,15 +44,28 @@ export declare interface FormProps {
34
44
  */
35
45
  export declare type LanguageReducer = (data: any, action: StateAction) => any | undefined;
36
46
 
47
+ /**
48
+ * Score the live model, or return `undefined` when it has nothing to check (no answer key, no
49
+ * blanks). Called on every change, so keep it cheap and never throw.
50
+ */
51
+ export declare type LanguageScore = (data: any) => Score | undefined;
52
+
53
+ /** A learner's score, as the Check button reports it. */
54
+ export declare interface Score {
55
+ score: number;
56
+ max: number;
57
+ }
58
+
37
59
  export declare interface StateAction {
38
60
  type: string;
39
61
  args?: any;
40
62
  }
41
63
 
42
- export declare const View: ({ Form, reduce, formModel, }: {
64
+ export declare const View: ({ Form, reduce, formModel, score, }: {
43
65
  Form: FormComponent;
44
66
  reduce?: LanguageReducer;
45
67
  formModel?: FormModel;
68
+ score?: LanguageScore;
46
69
  }) => JSX_2.Element;
47
70
 
48
71
  export { }
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { useCallback as e, useEffect as t, useMemo as n, useReducer as r, useRef as i, useState as a } from "react";
2
2
  import o from "swr";
3
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
3
+ import { Fragment as s, jsx as c, jsxs as l } from "react/jsx-runtime";
4
4
  //#region src/lib/api.ts
5
- function l() {
5
+ function u() {
6
6
  return window.document.location.host.indexOf("localhost") === 0 ? "http://localhost:3100" : "https://api.graffiticode.org";
7
7
  }
8
- var u = async ({ accessToken: e, id: t }) => {
9
- let { status: n, error: r, data: i } = await (await fetch(`${l()}/data?id=${t}`, { headers: { Authorization: e || "" } })).json();
8
+ var d = async ({ accessToken: e, id: t }) => {
9
+ let { status: n, error: r, data: i } = await (await fetch(`${u()}/data?id=${t}`, { headers: { Authorization: e || "" } })).json();
10
10
  if (n !== "success") throw Error(`failed to get data ${t}: ${r?.message}`);
11
11
  return i;
12
- }, d = async ({ accessToken: e, id: t, data: n }) => {
13
- let r = await (await fetch(`${l()}/compile`, {
12
+ }, f = async ({ accessToken: e, id: t, data: n }) => {
13
+ let r = await (await fetch(`${u()}/compile`, {
14
14
  method: "POST",
15
15
  headers: {
16
16
  "Content-Type": "application/json",
@@ -24,18 +24,18 @@ var u = async ({ accessToken: e, id: t }) => {
24
24
  })).json();
25
25
  if (r.status !== "success") throw Error(`failed to post compile ${t}: ${r.error?.message}`);
26
26
  return r.data;
27
- }, f = async ({ accessToken: e, id: t, data: n }) => {
27
+ }, p = async ({ accessToken: e, id: t, data: n }) => {
28
28
  let r = Object.keys(n).length > 0 ? 1 : 2;
29
- return t = t.split("+").slice(0, r).join("+"), await d({
29
+ return t = t.split("+").slice(0, r).join("+"), await f({
30
30
  accessToken: e,
31
31
  id: t,
32
32
  data: n
33
33
  });
34
- }, p = async ({ accessToken: e, id: t }) => await u({
34
+ }, m = async ({ accessToken: e, id: t }) => await d({
35
35
  accessToken: e,
36
36
  id: t
37
- }), m = /* @__PURE__ */ new Set(["update", "response"]), h = /* @__PURE__ */ new Set(["init", "loaded"]);
38
- function g(e) {
37
+ }), h = /* @__PURE__ */ new Set(["update", "response"]), g = /* @__PURE__ */ new Set(["init", "loaded"]);
38
+ function _(e) {
39
39
  return e && typeof e == "object" && Array.isArray(e.errors) ? {
40
40
  data: e.data ?? null,
41
41
  errors: e.errors
@@ -44,20 +44,20 @@ function g(e) {
44
44
  errors: []
45
45
  };
46
46
  }
47
- function _(e, t) {
47
+ function v(e, t) {
48
48
  return t.length > 0 || !(e == null || typeof e == "object" && !Array.isArray(e) && Object.keys(e).length === 0);
49
49
  }
50
- var v = (e) => typeof e == "object" && !!e && !Array.isArray(e), y = (e, t) => t == null ? e : v(e) && v(t) ? {
50
+ var y = (e) => typeof e == "object" && !!e && !Array.isArray(e), b = (e, t) => t == null ? e : y(e) && y(t) ? {
51
51
  ...e,
52
52
  ...t
53
53
  } : t;
54
- function b(e, { type: t, args: n }) {
54
+ function x(e, { type: t, args: n }) {
55
55
  switch (t) {
56
- case "init": return v(n) ? { ...n } : n;
56
+ case "init": return y(n) ? { ...n } : n;
57
57
  case "compiled":
58
58
  case "loaded":
59
59
  case "update":
60
- case "response": return y(e, n);
60
+ case "response": return b(e, n);
61
61
  case "focus": return {
62
62
  ...e,
63
63
  focus: n
@@ -65,126 +65,170 @@ function b(e, { type: t, args: n }) {
65
65
  default: return console.error(`Unimplemented action type: ${t}`), e;
66
66
  }
67
67
  }
68
- var x = (e, t) => {
68
+ var S = (e, t) => {
69
69
  if (e === t) return !0;
70
70
  if (typeof e != typeof t || e === null || t === null || typeof e != "object" || Array.isArray(e) !== Array.isArray(t)) return !1;
71
- if (Array.isArray(e)) return e.length === t.length && e.every((e, n) => x(e, t[n]));
71
+ if (Array.isArray(e)) return e.length === t.length && e.every((e, n) => S(e, t[n]));
72
72
  let n = Object.keys(e);
73
- return n.length === Object.keys(t).length && n.every((n) => Object.prototype.hasOwnProperty.call(t, n) && x(e[n], t[n]));
74
- }, S = (e, t) => (n, r) => {
75
- let i = e ? e(n.data, r) : void 0, a = i === void 0 ? b(n.data, r) : i;
76
- return a === n.data || x(a, n.data) ? n : {
73
+ return n.length === Object.keys(t).length && n.every((n) => Object.prototype.hasOwnProperty.call(t, n) && S(e[n], t[n]));
74
+ }, C = (e, t) => (n, r) => {
75
+ let i = e ? e(n.data, r) : void 0, a = i === void 0 ? x(n.data, r) : i;
76
+ return a === n.data || S(a, n.data) ? n : {
77
77
  data: a,
78
- renderData: t === "live" || h.has(r.type) ? a : n.renderData,
79
- compileSeq: n.compileSeq + +!!m.has(r.type),
78
+ renderData: t === "live" || g.has(r.type) ? a : n.renderData,
79
+ compileSeq: n.compileSeq + +!!h.has(r.type),
80
80
  version: n.version + 1
81
81
  };
82
- }, C = ({ Form: l, reduce: u, formModel: d = "live" }) => {
83
- let [m] = a(() => new URLSearchParams(window.location.search)), [h] = a(m.get("id") ?? void 0), [v] = a(m.get("access_token") ?? void 0), [y] = a(m.get("origin") ?? void 0), [b, x] = a([]), C = n(() => S(u, d), [u, d]), [E, D] = r(C, void 0, () => ({
82
+ }, w = ({ Form: u, reduce: d, formModel: f = "live", score: h }) => {
83
+ let [g] = a(() => new URLSearchParams(window.location.search)), [b] = a(g.get("id") ?? void 0), [x] = a(g.get("access_token") ?? void 0), [S] = a(g.get("origin") ?? void 0), [w, E] = a([]), D = n(() => C(d, f), [d, f]), [A, j] = r(D, void 0, () => ({
84
84
  data: {},
85
85
  renderData: {},
86
86
  compileSeq: 0,
87
87
  version: 0
88
- })), { data: O, renderData: k, compileSeq: A, version: j } = E;
88
+ })), { data: M, renderData: N, compileSeq: P, version: F } = A;
89
89
  t(() => {
90
- let e = m.get("data");
91
- e && D({
90
+ let e = g.get("data");
91
+ e && j({
92
92
  type: "init",
93
93
  args: JSON.parse(e)
94
94
  });
95
95
  }, []), t(() => {
96
- y && window.parent.postMessage({
96
+ S && window.parent.postMessage({
97
97
  type: "onload",
98
- data: O
99
- }, y);
98
+ data: M
99
+ }, S);
100
100
  }, []), t(() => {
101
- y && window.parent.postMessage({
101
+ S && window.parent.postMessage({
102
102
  type: "data-updated",
103
- data: O
104
- }, y);
105
- }, [j]);
106
- let M = o(h ? {
107
- accessToken: v,
108
- id: h
109
- } : null, p, {
103
+ data: M
104
+ }, S);
105
+ }, [F]);
106
+ let I = o(b ? {
107
+ accessToken: x,
108
+ id: b
109
+ } : null, m, {
110
110
  revalidateOnFocus: !1,
111
111
  revalidateOnReconnect: !1,
112
112
  revalidateIfStale: !1
113
113
  });
114
114
  t(() => {
115
- if (M.data === void 0) return;
116
- let e = g(M.data);
117
- D({
115
+ if (I.data === void 0) return;
116
+ let e = _(I.data);
117
+ j({
118
118
  type: "loaded",
119
119
  args: e.data
120
- }), x(e.errors);
121
- }, [M.data]);
122
- let N = i(0);
120
+ }), E(e.errors);
121
+ }, [I.data]);
122
+ let L = i(0);
123
123
  t(() => {
124
- if (A === 0 || !h) return;
125
- let e = ++N.current;
126
- y && window.parent.postMessage({ focus: {
124
+ if (P === 0 || !b) return;
125
+ let e = ++L.current;
126
+ S && window.parent.postMessage({ focus: {
127
127
  type: "update",
128
- value: O
129
- } }, y);
128
+ value: M
129
+ } }, S);
130
130
  let t = !1;
131
131
  return (async () => {
132
132
  try {
133
- let n = await f({
134
- accessToken: v,
135
- id: h,
136
- data: O
133
+ let n = await p({
134
+ accessToken: x,
135
+ id: b,
136
+ data: M
137
137
  });
138
- if (t || e !== N.current) return;
139
- let r = g(n);
140
- D({
138
+ if (t || e !== L.current) return;
139
+ let r = _(n);
140
+ j({
141
141
  type: "compiled",
142
142
  args: r.data
143
- }), x(r.errors);
143
+ }), E(r.errors);
144
144
  } catch (n) {
145
- !t && e === N.current && x([{ message: String(n?.message ?? n) }]);
145
+ !t && e === L.current && E([{ message: String(n?.message ?? n) }]);
146
146
  }
147
147
  })(), () => {
148
148
  t = !0;
149
149
  };
150
- }, [A]);
151
- let P = _(k, []) ? k : O, F = n(() => ({
152
- data: P,
153
- errors: b,
154
- apply: D
155
- }), [P, b]), I = e(() => M.mutate(), [M]);
156
- return _(O, b) ? /* @__PURE__ */ s(l, { state: F }) : M.error ? /* @__PURE__ */ c("div", {
150
+ }, [P]);
151
+ let R = v(N, []) ? N : M, [z, B] = a(!1);
152
+ t(() => B(!1), [P]);
153
+ let V = n(() => h && w.length === 0 ? h(M) : void 0, [
154
+ h,
155
+ M,
156
+ w
157
+ ]), H = z && V && y(R) ? {
158
+ ...R,
159
+ showValidationUI: !0
160
+ } : R, U = n(() => ({
161
+ data: H,
162
+ errors: w,
163
+ apply: j
164
+ }), [H, w]), W = e(() => I.mutate(), [I]);
165
+ return v(M, w) ? V ? /* @__PURE__ */ l(s, { children: [/* @__PURE__ */ c(u, { state: U }), /* @__PURE__ */ c(T, {
166
+ result: V,
167
+ checked: z,
168
+ onCheck: () => B(!0)
169
+ })] }) : /* @__PURE__ */ c(u, { state: U }) : I.error ? /* @__PURE__ */ l("div", {
157
170
  role: "alert",
158
- style: w,
171
+ style: O,
159
172
  children: [
160
- /* @__PURE__ */ s("p", {
173
+ /* @__PURE__ */ c("p", {
161
174
  style: {
162
175
  margin: 0,
163
176
  fontWeight: 600
164
177
  },
165
178
  children: "Couldn’t load this form."
166
179
  }),
167
- /* @__PURE__ */ s("p", {
180
+ /* @__PURE__ */ c("p", {
168
181
  style: {
169
182
  margin: "4px 0 12px",
170
183
  color: "#555"
171
184
  },
172
- children: String(M.error?.message ?? M.error)
185
+ children: String(I.error?.message ?? I.error)
173
186
  }),
174
- /* @__PURE__ */ s("button", {
187
+ /* @__PURE__ */ c("button", {
175
188
  type: "button",
176
- style: T,
177
- onClick: I,
189
+ style: k,
190
+ onClick: W,
178
191
  children: "Retry"
179
192
  })
180
193
  ]
181
- }) : M.isLoading ? /* @__PURE__ */ s("div", {
194
+ }) : I.isLoading ? /* @__PURE__ */ c("div", {
182
195
  role: "status",
183
196
  "aria-live": "polite",
184
- style: w,
197
+ style: O,
185
198
  children: "Loading…"
186
- }) : /* @__PURE__ */ s("div", {});
187
- }, w = {
199
+ }) : /* @__PURE__ */ c("div", {});
200
+ }, T = ({ result: e, checked: t, onCheck: n }) => /* @__PURE__ */ l("div", {
201
+ style: E,
202
+ children: [/* @__PURE__ */ c("button", {
203
+ type: "button",
204
+ style: D,
205
+ onClick: n,
206
+ disabled: t,
207
+ children: "Check"
208
+ }), /* @__PURE__ */ c("span", {
209
+ "aria-live": "polite",
210
+ style: {
211
+ color: "#52525b",
212
+ minWidth: "8em"
213
+ },
214
+ children: t ? `${e.score} of ${e.max} points` : ""
215
+ })]
216
+ }), E = {
217
+ display: "flex",
218
+ alignItems: "center",
219
+ justifyContent: "center",
220
+ gap: "12px",
221
+ padding: "12px",
222
+ font: "14px/1.4 system-ui, sans-serif"
223
+ }, D = {
224
+ padding: "8px 16px",
225
+ border: "none",
226
+ borderRadius: "6px",
227
+ background: "#18181b",
228
+ color: "#fff",
229
+ fontWeight: 500,
230
+ cursor: "pointer"
231
+ }, O = {
188
232
  display: "flex",
189
233
  flexDirection: "column",
190
234
  alignItems: "center",
@@ -193,7 +237,7 @@ var x = (e, t) => {
193
237
  padding: "24px",
194
238
  textAlign: "center",
195
239
  font: "14px/1.4 system-ui, sans-serif"
196
- }, T = {
240
+ }, k = {
197
241
  padding: "6px 14px",
198
242
  border: "1px solid #888",
199
243
  borderRadius: "4px",
@@ -202,12 +246,12 @@ var x = (e, t) => {
202
246
  };
203
247
  //#endregion
204
248
  //#region src/components/form/Form.tsx
205
- function E({ errors: e }) {
206
- return /* @__PURE__ */ s("div", {
249
+ function A({ errors: e }) {
250
+ return /* @__PURE__ */ c("div", {
207
251
  className: "flex flex-col gap-2 font-mono text-xs",
208
- children: e.map((e, t) => /* @__PURE__ */ c("div", {
252
+ children: e.map((e, t) => /* @__PURE__ */ l("div", {
209
253
  className: "rounded-md border border-red-200 bg-red-50 p-3 text-red-800",
210
- children: [e.message, typeof e.from == "number" && e.from >= 0 && /* @__PURE__ */ c("span", {
254
+ children: [e.message, typeof e.from == "number" && e.from >= 0 && /* @__PURE__ */ l("span", {
211
255
  className: "ml-2 text-red-400",
212
256
  children: [
213
257
  "[",
@@ -219,16 +263,16 @@ function E({ errors: e }) {
219
263
  }, t))
220
264
  });
221
265
  }
222
- var D = ({ state: e }) => {
266
+ var j = ({ state: e }) => {
223
267
  let t = e.errors ?? [];
224
- if (t.length > 0) return /* @__PURE__ */ s(E, { errors: t });
268
+ if (t.length > 0) return /* @__PURE__ */ c(A, { errors: t });
225
269
  let n = e.data;
226
- return /* @__PURE__ */ s("pre", {
270
+ return /* @__PURE__ */ c("pre", {
227
271
  className: "rounded-md bg-zinc-50 text-zinc-900 text-xs font-mono p-4 overflow-auto",
228
272
  children: JSON.stringify(n, null, 2)
229
273
  });
230
274
  };
231
275
  //#endregion
232
- export { D as Form, C as View };
276
+ export { T as CheckBar, j as Form, w as View };
233
277
 
234
278
  //# 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.\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/** 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}: {\n Form: FormComponent;\n reduce?: LanguageReducer;\n formModel?: FormModel;\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 const formState = useMemo(() => ({ data: formData, errors, apply }), [formData, 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 return <Form state={formState} />;\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\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// 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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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@graffiticode/l0000-view",
6
- "version": "0.1.5",
6
+ "version": "0.2.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",