@intellectif/lk-react 1.0.0 → 2.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.
Files changed (55) hide show
  1. package/README.md +112 -0
  2. package/dist/{chunk-5KW7TFX3.js → chunk-32WWQOG3.js} +13 -5
  3. package/dist/chunk-32WWQOG3.js.map +1 -0
  4. package/dist/{chunk-O3GWWJFS.cjs → chunk-73XCWYKD.cjs} +18 -10
  5. package/dist/chunk-73XCWYKD.cjs.map +1 -0
  6. package/dist/{chunk-IZNMQSQP.js → chunk-E4MNA2IQ.js} +17 -3
  7. package/dist/chunk-E4MNA2IQ.js.map +1 -0
  8. package/dist/{chunk-W2L66WHW.js → chunk-G5ZEWJRA.js} +28 -12
  9. package/dist/chunk-G5ZEWJRA.js.map +1 -0
  10. package/dist/{chunk-TVK3SVV6.cjs → chunk-IYLQSNLE.cjs} +17 -3
  11. package/dist/chunk-IYLQSNLE.cjs.map +1 -0
  12. package/dist/chunk-KJNQLAB5.cjs +158 -0
  13. package/dist/chunk-KJNQLAB5.cjs.map +1 -0
  14. package/dist/{chunk-QBA6UXMS.cjs → chunk-MKH5THDR.cjs} +32 -16
  15. package/dist/chunk-MKH5THDR.cjs.map +1 -0
  16. package/dist/{chunk-ZPD7BGCF.js → chunk-OS7MO2OW.js} +18 -5
  17. package/dist/chunk-OS7MO2OW.js.map +1 -0
  18. package/dist/{chunk-6OBRYL4C.cjs → chunk-QHOV2A6M.cjs} +11 -2
  19. package/dist/chunk-QHOV2A6M.cjs.map +1 -0
  20. package/dist/{chunk-RI53A36N.cjs → chunk-SH7BRP4M.cjs} +18 -5
  21. package/dist/chunk-SH7BRP4M.cjs.map +1 -0
  22. package/dist/{chunk-L3EV5LIX.js → chunk-UIGIYYGY.js} +11 -3
  23. package/dist/chunk-UIGIYYGY.js.map +1 -0
  24. package/dist/chunk-V5EN3ESV.js +156 -0
  25. package/dist/chunk-V5EN3ESV.js.map +1 -0
  26. package/dist/components/ActivitySequence.cjs +5 -5
  27. package/dist/components/ActivitySequence.js +4 -4
  28. package/dist/components/FillInTheBlanks.cjs +3 -3
  29. package/dist/components/FillInTheBlanks.js +2 -2
  30. package/dist/components/MultipleChoice.cjs +3 -3
  31. package/dist/components/MultipleChoice.js +2 -2
  32. package/dist/components/WrittenResponse.cjs +14 -0
  33. package/dist/components/WrittenResponse.cjs.map +1 -0
  34. package/dist/components/WrittenResponse.d.cts +48 -0
  35. package/dist/components/WrittenResponse.d.ts +48 -0
  36. package/dist/components/WrittenResponse.js +5 -0
  37. package/dist/components/WrittenResponse.js.map +1 -0
  38. package/dist/hooks/useXAPI.cjs +2 -2
  39. package/dist/hooks/useXAPI.js +1 -1
  40. package/dist/index.cjs +14 -9
  41. package/dist/index.d.cts +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +6 -5
  44. package/dist/theme/skin.css +85 -0
  45. package/package.json +18 -4
  46. package/dist/chunk-5KW7TFX3.js.map +0 -1
  47. package/dist/chunk-6OBRYL4C.cjs.map +0 -1
  48. package/dist/chunk-IZNMQSQP.js.map +0 -1
  49. package/dist/chunk-L3EV5LIX.js.map +0 -1
  50. package/dist/chunk-O3GWWJFS.cjs.map +0 -1
  51. package/dist/chunk-QBA6UXMS.cjs.map +0 -1
  52. package/dist/chunk-RI53A36N.cjs.map +0 -1
  53. package/dist/chunk-TVK3SVV6.cjs.map +0 -1
  54. package/dist/chunk-W2L66WHW.js.map +0 -1
  55. package/dist/chunk-ZPD7BGCF.js.map +0 -1
