@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/theme/ThemeProvider.tsx
|
|
7
|
+
|
|
8
|
+
// src/theme/dark-theme.ts
|
|
9
|
+
var darkTheme = {
|
|
10
|
+
"--lk-color-primary": "#fafafa",
|
|
11
|
+
"--lk-color-primary-hover": "#e4e4e7",
|
|
12
|
+
"--lk-color-surface": "#09090b",
|
|
13
|
+
"--lk-color-surface-raised": "#18181b",
|
|
14
|
+
"--lk-color-border": "#27272a",
|
|
15
|
+
"--lk-color-text": "#fafafa",
|
|
16
|
+
"--lk-color-text-muted": "#a1a1aa",
|
|
17
|
+
"--lk-color-success": "#4ade80",
|
|
18
|
+
"--lk-color-error": "#f87171",
|
|
19
|
+
"--lk-color-warning": "#fbbf24",
|
|
20
|
+
"--lk-color-focus-ring": "#fafafa"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/theme/tokens.ts
|
|
24
|
+
var defaultTheme = {
|
|
25
|
+
// ── Colours (light) — neutral monochrome; semantic success/error/warning
|
|
26
|
+
// retained for status legibility ────────────────────────────────────────
|
|
27
|
+
"--lk-color-primary": "#18181b",
|
|
28
|
+
"--lk-color-primary-hover": "#27272a",
|
|
29
|
+
"--lk-color-surface": "#ffffff",
|
|
30
|
+
"--lk-color-surface-raised": "#fafafa",
|
|
31
|
+
"--lk-color-border": "#e4e4e7",
|
|
32
|
+
"--lk-color-text": "#18181b",
|
|
33
|
+
"--lk-color-text-muted": "#52525b",
|
|
34
|
+
"--lk-color-success": "#15803d",
|
|
35
|
+
"--lk-color-error": "#b91c1c",
|
|
36
|
+
"--lk-color-warning": "#b45309",
|
|
37
|
+
"--lk-color-focus-ring": "#18181b",
|
|
38
|
+
// ── Spacing ──────────────────────────────────────────────────────────────
|
|
39
|
+
"--lk-spacing-xs": "4px",
|
|
40
|
+
"--lk-spacing-sm": "8px",
|
|
41
|
+
"--lk-spacing-md": "16px",
|
|
42
|
+
"--lk-spacing-lg": "24px",
|
|
43
|
+
"--lk-spacing-xl": "32px",
|
|
44
|
+
// ── Typography ───────────────────────────────────────────────────────────
|
|
45
|
+
"--lk-font-family-base": 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
46
|
+
"--lk-font-size-sm": "0.875rem",
|
|
47
|
+
"--lk-font-size-base": "1rem",
|
|
48
|
+
"--lk-font-size-lg": "1.125rem",
|
|
49
|
+
"--lk-font-weight-normal": "400",
|
|
50
|
+
"--lk-font-weight-bold": "700",
|
|
51
|
+
"--lk-line-height-base": "1.5",
|
|
52
|
+
// ── Shape ────────────────────────────────────────────────────────────────
|
|
53
|
+
"--lk-radius-sm": "0.25rem",
|
|
54
|
+
"--lk-radius-base": "0.5rem",
|
|
55
|
+
"--lk-radius-lg": "0.75rem",
|
|
56
|
+
// ── Motion ───────────────────────────────────────────────────────────────
|
|
57
|
+
"--lk-transition-fast": "150ms ease",
|
|
58
|
+
"--lk-transition-base": "250ms ease"
|
|
59
|
+
};
|
|
60
|
+
var ThemeContext = react.createContext(defaultTheme);
|
|
61
|
+
var DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
62
|
+
function subscribePrefersDark(onChange) {
|
|
63
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
64
|
+
return () => {
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const mql = window.matchMedia(DARK_QUERY);
|
|
68
|
+
mql.addEventListener("change", onChange);
|
|
69
|
+
return () => mql.removeEventListener("change", onChange);
|
|
70
|
+
}
|
|
71
|
+
function getPrefersDark() {
|
|
72
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return window.matchMedia(DARK_QUERY).matches;
|
|
76
|
+
}
|
|
77
|
+
function ThemeProvider({ theme, children }) {
|
|
78
|
+
const prefersDark = react.useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);
|
|
79
|
+
const activeTheme = react.useMemo(() => {
|
|
80
|
+
if (theme !== void 0) {
|
|
81
|
+
return { ...defaultTheme, ...theme };
|
|
82
|
+
}
|
|
83
|
+
return prefersDark ? { ...defaultTheme, ...darkTheme } : defaultTheme;
|
|
84
|
+
}, [theme, prefersDark]);
|
|
85
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeContext.Provider, { value: activeTheme, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: activeTheme, children }) });
|
|
86
|
+
}
|
|
87
|
+
function useTheme() {
|
|
88
|
+
return react.useContext(ThemeContext);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
exports.ThemeProvider = ThemeProvider;
|
|
92
|
+
exports.darkTheme = darkTheme;
|
|
93
|
+
exports.defaultTheme = defaultTheme;
|
|
94
|
+
exports.useTheme = useTheme;
|
|
95
|
+
//# sourceMappingURL=chunk-CPF2JJG6.cjs.map
|
|
96
|
+
//# sourceMappingURL=chunk-CPF2JJG6.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theme/dark-theme.ts","../src/theme/tokens.ts","../src/theme/ThemeProvider.tsx"],"names":["createContext","useSyncExternalStore","useMemo","jsx","useContext"],"mappings":";;;;;;;;AAOO,IAAM,SAAA,GAAkC;AAAA,EAC7C,oBAAA,EAAsB,SAAA;AAAA,EACtB,0BAAA,EAA4B,SAAA;AAAA,EAC5B,oBAAA,EAAsB,SAAA;AAAA,EACtB,2BAAA,EAA6B,SAAA;AAAA,EAC7B,mBAAA,EAAqB,SAAA;AAAA,EACrB,iBAAA,EAAmB,SAAA;AAAA,EACnB,uBAAA,EAAyB,SAAA;AAAA,EACzB,oBAAA,EAAsB,SAAA;AAAA,EACtB,kBAAA,EAAoB,SAAA;AAAA,EACpB,oBAAA,EAAsB,SAAA;AAAA,EACtB,uBAAA,EAAyB;AAC3B;;;ACVO,IAAM,YAAA,GAA4B;AAAA;AAAA;AAAA,EAGvC,oBAAA,EAAsB,SAAA;AAAA,EACtB,0BAAA,EAA4B,SAAA;AAAA,EAC5B,oBAAA,EAAsB,SAAA;AAAA,EACtB,2BAAA,EAA6B,SAAA;AAAA,EAC7B,mBAAA,EAAqB,SAAA;AAAA,EACrB,iBAAA,EAAmB,SAAA;AAAA,EACnB,uBAAA,EAAyB,SAAA;AAAA,EACzB,oBAAA,EAAsB,SAAA;AAAA,EACtB,kBAAA,EAAoB,SAAA;AAAA,EACpB,oBAAA,EAAsB,SAAA;AAAA,EACtB,uBAAA,EAAyB,SAAA;AAAA;AAAA,EAGzB,iBAAA,EAAmB,KAAA;AAAA,EACnB,iBAAA,EAAmB,KAAA;AAAA,EACnB,iBAAA,EAAmB,MAAA;AAAA,EACnB,iBAAA,EAAmB,MAAA;AAAA,EACnB,iBAAA,EAAmB,MAAA;AAAA;AAAA,EAGnB,uBAAA,EACE,4EAAA;AAAA,EACF,mBAAA,EAAqB,UAAA;AAAA,EACrB,qBAAA,EAAuB,MAAA;AAAA,EACvB,mBAAA,EAAqB,UAAA;AAAA,EACrB,yBAAA,EAA2B,KAAA;AAAA,EAC3B,uBAAA,EAAyB,KAAA;AAAA,EACzB,uBAAA,EAAyB,KAAA;AAAA;AAAA,EAGzB,gBAAA,EAAkB,SAAA;AAAA,EAClB,kBAAA,EAAoB,QAAA;AAAA,EACpB,gBAAA,EAAkB,SAAA;AAAA;AAAA,EAGlB,sBAAA,EAAwB,YAAA;AAAA,EACxB,sBAAA,EAAwB;AAC1B;AChCA,IAAM,YAAA,GAAeA,oBAA2B,YAAY,CAAA;AAE5D,IAAM,UAAA,GAAa,8BAAA;AAGnB,SAAS,qBAAqB,QAAA,EAAkC;AAC9D,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,eAAe,UAAA,EAAY;AAC5E,IAAA,OAAO,MAAM;AAAA,IAAC,CAAA;AAAA,EAChB;AACA,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,UAAA,CAAW,UAAU,CAAA;AACxC,EAAA,GAAA,CAAI,gBAAA,CAAiB,UAAU,QAAQ,CAAA;AACvC,EAAA,OAAO,MAAM,GAAA,CAAI,mBAAA,CAAoB,QAAA,EAAU,QAAQ,CAAA;AACzD;AAGA,SAAS,cAAA,GAA0B;AACjC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,eAAe,UAAA,EAAY;AAC5E,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,MAAA,CAAO,UAAA,CAAW,UAAU,CAAA,CAAE,OAAA;AACvC;AAiBO,SAAS,aAAA,CAAc,EAAE,KAAA,EAAO,QAAA,EAAS,EAAuB;AACrE,EAAA,MAAM,WAAA,GAAcC,0BAAA,CAAqB,oBAAA,EAAsB,cAAA,EAAgB,MAAM,KAAK,CAAA;AAE1F,EAAA,MAAM,WAAA,GAAcC,cAAqB,MAAM;AAC7C,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,OAAO,EAAE,GAAG,YAAA,EAAc,GAAG,KAAA,EAAM;AAAA,IACrC;AACA,IAAA,OAAO,cAAc,EAAE,GAAG,YAAA,EAAc,GAAG,WAAU,GAAI,YAAA;AAAA,EAC3D,CAAA,EAAG,CAAC,KAAA,EAAO,WAAW,CAAC,CAAA;AAEvB,EAAA,uBACEC,cAAA,CAAC,YAAA,CAAa,QAAA,EAAb,EAAsB,KAAA,EAAO,WAAA,EAC5B,QAAA,kBAAAA,cAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAO,WAAA,EAA+B,QAAA,EAAS,CAAA,EACtD,CAAA;AAEJ;AAGO,SAAS,QAAA,GAAwB;AACtC,EAAA,OAAOC,iBAAW,YAAY,CAAA;AAChC","file":"chunk-CPF2JJG6.cjs","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"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/hooks/useActivityState.ts
|
|
6
|
+
function useActivityState() {
|
|
7
|
+
const [state, setState] = react.useState("idle");
|
|
8
|
+
const startTimeRef = react.useRef(null);
|
|
9
|
+
const start = react.useCallback(() => {
|
|
10
|
+
if (startTimeRef.current === null) {
|
|
11
|
+
startTimeRef.current = Date.now();
|
|
12
|
+
}
|
|
13
|
+
setState((prev) => prev === "idle" ? "in-progress" : prev);
|
|
14
|
+
}, []);
|
|
15
|
+
const complete = react.useCallback(() => setState("completed"), []);
|
|
16
|
+
const review = react.useCallback(() => setState("reviewing"), []);
|
|
17
|
+
const reset = react.useCallback(() => {
|
|
18
|
+
startTimeRef.current = null;
|
|
19
|
+
setState("idle");
|
|
20
|
+
}, []);
|
|
21
|
+
const getTimeSpent = react.useCallback(
|
|
22
|
+
() => startTimeRef.current === null ? 0 : Date.now() - startTimeRef.current,
|
|
23
|
+
[]
|
|
24
|
+
);
|
|
25
|
+
return { state, start, complete, review, reset, getTimeSpent };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
exports.useActivityState = useActivityState;
|
|
29
|
+
//# sourceMappingURL=chunk-HD2AIQRD.cjs.map
|
|
30
|
+
//# sourceMappingURL=chunk-HD2AIQRD.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useActivityState.ts"],"names":["useState","useRef","useCallback"],"mappings":";;;;;AAyBO,SAAS,gBAAA,GAA2C;AACzD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,eAAwB,MAAM,CAAA;AACxD,EAAA,MAAM,YAAA,GAAeC,aAAsB,IAAI,CAAA;AAE/C,EAAA,MAAM,KAAA,GAAQC,kBAAY,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,WAAWA,iBAAA,CAAY,MAAM,SAAS,WAAW,CAAA,EAAG,EAAE,CAAA;AAE5D,EAAA,MAAM,SAASA,iBAAA,CAAY,MAAM,SAAS,WAAW,CAAA,EAAG,EAAE,CAAA;AAE1D,EAAA,MAAM,KAAA,GAAQA,kBAAY,MAAM;AAC9B,IAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AACvB,IAAA,QAAA,CAAS,MAAM,CAAA;AAAA,EACjB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,YAAA,GAAeA,iBAAA;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-HD2AIQRD.cjs","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,65 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/hooks/useXAPI.ts
|
|
4
|
+
var BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
5
|
+
function toBase64Utf8(input) {
|
|
6
|
+
const bytes = new TextEncoder().encode(input);
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (const byte of bytes) {
|
|
9
|
+
binary += String.fromCharCode(byte);
|
|
10
|
+
}
|
|
11
|
+
return btoa(binary);
|
|
12
|
+
}
|
|
13
|
+
function authHeader(auth) {
|
|
14
|
+
return auth.type === "basic" ? `Basic ${toBase64Utf8(`${auth.username}:${auth.password}`)}` : `Bearer ${auth.token}`;
|
|
15
|
+
}
|
|
16
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
function useXAPI(config) {
|
|
18
|
+
const configRef = useRef(config);
|
|
19
|
+
configRef.current = config;
|
|
20
|
+
const sendStatement = useCallback(async (statement) => {
|
|
21
|
+
const cfg = configRef.current;
|
|
22
|
+
const maxAttempts = 1 + BACKOFF_MS.length;
|
|
23
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
24
|
+
let statusCode;
|
|
25
|
+
let message = "Request failed";
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(cfg.endpoint, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"X-Experience-API-Version": "1.0.3",
|
|
32
|
+
Authorization: authHeader(cfg.auth)
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(statement)
|
|
35
|
+
});
|
|
36
|
+
if (response.ok) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
statusCode = response.status;
|
|
40
|
+
message = `LRS responded with ${response.status}`;
|
|
41
|
+
if (response.status < 500) {
|
|
42
|
+
cfg.onError?.({ statement, attempt, statusCode, message });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
message = error instanceof Error ? error.message : "Network error";
|
|
47
|
+
}
|
|
48
|
+
if (attempt === maxAttempts) {
|
|
49
|
+
cfg.onError?.({
|
|
50
|
+
statement,
|
|
51
|
+
attempt,
|
|
52
|
+
...statusCode !== void 0 ? { statusCode } : {},
|
|
53
|
+
message
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await sleep(BACKOFF_MS[attempt - 1]);
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
return { sendStatement };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { useXAPI };
|
|
64
|
+
//# sourceMappingURL=chunk-IZNMQSQP.js.map
|
|
65
|
+
//# sourceMappingURL=chunk-IZNMQSQP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useXAPI.ts"],"names":[],"mappings":";;;AAMA,IAAM,UAAA,GAAa,CAAC,GAAA,EAAM,GAAA,EAAM,GAAI,CAAA;AAGpC,SAAS,aAAa,KAAA,EAAuB;AAC3C,EAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,EAAY,CAAE,OAAO,KAAK,CAAA;AAC5C,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAAA,EACpC;AACA,EAAA,OAAO,KAAK,MAAM,CAAA;AACpB;AAEA,SAAS,WAAW,IAAA,EAAkC;AACpD,EAAA,OAAO,KAAK,IAAA,KAAS,OAAA,GACjB,CAAA,MAAA,EAAS,YAAA,CAAa,GAAG,IAAA,CAAK,QAAQ,CAAA,CAAA,EAAI,IAAA,CAAK,QAAQ,CAAA,CAAE,CAAC,CAAA,CAAA,GAC1D,CAAA,OAAA,EAAU,KAAK,KAAK,CAAA,CAAA;AAC1B;AAEA,IAAM,KAAA,GAAQ,CAAC,EAAA,KAA8B,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAatF,SAAS,QAAQ,MAAA,EAAmC;AAGzD,EAAA,MAAM,SAAA,GAAY,OAAO,MAAM,CAAA;AAC/B,EAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AAEpB,EAAA,MAAM,aAAA,GAAgB,WAAA,CAAY,OAAO,SAAA,KAA4C;AACnF,IAAA,MAAM,MAAM,SAAA,CAAU,OAAA;AACtB,IAAA,MAAM,WAAA,GAAc,IAAI,UAAA,CAAW,MAAA;AAEnC,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,WAAA,EAAa,WAAW,CAAA,EAAG;AAC1D,MAAA,IAAI,UAAA;AACJ,MAAA,IAAI,OAAA,GAAU,gBAAA;AAEd,MAAA,IAAI;AACF,QAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,CAAI,QAAA,EAAU;AAAA,UACzC,MAAA,EAAQ,MAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,cAAA,EAAgB,kBAAA;AAAA,YAChB,0BAAA,EAA4B,OAAA;AAAA,YAC5B,aAAA,EAAe,UAAA,CAAW,GAAA,CAAI,IAAI;AAAA,WACpC;AAAA,UACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,SAAS;AAAA,SAC/B,CAAA;AAED,QAAA,IAAI,SAAS,EAAA,EAAI;AACf,UAAA;AAAA,QACF;AAEA,QAAA,UAAA,GAAa,QAAA,CAAS,MAAA;AACtB,QAAA,OAAA,GAAU,CAAA,mBAAA,EAAsB,SAAS,MAAM,CAAA,CAAA;AAI/C,QAAA,IAAI,QAAA,CAAS,SAAS,GAAA,EAAK;AACzB,UAAA,GAAA,CAAI,UAAU,EAAE,SAAA,EAAW,OAAA,EAAS,UAAA,EAAY,SAAS,CAAA;AACzD,UAAA;AAAA,QACF;AAAA,MACF,SAAS,KAAA,EAAO;AACd,QAAA,OAAA,GAAU,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,eAAA;AAAA,MACrD;AAEA,MAAA,IAAI,YAAY,WAAA,EAAa;AAC3B,QAAA,GAAA,CAAI,OAAA,GAAU;AAAA,UACZ,SAAA;AAAA,UACA,OAAA;AAAA,UACA,GAAI,UAAA,KAAe,MAAA,GAAY,EAAE,UAAA,KAAe,EAAC;AAAA,UACjD;AAAA,SACD,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,KAAA,CAAM,UAAA,CAAW,OAAA,GAAU,CAAC,CAAC,CAAA;AAAA,IACrC;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,aAAA,EAAc;AACzB","file":"chunk-IZNMQSQP.js","sourcesContent":["'use client';\n\nimport type { XAPIConfig, XAPIStatement } from '@intellectif/lk-core';\nimport { useCallback, useRef } from 'react';\n\n/** Backoff before retry 1 / 2 / 3 (Req 10.4). */\nconst BACKOFF_MS = [1000, 2000, 4000] as const;\n\n/** UTF-8-safe base64 (`btoa` is Latin1-only and breaks on non-ASCII creds). */\nfunction toBase64Utf8(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n\nfunction authHeader(auth: XAPIConfig['auth']): string {\n return auth.type === 'basic'\n ? `Basic ${toBase64Utf8(`${auth.username}:${auth.password}`)}`\n : `Bearer ${auth.token}`;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport interface UseXAPIResult {\n /**\n * Sends a statement to the configured LRS. **Never rejects** — xAPI is\n * fire-and-forget telemetry that must not break the learner's activity\n * flow. Network errors and 5xx responses are retried (backoff 1s/2s/4s,\n * up to 3 retries); a 4xx fails immediately. Any final failure is delivered\n * to `config.onError`.\n */\n sendStatement: (statement: XAPIStatement) => Promise<void>;\n}\n\nexport function useXAPI(config: XAPIConfig): UseXAPIResult {\n // Keep the latest config without changing `sendStatement`'s identity when\n // callers pass an inline config object.\n const configRef = useRef(config);\n configRef.current = config;\n\n const sendStatement = useCallback(async (statement: XAPIStatement): Promise<void> => {\n const cfg = configRef.current;\n const maxAttempts = 1 + BACKOFF_MS.length;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n let statusCode: number | undefined;\n let message = 'Request failed';\n\n try {\n const response = await fetch(cfg.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Experience-API-Version': '1.0.3',\n Authorization: authHeader(cfg.auth),\n },\n body: JSON.stringify(statement),\n });\n\n if (response.ok) {\n return;\n }\n\n statusCode = response.status;\n message = `LRS responded with ${response.status}`;\n\n // Only network errors and 5xx are retryable (Req 10.4). A 4xx is a\n // client error — fail immediately rather than waste retries.\n if (response.status < 500) {\n cfg.onError?.({ statement, attempt, statusCode, message });\n return;\n }\n } catch (error) {\n message = error instanceof Error ? error.message : 'Network error';\n }\n\n if (attempt === maxAttempts) {\n cfg.onError?.({\n statement,\n attempt,\n ...(statusCode !== undefined ? { statusCode } : {}),\n message,\n });\n return;\n }\n\n await sleep(BACKOFF_MS[attempt - 1]);\n }\n }, []);\n\n return { sendStatement };\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/components/ActivityErrorBoundary.tsx
|
|
5
|
+
function isProduction() {
|
|
6
|
+
const g = globalThis;
|
|
7
|
+
return g.process?.env?.NODE_ENV === "production";
|
|
8
|
+
}
|
|
9
|
+
var ActivityErrorBoundary = class extends Component {
|
|
10
|
+
state = { error: null };
|
|
11
|
+
static getDerivedStateFromError(error) {
|
|
12
|
+
return { error };
|
|
13
|
+
}
|
|
14
|
+
render() {
|
|
15
|
+
const { error } = this.state;
|
|
16
|
+
if (!error) {
|
|
17
|
+
return this.props.children;
|
|
18
|
+
}
|
|
19
|
+
if (!isProduction()) {
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", { role: "alert", children: [
|
|
21
|
+
/* @__PURE__ */ jsx("strong", { children: "Activity failed to render" }),
|
|
22
|
+
/* @__PURE__ */ jsx("pre", { children: error.stack ?? error.message })
|
|
23
|
+
] });
|
|
24
|
+
}
|
|
25
|
+
return /* @__PURE__ */ jsx("div", { role: "alert", children: this.props.activityTitle ? `"${this.props.activityTitle}" could not be displayed.` : "This activity could not be displayed." });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/components/_internal.ts
|
|
30
|
+
function isDevelopment() {
|
|
31
|
+
const g = globalThis;
|
|
32
|
+
return g.process?.env?.NODE_ENV !== "production";
|
|
33
|
+
}
|
|
34
|
+
var ANONYMOUS_ACTOR = {
|
|
35
|
+
objectType: "Agent",
|
|
36
|
+
account: { homePage: "https://github.com/intellectif/learning-kit", name: "anonymous" }
|
|
37
|
+
};
|
|
38
|
+
function objectIdFor(id) {
|
|
39
|
+
return `urn:learning-kit:activity:${encodeURIComponent(id)}`;
|
|
40
|
+
}
|
|
41
|
+
function ActivityMedia({ media }) {
|
|
42
|
+
const { type, url, alt, captionsUrl } = media;
|
|
43
|
+
if (type === "image") {
|
|
44
|
+
return /* @__PURE__ */ jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsx("img", { className: "lk-media-el", src: url, alt: alt ?? "" }) });
|
|
45
|
+
}
|
|
46
|
+
if (type === "audio") {
|
|
47
|
+
return /* @__PURE__ */ jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxs("audio", { className: "lk-media-el", controls: true, "aria-label": alt || void 0, children: [
|
|
48
|
+
/* @__PURE__ */ jsx("source", { src: url }),
|
|
49
|
+
captionsUrl ? /* @__PURE__ */ jsx("track", { kind: "captions", src: captionsUrl, default: true }) : null
|
|
50
|
+
] }) });
|
|
51
|
+
}
|
|
52
|
+
if (type === "embed") {
|
|
53
|
+
return /* @__PURE__ */ jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsx("div", { className: "lk-media-embed", children: /* @__PURE__ */ jsx(
|
|
54
|
+
"iframe",
|
|
55
|
+
{
|
|
56
|
+
className: "lk-media-el",
|
|
57
|
+
src: url,
|
|
58
|
+
title: alt ?? "Embedded media",
|
|
59
|
+
loading: "lazy",
|
|
60
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
61
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
62
|
+
allowFullScreen: true
|
|
63
|
+
}
|
|
64
|
+
) }) });
|
|
65
|
+
}
|
|
66
|
+
return /* @__PURE__ */ jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxs("video", { className: "lk-media-el", controls: true, "aria-label": alt || void 0, children: [
|
|
67
|
+
/* @__PURE__ */ jsx("source", { src: url }),
|
|
68
|
+
captionsUrl ? /* @__PURE__ */ jsx("track", { kind: "captions", src: captionsUrl, default: true }) : null
|
|
69
|
+
] }) });
|
|
70
|
+
}
|
|
71
|
+
function FeedbackRegion({ id, className, children }) {
|
|
72
|
+
return /* @__PURE__ */ jsx("div", { id, className, "aria-live": "polite", "aria-atomic": "true", children });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { ANONYMOUS_ACTOR, ActivityErrorBoundary, ActivityMedia, FeedbackRegion, isDevelopment, objectIdFor };
|
|
76
|
+
//# sourceMappingURL=chunk-L3EV5LIX.js.map
|
|
77
|
+
//# sourceMappingURL=chunk-L3EV5LIX.js.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":["jsx","jsxs"],"mappings":"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","file":"chunk-L3EV5LIX.js","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,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk6OBRYL4C_cjs = require('./chunk-6OBRYL4C.cjs');
|
|
4
|
+
var chunkHD2AIQRD_cjs = require('./chunk-HD2AIQRD.cjs');
|
|
5
|
+
var lkCore = require('@intellectif/lk-core');
|
|
6
|
+
var react = require('react');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
var PLACEHOLDER = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
10
|
+
function parsePassage(passage) {
|
|
11
|
+
const segments = [];
|
|
12
|
+
const re = new RegExp(PLACEHOLDER);
|
|
13
|
+
let last = 0;
|
|
14
|
+
let ordinal = 0;
|
|
15
|
+
let match = re.exec(passage);
|
|
16
|
+
while (match !== null) {
|
|
17
|
+
if (match.index > last) {
|
|
18
|
+
segments.push({ kind: "text", value: passage.slice(last, match.index) });
|
|
19
|
+
}
|
|
20
|
+
ordinal += 1;
|
|
21
|
+
segments.push({ kind: "blank", id: match[1], ordinal });
|
|
22
|
+
last = match.index + match[0].length;
|
|
23
|
+
match = re.exec(passage);
|
|
24
|
+
}
|
|
25
|
+
if (last < passage.length) {
|
|
26
|
+
segments.push({ kind: "text", value: passage.slice(last) });
|
|
27
|
+
}
|
|
28
|
+
return segments;
|
|
29
|
+
}
|
|
30
|
+
function FillInTheBlanks({
|
|
31
|
+
data,
|
|
32
|
+
onComplete,
|
|
33
|
+
onInteraction,
|
|
34
|
+
theme,
|
|
35
|
+
locale,
|
|
36
|
+
disabled,
|
|
37
|
+
showCorrectAnswers
|
|
38
|
+
}) {
|
|
39
|
+
const devError = react.useMemo(() => {
|
|
40
|
+
if (!chunk6OBRYL4C_cjs.isDevelopment()) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const result2 = lkCore.validateActivity("fill-in-the-blanks", data);
|
|
44
|
+
return result2.success ? null : new lkCore.ActivitySchemaError("fill-in-the-blanks", result2.errors);
|
|
45
|
+
}, [data]);
|
|
46
|
+
const { state, start, complete, getTimeSpent, reset } = chunkHD2AIQRD_cjs.useActivityState();
|
|
47
|
+
const [answers, setAnswers] = react.useState({});
|
|
48
|
+
const [revealed, setRevealed] = react.useState(() => /* @__PURE__ */ new Set());
|
|
49
|
+
const [result, setResult] = react.useState(null);
|
|
50
|
+
const [summary, setSummary] = react.useState(null);
|
|
51
|
+
const [feedbackHidden, setFeedbackHidden] = react.useState(false);
|
|
52
|
+
react.useEffect(() => {
|
|
53
|
+
setAnswers({});
|
|
54
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
55
|
+
setResult(null);
|
|
56
|
+
setSummary(null);
|
|
57
|
+
setFeedbackHidden(false);
|
|
58
|
+
reset();
|
|
59
|
+
}, [data, reset]);
|
|
60
|
+
const segments = react.useMemo(() => parsePassage(data.passage), [data.passage]);
|
|
61
|
+
const blankById = react.useMemo(() => new Map(data.blanks.map((b) => [b.id, b])), [data.blanks]);
|
|
62
|
+
if (devError) {
|
|
63
|
+
throw devError;
|
|
64
|
+
}
|
|
65
|
+
const submitted = state === "completed";
|
|
66
|
+
const inactive = disabled === true || submitted;
|
|
67
|
+
const fireInteraction = (type, payload) => {
|
|
68
|
+
onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });
|
|
69
|
+
};
|
|
70
|
+
const handleChange = (blankId, value) => {
|
|
71
|
+
if (inactive) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (state === "idle") {
|
|
75
|
+
start();
|
|
76
|
+
}
|
|
77
|
+
setAnswers((prev) => ({ ...prev, [blankId]: value }));
|
|
78
|
+
fireInteraction("blank-filled", { blankId, value });
|
|
79
|
+
};
|
|
80
|
+
const toggleHint = (blankId) => {
|
|
81
|
+
if (inactive) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (state === "idle") {
|
|
85
|
+
start();
|
|
86
|
+
}
|
|
87
|
+
setRevealed((prev) => {
|
|
88
|
+
const next = new Set(prev);
|
|
89
|
+
if (next.has(blankId)) {
|
|
90
|
+
next.delete(blankId);
|
|
91
|
+
return next;
|
|
92
|
+
}
|
|
93
|
+
next.add(blankId);
|
|
94
|
+
return next;
|
|
95
|
+
});
|
|
96
|
+
if (!revealed.has(blankId)) {
|
|
97
|
+
fireInteraction("hint-requested", { blankId });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const handleSubmit = (event) => {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
if (inactive) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const response = { type: "fill-in-the-blanks", answers };
|
|
106
|
+
const scoringResult = lkCore.score("fill-in-the-blanks", data, response);
|
|
107
|
+
complete();
|
|
108
|
+
const timeSpent = getTimeSpent();
|
|
109
|
+
const xapiStatement = lkCore.xAPIBuilder.buildAnsweredStatement({
|
|
110
|
+
actor: chunk6OBRYL4C_cjs.ANONYMOUS_ACTOR,
|
|
111
|
+
object: { id: chunk6OBRYL4C_cjs.objectIdFor(data.id), name: { "en-US": data.title } },
|
|
112
|
+
scoringResult,
|
|
113
|
+
timeSpentMs: timeSpent,
|
|
114
|
+
response: JSON.stringify(answers)
|
|
115
|
+
});
|
|
116
|
+
setResult(scoringResult);
|
|
117
|
+
onComplete({
|
|
118
|
+
score: scoringResult.score,
|
|
119
|
+
maxScore: scoringResult.maxScore,
|
|
120
|
+
passed: scoringResult.passed,
|
|
121
|
+
timeSpent,
|
|
122
|
+
xapiStatement
|
|
123
|
+
});
|
|
124
|
+
const overall = scoringResult.passed ? data.feedback?.correct : data.feedback?.incorrect;
|
|
125
|
+
setSummary(
|
|
126
|
+
`Answer submitted. Score ${Math.round(scoringResult.score * 100)}%. ${scoringResult.passed ? "Passed." : "Not passed."}${overall ? ` ${overall}` : ""}`
|
|
127
|
+
);
|
|
128
|
+
fireInteraction("submitted", { answers, score: scoringResult.score });
|
|
129
|
+
};
|
|
130
|
+
const correctByBlank = react.useMemo(() => {
|
|
131
|
+
const map = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const detail of result?.details ?? []) {
|
|
133
|
+
map.set(detail.itemId, detail.correct);
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}, [result]);
|
|
137
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
138
|
+
"form",
|
|
139
|
+
{
|
|
140
|
+
className: "lk-fib",
|
|
141
|
+
"aria-label": data.title,
|
|
142
|
+
lang: locale,
|
|
143
|
+
style: theme,
|
|
144
|
+
onSubmit: handleSubmit,
|
|
145
|
+
children: [
|
|
146
|
+
data.media ? /* @__PURE__ */ jsxRuntime.jsx(chunk6OBRYL4C_cjs.ActivityMedia, { media: data.media }) : null,
|
|
147
|
+
/* @__PURE__ */ jsxRuntime.jsxs("fieldset", { disabled: inactive, children: [
|
|
148
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-fib-passage", children: segments.map((seg, i) => {
|
|
149
|
+
if (seg.kind === "text") {
|
|
150
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: seg.value }, `t${i}`);
|
|
151
|
+
}
|
|
152
|
+
const blank = blankById.get(seg.id);
|
|
153
|
+
if (!blank) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const isCorrect = correctByBlank.get(seg.id) === true;
|
|
157
|
+
const hintId = `${data.id}-hint-${seg.id}`;
|
|
158
|
+
if (submitted && showCorrectAnswers && !isCorrect) {
|
|
159
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-fib-answer", "data-correct": "false", children: blank.acceptedAnswers[0] }, seg.id);
|
|
160
|
+
}
|
|
161
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-fib-blank", children: [
|
|
162
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
163
|
+
"input",
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
"aria-label": `Fill in blank ${seg.ordinal}`,
|
|
167
|
+
"aria-describedby": blank.hint ? hintId : void 0,
|
|
168
|
+
value: answers[seg.id] ?? "",
|
|
169
|
+
disabled: inactive,
|
|
170
|
+
"aria-disabled": inactive || void 0,
|
|
171
|
+
"data-correct": submitted ? String(isCorrect) : void 0,
|
|
172
|
+
onChange: (e) => handleChange(seg.id, e.target.value)
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
blank.hint ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
176
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
177
|
+
"button",
|
|
178
|
+
{
|
|
179
|
+
type: "button",
|
|
180
|
+
className: "lk-fib-hint-btn",
|
|
181
|
+
"aria-controls": hintId,
|
|
182
|
+
"aria-expanded": revealed.has(seg.id),
|
|
183
|
+
"aria-label": revealed.has(seg.id) ? "Hide hint" : "Show hint",
|
|
184
|
+
disabled: inactive,
|
|
185
|
+
onClick: () => toggleHint(seg.id),
|
|
186
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
187
|
+
"svg",
|
|
188
|
+
{
|
|
189
|
+
className: "lk-fib-hint-icon",
|
|
190
|
+
viewBox: "0 0 24 24",
|
|
191
|
+
width: "16",
|
|
192
|
+
height: "16",
|
|
193
|
+
"aria-hidden": "true",
|
|
194
|
+
focusable: "false",
|
|
195
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
196
|
+
"path",
|
|
197
|
+
{
|
|
198
|
+
fill: "currentColor",
|
|
199
|
+
d: "M9 21h6v-1H9v1Zm3-19a7 7 0 0 0-4 12.74V17h8v-2.26A7 7 0 0 0 12 2Z"
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
),
|
|
206
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: hintId, "aria-live": "polite", children: revealed.has(seg.id) ? blank.hint : "" })
|
|
207
|
+
] }) : null,
|
|
208
|
+
submitted && !feedbackHidden && blank.feedback ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
209
|
+
"span",
|
|
210
|
+
{
|
|
211
|
+
className: "lk-fib-blank-feedback",
|
|
212
|
+
role: "note",
|
|
213
|
+
"data-correct": String(isCorrect),
|
|
214
|
+
children: blank.feedback
|
|
215
|
+
}
|
|
216
|
+
) : null
|
|
217
|
+
] }, seg.id);
|
|
218
|
+
}) }),
|
|
219
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: "Check answers" })
|
|
220
|
+
] }),
|
|
221
|
+
submitted && data.blanks.some((b) => b.feedback) ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
222
|
+
"button",
|
|
223
|
+
{
|
|
224
|
+
type: "button",
|
|
225
|
+
className: "lk-fib-feedback-toggle",
|
|
226
|
+
"aria-expanded": !feedbackHidden,
|
|
227
|
+
onClick: () => setFeedbackHidden((h) => !h),
|
|
228
|
+
children: feedbackHidden ? "Show feedback" : "Hide feedback"
|
|
229
|
+
}
|
|
230
|
+
) : null,
|
|
231
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunk6OBRYL4C_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: summary })
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
function FillInTheBlanks2(props) {
|
|
237
|
+
return /* @__PURE__ */ jsxRuntime.jsx(chunk6OBRYL4C_cjs.ActivityErrorBoundary, { activityTitle: props.data?.title, children: /* @__PURE__ */ jsxRuntime.jsx(FillInTheBlanks, { ...props }) });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
exports.FillInTheBlanks = FillInTheBlanks2;
|
|
241
|
+
//# sourceMappingURL=chunk-O3GWWJFS.cjs.map
|
|
242
|
+
//# sourceMappingURL=chunk-O3GWWJFS.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/FillInTheBlanks/FillInTheBlanks.tsx","../src/components/FillInTheBlanks/index.tsx"],"names":["useMemo","isDevelopment","result","validateActivity","ActivitySchemaError","useActivityState","useState","useEffect","score","xAPIBuilder","ANONYMOUS_ACTOR","objectIdFor","jsxs","jsx","ActivityMedia","Fragment","FeedbackRegion","FillInTheBlanks","ActivityErrorBoundary"],"mappings":"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","file":"chunk-O3GWWJFS.cjs","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"]}
|