@intellectif/lk-react 1.0.0
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/LICENSE +21 -0
- package/dist/chunk-5HVUKXF3.js +28 -0
- package/dist/chunk-5HVUKXF3.js.map +1 -0
- package/dist/chunk-5KW7TFX3.js +240 -0
- package/dist/chunk-5KW7TFX3.js.map +1 -0
- package/dist/chunk-6OBRYL4C.cjs +84 -0
- package/dist/chunk-6OBRYL4C.cjs.map +1 -0
- package/dist/chunk-B3FUUKZ2.js +91 -0
- package/dist/chunk-B3FUUKZ2.js.map +1 -0
- package/dist/chunk-CPF2JJG6.cjs +96 -0
- package/dist/chunk-CPF2JJG6.cjs.map +1 -0
- package/dist/chunk-HD2AIQRD.cjs +30 -0
- package/dist/chunk-HD2AIQRD.cjs.map +1 -0
- package/dist/chunk-IZNMQSQP.js +65 -0
- package/dist/chunk-IZNMQSQP.js.map +1 -0
- package/dist/chunk-L3EV5LIX.js +77 -0
- package/dist/chunk-L3EV5LIX.js.map +1 -0
- package/dist/chunk-O3GWWJFS.cjs +242 -0
- package/dist/chunk-O3GWWJFS.cjs.map +1 -0
- package/dist/chunk-QBA6UXMS.cjs +160 -0
- package/dist/chunk-QBA6UXMS.cjs.map +1 -0
- package/dist/chunk-RI53A36N.cjs +98 -0
- package/dist/chunk-RI53A36N.cjs.map +1 -0
- package/dist/chunk-TVK3SVV6.cjs +67 -0
- package/dist/chunk-TVK3SVV6.cjs.map +1 -0
- package/dist/chunk-W2L66WHW.js +158 -0
- package/dist/chunk-W2L66WHW.js.map +1 -0
- package/dist/chunk-ZPD7BGCF.js +96 -0
- package/dist/chunk-ZPD7BGCF.js.map +1 -0
- package/dist/components/ActivitySequence.cjs +16 -0
- package/dist/components/ActivitySequence.cjs.map +1 -0
- package/dist/components/ActivitySequence.d.cts +25 -0
- package/dist/components/ActivitySequence.d.ts +25 -0
- package/dist/components/ActivitySequence.js +7 -0
- package/dist/components/ActivitySequence.js.map +1 -0
- package/dist/components/FillInTheBlanks.cjs +14 -0
- package/dist/components/FillInTheBlanks.cjs.map +1 -0
- package/dist/components/FillInTheBlanks.d.cts +21 -0
- package/dist/components/FillInTheBlanks.d.ts +21 -0
- package/dist/components/FillInTheBlanks.js +5 -0
- package/dist/components/FillInTheBlanks.js.map +1 -0
- package/dist/components/MultipleChoice.cjs +14 -0
- package/dist/components/MultipleChoice.cjs.map +1 -0
- package/dist/components/MultipleChoice.d.cts +12 -0
- package/dist/components/MultipleChoice.d.ts +12 -0
- package/dist/components/MultipleChoice.js +5 -0
- package/dist/components/MultipleChoice.js.map +1 -0
- package/dist/hooks/useActivityState.cjs +12 -0
- package/dist/hooks/useActivityState.cjs.map +1 -0
- package/dist/hooks/useActivityState.d.cts +22 -0
- package/dist/hooks/useActivityState.d.ts +22 -0
- package/dist/hooks/useActivityState.js +3 -0
- package/dist/hooks/useActivityState.js.map +1 -0
- package/dist/hooks/useXAPI.cjs +12 -0
- package/dist/hooks/useXAPI.cjs.map +1 -0
- package/dist/hooks/useXAPI.d.cts +15 -0
- package/dist/hooks/useXAPI.d.ts +15 -0
- package/dist/hooks/useXAPI.js +3 -0
- package/dist/hooks/useXAPI.js.map +1 -0
- package/dist/index.cjs +50 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/theme/ThemeProvider.cjs +24 -0
- package/dist/theme/ThemeProvider.cjs.map +1 -0
- package/dist/theme/ThemeProvider.d.cts +39 -0
- package/dist/theme/ThemeProvider.d.ts +39 -0
- package/dist/theme/ThemeProvider.js +3 -0
- package/dist/theme/ThemeProvider.js.map +1 -0
- package/dist/theme/defaults.css +50 -0
- package/dist/theme/skin.css +352 -0
- package/dist/types-iknDq5tf.d.cts +17 -0
- package/dist/types-iknDq5tf.d.ts +17 -0
- package/package.json +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Intellectif LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/hooks/useActivityState.ts
|
|
4
|
+
function useActivityState() {
|
|
5
|
+
const [state, setState] = useState("idle");
|
|
6
|
+
const startTimeRef = useRef(null);
|
|
7
|
+
const start = useCallback(() => {
|
|
8
|
+
if (startTimeRef.current === null) {
|
|
9
|
+
startTimeRef.current = Date.now();
|
|
10
|
+
}
|
|
11
|
+
setState((prev) => prev === "idle" ? "in-progress" : prev);
|
|
12
|
+
}, []);
|
|
13
|
+
const complete = useCallback(() => setState("completed"), []);
|
|
14
|
+
const review = useCallback(() => setState("reviewing"), []);
|
|
15
|
+
const reset = useCallback(() => {
|
|
16
|
+
startTimeRef.current = null;
|
|
17
|
+
setState("idle");
|
|
18
|
+
}, []);
|
|
19
|
+
const getTimeSpent = useCallback(
|
|
20
|
+
() => startTimeRef.current === null ? 0 : Date.now() - startTimeRef.current,
|
|
21
|
+
[]
|
|
22
|
+
);
|
|
23
|
+
return { state, start, complete, review, reset, getTimeSpent };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { useActivityState };
|
|
27
|
+
//# sourceMappingURL=chunk-5HVUKXF3.js.map
|
|
28
|
+
//# sourceMappingURL=chunk-5HVUKXF3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useActivityState.ts"],"names":[],"mappings":";;;AAyBO,SAAS,gBAAA,GAA2C;AACzD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAwB,MAAM,CAAA;AACxD,EAAA,MAAM,YAAA,GAAe,OAAsB,IAAI,CAAA;AAE/C,EAAA,MAAM,KAAA,GAAQ,YAAY,MAAM;AAG9B,IAAA,IAAI,YAAA,CAAa,YAAY,IAAA,EAAM;AACjC,MAAA,YAAA,CAAa,OAAA,GAAU,KAAK,GAAA,EAAI;AAAA,IAClC;AACA,IAAA,QAAA,CAAS,CAAC,IAAA,KAAU,IAAA,KAAS,MAAA,GAAS,gBAAgB,IAAK,CAAA;AAAA,EAC7D,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,WAAW,WAAA,CAAY,MAAM,SAAS,WAAW,CAAA,EAAG,EAAE,CAAA;AAE5D,EAAA,MAAM,SAAS,WAAA,CAAY,MAAM,SAAS,WAAW,CAAA,EAAG,EAAE,CAAA;AAE1D,EAAA,MAAM,KAAA,GAAQ,YAAY,MAAM;AAC9B,IAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AACvB,IAAA,QAAA,CAAS,MAAM,CAAA;AAAA,EACjB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,YAAA,GAAe,WAAA;AAAA,IACnB,MAAO,aAAa,OAAA,KAAY,IAAA,GAAO,IAAI,IAAA,CAAK,GAAA,KAAQ,YAAA,CAAa,OAAA;AAAA,IACrE;AAAC,GACH;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,QAAA,EAAU,MAAA,EAAQ,OAAO,YAAA,EAAa;AAC/D","file":"chunk-5HVUKXF3.js","sourcesContent":["'use client';\n\nimport { useCallback, useRef, useState } from 'react';\n\nexport type ActivityState = 'idle' | 'in-progress' | 'completed' | 'reviewing';\n\nexport interface UseActivityStateResult {\n state: ActivityState;\n /** idle → in-progress; records start time once. Idempotent: repeated calls\n * (e.g. every learner interaction) neither restart timing nor regress state. */\n start: () => void;\n /** → completed. */\n complete: () => void;\n /** → reviewing (e.g. when showing correct answers). */\n review: () => void;\n /** → idle and clears timing. Call when the activity `data` prop changes. */\n reset: () => void;\n /** Elapsed ms since `start()`; 0 when not started (or after `reset()`). */\n getTimeSpent: () => number;\n}\n\n/**\n * Tracks an activity's lifecycle state and elapsed time. Timing is held in a\n * ref (no re-render) and `getTimeSpent()` is read imperatively at submission.\n */\nexport function useActivityState(): UseActivityStateResult {\n const [state, setState] = useState<ActivityState>('idle');\n const startTimeRef = useRef<number | null>(null);\n\n const start = useCallback(() => {\n // Ref guard makes timing idempotent without a side effect in the updater\n // (StrictMode double-invokes updaters); the updater itself stays pure.\n if (startTimeRef.current === null) {\n startTimeRef.current = Date.now();\n }\n setState((prev) => (prev === 'idle' ? 'in-progress' : prev));\n }, []);\n\n const complete = useCallback(() => setState('completed'), []);\n\n const review = useCallback(() => setState('reviewing'), []);\n\n const reset = useCallback(() => {\n startTimeRef.current = null;\n setState('idle');\n }, []);\n\n const getTimeSpent = useCallback(\n () => (startTimeRef.current === null ? 0 : Date.now() - startTimeRef.current),\n [],\n );\n\n return { state, start, complete, review, reset, getTimeSpent };\n}\n"]}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { ActivityErrorBoundary, isDevelopment, ActivityMedia, FeedbackRegion, ANONYMOUS_ACTOR, objectIdFor } from './chunk-L3EV5LIX.js';
|
|
2
|
+
import { useActivityState } from './chunk-5HVUKXF3.js';
|
|
3
|
+
import { validateActivity, ActivitySchemaError, score, xAPIBuilder } from '@intellectif/lk-core';
|
|
4
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
var PLACEHOLDER = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
8
|
+
function parsePassage(passage) {
|
|
9
|
+
const segments = [];
|
|
10
|
+
const re = new RegExp(PLACEHOLDER);
|
|
11
|
+
let last = 0;
|
|
12
|
+
let ordinal = 0;
|
|
13
|
+
let match = re.exec(passage);
|
|
14
|
+
while (match !== null) {
|
|
15
|
+
if (match.index > last) {
|
|
16
|
+
segments.push({ kind: "text", value: passage.slice(last, match.index) });
|
|
17
|
+
}
|
|
18
|
+
ordinal += 1;
|
|
19
|
+
segments.push({ kind: "blank", id: match[1], ordinal });
|
|
20
|
+
last = match.index + match[0].length;
|
|
21
|
+
match = re.exec(passage);
|
|
22
|
+
}
|
|
23
|
+
if (last < passage.length) {
|
|
24
|
+
segments.push({ kind: "text", value: passage.slice(last) });
|
|
25
|
+
}
|
|
26
|
+
return segments;
|
|
27
|
+
}
|
|
28
|
+
function FillInTheBlanks({
|
|
29
|
+
data,
|
|
30
|
+
onComplete,
|
|
31
|
+
onInteraction,
|
|
32
|
+
theme,
|
|
33
|
+
locale,
|
|
34
|
+
disabled,
|
|
35
|
+
showCorrectAnswers
|
|
36
|
+
}) {
|
|
37
|
+
const devError = useMemo(() => {
|
|
38
|
+
if (!isDevelopment()) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const result2 = validateActivity("fill-in-the-blanks", data);
|
|
42
|
+
return result2.success ? null : new ActivitySchemaError("fill-in-the-blanks", result2.errors);
|
|
43
|
+
}, [data]);
|
|
44
|
+
const { state, start, complete, getTimeSpent, reset } = useActivityState();
|
|
45
|
+
const [answers, setAnswers] = useState({});
|
|
46
|
+
const [revealed, setRevealed] = useState(() => /* @__PURE__ */ new Set());
|
|
47
|
+
const [result, setResult] = useState(null);
|
|
48
|
+
const [summary, setSummary] = useState(null);
|
|
49
|
+
const [feedbackHidden, setFeedbackHidden] = useState(false);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setAnswers({});
|
|
52
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
53
|
+
setResult(null);
|
|
54
|
+
setSummary(null);
|
|
55
|
+
setFeedbackHidden(false);
|
|
56
|
+
reset();
|
|
57
|
+
}, [data, reset]);
|
|
58
|
+
const segments = useMemo(() => parsePassage(data.passage), [data.passage]);
|
|
59
|
+
const blankById = useMemo(() => new Map(data.blanks.map((b) => [b.id, b])), [data.blanks]);
|
|
60
|
+
if (devError) {
|
|
61
|
+
throw devError;
|
|
62
|
+
}
|
|
63
|
+
const submitted = state === "completed";
|
|
64
|
+
const inactive = disabled === true || submitted;
|
|
65
|
+
const fireInteraction = (type, payload) => {
|
|
66
|
+
onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });
|
|
67
|
+
};
|
|
68
|
+
const handleChange = (blankId, value) => {
|
|
69
|
+
if (inactive) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (state === "idle") {
|
|
73
|
+
start();
|
|
74
|
+
}
|
|
75
|
+
setAnswers((prev) => ({ ...prev, [blankId]: value }));
|
|
76
|
+
fireInteraction("blank-filled", { blankId, value });
|
|
77
|
+
};
|
|
78
|
+
const toggleHint = (blankId) => {
|
|
79
|
+
if (inactive) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (state === "idle") {
|
|
83
|
+
start();
|
|
84
|
+
}
|
|
85
|
+
setRevealed((prev) => {
|
|
86
|
+
const next = new Set(prev);
|
|
87
|
+
if (next.has(blankId)) {
|
|
88
|
+
next.delete(blankId);
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
next.add(blankId);
|
|
92
|
+
return next;
|
|
93
|
+
});
|
|
94
|
+
if (!revealed.has(blankId)) {
|
|
95
|
+
fireInteraction("hint-requested", { blankId });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handleSubmit = (event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
if (inactive) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const response = { type: "fill-in-the-blanks", answers };
|
|
104
|
+
const scoringResult = score("fill-in-the-blanks", data, response);
|
|
105
|
+
complete();
|
|
106
|
+
const timeSpent = getTimeSpent();
|
|
107
|
+
const xapiStatement = xAPIBuilder.buildAnsweredStatement({
|
|
108
|
+
actor: ANONYMOUS_ACTOR,
|
|
109
|
+
object: { id: objectIdFor(data.id), name: { "en-US": data.title } },
|
|
110
|
+
scoringResult,
|
|
111
|
+
timeSpentMs: timeSpent,
|
|
112
|
+
response: JSON.stringify(answers)
|
|
113
|
+
});
|
|
114
|
+
setResult(scoringResult);
|
|
115
|
+
onComplete({
|
|
116
|
+
score: scoringResult.score,
|
|
117
|
+
maxScore: scoringResult.maxScore,
|
|
118
|
+
passed: scoringResult.passed,
|
|
119
|
+
timeSpent,
|
|
120
|
+
xapiStatement
|
|
121
|
+
});
|
|
122
|
+
const overall = scoringResult.passed ? data.feedback?.correct : data.feedback?.incorrect;
|
|
123
|
+
setSummary(
|
|
124
|
+
`Answer submitted. Score ${Math.round(scoringResult.score * 100)}%. ${scoringResult.passed ? "Passed." : "Not passed."}${overall ? ` ${overall}` : ""}`
|
|
125
|
+
);
|
|
126
|
+
fireInteraction("submitted", { answers, score: scoringResult.score });
|
|
127
|
+
};
|
|
128
|
+
const correctByBlank = useMemo(() => {
|
|
129
|
+
const map = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const detail of result?.details ?? []) {
|
|
131
|
+
map.set(detail.itemId, detail.correct);
|
|
132
|
+
}
|
|
133
|
+
return map;
|
|
134
|
+
}, [result]);
|
|
135
|
+
return /* @__PURE__ */ jsxs(
|
|
136
|
+
"form",
|
|
137
|
+
{
|
|
138
|
+
className: "lk-fib",
|
|
139
|
+
"aria-label": data.title,
|
|
140
|
+
lang: locale,
|
|
141
|
+
style: theme,
|
|
142
|
+
onSubmit: handleSubmit,
|
|
143
|
+
children: [
|
|
144
|
+
data.media ? /* @__PURE__ */ jsx(ActivityMedia, { media: data.media }) : null,
|
|
145
|
+
/* @__PURE__ */ jsxs("fieldset", { disabled: inactive, children: [
|
|
146
|
+
/* @__PURE__ */ jsx("p", { className: "lk-fib-passage", children: segments.map((seg, i) => {
|
|
147
|
+
if (seg.kind === "text") {
|
|
148
|
+
return /* @__PURE__ */ jsx("span", { children: seg.value }, `t${i}`);
|
|
149
|
+
}
|
|
150
|
+
const blank = blankById.get(seg.id);
|
|
151
|
+
if (!blank) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const isCorrect = correctByBlank.get(seg.id) === true;
|
|
155
|
+
const hintId = `${data.id}-hint-${seg.id}`;
|
|
156
|
+
if (submitted && showCorrectAnswers && !isCorrect) {
|
|
157
|
+
return /* @__PURE__ */ jsx("span", { className: "lk-fib-answer", "data-correct": "false", children: blank.acceptedAnswers[0] }, seg.id);
|
|
158
|
+
}
|
|
159
|
+
return /* @__PURE__ */ jsxs("span", { className: "lk-fib-blank", children: [
|
|
160
|
+
/* @__PURE__ */ jsx(
|
|
161
|
+
"input",
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
"aria-label": `Fill in blank ${seg.ordinal}`,
|
|
165
|
+
"aria-describedby": blank.hint ? hintId : void 0,
|
|
166
|
+
value: answers[seg.id] ?? "",
|
|
167
|
+
disabled: inactive,
|
|
168
|
+
"aria-disabled": inactive || void 0,
|
|
169
|
+
"data-correct": submitted ? String(isCorrect) : void 0,
|
|
170
|
+
onChange: (e) => handleChange(seg.id, e.target.value)
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
blank.hint ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
174
|
+
/* @__PURE__ */ jsx(
|
|
175
|
+
"button",
|
|
176
|
+
{
|
|
177
|
+
type: "button",
|
|
178
|
+
className: "lk-fib-hint-btn",
|
|
179
|
+
"aria-controls": hintId,
|
|
180
|
+
"aria-expanded": revealed.has(seg.id),
|
|
181
|
+
"aria-label": revealed.has(seg.id) ? "Hide hint" : "Show hint",
|
|
182
|
+
disabled: inactive,
|
|
183
|
+
onClick: () => toggleHint(seg.id),
|
|
184
|
+
children: /* @__PURE__ */ jsx(
|
|
185
|
+
"svg",
|
|
186
|
+
{
|
|
187
|
+
className: "lk-fib-hint-icon",
|
|
188
|
+
viewBox: "0 0 24 24",
|
|
189
|
+
width: "16",
|
|
190
|
+
height: "16",
|
|
191
|
+
"aria-hidden": "true",
|
|
192
|
+
focusable: "false",
|
|
193
|
+
children: /* @__PURE__ */ jsx(
|
|
194
|
+
"path",
|
|
195
|
+
{
|
|
196
|
+
fill: "currentColor",
|
|
197
|
+
d: "M9 21h6v-1H9v1Zm3-19a7 7 0 0 0-4 12.74V17h8v-2.26A7 7 0 0 0 12 2Z"
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
),
|
|
204
|
+
/* @__PURE__ */ jsx("span", { id: hintId, "aria-live": "polite", children: revealed.has(seg.id) ? blank.hint : "" })
|
|
205
|
+
] }) : null,
|
|
206
|
+
submitted && !feedbackHidden && blank.feedback ? /* @__PURE__ */ jsx(
|
|
207
|
+
"span",
|
|
208
|
+
{
|
|
209
|
+
className: "lk-fib-blank-feedback",
|
|
210
|
+
role: "note",
|
|
211
|
+
"data-correct": String(isCorrect),
|
|
212
|
+
children: blank.feedback
|
|
213
|
+
}
|
|
214
|
+
) : null
|
|
215
|
+
] }, seg.id);
|
|
216
|
+
}) }),
|
|
217
|
+
/* @__PURE__ */ jsx("button", { type: "submit", disabled: inactive, children: "Check answers" })
|
|
218
|
+
] }),
|
|
219
|
+
submitted && data.blanks.some((b) => b.feedback) ? /* @__PURE__ */ jsx(
|
|
220
|
+
"button",
|
|
221
|
+
{
|
|
222
|
+
type: "button",
|
|
223
|
+
className: "lk-fib-feedback-toggle",
|
|
224
|
+
"aria-expanded": !feedbackHidden,
|
|
225
|
+
onClick: () => setFeedbackHidden((h) => !h),
|
|
226
|
+
children: feedbackHidden ? "Show feedback" : "Hide feedback"
|
|
227
|
+
}
|
|
228
|
+
) : null,
|
|
229
|
+
/* @__PURE__ */ jsx(FeedbackRegion, { id: `${data.id}-feedback`, children: summary })
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
function FillInTheBlanks2(props) {
|
|
235
|
+
return /* @__PURE__ */ jsx(ActivityErrorBoundary, { activityTitle: props.data?.title, children: /* @__PURE__ */ jsx(FillInTheBlanks, { ...props }) });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { FillInTheBlanks2 as FillInTheBlanks };
|
|
239
|
+
//# sourceMappingURL=chunk-5KW7TFX3.js.map
|
|
240
|
+
//# sourceMappingURL=chunk-5KW7TFX3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/FillInTheBlanks/FillInTheBlanks.tsx","../src/components/FillInTheBlanks/index.tsx"],"names":["result","FillInTheBlanks","jsx"],"mappings":"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","file":"chunk-5KW7TFX3.js","sourcesContent":["'use client';\n\nimport {\n ActivitySchemaError,\n type FillInTheBlanksData,\n type ScoringResult,\n score,\n validateActivity,\n xAPIBuilder,\n} from '@intellectif/lk-core';\nimport { type CSSProperties, useEffect, useMemo, useState } from 'react';\nimport { useActivityState } from '../../hooks/useActivityState.js';\nimport { ANONYMOUS_ACTOR, isDevelopment, objectIdFor } from '../_internal.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport { FeedbackRegion } from '../shared/FeedbackRegion.js';\nimport type { ActivityProps } from '../types.js';\n\nexport interface FillInTheBlanksProps extends ActivityProps<FillInTheBlanksData> {\n /**\n * After submission, replace each incorrectly-answered blank with its first\n * accepted answer, styled distinctly (Req 5.7). Component-specific prop: it\n * is presentation, not activity data, and is outside the fixed Req-3.1 set.\n */\n showCorrectAnswers?: boolean;\n}\n\nconst PLACEHOLDER = /\\{\\{\\s*([^{}]+?)\\s*\\}\\}/g;\n\ntype Segment = { kind: 'text'; value: string } | { kind: 'blank'; id: string; ordinal: number };\n\nfunction parsePassage(passage: string): Segment[] {\n const segments: Segment[] = [];\n const re = new RegExp(PLACEHOLDER);\n let last = 0;\n let ordinal = 0;\n let match = re.exec(passage);\n while (match !== null) {\n if (match.index > last) {\n segments.push({ kind: 'text', value: passage.slice(last, match.index) });\n }\n ordinal += 1;\n segments.push({ kind: 'blank', id: match[1] as string, ordinal });\n last = match.index + match[0].length;\n match = re.exec(passage);\n }\n if (last < passage.length) {\n segments.push({ kind: 'text', value: passage.slice(last) });\n }\n return segments;\n}\n\nexport function FillInTheBlanks({\n data,\n onComplete,\n onInteraction,\n theme,\n locale,\n disabled,\n showCorrectAnswers,\n}: FillInTheBlanksProps) {\n // Dev-only boundary validation (Req 2.3); throws in render so the wrapping\n // ActivityErrorBoundary catches it. Re-runs only when data changes.\n const devError = useMemo(() => {\n if (!isDevelopment()) {\n return null;\n }\n const result = validateActivity('fill-in-the-blanks', data);\n return result.success ? null : new ActivitySchemaError('fill-in-the-blanks', result.errors);\n }, [data]);\n\n const { state, start, complete, getTimeSpent, reset } = useActivityState();\n const [answers, setAnswers] = useState<Record<string, string>>({});\n const [revealed, setRevealed] = useState<Set<string>>(() => new Set());\n const [result, setResult] = useState<ScoringResult | null>(null);\n const [summary, setSummary] = useState<string | null>(null);\n const [feedbackHidden, setFeedbackHidden] = useState(false);\n\n // `data` is an intentional reset trigger (Req 3.7), not read in the body.\n // biome-ignore lint/correctness/useExhaustiveDependencies: data is the reset trigger (Req 3.7)\n useEffect(() => {\n setAnswers({});\n setRevealed(new Set());\n setResult(null);\n setSummary(null);\n setFeedbackHidden(false);\n reset();\n }, [data, reset]);\n\n const segments = useMemo(() => parsePassage(data.passage), [data.passage]);\n const blankById = useMemo(() => new Map(data.blanks.map((b) => [b.id, b])), [data.blanks]);\n\n if (devError) {\n throw devError;\n }\n\n const submitted = state === 'completed';\n const inactive = disabled === true || submitted;\n\n const fireInteraction = (\n type: 'blank-filled' | 'hint-requested' | 'submitted',\n payload: Record<string, unknown>,\n ): void => {\n onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });\n };\n\n const handleChange = (blankId: string, value: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n setAnswers((prev) => ({ ...prev, [blankId]: value }));\n fireInteraction('blank-filled', { blankId, value });\n };\n\n const toggleHint = (blankId: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n setRevealed((prev) => {\n const next = new Set(prev);\n if (next.has(blankId)) {\n next.delete(blankId);\n return next;\n }\n next.add(blankId);\n return next;\n });\n // Only a reveal is a \"hint requested\"; hiding is not a new request.\n if (!revealed.has(blankId)) {\n fireInteraction('hint-requested', { blankId });\n }\n };\n\n const handleSubmit = (event: React.FormEvent): void => {\n event.preventDefault();\n if (inactive) {\n return;\n }\n const response = { type: 'fill-in-the-blanks', answers } as const;\n const scoringResult = score('fill-in-the-blanks', data, response);\n complete();\n const timeSpent = getTimeSpent();\n const xapiStatement = xAPIBuilder.buildAnsweredStatement({\n actor: ANONYMOUS_ACTOR,\n object: { id: objectIdFor(data.id), name: { 'en-US': data.title } },\n scoringResult,\n timeSpentMs: timeSpent,\n response: JSON.stringify(answers),\n });\n setResult(scoringResult);\n onComplete({\n score: scoringResult.score,\n maxScore: scoringResult.maxScore,\n passed: scoringResult.passed,\n timeSpent,\n xapiStatement,\n });\n const overall = scoringResult.passed ? data.feedback?.correct : data.feedback?.incorrect;\n setSummary(\n `Answer submitted. Score ${Math.round(scoringResult.score * 100)}%. ${\n scoringResult.passed ? 'Passed.' : 'Not passed.'\n }${overall ? ` ${overall}` : ''}`,\n );\n fireInteraction('submitted', { answers, score: scoringResult.score });\n };\n\n const correctByBlank = useMemo(() => {\n const map = new Map<string, boolean>();\n for (const detail of result?.details ?? []) {\n map.set(detail.itemId, detail.correct);\n }\n return map;\n }, [result]);\n\n return (\n <form\n className=\"lk-fib\"\n aria-label={data.title}\n lang={locale}\n style={theme as CSSProperties | undefined}\n onSubmit={handleSubmit}\n >\n {data.media ? <ActivityMedia media={data.media} /> : null}\n <fieldset disabled={inactive}>\n <p className=\"lk-fib-passage\">\n {segments.map((seg, i) => {\n if (seg.kind === 'text') {\n // biome-ignore lint/suspicious/noArrayIndexKey: passage segments are positional and static\n return <span key={`t${i}`}>{seg.value}</span>;\n }\n const blank = blankById.get(seg.id);\n if (!blank) {\n return null;\n }\n const isCorrect = correctByBlank.get(seg.id) === true;\n const hintId = `${data.id}-hint-${seg.id}`;\n if (submitted && showCorrectAnswers && !isCorrect) {\n return (\n <span key={seg.id} className=\"lk-fib-answer\" data-correct=\"false\">\n {blank.acceptedAnswers[0]}\n </span>\n );\n }\n return (\n <span key={seg.id} className=\"lk-fib-blank\">\n <input\n type=\"text\"\n aria-label={`Fill in blank ${seg.ordinal}`}\n aria-describedby={blank.hint ? hintId : undefined}\n value={answers[seg.id] ?? ''}\n disabled={inactive}\n aria-disabled={inactive || undefined}\n data-correct={submitted ? String(isCorrect) : undefined}\n onChange={(e) => handleChange(seg.id, e.target.value)}\n />\n {blank.hint ? (\n <>\n {/*\n Default affordance is an icon; the accessible NAME stays\n text via aria-label (screen readers + tests rely on it).\n The glyph is restylable/replaceable via .lk-fib-hint-btn.\n */}\n <button\n type=\"button\"\n className=\"lk-fib-hint-btn\"\n aria-controls={hintId}\n aria-expanded={revealed.has(seg.id)}\n aria-label={revealed.has(seg.id) ? 'Hide hint' : 'Show hint'}\n disabled={inactive}\n onClick={() => toggleHint(seg.id)}\n >\n <svg\n className=\"lk-fib-hint-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n fill=\"currentColor\"\n d=\"M9 21h6v-1H9v1Zm3-19a7 7 0 0 0-4 12.74V17h8v-2.26A7 7 0 0 0 12 2Z\"\n />\n </svg>\n </button>\n {/*\n Not role=\"tooltip\": a real ARIA tooltip is a named\n hover/focus popup with strict WCAG 1.4.13 constraints.\n This is an accessible click-to-toggle disclosure that is\n the input's aria-describedby target and is announced via\n aria-live; the skin gives it a tooltip-like *visual*\n without the tooltip *semantics*. (Refines Task 15.3.)\n */}\n <span id={hintId} aria-live=\"polite\">\n {revealed.has(seg.id) ? blank.hint : ''}\n </span>\n </>\n ) : null}\n {submitted && !feedbackHidden && blank.feedback ? (\n <span\n className=\"lk-fib-blank-feedback\"\n role=\"note\"\n data-correct={String(isCorrect)}\n >\n {blank.feedback}\n </span>\n ) : null}\n </span>\n );\n })}\n </p>\n <button type=\"submit\" disabled={inactive}>\n Check answers\n </button>\n </fieldset>\n {submitted && data.blanks.some((b) => b.feedback) ? (\n <button\n type=\"button\"\n className=\"lk-fib-feedback-toggle\"\n aria-expanded={!feedbackHidden}\n onClick={() => setFeedbackHidden((h) => !h)}\n >\n {feedbackHidden ? 'Show feedback' : 'Hide feedback'}\n </button>\n ) : null}\n <FeedbackRegion id={`${data.id}-feedback`}>{summary}</FeedbackRegion>\n </form>\n );\n}\n","'use client';\n\nimport { ActivityErrorBoundary } from '../ActivityErrorBoundary.js';\nimport {\n FillInTheBlanks as FillInTheBlanksCore,\n type FillInTheBlanksProps,\n} from './FillInTheBlanks.js';\n\nexport type { FillInTheBlanksProps } from './FillInTheBlanks.js';\n\n/**\n * Public Fill-in-the-Blanks activity: the core wrapped in\n * `ActivityErrorBoundary` so a render failure (incl. the dev schema-validation\n * throw) degrades to an accessible fallback instead of crashing the host.\n */\nexport function FillInTheBlanks(props: FillInTheBlanksProps) {\n return (\n <ActivityErrorBoundary activityTitle={props.data?.title}>\n <FillInTheBlanksCore {...props} />\n </ActivityErrorBoundary>\n );\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/components/ActivityErrorBoundary.tsx
|
|
7
|
+
function isProduction() {
|
|
8
|
+
const g = globalThis;
|
|
9
|
+
return g.process?.env?.NODE_ENV === "production";
|
|
10
|
+
}
|
|
11
|
+
var ActivityErrorBoundary = class extends react.Component {
|
|
12
|
+
state = { error: null };
|
|
13
|
+
static getDerivedStateFromError(error) {
|
|
14
|
+
return { error };
|
|
15
|
+
}
|
|
16
|
+
render() {
|
|
17
|
+
const { error } = this.state;
|
|
18
|
+
if (!error) {
|
|
19
|
+
return this.props.children;
|
|
20
|
+
}
|
|
21
|
+
if (!isProduction()) {
|
|
22
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", children: [
|
|
23
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Activity failed to render" }),
|
|
24
|
+
/* @__PURE__ */ jsxRuntime.jsx("pre", { children: error.stack ?? error.message })
|
|
25
|
+
] });
|
|
26
|
+
}
|
|
27
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "alert", children: this.props.activityTitle ? `"${this.props.activityTitle}" could not be displayed.` : "This activity could not be displayed." });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/components/_internal.ts
|
|
32
|
+
function isDevelopment() {
|
|
33
|
+
const g = globalThis;
|
|
34
|
+
return g.process?.env?.NODE_ENV !== "production";
|
|
35
|
+
}
|
|
36
|
+
var ANONYMOUS_ACTOR = {
|
|
37
|
+
objectType: "Agent",
|
|
38
|
+
account: { homePage: "https://github.com/intellectif/learning-kit", name: "anonymous" }
|
|
39
|
+
};
|
|
40
|
+
function objectIdFor(id) {
|
|
41
|
+
return `urn:learning-kit:activity:${encodeURIComponent(id)}`;
|
|
42
|
+
}
|
|
43
|
+
function ActivityMedia({ media }) {
|
|
44
|
+
const { type, url, alt, captionsUrl } = media;
|
|
45
|
+
if (type === "image") {
|
|
46
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsx("img", { className: "lk-media-el", src: url, alt: alt ?? "" }) });
|
|
47
|
+
}
|
|
48
|
+
if (type === "audio") {
|
|
49
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsxs("audio", { className: "lk-media-el", controls: true, "aria-label": alt || void 0, children: [
|
|
50
|
+
/* @__PURE__ */ jsxRuntime.jsx("source", { src: url }),
|
|
51
|
+
captionsUrl ? /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions", src: captionsUrl, default: true }) : null
|
|
52
|
+
] }) });
|
|
53
|
+
}
|
|
54
|
+
if (type === "embed") {
|
|
55
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-media-embed", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
56
|
+
"iframe",
|
|
57
|
+
{
|
|
58
|
+
className: "lk-media-el",
|
|
59
|
+
src: url,
|
|
60
|
+
title: alt ?? "Embedded media",
|
|
61
|
+
loading: "lazy",
|
|
62
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
63
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
64
|
+
allowFullScreen: true
|
|
65
|
+
}
|
|
66
|
+
) }) });
|
|
67
|
+
}
|
|
68
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsxs("video", { className: "lk-media-el", controls: true, "aria-label": alt || void 0, children: [
|
|
69
|
+
/* @__PURE__ */ jsxRuntime.jsx("source", { src: url }),
|
|
70
|
+
captionsUrl ? /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions", src: captionsUrl, default: true }) : null
|
|
71
|
+
] }) });
|
|
72
|
+
}
|
|
73
|
+
function FeedbackRegion({ id, className, children }) {
|
|
74
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { id, className, "aria-live": "polite", "aria-atomic": "true", children });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
exports.ANONYMOUS_ACTOR = ANONYMOUS_ACTOR;
|
|
78
|
+
exports.ActivityErrorBoundary = ActivityErrorBoundary;
|
|
79
|
+
exports.ActivityMedia = ActivityMedia;
|
|
80
|
+
exports.FeedbackRegion = FeedbackRegion;
|
|
81
|
+
exports.isDevelopment = isDevelopment;
|
|
82
|
+
exports.objectIdFor = objectIdFor;
|
|
83
|
+
//# sourceMappingURL=chunk-6OBRYL4C.cjs.map
|
|
84
|
+
//# sourceMappingURL=chunk-6OBRYL4C.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ActivityErrorBoundary.tsx","../src/components/_internal.ts","../src/components/shared/ActivityMedia.tsx","../src/components/shared/FeedbackRegion.tsx"],"names":["Component","jsxs","jsx"],"mappings":"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","file":"chunk-6OBRYL4C.cjs","sourcesContent":["'use client';\n\nimport { Component, type ReactNode } from 'react';\n\nfunction isProduction(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV === 'production';\n}\n\nexport interface ActivityErrorBoundaryProps {\n /** Shown in the production fallback when a render error occurs. */\n activityTitle?: string;\n children: ReactNode;\n}\n\ninterface ActivityErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render errors in an activity subtree. In development it renders the\n * full error stack to aid debugging; in production it renders an accessible,\n * generic fallback (optionally naming the activity) so a single broken\n * activity never crashes the host application.\n */\nexport class ActivityErrorBoundary extends Component<\n ActivityErrorBoundaryProps,\n ActivityErrorBoundaryState\n> {\n state: ActivityErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ActivityErrorBoundaryState {\n return { error };\n }\n\n render(): ReactNode {\n const { error } = this.state;\n if (!error) {\n return this.props.children;\n }\n\n if (!isProduction()) {\n return (\n <div role=\"alert\">\n <strong>Activity failed to render</strong>\n <pre>{error.stack ?? error.message}</pre>\n </div>\n );\n }\n\n return (\n <div role=\"alert\">\n {this.props.activityTitle\n ? `\"${this.props.activityTitle}\" could not be displayed.`\n : 'This activity could not be displayed.'}\n </div>\n );\n }\n}\n","import type { XAPIActor } from '@intellectif/lk-core';\n\n/** Dev = anything other than an explicit production NODE_ENV (browser-safe). */\nexport function isDevelopment(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV !== 'production';\n}\n\n/**\n * Activity components cannot know the learner's identity (Req 3.1 fixes the\n * prop set). They emit a structurally-valid statement with this anonymous\n * actor; real identity is applied by the useXAPI/LRS layer (XAPIConfig.actor).\n */\nexport const ANONYMOUS_ACTOR: XAPIActor = {\n objectType: 'Agent',\n account: { homePage: 'https://github.com/intellectif/learning-kit', name: 'anonymous' },\n};\n\n/** `data.id` is not an IRI; xAPI object ids must be. Wrap it as a URN. */\nexport function objectIdFor(id: string): string {\n return `urn:learning-kit:activity:${encodeURIComponent(id)}`;\n}\n","import type { ActivityMedia as ActivityMediaData } from '@intellectif/lk-core';\n\n/**\n * Presentational media block shown above a question or passage. URL-only:\n * the SDK does not host media (consumer responsibility). Accessibility:\n * images use `alt` (schema requires it non-empty); audio/video expose an\n * optional label and a captions `<track>` when `captionsUrl` is provided;\n * `embed` renders a responsive sandboxed iframe with `alt` as its required\n * accessible `title` (Req 14.5).\n */\nexport function ActivityMedia({ media }: { media: ActivityMediaData }): React.JSX.Element {\n const { type, url, alt, captionsUrl } = media;\n\n if (type === 'image') {\n return (\n <figure className=\"lk-media\">\n <img className=\"lk-media-el\" src={url} alt={alt ?? ''} />\n </figure>\n );\n }\n\n if (type === 'audio') {\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio className=\"lk-media-el\" controls aria-label={alt || undefined}>\n <source src={url} />\n {captionsUrl ? <track kind=\"captions\" src={captionsUrl} default /> : null}\n </audio>\n </figure>\n );\n }\n\n if (type === 'embed') {\n return (\n <figure className=\"lk-media\">\n <div className=\"lk-media-embed\">\n <iframe\n className=\"lk-media-el\"\n src={url}\n title={alt ?? 'Embedded media'}\n loading=\"lazy\"\n referrerPolicy=\"strict-origin-when-cross-origin\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n />\n </div>\n </figure>\n );\n }\n\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <video className=\"lk-media-el\" controls aria-label={alt || undefined}>\n <source src={url} />\n {captionsUrl ? <track kind=\"captions\" src={captionsUrl} default /> : null}\n </video>\n </figure>\n );\n}\n","import type { ReactNode } from 'react';\n\nexport interface FeedbackRegionProps {\n /** Identifier so activity inputs can reference this region via aria. */\n id?: string;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * A polite ARIA live region. Screen readers announce content injected here\n * (score, feedback, state changes) without moving focus. Pure/presentational\n * — safe to render in any environment.\n */\nexport function FeedbackRegion({ id, className, children }: FeedbackRegionProps) {\n return (\n <div id={id} className={className} aria-live=\"polite\" aria-atomic=\"true\">\n {children}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createContext, useSyncExternalStore, useMemo, useContext } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/theme/ThemeProvider.tsx
|
|
5
|
+
|
|
6
|
+
// src/theme/dark-theme.ts
|
|
7
|
+
var darkTheme = {
|
|
8
|
+
"--lk-color-primary": "#fafafa",
|
|
9
|
+
"--lk-color-primary-hover": "#e4e4e7",
|
|
10
|
+
"--lk-color-surface": "#09090b",
|
|
11
|
+
"--lk-color-surface-raised": "#18181b",
|
|
12
|
+
"--lk-color-border": "#27272a",
|
|
13
|
+
"--lk-color-text": "#fafafa",
|
|
14
|
+
"--lk-color-text-muted": "#a1a1aa",
|
|
15
|
+
"--lk-color-success": "#4ade80",
|
|
16
|
+
"--lk-color-error": "#f87171",
|
|
17
|
+
"--lk-color-warning": "#fbbf24",
|
|
18
|
+
"--lk-color-focus-ring": "#fafafa"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/theme/tokens.ts
|
|
22
|
+
var defaultTheme = {
|
|
23
|
+
// ── Colours (light) — neutral monochrome; semantic success/error/warning
|
|
24
|
+
// retained for status legibility ────────────────────────────────────────
|
|
25
|
+
"--lk-color-primary": "#18181b",
|
|
26
|
+
"--lk-color-primary-hover": "#27272a",
|
|
27
|
+
"--lk-color-surface": "#ffffff",
|
|
28
|
+
"--lk-color-surface-raised": "#fafafa",
|
|
29
|
+
"--lk-color-border": "#e4e4e7",
|
|
30
|
+
"--lk-color-text": "#18181b",
|
|
31
|
+
"--lk-color-text-muted": "#52525b",
|
|
32
|
+
"--lk-color-success": "#15803d",
|
|
33
|
+
"--lk-color-error": "#b91c1c",
|
|
34
|
+
"--lk-color-warning": "#b45309",
|
|
35
|
+
"--lk-color-focus-ring": "#18181b",
|
|
36
|
+
// ── Spacing ──────────────────────────────────────────────────────────────
|
|
37
|
+
"--lk-spacing-xs": "4px",
|
|
38
|
+
"--lk-spacing-sm": "8px",
|
|
39
|
+
"--lk-spacing-md": "16px",
|
|
40
|
+
"--lk-spacing-lg": "24px",
|
|
41
|
+
"--lk-spacing-xl": "32px",
|
|
42
|
+
// ── Typography ───────────────────────────────────────────────────────────
|
|
43
|
+
"--lk-font-family-base": 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
44
|
+
"--lk-font-size-sm": "0.875rem",
|
|
45
|
+
"--lk-font-size-base": "1rem",
|
|
46
|
+
"--lk-font-size-lg": "1.125rem",
|
|
47
|
+
"--lk-font-weight-normal": "400",
|
|
48
|
+
"--lk-font-weight-bold": "700",
|
|
49
|
+
"--lk-line-height-base": "1.5",
|
|
50
|
+
// ── Shape ────────────────────────────────────────────────────────────────
|
|
51
|
+
"--lk-radius-sm": "0.25rem",
|
|
52
|
+
"--lk-radius-base": "0.5rem",
|
|
53
|
+
"--lk-radius-lg": "0.75rem",
|
|
54
|
+
// ── Motion ───────────────────────────────────────────────────────────────
|
|
55
|
+
"--lk-transition-fast": "150ms ease",
|
|
56
|
+
"--lk-transition-base": "250ms ease"
|
|
57
|
+
};
|
|
58
|
+
var ThemeContext = createContext(defaultTheme);
|
|
59
|
+
var DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
60
|
+
function subscribePrefersDark(onChange) {
|
|
61
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
62
|
+
return () => {
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const mql = window.matchMedia(DARK_QUERY);
|
|
66
|
+
mql.addEventListener("change", onChange);
|
|
67
|
+
return () => mql.removeEventListener("change", onChange);
|
|
68
|
+
}
|
|
69
|
+
function getPrefersDark() {
|
|
70
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return window.matchMedia(DARK_QUERY).matches;
|
|
74
|
+
}
|
|
75
|
+
function ThemeProvider({ theme, children }) {
|
|
76
|
+
const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);
|
|
77
|
+
const activeTheme = useMemo(() => {
|
|
78
|
+
if (theme !== void 0) {
|
|
79
|
+
return { ...defaultTheme, ...theme };
|
|
80
|
+
}
|
|
81
|
+
return prefersDark ? { ...defaultTheme, ...darkTheme } : defaultTheme;
|
|
82
|
+
}, [theme, prefersDark]);
|
|
83
|
+
return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: activeTheme, children: /* @__PURE__ */ jsx("div", { style: activeTheme, children }) });
|
|
84
|
+
}
|
|
85
|
+
function useTheme() {
|
|
86
|
+
return useContext(ThemeContext);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { ThemeProvider, darkTheme, defaultTheme, useTheme };
|
|
90
|
+
//# sourceMappingURL=chunk-B3FUUKZ2.js.map
|
|
91
|
+
//# sourceMappingURL=chunk-B3FUUKZ2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theme/dark-theme.ts","../src/theme/tokens.ts","../src/theme/ThemeProvider.tsx"],"names":[],"mappings":"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","file":"chunk-B3FUUKZ2.js","sourcesContent":["import type { ThemeTokens } from '@intellectif/lk-core';\n\n/**\n * Dark-mode colour overrides, merged over {@link defaultTheme} by\n * `ThemeProvider`. Colours only — spacing/typography/shape/motion are\n * theme-invariant. All pairs verified WCAG 2.2 AA against the dark surface.\n */\nexport const darkTheme: Partial<ThemeTokens> = {\n '--lk-color-primary': '#fafafa',\n '--lk-color-primary-hover': '#e4e4e7',\n '--lk-color-surface': '#09090b',\n '--lk-color-surface-raised': '#18181b',\n '--lk-color-border': '#27272a',\n '--lk-color-text': '#fafafa',\n '--lk-color-text-muted': '#a1a1aa',\n '--lk-color-success': '#4ade80',\n '--lk-color-error': '#f87171',\n '--lk-color-warning': '#fbbf24',\n '--lk-color-focus-ring': '#fafafa',\n};\n","import type { ThemeTokens } from '@intellectif/lk-core';\n\n/**\n * Canonical default theme token values. `defaults.css` mirrors these exactly.\n * Palette is a neutral monochrome (ElevenLabs-like) so the SDK is brand-neutral\n * out of the box; consumers rebrand via `ThemeProvider`. All colour pairs are\n * verified to meet WCAG 2.2 AA: 4.5:1 for normal text, 3:1 for UI / focus ring\n * (see theme/__tests__/contrast.test.ts).\n */\nexport const defaultTheme: ThemeTokens = {\n // ── Colours (light) — neutral monochrome; semantic success/error/warning\n // retained for status legibility ────────────────────────────────────────\n '--lk-color-primary': '#18181b',\n '--lk-color-primary-hover': '#27272a',\n '--lk-color-surface': '#ffffff',\n '--lk-color-surface-raised': '#fafafa',\n '--lk-color-border': '#e4e4e7',\n '--lk-color-text': '#18181b',\n '--lk-color-text-muted': '#52525b',\n '--lk-color-success': '#15803d',\n '--lk-color-error': '#b91c1c',\n '--lk-color-warning': '#b45309',\n '--lk-color-focus-ring': '#18181b',\n\n // ── Spacing ──────────────────────────────────────────────────────────────\n '--lk-spacing-xs': '4px',\n '--lk-spacing-sm': '8px',\n '--lk-spacing-md': '16px',\n '--lk-spacing-lg': '24px',\n '--lk-spacing-xl': '32px',\n\n // ── Typography ───────────────────────────────────────────────────────────\n '--lk-font-family-base':\n 'system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif',\n '--lk-font-size-sm': '0.875rem',\n '--lk-font-size-base': '1rem',\n '--lk-font-size-lg': '1.125rem',\n '--lk-font-weight-normal': '400',\n '--lk-font-weight-bold': '700',\n '--lk-line-height-base': '1.5',\n\n // ── Shape ────────────────────────────────────────────────────────────────\n '--lk-radius-sm': '0.25rem',\n '--lk-radius-base': '0.5rem',\n '--lk-radius-lg': '0.75rem',\n\n // ── Motion ───────────────────────────────────────────────────────────────\n '--lk-transition-fast': '150ms ease',\n '--lk-transition-base': '250ms ease',\n};\n","'use client';\n\nimport type { ThemeTokens } from '@intellectif/lk-core';\nimport {\n type CSSProperties,\n createContext,\n type ReactNode,\n useContext,\n useMemo,\n useSyncExternalStore,\n} from 'react';\nimport { darkTheme } from './dark-theme.js';\nimport { defaultTheme } from './tokens.js';\n\nexport { darkTheme } from './dark-theme.js';\nexport { defaultTheme } from './tokens.js';\n\nconst ThemeContext = createContext<ThemeTokens>(defaultTheme);\n\nconst DARK_QUERY = '(prefers-color-scheme: dark)';\n\n/** Subscribe to OS dark-mode changes; no-op when there is no `window`. */\nfunction subscribePrefersDark(onChange: () => void): () => void {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {\n return () => {};\n }\n const mql = window.matchMedia(DARK_QUERY);\n mql.addEventListener('change', onChange);\n return () => mql.removeEventListener('change', onChange);\n}\n\n/** Current OS dark-mode preference; `false` when there is no `window`. */\nfunction getPrefersDark(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {\n return false;\n }\n return window.matchMedia(DARK_QUERY).matches;\n}\n\nexport interface ThemeProviderProps {\n /** Partial token overrides. When provided, dark auto-detection is disabled. */\n theme?: Partial<ThemeTokens>;\n children: ReactNode;\n}\n\n/**\n * Provides theme tokens to descendant activity components as scoped inline CSS\n * custom properties on a wrapper `<div>`, and via {@link useTheme}.\n *\n * Resolution: an explicit `theme` prop merges over {@link defaultTheme} (and\n * suppresses dark auto-detect, per Req 13.5). With no `theme` prop, the OS\n * `prefers-color-scheme: dark` preference auto-applies {@link darkTheme}.\n * SSR-safe: the server snapshot is always light, so hydration never mismatches.\n */\nexport function ThemeProvider({ theme, children }: ThemeProviderProps) {\n const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);\n\n const activeTheme = useMemo<ThemeTokens>(() => {\n if (theme !== undefined) {\n return { ...defaultTheme, ...theme };\n }\n return prefersDark ? { ...defaultTheme, ...darkTheme } : defaultTheme;\n }, [theme, prefersDark]);\n\n return (\n <ThemeContext.Provider value={activeTheme}>\n <div style={activeTheme as CSSProperties}>{children}</div>\n </ThemeContext.Provider>\n );\n}\n\n/** Returns the currently active theme tokens (defaults when no provider). */\nexport function useTheme(): ThemeTokens {\n return useContext(ThemeContext);\n}\n"]}
|