@@ -0,0 +1,156 @@
1
+ import { ActivityErrorBoundary, isDevelopment, ActivityMedia, FeedbackRegion, ANONYMOUS_ACTOR, objectIdFor } from './chunk-UIGIYYGY.js';
2
+ import { useActivityState } from './chunk-5HVUKXF3.js';
3
+ import { validateActivity, ActivitySchemaError, countWords, evaluate, xAPIBuilder } from '@intellectif/lk-core';
4
+ import { useMemo, useState, useEffect } from 'react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ function WrittenResponse({
8
+ data,
9
+ onSubmitted,
10
+ onInteraction,
11
+ theme,
12
+ locale,
13
+ disabled
14
+ }) {
15
+ const devError = useMemo(() => {
16
+ if (!isDevelopment()) {
17
+ return null;
18
+ }
19
+ const result = validateActivity("written-response", data);
20
+ return result.success ? null : new ActivitySchemaError("written-response", result.errors);
21
+ }, [data]);
22
+ const { state, start, complete, getTimeSpent, reset } = useActivityState();
23
+ const [text, setText] = useState("");
24
+ const [summary, setSummary] = useState(null);
25
+ useEffect(() => {
26
+ setText("");
27
+ setSummary(null);
28
+ reset();
29
+ }, [data, reset]);
30
+ if (devError) {
31
+ throw devError;
32
+ }
33
+ const submitted = state === "completed";
34
+ const inactive = disabled === true || submitted;
35
+ const wordCount = countWords(text);
36
+ const withinBounds = wordCount >= data.minWords && wordCount <= data.maxWords;
37
+ const handleChange = (value) => {
38
+ if (inactive) {
39
+ return;
40
+ }
41
+ if (state === "idle") {
42
+ start();
43
+ }
44
+ setText(value);
45
+ onInteraction?.({
46
+ type: "text-changed",
47
+ activityId: data.id,
48
+ timestamp: Date.now(),
49
+ payload: { wordCount: countWords(value) }
50
+ });
51
+ };
52
+ const handleSubmit = (event) => {
53
+ event.preventDefault();
54
+ if (inactive || text.trim().length === 0) {
55
+ return;
56
+ }
57
+ const outcome = evaluate(data, { type: "written-response", text, wordCount });
58
+ complete();
59
+ const timeSpent = getTimeSpent();
60
+ const partial = outcome.status === "deferred" ? outcome.partial : void 0;
61
+ const finalWordCount = typeof partial?.wordCount === "number" ? partial.wordCount : wordCount;
62
+ const finalWithinBounds = typeof partial?.withinWordBounds === "boolean" ? partial.withinWordBounds : withinBounds;
63
+ const xapiStatement = xAPIBuilder.buildSubmittedStatement({
64
+ actor: ANONYMOUS_ACTOR,
65
+ object: {
66
+ id: objectIdFor(data.id),
67
+ name: { [data.locale ?? "en-US"]: data.title },
68
+ type: "http://adlnet.gov/expapi/activities/cmi.interaction",
69
+ interactionType: "long-fill-in"
70
+ },
71
+ timeSpentMs: timeSpent,
72
+ response: text,
73
+ resultExtensions: {
74
+ "urn:learning-kit:extension:word-count": finalWordCount,
75
+ "urn:learning-kit:extension:within-word-bounds": finalWithinBounds
76
+ }
77
+ });
78
+ onSubmitted({
79
+ text,
80
+ wordCount: finalWordCount,
81
+ withinWordBounds: finalWithinBounds,
82
+ timeSpent,
83
+ xapiStatement
84
+ });
85
+ setSummary("Response submitted. It will be graded and your result will appear here later.");
86
+ onInteraction?.({
87
+ type: "submitted",
88
+ activityId: data.id,
89
+ timestamp: Date.now(),
90
+ payload: { wordCount: finalWordCount, withinWordBounds: finalWithinBounds }
91
+ });
92
+ };
93
+ const promptId = `${data.id}-prompt`;
94
+ const counterId = `${data.id}-counter`;
95
+ const boundsLabel = data.minWords > 0 ? `${data.minWords}\u2013${data.maxWords} words` : `up to ${data.maxWords} words`;
96
+ return /* @__PURE__ */ jsxs(
97
+ "form",
98
+ {
99
+ className: "lk-wr",
100
+ "aria-label": data.title,
101
+ lang: locale,
102
+ style: theme,
103
+ onSubmit: handleSubmit,
104
+ children: [
105
+ data.media ? /* @__PURE__ */ jsx(ActivityMedia, { media: data.media }) : null,
106
+ /* @__PURE__ */ jsx("p", { className: "lk-wr-prompt", id: promptId, children: data.prompt }),
107
+ /* @__PURE__ */ jsx(
108
+ "textarea",
109
+ {
110
+ className: "lk-wr-textarea",
111
+ "aria-labelledby": promptId,
112
+ "aria-describedby": counterId,
113
+ value: text,
114
+ onChange: (event) => handleChange(event.target.value),
115
+ disabled: inactive,
116
+ rows: 8
117
+ }
118
+ ),
119
+ /* @__PURE__ */ jsxs(
120
+ "div",
121
+ {
122
+ className: "lk-wr-counter",
123
+ id: counterId,
124
+ "aria-live": "polite",
125
+ "data-within-bounds": withinBounds,
126
+ children: [
127
+ wordCount,
128
+ " ",
129
+ wordCount === 1 ? "word" : "words",
130
+ " (",
131
+ boundsLabel,
132
+ ")"
133
+ ]
134
+ }
135
+ ),
136
+ /* @__PURE__ */ jsx(
137
+ "button",
138
+ {
139
+ type: "submit",
140
+ className: "lk-wr-submit",
141
+ disabled: inactive || text.trim().length === 0,
142
+ children: "Submit"
143
+ }
144
+ ),
145
+ /* @__PURE__ */ jsx(FeedbackRegion, { id: `${data.id}-feedback`, children: summary })
146
+ ]
147
+ }
148
+ );
149
+ }
150
+ function WrittenResponse2(props) {
151
+ return /* @__PURE__ */ jsx(ActivityErrorBoundary, { activityTitle: props.data?.title, children: /* @__PURE__ */ jsx(WrittenResponse, { ...props }) });
152
+ }
153
+
154
+ export { WrittenResponse2 as WrittenResponse };
155
+ //# sourceMappingURL=chunk-V5EN3ESV.js.map
156
+ //# sourceMappingURL=chunk-V5EN3ESV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/WrittenResponse/WrittenResponse.tsx","../src/components/WrittenResponse/index.tsx"],"names":["WrittenResponse","jsx"],"mappings":"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","file":"chunk-V5EN3ESV.js","sourcesContent":["'use client';\n\nimport {\n ActivitySchemaError,\n countWords,\n evaluate,\n type InteractionEvent,\n type ThemeTokens,\n validateActivity,\n type WrittenResponseData,\n type XAPIStatement,\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';\n\n/**\n * Payload delivered when the learner submits a written response. Grading is\n * DEFERRED (asynchronous AI/human grading), so there is no score here — and\n * deliberately no fake one: an ungraded submission must never be shown or\n * stored as a 0. The consumer persists `text` and grades it out-of-band.\n */\nexport interface WrittenResponseSubmission {\n /** The submitted text, verbatim. */\n text: string;\n /** Word count of `text`, recomputed with the canonical `countWords()`. */\n wordCount: number;\n /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */\n withinWordBounds: boolean;\n /** Time in milliseconds from first interaction to submission. */\n timeSpent: number;\n /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */\n xapiStatement: XAPIStatement;\n}\n\n/**\n * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the\n * completion callback: a deferred-grading activity completes with a\n * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored\n * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the\n * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact\n * bug this type exists to fix.\n */\nexport interface WrittenResponseProps {\n data: WrittenResponseData;\n onSubmitted: (submission: WrittenResponseSubmission) => void;\n onInteraction?: (event: InteractionEvent) => void;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\nexport function WrittenResponse({\n data,\n onSubmitted,\n onInteraction,\n theme,\n locale,\n disabled,\n}: WrittenResponseProps) {\n // Dev-only boundary validation (Req 2.3), same convention as MC/FIB.\n const devError = useMemo(() => {\n if (!isDevelopment()) {\n return null;\n }\n const result = validateActivity('written-response', data);\n return result.success ? null : new ActivitySchemaError('written-response', result.errors);\n }, [data]);\n\n const { state, start, complete, getTimeSpent, reset } = useActivityState();\n const [text, setText] = useState('');\n const [summary, setSummary] = useState<string | null>(null);\n\n // Reset on data-prop change (Req 3.7).\n // biome-ignore lint/correctness/useExhaustiveDependencies: data is the reset trigger (Req 3.7)\n useEffect(() => {\n setText('');\n setSummary(null);\n reset();\n }, [data, reset]);\n\n if (devError) {\n throw devError;\n }\n\n const submitted = state === 'completed';\n const inactive = disabled === true || submitted;\n const wordCount = countWords(text);\n const withinBounds = wordCount >= data.minWords && wordCount <= data.maxWords;\n\n const handleChange = (value: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n setText(value);\n onInteraction?.({\n type: 'text-changed',\n activityId: data.id,\n timestamp: Date.now(),\n payload: { wordCount: countWords(value) },\n });\n };\n\n const handleSubmit = (event: React.FormEvent): void => {\n event.preventDefault();\n if (inactive || text.trim().length === 0) {\n return;\n }\n const outcome = evaluate(data, { type: 'written-response', text, wordCount });\n complete();\n const timeSpent = getTimeSpent();\n const partial = outcome.status === 'deferred' ? outcome.partial : undefined;\n const finalWordCount = typeof partial?.wordCount === 'number' ? partial.wordCount : wordCount;\n const finalWithinBounds =\n typeof partial?.withinWordBounds === 'boolean' ? partial.withinWordBounds : withinBounds;\n const xapiStatement = xAPIBuilder.buildSubmittedStatement({\n actor: ANONYMOUS_ACTOR,\n object: {\n id: objectIdFor(data.id),\n name: { [data.locale ?? 'en-US']: data.title },\n type: 'http://adlnet.gov/expapi/activities/cmi.interaction',\n interactionType: 'long-fill-in',\n },\n timeSpentMs: timeSpent,\n response: text,\n resultExtensions: {\n 'urn:learning-kit:extension:word-count': finalWordCount,\n 'urn:learning-kit:extension:within-word-bounds': finalWithinBounds,\n },\n });\n onSubmitted({\n text,\n wordCount: finalWordCount,\n withinWordBounds: finalWithinBounds,\n timeSpent,\n xapiStatement,\n });\n setSummary('Response submitted. It will be graded and your result will appear here later.');\n onInteraction?.({\n type: 'submitted',\n activityId: data.id,\n timestamp: Date.now(),\n payload: { wordCount: finalWordCount, withinWordBounds: finalWithinBounds },\n });\n };\n\n const promptId = `${data.id}-prompt`;\n const counterId = `${data.id}-counter`;\n const boundsLabel =\n data.minWords > 0 ? `${data.minWords}–${data.maxWords} words` : `up to ${data.maxWords} words`;\n\n return (\n <form\n className=\"lk-wr\"\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 <p className=\"lk-wr-prompt\" id={promptId}>\n {data.prompt}\n </p>\n <textarea\n className=\"lk-wr-textarea\"\n aria-labelledby={promptId}\n aria-describedby={counterId}\n value={text}\n onChange={(event) => handleChange(event.target.value)}\n disabled={inactive}\n rows={8}\n />\n <div\n className=\"lk-wr-counter\"\n id={counterId}\n aria-live=\"polite\"\n data-within-bounds={withinBounds}\n >\n {wordCount} {wordCount === 1 ? 'word' : 'words'} ({boundsLabel})\n </div>\n <button\n type=\"submit\"\n className=\"lk-wr-submit\"\n disabled={inactive || text.trim().length === 0}\n >\n Submit\n </button>\n <FeedbackRegion id={`${data.id}-feedback`}>{summary}</FeedbackRegion>\n </form>\n );\n}\n","'use client';\n\nimport { ActivityErrorBoundary } from '../ActivityErrorBoundary.js';\nimport {\n WrittenResponse as WrittenResponseCore,\n type WrittenResponseProps,\n} from './WrittenResponse.js';\n\nexport type { WrittenResponseProps, WrittenResponseSubmission } from './WrittenResponse.js';\n\n/**\n * Public Written Response activity: a labelled textarea with a live word\n * counter for free-text writing graded asynchronously (AI or human). The core\n * is wrapped in `ActivityErrorBoundary` so a render failure (incl. the dev\n * schema-validation throw) degrades to an accessible fallback.\n */\nexport function WrittenResponse(props: WrittenResponseProps) {\n return (\n <ActivityErrorBoundary activityTitle={props.data?.title}>\n <WrittenResponseCore {...props} />\n </ActivityErrorBoundary>\n );\n}\n"]}
@@ -1,16 +1,16 @@
1
1
  'use strict';
2
2
 
3
- var chunkRI53A36N_cjs = require('../chunk-RI53A36N.cjs');
4
- require('../chunk-QBA6UXMS.cjs');
5
- require('../chunk-O3GWWJFS.cjs');
6
- require('../chunk-6OBRYL4C.cjs');
3
+ var chunkSH7BRP4M_cjs = require('../chunk-SH7BRP4M.cjs');
4
+ require('../chunk-MKH5THDR.cjs');
5
+ require('../chunk-73XCWYKD.cjs');
6
+ require('../chunk-QHOV2A6M.cjs');
7
7
  require('../chunk-HD2AIQRD.cjs');
8
8
 
9
9
 
10
10
 
11
11
  Object.defineProperty(exports, "ActivitySequence", {
12
12
  enumerable: true,
13
- get: function () { return chunkRI53A36N_cjs.ActivitySequence; }
13
+ get: function () { return chunkSH7BRP4M_cjs.ActivitySequence; }
14
14
  });
15
15
  //# sourceMappingURL=ActivitySequence.cjs.map
16
16
  //# sourceMappingURL=ActivitySequence.cjs.map
@@ -1,7 +1,7 @@
1
- export { ActivitySequence } from '../chunk-ZPD7BGCF.js';
2
- import '../chunk-W2L66WHW.js';
3
- import '../chunk-5KW7TFX3.js';
4
- import '../chunk-L3EV5LIX.js';
1
+ export { ActivitySequence } from '../chunk-OS7MO2OW.js';
2
+ import '../chunk-G5ZEWJRA.js';
3
+ import '../chunk-32WWQOG3.js';
4
+ import '../chunk-UIGIYYGY.js';
5
5
  import '../chunk-5HVUKXF3.js';
6
6
  //# sourceMappingURL=ActivitySequence.js.map
7
7
  //# sourceMappingURL=ActivitySequence.js.map
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
- var chunkO3GWWJFS_cjs = require('../chunk-O3GWWJFS.cjs');
4
- require('../chunk-6OBRYL4C.cjs');
3
+ var chunk73XCWYKD_cjs = require('../chunk-73XCWYKD.cjs');
4
+ require('../chunk-QHOV2A6M.cjs');
5
5
  require('../chunk-HD2AIQRD.cjs');
6
6
 
7
7
 
8
8
 
9
9
  Object.defineProperty(exports, "FillInTheBlanks", {
10
10
  enumerable: true,
11
- get: function () { return chunkO3GWWJFS_cjs.FillInTheBlanks; }
11
+ get: function () { return chunk73XCWYKD_cjs.FillInTheBlanks; }
12
12
  });
13
13
  //# sourceMappingURL=FillInTheBlanks.cjs.map
14
14
  //# sourceMappingURL=FillInTheBlanks.cjs.map
@@ -1,5 +1,5 @@
1
- export { FillInTheBlanks } from '../chunk-5KW7TFX3.js';
2
- import '../chunk-L3EV5LIX.js';
1
+ export { FillInTheBlanks } from '../chunk-32WWQOG3.js';
2
+ import '../chunk-UIGIYYGY.js';
3
3
  import '../chunk-5HVUKXF3.js';
4
4
  //# sourceMappingURL=FillInTheBlanks.js.map
5
5
  //# sourceMappingURL=FillInTheBlanks.js.map
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
- var chunkQBA6UXMS_cjs = require('../chunk-QBA6UXMS.cjs');
4
- require('../chunk-6OBRYL4C.cjs');
3
+ var chunkMKH5THDR_cjs = require('../chunk-MKH5THDR.cjs');
4
+ require('../chunk-QHOV2A6M.cjs');
5
5
  require('../chunk-HD2AIQRD.cjs');
6
6
 
7
7
 
8
8
 
9
9
  Object.defineProperty(exports, "MultipleChoice", {
10
10
  enumerable: true,
11
- get: function () { return chunkQBA6UXMS_cjs.MultipleChoice; }
11
+ get: function () { return chunkMKH5THDR_cjs.MultipleChoice; }
12
12
  });
13
13
  //# sourceMappingURL=MultipleChoice.cjs.map
14
14
  //# sourceMappingURL=MultipleChoice.cjs.map
@@ -1,5 +1,5 @@
1
- export { MultipleChoice } from '../chunk-W2L66WHW.js';
2
- import '../chunk-L3EV5LIX.js';
1
+ export { MultipleChoice } from '../chunk-G5ZEWJRA.js';
2
+ import '../chunk-UIGIYYGY.js';
3
3
  import '../chunk-5HVUKXF3.js';
4
4
  //# sourceMappingURL=MultipleChoice.js.map
5
5
  //# sourceMappingURL=MultipleChoice.js.map
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var chunkKJNQLAB5_cjs = require('../chunk-KJNQLAB5.cjs');
4
+ require('../chunk-QHOV2A6M.cjs');
5
+ require('../chunk-HD2AIQRD.cjs');
6
+
7
+
8
+
9
+ Object.defineProperty(exports, "WrittenResponse", {
10
+ enumerable: true,
11
+ get: function () { return chunkKJNQLAB5_cjs.WrittenResponse; }
12
+ });
13
+ //# sourceMappingURL=WrittenResponse.cjs.map
14
+ //# sourceMappingURL=WrittenResponse.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"WrittenResponse.cjs"}
@@ -0,0 +1,48 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { WrittenResponseData, XAPIStatement, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
3
+
4
+ /**
5
+ * Payload delivered when the learner submits a written response. Grading is
6
+ * DEFERRED (asynchronous AI/human grading), so there is no score here — and
7
+ * deliberately no fake one: an ungraded submission must never be shown or
8
+ * stored as a 0. The consumer persists `text` and grades it out-of-band.
9
+ */
10
+ interface WrittenResponseSubmission {
11
+ /** The submitted text, verbatim. */
12
+ text: string;
13
+ /** Word count of `text`, recomputed with the canonical `countWords()`. */
14
+ wordCount: number;
15
+ /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */
16
+ withinWordBounds: boolean;
17
+ /** Time in milliseconds from first interaction to submission. */
18
+ timeSpent: number;
19
+ /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */
20
+ xapiStatement: XAPIStatement;
21
+ }
22
+ /**
23
+ * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the
24
+ * completion callback: a deferred-grading activity completes with a
25
+ * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored
26
+ * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the
27
+ * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact
28
+ * bug this type exists to fix.
29
+ */
30
+ interface WrittenResponseProps {
31
+ data: WrittenResponseData;
32
+ onSubmitted: (submission: WrittenResponseSubmission) => void;
33
+ onInteraction?: (event: InteractionEvent) => void;
34
+ /** Per-instance token overrides, applied as inline CSS vars on the root. */
35
+ theme?: Partial<ThemeTokens>;
36
+ locale?: string;
37
+ disabled?: boolean;
38
+ }
39
+
40
+ /**
41
+ * Public Written Response activity: a labelled textarea with a live word
42
+ * counter for free-text writing graded asynchronously (AI or human). The core
43
+ * is wrapped in `ActivityErrorBoundary` so a render failure (incl. the dev
44
+ * schema-validation throw) degrades to an accessible fallback.
45
+ */
46
+ declare function WrittenResponse(props: WrittenResponseProps): react_jsx_runtime.JSX.Element;
47
+
48
+ export { WrittenResponse, type WrittenResponseProps, type WrittenResponseSubmission };
@@ -0,0 +1,48 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { WrittenResponseData, XAPIStatement, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
3
+
4
+ /**
5
+ * Payload delivered when the learner submits a written response. Grading is
6
+ * DEFERRED (asynchronous AI/human grading), so there is no score here — and
7
+ * deliberately no fake one: an ungraded submission must never be shown or
8
+ * stored as a 0. The consumer persists `text` and grades it out-of-band.
9
+ */
10
+ interface WrittenResponseSubmission {
11
+ /** The submitted text, verbatim. */
12
+ text: string;
13
+ /** Word count of `text`, recomputed with the canonical `countWords()`. */
14
+ wordCount: number;
15
+ /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */
16
+ withinWordBounds: boolean;
17
+ /** Time in milliseconds from first interaction to submission. */
18
+ timeSpent: number;
19
+ /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */
20
+ xapiStatement: XAPIStatement;
21
+ }
22
+ /**
23
+ * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the
24
+ * completion callback: a deferred-grading activity completes with a
25
+ * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored
26
+ * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the
27
+ * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact
28
+ * bug this type exists to fix.
29
+ */
30
+ interface WrittenResponseProps {
31
+ data: WrittenResponseData;
32
+ onSubmitted: (submission: WrittenResponseSubmission) => void;
33
+ onInteraction?: (event: InteractionEvent) => void;
34
+ /** Per-instance token overrides, applied as inline CSS vars on the root. */
35
+ theme?: Partial<ThemeTokens>;
36
+ locale?: string;
37
+ disabled?: boolean;
38
+ }
39
+
40
+ /**
41
+ * Public Written Response activity: a labelled textarea with a live word
42
+ * counter for free-text writing graded asynchronously (AI or human). The core
43
+ * is wrapped in `ActivityErrorBoundary` so a render failure (incl. the dev
44
+ * schema-validation throw) degrades to an accessible fallback.
45
+ */
46
+ declare function WrittenResponse(props: WrittenResponseProps): react_jsx_runtime.JSX.Element;
47
+
48
+ export { WrittenResponse, type WrittenResponseProps, type WrittenResponseSubmission };
@@ -0,0 +1,5 @@
1
+ export { WrittenResponse } from '../chunk-V5EN3ESV.js';
2
+ import '../chunk-UIGIYYGY.js';
3
+ import '../chunk-5HVUKXF3.js';
4
+ //# sourceMappingURL=WrittenResponse.js.map
5
+ //# sourceMappingURL=WrittenResponse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"WrittenResponse.js"}
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var chunkTVK3SVV6_cjs = require('../chunk-TVK3SVV6.cjs');
3
+ var chunkIYLQSNLE_cjs = require('../chunk-IYLQSNLE.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "useXAPI", {
8
8
  enumerable: true,
9
- get: function () { return chunkTVK3SVV6_cjs.useXAPI; }
9
+ get: function () { return chunkIYLQSNLE_cjs.useXAPI; }
10
10
  });
11
11
  //# sourceMappingURL=useXAPI.cjs.map
12
12
  //# sourceMappingURL=useXAPI.cjs.map
@@ -1,3 +1,3 @@
1
- export { useXAPI } from '../chunk-IZNMQSQP.js';
1
+ export { useXAPI } from '../chunk-E4MNA2IQ.js';
2
2
  //# sourceMappingURL=useXAPI.js.map
3
3
  //# sourceMappingURL=useXAPI.js.map
package/dist/index.cjs CHANGED
@@ -1,26 +1,31 @@
1
1
  'use strict';
2
2
 
3
- var chunkRI53A36N_cjs = require('./chunk-RI53A36N.cjs');
4
- var chunkQBA6UXMS_cjs = require('./chunk-QBA6UXMS.cjs');
5
- var chunkO3GWWJFS_cjs = require('./chunk-O3GWWJFS.cjs');
6
- require('./chunk-6OBRYL4C.cjs');
3
+ var chunkSH7BRP4M_cjs = require('./chunk-SH7BRP4M.cjs');
4
+ var chunkMKH5THDR_cjs = require('./chunk-MKH5THDR.cjs');
5
+ var chunk73XCWYKD_cjs = require('./chunk-73XCWYKD.cjs');
6
+ var chunkKJNQLAB5_cjs = require('./chunk-KJNQLAB5.cjs');
7
+ require('./chunk-QHOV2A6M.cjs');
7
8
  var chunkHD2AIQRD_cjs = require('./chunk-HD2AIQRD.cjs');
8
- var chunkTVK3SVV6_cjs = require('./chunk-TVK3SVV6.cjs');
9
+ var chunkIYLQSNLE_cjs = require('./chunk-IYLQSNLE.cjs');
9
10
  var chunkCPF2JJG6_cjs = require('./chunk-CPF2JJG6.cjs');
10
11
 
11
12
 
12
13
 
13
14
  Object.defineProperty(exports, "ActivitySequence", {
14
15
  enumerable: true,
15
- get: function () { return chunkRI53A36N_cjs.ActivitySequence; }
16
+ get: function () { return chunkSH7BRP4M_cjs.ActivitySequence; }
16
17
  });
17
18
  Object.defineProperty(exports, "MultipleChoice", {
18
19
  enumerable: true,
19
- get: function () { return chunkQBA6UXMS_cjs.MultipleChoice; }
20
+ get: function () { return chunkMKH5THDR_cjs.MultipleChoice; }
20
21
  });
21
22
  Object.defineProperty(exports, "FillInTheBlanks", {
22
23
  enumerable: true,
23
- get: function () { return chunkO3GWWJFS_cjs.FillInTheBlanks; }
24
+ get: function () { return chunk73XCWYKD_cjs.FillInTheBlanks; }
25
+ });
26
+ Object.defineProperty(exports, "WrittenResponse", {
27
+ enumerable: true,
28
+ get: function () { return chunkKJNQLAB5_cjs.WrittenResponse; }
24
29
  });
25
30
  Object.defineProperty(exports, "useActivityState", {
26
31
  enumerable: true,
@@ -28,7 +33,7 @@ Object.defineProperty(exports, "useActivityState", {
28
33
  });
29
34
  Object.defineProperty(exports, "useXAPI", {
30
35
  enumerable: true,
31
- get: function () { return chunkTVK3SVV6_cjs.useXAPI; }
36
+ get: function () { return chunkIYLQSNLE_cjs.useXAPI; }
32
37
  });
33
38
  Object.defineProperty(exports, "ThemeProvider", {
34
39
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { ActivitySequence, ActivitySequenceProps } from './components/ActivitySequence.cjs';
2
2
  export { FillInTheBlanks, FillInTheBlanksProps } from './components/FillInTheBlanks.cjs';
3
3
  export { MultipleChoice } from './components/MultipleChoice.cjs';
4
+ export { WrittenResponse, WrittenResponseProps, WrittenResponseSubmission } from './components/WrittenResponse.cjs';
4
5
  export { ActivityState, UseActivityStateResult, useActivityState } from './hooks/useActivityState.cjs';
5
6
  export { UseXAPIResult, useXAPI } from './hooks/useXAPI.cjs';
6
7
  export { ThemeProvider, ThemeProviderProps, darkTheme, defaultTheme, useTheme } from './theme/ThemeProvider.cjs';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { ActivitySequence, ActivitySequenceProps } from './components/ActivitySequence.js';
2
2
  export { FillInTheBlanks, FillInTheBlanksProps } from './components/FillInTheBlanks.js';
3
3
  export { MultipleChoice } from './components/MultipleChoice.js';
4
+ export { WrittenResponse, WrittenResponseProps, WrittenResponseSubmission } from './components/WrittenResponse.js';
4
5
  export { ActivityState, UseActivityStateResult, useActivityState } from './hooks/useActivityState.js';
5
6
  export { UseXAPIResult, useXAPI } from './hooks/useXAPI.js';
6
7
  export { ThemeProvider, ThemeProviderProps, darkTheme, defaultTheme, useTheme } from './theme/ThemeProvider.js';
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- export { ActivitySequence } from './chunk-ZPD7BGCF.js';
2
- export { MultipleChoice } from './chunk-W2L66WHW.js';
3
- export { FillInTheBlanks } from './chunk-5KW7TFX3.js';
4
- import './chunk-L3EV5LIX.js';
1
+ export { ActivitySequence } from './chunk-OS7MO2OW.js';
2
+ export { MultipleChoice } from './chunk-G5ZEWJRA.js';
3
+ export { FillInTheBlanks } from './chunk-32WWQOG3.js';
4
+ export { WrittenResponse } from './chunk-V5EN3ESV.js';
5
+ import './chunk-UIGIYYGY.js';
5
6
  export { useActivityState } from './chunk-5HVUKXF3.js';
6
- export { useXAPI } from './chunk-IZNMQSQP.js';
7
+ export { useXAPI } from './chunk-E4MNA2IQ.js';
7
8
  export { ThemeProvider, darkTheme, defaultTheme, useTheme } from './chunk-B3FUUKZ2.js';
8
9
  //# sourceMappingURL=index.js.map
9
10
  //# sourceMappingURL=index.js.map
@@ -349,4 +349,89 @@
349
349
  color: var(--lk-color-text-muted);
350
350
  font-size: var(--lk-font-size-sm);
351
351
  }
352
+
353
+ /* ── Written Response ────────────────────────────────────────────────── */
354
+ .lk-wr {
355
+ display: flex;
356
+ flex-direction: column;
357
+ gap: var(--lk-spacing-md);
358
+ }
359
+
360
+ .lk-wr-prompt {
361
+ margin: 0;
362
+ color: var(--lk-color-text);
363
+ font-family: var(--lk-font-family);
364
+ font-size: var(--lk-font-size-base);
365
+ line-height: 1.6;
366
+ }
367
+
368
+ .lk-wr-textarea {
369
+ width: 100%;
370
+ padding: var(--lk-spacing-sm) var(--lk-spacing-md);
371
+ border: 1px solid var(--lk-color-border);
372
+ border-radius: var(--lk-border-radius-md);
373
+ background: var(--lk-color-surface);
374
+ color: var(--lk-color-text);
375
+ font-family: var(--lk-font-family);
376
+ font-size: var(--lk-font-size-base);
377
+ line-height: 1.5;
378
+ resize: vertical;
379
+ transition: var(--lk-transition-fast);
380
+ }
381
+
382
+ .lk-wr-textarea:disabled {
383
+ opacity: 0.7;
384
+ }
385
+
386
+ .lk-wr-counter {
387
+ color: var(--lk-color-text-muted);
388
+ font-size: var(--lk-font-size-sm);
389
+ }
390
+
391
+ .lk-wr-counter[data-within-bounds="true"] {
392
+ color: var(--lk-color-success);
393
+ }
394
+
395
+ .lk-wr button[type="submit"] {
396
+ align-self: flex-start;
397
+ padding: var(--lk-spacing-sm) var(--lk-spacing-lg);
398
+ border: none;
399
+ border-radius: var(--lk-border-radius-md);
400
+ background: var(--lk-color-primary);
401
+ color: var(--lk-color-on-primary);
402
+ font-family: var(--lk-font-family);
403
+ font-size: var(--lk-font-size-base);
404
+ cursor: pointer;
405
+ transition: var(--lk-transition-fast);
406
+ }
407
+
408
+ .lk-wr button[type="submit"]:hover:not(:disabled) {
409
+ background: var(--lk-color-primary-hover);
410
+ }
411
+
412
+ .lk-wr button[type="submit"]:disabled {
413
+ opacity: 0.55;
414
+ cursor: default;
415
+ }
416
+
417
+ .lk-wr :focus-visible {
418
+ outline: 2px solid var(--lk-color-focus-ring);
419
+ outline-offset: 2px;
420
+ }
421
+
422
+ .lk-wr > [aria-live] {
423
+ color: var(--lk-color-text-muted);
424
+ font-size: var(--lk-font-size-sm);
425
+ }
426
+
427
+ /* ── Reduced motion (WCAG 2.3.3) ─────────────────────────────────────── */
428
+ @media (prefers-reduced-motion: reduce) {
429
+ .lk-mc *,
430
+ .lk-fib *,
431
+ .lk-wr *,
432
+ .lk-seq * {
433
+ transition: none;
434
+ animation: none;
435
+ }
436
+ }
352
437
  }
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@intellectif/lk-react",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "React 19 activity components, hooks, and theming for learning-kit",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/intellectif/learning-kit.git",
8
+ "directory": "packages/lk-react"
9
+ },
5
10
  "type": "module",
6
11
  "sideEffects": [
7
12
  "**/*.css"
@@ -27,6 +32,11 @@
27
32
  "import": "./dist/components/FillInTheBlanks.js",
28
33
  "require": "./dist/components/FillInTheBlanks.cjs"
29
34
  },
35
+ "./components/WrittenResponse": {
36
+ "types": "./dist/components/WrittenResponse.d.ts",
37
+ "import": "./dist/components/WrittenResponse.js",
38
+ "require": "./dist/components/WrittenResponse.cjs"
39
+ },
30
40
  "./hooks/useActivityState": {
31
41
  "types": "./dist/hooks/useActivityState.d.ts",
32
42
  "import": "./dist/hooks/useActivityState.js",
@@ -49,12 +59,13 @@
49
59
  "module": "./dist/index.js",
50
60
  "types": "./dist/index.d.ts",
51
61
  "files": [
52
- "dist"
62
+ "dist",
63
+ "LICENSE"
53
64
  ],
54
65
  "peerDependencies": {
55
66
  "react": "^19",
56
67
  "react-dom": "^19",
57
- "@intellectif/lk-core": "^0.2.0"
68
+ "@intellectif/lk-core": "^0.3.0"
58
69
  },
59
70
  "devDependencies": {
60
71
  "@axe-core/react": "^4.10.0",
@@ -73,9 +84,12 @@
73
84
  "tsup": "^8.3.0",
74
85
  "vitest": "^4.1.6",
75
86
  "vitest-axe": "^0.1.0",
76
- "@intellectif/lk-core": "0.2.0"
87
+ "@intellectif/lk-core": "0.3.0"
77
88
  },
78
89
  "license": "MIT",
90
+ "engines": {
91
+ "node": ">=20"
92
+ },
79
93
  "scripts": {
80
94
  "build": "tsup",
81
95
  "test": "vitest run",