@burdenoff/microfe-technospam 2026.1102.2 → 2026.1102.4

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 (94) hide show
  1. package/dist/generated/global-types.d.ts +307 -41
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +1730 -8
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
  6. package/dist/technospam/TechnoSpamRoot.d.ts.map +1 -1
  7. package/dist/technospam/TechnoSpamRoot.js +187 -178
  8. package/dist/technospam/TechnoSpamRoot.js.map +1 -1
  9. package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
  10. package/dist/technospam/TechnoSpamRoutes.js +32 -28
  11. package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
  12. package/dist/technospam/api/assessmentApi.d.ts +183 -0
  13. package/dist/technospam/api/assessmentApi.d.ts.map +1 -0
  14. package/dist/technospam/api/assessmentApi.js +278 -0
  15. package/dist/technospam/api/assessmentApi.js.map +1 -0
  16. package/dist/technospam/api/technospamApi.js +141 -141
  17. package/dist/technospam/api/technospamApi.js.map +1 -1
  18. package/dist/technospam/components/CourseAssessments.d.ts +9 -0
  19. package/dist/technospam/components/CourseAssessments.d.ts.map +1 -0
  20. package/dist/technospam/components/CourseAssessments.js +66 -0
  21. package/dist/technospam/components/CourseAssessments.js.map +1 -0
  22. package/dist/technospam/components/MediaCover.js +2 -2
  23. package/dist/technospam/components/MediaCover.js.map +1 -1
  24. package/dist/technospam/components/MobileSectionTabs.d.ts +9 -0
  25. package/dist/technospam/components/MobileSectionTabs.d.ts.map +1 -0
  26. package/dist/technospam/components/MobileSectionTabs.js +43 -0
  27. package/dist/technospam/components/MobileSectionTabs.js.map +1 -0
  28. package/dist/technospam/components/PageLayout.d.ts +7 -1
  29. package/dist/technospam/components/PageLayout.d.ts.map +1 -1
  30. package/dist/technospam/components/PageLayout.js +40 -28
  31. package/dist/technospam/components/PageLayout.js.map +1 -1
  32. package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
  33. package/dist/technospam/components/StatusBadge.js +8 -1
  34. package/dist/technospam/components/StatusBadge.js.map +1 -1
  35. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts +7 -0
  36. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts.map +1 -0
  37. package/dist/technospam/components/admin/AssessmentsAdminTab.js +421 -0
  38. package/dist/technospam/components/admin/AssessmentsAdminTab.js.map +1 -0
  39. package/dist/technospam/components/lesson/LessonKindFields.d.ts +3 -1
  40. package/dist/technospam/components/lesson/LessonKindFields.d.ts.map +1 -1
  41. package/dist/technospam/components/lesson/LessonKindFields.js +2 -2
  42. package/dist/technospam/components/lesson/LessonKindFields.js.map +1 -1
  43. package/dist/technospam/components/lesson/SaveCourseOffline.js +6 -6
  44. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts +29 -0
  45. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts.map +1 -0
  46. package/dist/technospam/hooks/useAssessmentIntegrity.js +57 -0
  47. package/dist/technospam/hooks/useAssessmentIntegrity.js.map +1 -0
  48. package/dist/technospam/hooks/usePhoneLayout.d.ts +18 -0
  49. package/dist/technospam/hooks/usePhoneLayout.d.ts.map +1 -0
  50. package/dist/technospam/hooks/usePhoneLayout.js +14 -0
  51. package/dist/technospam/hooks/usePhoneLayout.js.map +1 -0
  52. package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
  53. package/dist/technospam/pages/AdminAdminDashboardPagePage.js +365 -360
  54. package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
  55. package/dist/technospam/pages/ApplicationsProblemsPagePage.d.ts.map +1 -1
  56. package/dist/technospam/pages/ApplicationsProblemsPagePage.js +89 -87
  57. package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
  58. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts +9 -0
  59. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts.map +1 -0
  60. package/dist/technospam/pages/CoursesAssessmentPagePage.js +464 -0
  61. package/dist/technospam/pages/CoursesAssessmentPagePage.js.map +1 -0
  62. package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
  63. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +213 -211
  64. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  65. package/dist/technospam/pages/CoursesCoursesPagePage.d.ts.map +1 -1
  66. package/dist/technospam/pages/CoursesCoursesPagePage.js +117 -81
  67. package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
  68. package/dist/technospam/pages/CoursesMyLearningPagePage.js +1 -1
  69. package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
  70. package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  71. package/dist/technospam/pages/DashboardDashboardPagePage.js +21 -17
  72. package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
  73. package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js +3 -3
  74. package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js.map +1 -1
  75. package/dist/technospam/pages/MentorshipMentorProfilePagePage.js +4 -4
  76. package/dist/technospam/pages/MentorshipMentorProfilePagePage.js.map +1 -1
  77. package/dist/technospam/pages/MentorshipMentorsPagePage.d.ts.map +1 -1
  78. package/dist/technospam/pages/MentorshipMentorsPagePage.js +6 -4
  79. package/dist/technospam/pages/MentorshipMentorsPagePage.js.map +1 -1
  80. package/dist/technospam/pages/MentorshipSessionsPagePage.js +1 -1
  81. package/dist/technospam/pages/MentorshipSessionsPagePage.js.map +1 -1
  82. package/dist/technospam/types/index.d.ts +7 -0
  83. package/dist/technospam/types/index.d.ts.map +1 -1
  84. package/dist/technospam/utils/assessment.d.ts +64 -0
  85. package/dist/technospam/utils/assessment.d.ts.map +1 -0
  86. package/dist/technospam/utils/assessment.js +122 -0
  87. package/dist/technospam/utils/assessment.js.map +1 -0
  88. package/dist/technospam/utils/mobileTabs.d.ts +45 -0
  89. package/dist/technospam/utils/mobileTabs.d.ts.map +1 -0
  90. package/dist/technospam/utils/mobileTabs.js +70 -0
  91. package/dist/technospam/utils/mobileTabs.js.map +1 -0
  92. package/dist/test/matchMedia.d.ts +6 -0
  93. package/dist/test/matchMedia.d.ts.map +1 -0
  94. package/package.json +3 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssessmentsAdminTab.js","names":[],"sources":["../../../../src/technospam/components/admin/AssessmentsAdminTab.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { Archive, Check, ListChecks, Pencil, Plus, Rocket, ShieldCheck, X } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useCoursesQuery } from '../../api/technospamApi';\nimport {\n useArchiveAssessment,\n useAssessmentAttemptEventsQuery,\n useAssessmentAttemptsQuery,\n useAssessmentQuestionKeyQuery,\n useAssessmentsQuery,\n useCreateAssessment,\n usePublishAssessment,\n useReviewAssessmentAttempt,\n useUpdateAssessment,\n type TecAssessment,\n type TecAssessmentAttempt,\n type TecProctoringMode,\n type TecProctoringStatus,\n} from '../../api/assessmentApi';\nimport { LessonKindFields } from '../lesson/LessonKindFields';\nimport { draftFromLesson, draftToInput, type LessonKindDraft } from '../lesson/lessonAuthoring';\nimport { EmptyView, ErrorView, LoadingRows } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\nimport { notify } from '../../lib/toast';\nimport { formatDateTime, humanizeEnum } from '../../utils/format';\nimport {\n assessmentSettingsToInput,\n draftFromAssessment,\n emptyAssessmentDraft,\n type AssessmentSettingsDraft,\n} from '../../utils/assessment';\n\nconst MODES: TecProctoringMode[] = ['INTEGRITY_MONITORING', 'REVIEW_REQUIRED', 'NONE'];\nconst REVIEW_FILTERS: (TecProctoringStatus | 'ALL')[] = ['FLAGGED', 'PENDING_REVIEW', 'ALL'];\n\nfunction Field({ label, children }: { label: string; children: ReactNode }) {\n return (\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n {children}\n </label>\n );\n}\n\nfunction Toggle({\n label,\n checked,\n onChange,\n}: {\n label: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input type=\"checkbox\" checked={checked} onChange={(e) => onChange(e.target.checked)} />\n {label}\n </label>\n );\n}\n\n/**\n * Create / edit form for one assessment: settings plus the question set with\n * its answer key (reusing the quiz-lesson editor).\n */\nfunction AssessmentForm({\n courseId,\n assessment,\n onDone,\n}: {\n courseId: string;\n assessment: TecAssessment | null;\n onDone: () => void;\n}) {\n const [settings, setSettings] = useState<AssessmentSettingsDraft>(() =>\n assessment ? draftFromAssessment(assessment) : emptyAssessmentDraft()\n );\n const { data: keyData, loading: keyLoading } = useAssessmentQuestionKeyQuery(\n assessment?.id ?? null\n );\n const [questions, setQuestions] = useState<LessonKindDraft | null>(\n assessment ? null : draftFromLesson()\n );\n const [create, { loading: creating }] = useCreateAssessment();\n const [update, { loading: updating }] = useUpdateAssessment();\n const quiz =\n questions ?? (keyData ? draftFromLesson(null, keyData.tecAssessmentQuestionKey) : null);\n const set = <K extends keyof AssessmentSettingsDraft>(\n key: K,\n value: AssessmentSettingsDraft[K]\n ): void => setSettings((prev) => ({ ...prev, [key]: value }));\n\n const save = async (): Promise<void> => {\n const parsed = assessmentSettingsToInput(settings);\n if (!parsed.ok) return notify.error(parsed.error);\n let questionInput: Record<string, unknown> = {};\n if (quiz) {\n const q = draftToInput('QUIZ', quiz);\n if (!q.ok) return notify.error(q.error);\n questionInput = { questions: q.input.quizQuestions };\n }\n if (assessment) {\n const res = await update({\n variables: { id: assessment.id, input: { ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecUpdateAssessment) {\n notify.success('Assessment saved');\n onDone();\n }\n return;\n }\n const res = await create({\n variables: { input: { courseId, ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecCreateAssessment) {\n notify.success('Assessment created', 'Publish it when the questions are ready.');\n onDone();\n }\n };\n\n return (\n <GlassCard className=\"space-y-4 p-4 sm:p-5\" data-testid=\"assessment-form\">\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <Field label=\"Title\">\n <Input value={settings.title} onChange={(e) => set('title', e.target.value)} />\n </Field>\n <Field label=\"Integrity mode\">\n <Select\n value={settings.proctoringMode}\n onValueChange={(v) => set('proctoringMode', v as TecProctoringMode)}\n >\n <SelectTrigger aria-label=\"Integrity mode\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {MODES.map((mode) => (\n <SelectItem key={mode} value={mode}>\n {humanizeEnum(mode)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n <Field label=\"Time limit (minutes)\">\n <Input\n inputMode=\"numeric\"\n value={settings.timeLimitMinutes}\n onChange={(e) => set('timeLimitMinutes', e.target.value)}\n />\n </Field>\n <Field label=\"Pass mark (%)\">\n <Input\n inputMode=\"numeric\"\n value={settings.passingScore}\n onChange={(e) => set('passingScore', e.target.value)}\n />\n </Field>\n <Field label=\"Attempts per learner\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxAttempts}\n onChange={(e) => set('maxAttempts', e.target.value)}\n />\n </Field>\n <Field label=\"Allowed integrity events\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxIntegrityViolations}\n onChange={(e) => set('maxIntegrityViolations', e.target.value)}\n />\n </Field>\n <div className=\"sm:col-span-2\">\n <Field label=\"Description\">\n <Textarea\n value={settings.description}\n onChange={(e) => set('description', e.target.value)}\n />\n </Field>\n </div>\n <div className=\"flex flex-wrap gap-x-5 gap-y-2 sm:col-span-2\">\n <Toggle\n label=\"Require fullscreen\"\n checked={settings.requireFullscreen}\n onChange={(v) => set('requireFullscreen', v)}\n />\n <Toggle\n label=\"Block copy and paste\"\n checked={settings.blockClipboard}\n onChange={(v) => set('blockClipboard', v)}\n />\n <Toggle\n label=\"Shuffle questions\"\n checked={settings.shuffleQuestions}\n onChange={(v) => set('shuffleQuestions', v)}\n />\n <Toggle\n label=\"Shuffle options\"\n checked={settings.shuffleOptions}\n onChange={(v) => set('shuffleOptions', v)}\n />\n <Toggle\n label=\"Issue course certificate\"\n checked={settings.issuesCertificate}\n onChange={(v) => set('issuesCertificate', v)}\n />\n </div>\n </div>\n {quiz ? (\n <LessonKindFields\n kind=\"QUIZ\"\n draft={quiz}\n onChange={setQuestions}\n heading={`Questions (learners need ${settings.passingScore || '?'}% to pass)`}\n />\n ) : keyLoading ? (\n <LoadingRows count={2} />\n ) : null}\n <div className=\"flex justify-end gap-2 border-t border-border-seam pt-3\">\n <Button variant=\"outline\" onClick={onDone}>\n <X size={15} /> Cancel\n </Button>\n <Button\n onClick={() => void save()}\n disabled={creating || updating}\n data-testid=\"assessment-save\"\n >\n <Check size={15} /> {creating || updating ? 'Saving…' : 'Save assessment'}\n </Button>\n </div>\n </GlassCard>\n );\n}\n\nfunction AttemptReviewRow({ attempt, title }: { attempt: TecAssessmentAttempt; title: string }) {\n const [open, setOpen] = useState(false);\n const [notes, setNotes] = useState('');\n const { data: eventsData } = useAssessmentAttemptEventsQuery(open ? attempt.id : null);\n const [review, { loading }] = useReviewAssessmentAttempt();\n const reviewable =\n attempt.proctoringStatus === 'FLAGGED' || attempt.proctoringStatus === 'PENDING_REVIEW';\n\n const decide = async (decision: 'VERIFIED' | 'REJECTED'): Promise<void> => {\n const res = await review({\n variables: { id: attempt.id, decision, notes: notes.trim() || null },\n });\n const done = res.data?.tecReviewAssessmentAttempt;\n if (done) {\n notify.success(\n decision === 'VERIFIED' ? 'Attempt verified' : 'Attempt rejected',\n done.certificateId ? 'The certificate was issued.' : undefined\n );\n }\n };\n\n return (\n <GlassCard className=\"space-y-3 p-4\" data-testid=\"assessment-review-row\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">{title}</div>\n <div className=\"text-xs text-text-muted\">\n Learner {attempt.learnerId} · attempt {attempt.attemptNumber} · {attempt.score ?? 0}% (\n {attempt.passed ? 'passed' : 'not passed'}) · {attempt.integrityViolations} integrity\n events · {formatDateTime(attempt.submittedAt ?? attempt.startedAt)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={attempt.proctoringStatus} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setOpen((v) => !v)}>\n {open ? 'Hide' : 'Review'}\n </Button>\n </div>\n </div>\n {open ? (\n <div className=\"space-y-3\">\n <ol\n className=\"max-h-56 space-y-1 overflow-auto rounded-lg bg-bg-sunken p-3 text-xs\"\n data-testid=\"assessment-events\"\n >\n {(eventsData?.tecAssessmentAttemptEvents ?? []).map((event) => (\n <li\n key={event.id}\n className={event.violation ? 'text-status-warning-text' : 'text-text-secondary'}\n >\n {formatDateTime(event.occurredAt)} · {humanizeEnum(event.type)}\n {event.violation ? ' (violation)' : ''}\n </li>\n ))}\n </ol>\n {reviewable ? (\n <>\n <Textarea\n aria-label=\"Review notes\"\n placeholder=\"Notes for the learner (optional)\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n />\n <div className=\"flex gap-2\">\n <Button size=\"sm\" disabled={loading} onClick={() => void decide('VERIFIED')}>\n <Check size={14} /> Verify\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={() => void decide('REJECTED')}\n >\n <X size={14} /> Reject\n </Button>\n </div>\n </>\n ) : attempt.reviewNotes ? (\n <p className=\"text-sm text-text-secondary\">Reviewer note: {attempt.reviewNotes}</p>\n ) : null}\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\n/**\n * Authoring console tab for certification assessments (BOFF-4427): pick a\n * course, create / edit / publish / archive its assessments, and work the\n * review queue of flagged and review-required attempts.\n */\nexport function AssessmentsAdminTab() {\n const { data: courseData } = useCoursesQuery({});\n const courses = courseData?.tecListCourses?.items ?? [];\n const [courseId, setCourseId] = useState<string>('');\n const activeCourseId = courseId || courses[0]?.id || '';\n const { data, loading, error } = useAssessmentsQuery(\n { courseId: activeCourseId },\n !activeCourseId\n );\n const [editing, setEditing] = useState<TecAssessment | 'new' | null>(null);\n const [publish] = usePublishAssessment();\n const [archive] = useArchiveAssessment();\n const [filter, setFilter] = useState<TecProctoringStatus | 'ALL'>('FLAGGED');\n const attemptsQuery = useAssessmentAttemptsQuery({\n proctoringStatus: filter === 'ALL' ? null : filter,\n });\n const assessments = data?.tecListAssessments ?? [];\n const titles = new Map(assessments.map((a) => [a.id, a.title]));\n\n return (\n <div className=\"space-y-6\">\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <Field label=\"Course\">\n <Select\n value={activeCourseId}\n onValueChange={(v) => {\n setCourseId(v);\n setEditing(null);\n }}\n >\n <SelectTrigger aria-label=\"Course\" className=\"min-w-64\">\n <SelectValue placeholder=\"Choose a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((course) => (\n <SelectItem key={course.id} value={course.id}>\n {course.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n {activeCourseId && editing === null ? (\n <Button size=\"sm\" onClick={() => setEditing('new')} data-testid=\"assessment-new\">\n <Plus size={15} /> New assessment\n </Button>\n ) : null}\n </div>\n\n {editing !== null && activeCourseId ? (\n <AssessmentForm\n key={editing === 'new' ? 'new' : editing.id}\n courseId={activeCourseId}\n assessment={editing === 'new' ? null : editing}\n onDone={() => setEditing(null)}\n />\n ) : null}\n\n {!activeCourseId ? (\n <EmptyView\n title=\"No courses yet\"\n description=\"Create a course first, then add its assessments.\"\n />\n ) : error ? (\n <ErrorView title=\"Could not load assessments\" error={error} />\n ) : loading && assessments.length === 0 ? (\n <LoadingRows count={2} />\n ) : assessments.length === 0 ? (\n <EmptyView\n icon={<ShieldCheck size={26} />}\n title=\"No assessments for this course\"\n description=\"Add a timed, integrity-monitored exam that issues the course certificate.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {assessments.map((assessment) => (\n <GlassCard\n key={assessment.id}\n className=\"flex flex-wrap items-center justify-between gap-3 p-4\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">\n {assessment.title}\n </div>\n <div className=\"text-xs text-text-muted\">\n {assessment.questionCount} questions · {assessment.timeLimitMinutes} min · pass{' '}\n {assessment.passingScore}% · {humanizeEnum(assessment.proctoringMode)}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.status} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(assessment)}>\n <Pencil size={14} /> Edit\n </Button>\n {assessment.status !== 'PUBLISHED' ? (\n <Button\n size=\"sm\"\n onClick={() =>\n void publish({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecPublishAssessment)\n notify.success('Assessment published');\n })\n }\n >\n <Rocket size={14} /> Publish\n </Button>\n ) : (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n void archive({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecArchiveAssessment) notify.success('Assessment archived');\n })\n }\n >\n <Archive size={14} /> Archive\n </Button>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </section>\n\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <ListChecks size={16} /> Review queue\n </h3>\n <Select value={filter} onValueChange={(v) => setFilter(v as TecProctoringStatus | 'ALL')}>\n <SelectTrigger aria-label=\"Review filter\" className=\"min-w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {REVIEW_FILTERS.map((f) => (\n <SelectItem key={f} value={f}>\n {f === 'ALL' ? 'All attempts' : humanizeEnum(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {attemptsQuery.error ? (\n <ErrorView title=\"Could not load attempts\" error={attemptsQuery.error} />\n ) : (attemptsQuery.data?.tecListAssessmentAttempts ?? []).length === 0 ? (\n <EmptyView\n title=\"Nothing to review\"\n description=\"Flagged and review-required attempts appear here.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {(attemptsQuery.data?.tecListAssessmentAttempts ?? []).map((attempt) => (\n <AttemptReviewRow\n key={attempt.id}\n attempt={attempt}\n title={titles.get(attempt.assessmentId) ?? 'Assessment attempt'}\n />\n ))}\n </div>\n )}\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -7,9 +7,11 @@ import { LessonKindDraft } from './lessonAuthoring';
7
7
  * expected output) and the multiple-choice questions of a QUIZ lesson with
8
8
  * their answer key. Renders nothing for other kinds.
9
9
  */
10
- export declare function LessonKindFields({ kind, draft, onChange, }: {
10
+ export declare function LessonKindFields({ kind, draft, onChange, heading, }: {
11
11
  kind: TecLessonKind;
12
12
  draft: LessonKindDraft;
13
13
  onChange: (next: LessonKindDraft) => void;
14
+ /** Overrides the QUIZ section heading (assessments carry their own pass mark; BOFF-4427). */
15
+ heading?: string;
14
16
  }): import("react").JSX.Element | null;
15
17
  //# sourceMappingURL=LessonKindFields.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LessonKindFields.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;CAC3C,sCA6HA"}
1
+ {"version":3,"file":"LessonKindFields.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IAC1C,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,sCA6HA"}
@@ -4,7 +4,7 @@ import { Plus as i, Trash2 as a } from "lucide-react";
4
4
  import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
5
  import { Button as c, Input as l, Select as u, SelectContent as d, SelectItem as f, SelectTrigger as p, SelectValue as m, Textarea as h } from "@burdenoff/fe-libs/ui";
6
6
  //#region src/technospam/components/lesson/LessonKindFields.tsx
7
- function g({ kind: g, draft: _, onChange: v }) {
7
+ function g({ kind: g, draft: _, onChange: v, heading: y }) {
8
8
  if (g === "INTERACTIVE") return /* @__PURE__ */ s("div", {
9
9
  className: "mt-2 space-y-2 rounded-lg border border-border-seam p-3",
10
10
  children: [
@@ -76,7 +76,7 @@ function g({ kind: g, draft: _, onChange: v }) {
76
76
  children: [
77
77
  /* @__PURE__ */ o("p", {
78
78
  className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
79
- children: "Quiz questions (learners need 70% to pass)"
79
+ children: y ?? "Quiz questions (learners need 70% to pass)"
80
80
  }),
81
81
  _.quiz.map((t, n) => {
82
82
  let i = r(t.options);
@@ -1 +1 @@
1
- {"version":3,"file":"LessonKindFields.js","names":[],"sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"sourcesContent":["import { Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport type { TecLessonKind } from '../../api/technospamApi';\nimport { humanizeEnum } from '../../utils/format';\nimport {\n LESSON_LANGUAGES,\n emptyQuestion,\n splitOptions,\n type LessonKindDraft,\n type QuizQuestionDraft,\n} from './lessonAuthoring';\n\n/**\n * Kind-specific authoring fields for a lesson (BOFF-4107): the runnable\n * workspace of an INTERACTIVE lesson (language, starter code, sample input,\n * expected output) and the multiple-choice questions of a QUIZ lesson with\n * their answer key. Renders nothing for other kinds.\n */\nexport function LessonKindFields({\n kind,\n draft,\n onChange,\n}: {\n kind: TecLessonKind;\n draft: LessonKindDraft;\n onChange: (next: LessonKindDraft) => void;\n}) {\n if (kind === 'INTERACTIVE') {\n return (\n <div className=\"mt-2 space-y-2 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Runnable workspace\n </p>\n <Select value={draft.language} onValueChange={(v) => onChange({ ...draft, language: v })}>\n <SelectTrigger aria-label=\"Workspace language\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {LESSON_LANGUAGES.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Textarea\n aria-label=\"Starter code\"\n placeholder=\"Starter code shown in the editor\"\n className=\"font-mono text-xs\"\n value={draft.starterCode}\n onChange={(e) => onChange({ ...draft, starterCode: e.target.value })}\n spellCheck={false}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Textarea\n aria-label=\"Sample input\"\n placeholder=\"Sample input (stdin, optional)\"\n className=\"font-mono text-xs\"\n value={draft.sampleInput}\n onChange={(e) => onChange({ ...draft, sampleInput: e.target.value })}\n spellCheck={false}\n />\n <Textarea\n aria-label=\"Expected output\"\n placeholder=\"Expected output (optional; a matching run completes the lesson)\"\n className=\"font-mono text-xs\"\n value={draft.expectedOutput}\n onChange={(e) => onChange({ ...draft, expectedOutput: e.target.value })}\n spellCheck={false}\n />\n </div>\n </div>\n );\n }\n\n if (kind === 'QUIZ') {\n const update = (index: number, patch: Partial<QuizQuestionDraft>) =>\n onChange({\n ...draft,\n quiz: draft.quiz.map((q, i) => (i === index ? { ...q, ...patch } : q)),\n });\n return (\n <div className=\"mt-2 space-y-3 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Quiz questions (learners need 70% to pass)\n </p>\n {draft.quiz.map((q, i) => {\n const options = splitOptions(q.options);\n return (\n <div key={i} className=\"space-y-2 rounded-lg bg-bg-sunken p-3\">\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label={`Question ${i + 1} prompt`}\n placeholder={`Question ${i + 1}`}\n value={q.prompt}\n onChange={(e) => update(i, { prompt: e.target.value })}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove question ${i + 1}`}\n onClick={() => onChange({ ...draft, quiz: draft.quiz.filter((_, j) => j !== i) })}\n >\n <Trash2 size={14} />\n </Button>\n </div>\n <Textarea\n aria-label={`Question ${i + 1} options`}\n placeholder=\"Answer options, one per line\"\n value={q.options}\n onChange={(e) => update(i, { options: e.target.value })}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Select\n value={String(q.correctOption)}\n onValueChange={(v) => update(i, { correctOption: Number(v) })}\n disabled={options.length === 0}\n >\n <SelectTrigger aria-label={`Question ${i + 1} correct option`}>\n <SelectValue placeholder=\"Correct option\" />\n </SelectTrigger>\n <SelectContent>\n {options.map((o, oi) => (\n <SelectItem key={oi} value={String(oi)}>\n Correct: {o}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n aria-label={`Question ${i + 1} explanation`}\n placeholder=\"Explanation shown after grading (optional)\"\n value={q.explanation}\n onChange={(e) => update(i, { explanation: e.target.value })}\n />\n </div>\n </div>\n );\n })}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onChange({ ...draft, quiz: [...draft.quiz, emptyQuestion()] })}\n >\n <Plus size={14} /> Add question\n </Button>\n </div>\n );\n }\n\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LessonKindFields.js","names":[],"sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"sourcesContent":["import { Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport type { TecLessonKind } from '../../api/technospamApi';\nimport { humanizeEnum } from '../../utils/format';\nimport {\n LESSON_LANGUAGES,\n emptyQuestion,\n splitOptions,\n type LessonKindDraft,\n type QuizQuestionDraft,\n} from './lessonAuthoring';\n\n/**\n * Kind-specific authoring fields for a lesson (BOFF-4107): the runnable\n * workspace of an INTERACTIVE lesson (language, starter code, sample input,\n * expected output) and the multiple-choice questions of a QUIZ lesson with\n * their answer key. Renders nothing for other kinds.\n */\nexport function LessonKindFields({\n kind,\n draft,\n onChange,\n heading,\n}: {\n kind: TecLessonKind;\n draft: LessonKindDraft;\n onChange: (next: LessonKindDraft) => void;\n /** Overrides the QUIZ section heading (assessments carry their own pass mark; BOFF-4427). */\n heading?: string;\n}) {\n if (kind === 'INTERACTIVE') {\n return (\n <div className=\"mt-2 space-y-2 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Runnable workspace\n </p>\n <Select value={draft.language} onValueChange={(v) => onChange({ ...draft, language: v })}>\n <SelectTrigger aria-label=\"Workspace language\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {LESSON_LANGUAGES.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Textarea\n aria-label=\"Starter code\"\n placeholder=\"Starter code shown in the editor\"\n className=\"font-mono text-xs\"\n value={draft.starterCode}\n onChange={(e) => onChange({ ...draft, starterCode: e.target.value })}\n spellCheck={false}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Textarea\n aria-label=\"Sample input\"\n placeholder=\"Sample input (stdin, optional)\"\n className=\"font-mono text-xs\"\n value={draft.sampleInput}\n onChange={(e) => onChange({ ...draft, sampleInput: e.target.value })}\n spellCheck={false}\n />\n <Textarea\n aria-label=\"Expected output\"\n placeholder=\"Expected output (optional; a matching run completes the lesson)\"\n className=\"font-mono text-xs\"\n value={draft.expectedOutput}\n onChange={(e) => onChange({ ...draft, expectedOutput: e.target.value })}\n spellCheck={false}\n />\n </div>\n </div>\n );\n }\n\n if (kind === 'QUIZ') {\n const update = (index: number, patch: Partial<QuizQuestionDraft>) =>\n onChange({\n ...draft,\n quiz: draft.quiz.map((q, i) => (i === index ? { ...q, ...patch } : q)),\n });\n return (\n <div className=\"mt-2 space-y-3 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n {heading ?? 'Quiz questions (learners need 70% to pass)'}\n </p>\n {draft.quiz.map((q, i) => {\n const options = splitOptions(q.options);\n return (\n <div key={i} className=\"space-y-2 rounded-lg bg-bg-sunken p-3\">\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label={`Question ${i + 1} prompt`}\n placeholder={`Question ${i + 1}`}\n value={q.prompt}\n onChange={(e) => update(i, { prompt: e.target.value })}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove question ${i + 1}`}\n onClick={() => onChange({ ...draft, quiz: draft.quiz.filter((_, j) => j !== i) })}\n >\n <Trash2 size={14} />\n </Button>\n </div>\n <Textarea\n aria-label={`Question ${i + 1} options`}\n placeholder=\"Answer options, one per line\"\n value={q.options}\n onChange={(e) => update(i, { options: e.target.value })}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Select\n value={String(q.correctOption)}\n onValueChange={(v) => update(i, { correctOption: Number(v) })}\n disabled={options.length === 0}\n >\n <SelectTrigger aria-label={`Question ${i + 1} correct option`}>\n <SelectValue placeholder=\"Correct option\" />\n </SelectTrigger>\n <SelectContent>\n {options.map((o, oi) => (\n <SelectItem key={oi} value={String(oi)}>\n Correct: {o}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n aria-label={`Question ${i + 1} explanation`}\n placeholder=\"Explanation shown after grading (optional)\"\n value={q.explanation}\n onChange={(e) => update(i, { explanation: e.target.value })}\n />\n </div>\n </div>\n );\n })}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onChange({ ...draft, quiz: [...draft.quiz, emptyQuestion()] })}\n >\n <Plus size={14} /> Add question\n </Button>\n </div>\n );\n }\n\n return null;\n}\n"],"mappings":"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"}
@@ -5,11 +5,11 @@ import { useOnlineStatus as a } from "../../hooks/useOfflineLessons.js";
5
5
  import { useMemo as o, useState as s } from "react";
6
6
  import { CloudDownload as c, CloudOff as l, Trash2 as u } from "lucide-react";
7
7
  import { jsx as d, jsxs as f } from "react/jsx-runtime";
8
- import { Button as p } from "@burdenoff/fe-libs/ui";
9
- import { useApolloClient as m } from "@apollo/client/react";
8
+ import { useApolloClient as p } from "@apollo/client/react";
9
+ import { Button as m } from "@burdenoff/fe-libs/ui";
10
10
  //#region src/technospam/components/lesson/SaveCourseOffline.tsx
11
11
  function h({ scope: h, courseId: g, lessons: _, enrollment: v }) {
12
- let y = m(), b = a(), x = _.filter((e) => !e.isLocked), [S, C] = s(0), w = o(() => i(h, g), [
12
+ let y = p(), b = a(), x = _.filter((e) => !e.isLocked), [S, C] = s(0), w = o(() => i(h, g), [
13
13
  h,
14
14
  g,
15
15
  S
@@ -31,7 +31,7 @@ function h({ scope: h, courseId: g, lessons: _, enrollment: v }) {
31
31
  children: [/* @__PURE__ */ f("span", {
32
32
  className: "inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2.5 py-1 text-xs font-medium text-accent-primary",
33
33
  children: [/* @__PURE__ */ d(l, { size: 12 }), " Available offline"]
34
- }), /* @__PURE__ */ d(p, {
34
+ }), /* @__PURE__ */ d(m, {
35
35
  size: "sm",
36
36
  variant: "ghost",
37
37
  onClick: () => {
@@ -40,7 +40,7 @@ function h({ scope: h, courseId: g, lessons: _, enrollment: v }) {
40
40
  "aria-label": "Remove offline copy",
41
41
  children: /* @__PURE__ */ d(u, { size: 14 })
42
42
  })]
43
- }) : /* @__PURE__ */ f(p, {
43
+ }) : /* @__PURE__ */ f(m, {
44
44
  size: "sm",
45
45
  variant: "outline",
46
46
  onClick: () => void k(),
@@ -52,7 +52,7 @@ function h({ scope: h, courseId: g, lessons: _, enrollment: v }) {
52
52
  " Save for offline",
53
53
  D > 0 ? ` (${D}/${x.length})` : ""
54
54
  ]
55
- }) : /* @__PURE__ */ f(p, {
55
+ }) : /* @__PURE__ */ f(m, {
56
56
  size: "sm",
57
57
  variant: "outline",
58
58
  disabled: !0,
@@ -0,0 +1,29 @@
1
+ import { TecAssessmentSignal } from '../api/assessmentApi';
2
+
3
+ /**
4
+ * Browser-side integrity hooks for a running assessment attempt (BOFF-4427).
5
+ *
6
+ * While `active`, it:
7
+ * - reports leaving the tab (`visibilitychange`) and the window (`blur`);
8
+ * a blur that the tab switch already explains is not reported twice;
9
+ * - tracks fullscreen and reports leaving it when the attempt requires it;
10
+ * - blocks copy / cut / paste and the context menu when the attempt says so,
11
+ * and reports each try.
12
+ *
13
+ * These are deterrents and evidence, not a lockdown: the server decides what
14
+ * counts as a violation and what happens to the attempt.
15
+ */
16
+ export interface IntegrityOptions {
17
+ active: boolean;
18
+ requireFullscreen: boolean;
19
+ blockClipboard: boolean;
20
+ onSignal: (signal: TecAssessmentSignal, detail?: Record<string, string | number | boolean>) => void;
21
+ }
22
+ export interface IntegrityState {
23
+ /** Fullscreen is supported by this browser (and not blocked by the embedding page). */
24
+ fullscreenSupported: boolean;
25
+ isFullscreen: boolean;
26
+ enterFullscreen: () => Promise<boolean>;
27
+ }
28
+ export declare function useAssessmentIntegrity({ active, requireFullscreen, blockClipboard, onSignal, }: IntegrityOptions): IntegrityState;
29
+ //# sourceMappingURL=useAssessmentIntegrity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAssessmentIntegrity.d.ts","sourceRoot":"","sources":["../../../src/technospam/hooks/useAssessmentIntegrity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,CACR,MAAM,EAAE,mBAAmB,EAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,KAC/C,IAAI,CAAC;CACX;AAED,MAAM,WAAW,cAAc;IAC7B,uFAAuF;IACvF,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACzC;AASD,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,QAAQ,GACT,EAAE,gBAAgB,GAAG,cAAc,CA8GnC"}
@@ -0,0 +1,57 @@
1
+ import { useCallback as e, useEffect as t, useRef as n, useState as r } from "react";
2
+ //#region src/technospam/hooks/useAssessmentIntegrity.ts
3
+ var i = 800;
4
+ function a() {
5
+ return typeof document > "u" ? null : document.fullscreenElement;
6
+ }
7
+ function o({ active: o, requireFullscreen: s, blockClipboard: c, onSignal: l }) {
8
+ let u = typeof document < "u" && document.fullscreenEnabled === !0 && typeof document.documentElement.requestFullscreen == "function", [d, f] = r(() => a() !== null), p = n(l);
9
+ t(() => {
10
+ p.current = l;
11
+ }, [l]);
12
+ let m = n(0), h = n(!1), g = e(async () => {
13
+ if (!u) return !1;
14
+ try {
15
+ return await document.documentElement.requestFullscreen(), h.current = !0, !0;
16
+ } catch {
17
+ return !1;
18
+ }
19
+ }, [u]);
20
+ return t(() => {
21
+ if (!o) return;
22
+ let e = (e, t) => p.current(e, t), t, n = () => {
23
+ t !== void 0 && window.clearTimeout(t), t = void 0;
24
+ }, r = () => {
25
+ document.visibilityState === "hidden" && (m.current = Date.now(), n(), e("VISIBILITY_HIDDEN"));
26
+ }, l = () => {
27
+ Date.now() - m.current < i || document.visibilityState !== "hidden" && (n(), t = window.setTimeout(() => {
28
+ t = void 0, document.visibilityState !== "hidden" && (Date.now() - m.current < i || e("WINDOW_BLUR"));
29
+ }, i));
30
+ }, u = () => {
31
+ let t = a() !== null;
32
+ f(t), s && e(t ? "FULLSCREEN_ENTER" : "FULLSCREEN_EXIT");
33
+ }, d = (t) => (n) => {
34
+ c && (n.preventDefault(), e(t));
35
+ }, h = d("COPY_ATTEMPT"), g = d("CUT_ATTEMPT"), _ = d("PASTE_ATTEMPT"), v = d("CONTEXT_MENU");
36
+ return document.addEventListener("visibilitychange", r), window.addEventListener("blur", l), document.addEventListener("fullscreenchange", u), document.addEventListener("copy", h), document.addEventListener("cut", g), document.addEventListener("paste", _), document.addEventListener("contextmenu", v), () => {
37
+ n(), document.removeEventListener("visibilitychange", r), window.removeEventListener("blur", l), document.removeEventListener("fullscreenchange", u), document.removeEventListener("copy", h), document.removeEventListener("cut", g), document.removeEventListener("paste", _), document.removeEventListener("contextmenu", v);
38
+ };
39
+ }, [
40
+ o,
41
+ s,
42
+ c
43
+ ]), t(() => {
44
+ let e = () => {
45
+ h.current && (h.current = !1, a() !== null && typeof document.exitFullscreen == "function" && document.exitFullscreen().catch(() => void 0));
46
+ };
47
+ return o || e(), e;
48
+ }, [o]), {
49
+ fullscreenSupported: u,
50
+ isFullscreen: d,
51
+ enterFullscreen: g
52
+ };
53
+ }
54
+ //#endregion
55
+ export { o as useAssessmentIntegrity };
56
+
57
+ //# sourceMappingURL=useAssessmentIntegrity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAssessmentIntegrity.js","names":[],"sources":["../../../src/technospam/hooks/useAssessmentIntegrity.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { TecAssessmentSignal } from '../api/assessmentApi';\n\n/**\n * Browser-side integrity hooks for a running assessment attempt (BOFF-4427).\n *\n * While `active`, it:\n * - reports leaving the tab (`visibilitychange`) and the window (`blur`);\n * a blur that the tab switch already explains is not reported twice;\n * - tracks fullscreen and reports leaving it when the attempt requires it;\n * - blocks copy / cut / paste and the context menu when the attempt says so,\n * and reports each try.\n *\n * These are deterrents and evidence, not a lockdown: the server decides what\n * counts as a violation and what happens to the attempt.\n */\nexport interface IntegrityOptions {\n active: boolean;\n requireFullscreen: boolean;\n blockClipboard: boolean;\n onSignal: (\n signal: TecAssessmentSignal,\n detail?: Record<string, string | number | boolean>\n ) => void;\n}\n\nexport interface IntegrityState {\n /** Fullscreen is supported by this browser (and not blocked by the embedding page). */\n fullscreenSupported: boolean;\n isFullscreen: boolean;\n enterFullscreen: () => Promise<boolean>;\n}\n\n/** Window-level blur within this window of a visibility change is the same event. */\nconst BLUR_DEDUP_MS = 800;\n\nfunction fullscreenElement(): Element | null {\n return typeof document === 'undefined' ? null : document.fullscreenElement;\n}\n\nexport function useAssessmentIntegrity({\n active,\n requireFullscreen,\n blockClipboard,\n onSignal,\n}: IntegrityOptions): IntegrityState {\n const fullscreenSupported =\n typeof document !== 'undefined' &&\n document.fullscreenEnabled === true &&\n typeof document.documentElement.requestFullscreen === 'function';\n const [isFullscreen, setIsFullscreen] = useState<boolean>(() => fullscreenElement() !== null);\n const signalRef = useRef(onSignal);\n useEffect(() => {\n signalRef.current = onSignal;\n }, [onSignal]);\n const lastHiddenAt = useRef(0);\n // Whether this hook put the page into fullscreen, so it only exits what it entered.\n const acquiredFullscreen = useRef(false);\n\n const enterFullscreen = useCallback(async (): Promise<boolean> => {\n if (!fullscreenSupported) return false;\n try {\n await document.documentElement.requestFullscreen();\n acquiredFullscreen.current = true;\n return true;\n } catch {\n return false;\n }\n }, [fullscreenSupported]);\n\n useEffect(() => {\n if (!active) return undefined;\n const report = (\n signal: TecAssessmentSignal,\n detail?: Record<string, string | number | boolean>\n ): void => signalRef.current(signal, detail);\n\n // A tab switch fires both blur and visibilitychange, in either order.\n // Blur is reported only after a short wait, and dropped if the tab was\n // hidden around it, so one switch counts once.\n let pendingBlur: number | undefined;\n const cancelBlur = (): void => {\n if (pendingBlur !== undefined) window.clearTimeout(pendingBlur);\n pendingBlur = undefined;\n };\n const onVisibility = (): void => {\n if (document.visibilityState === 'hidden') {\n lastHiddenAt.current = Date.now();\n cancelBlur();\n report('VISIBILITY_HIDDEN');\n }\n };\n const onBlur = (): void => {\n if (Date.now() - lastHiddenAt.current < BLUR_DEDUP_MS) return;\n if (document.visibilityState === 'hidden') return;\n cancelBlur();\n pendingBlur = window.setTimeout(() => {\n pendingBlur = undefined;\n if (document.visibilityState === 'hidden') return;\n if (Date.now() - lastHiddenAt.current < BLUR_DEDUP_MS) return;\n report('WINDOW_BLUR');\n }, BLUR_DEDUP_MS);\n };\n const onFullscreen = (): void => {\n const now = fullscreenElement() !== null;\n setIsFullscreen(now);\n if (!requireFullscreen) return;\n report(now ? 'FULLSCREEN_ENTER' : 'FULLSCREEN_EXIT');\n };\n const clipboard =\n (signal: TecAssessmentSignal) =>\n (event: Event): void => {\n if (!blockClipboard) return;\n event.preventDefault();\n report(signal);\n };\n const onCopy = clipboard('COPY_ATTEMPT');\n const onCut = clipboard('CUT_ATTEMPT');\n const onPaste = clipboard('PASTE_ATTEMPT');\n const onContextMenu = clipboard('CONTEXT_MENU');\n\n document.addEventListener('visibilitychange', onVisibility);\n window.addEventListener('blur', onBlur);\n document.addEventListener('fullscreenchange', onFullscreen);\n document.addEventListener('copy', onCopy);\n document.addEventListener('cut', onCut);\n document.addEventListener('paste', onPaste);\n document.addEventListener('contextmenu', onContextMenu);\n return () => {\n cancelBlur();\n document.removeEventListener('visibilitychange', onVisibility);\n window.removeEventListener('blur', onBlur);\n document.removeEventListener('fullscreenchange', onFullscreen);\n document.removeEventListener('copy', onCopy);\n document.removeEventListener('cut', onCut);\n document.removeEventListener('paste', onPaste);\n document.removeEventListener('contextmenu', onContextMenu);\n };\n }, [active, requireFullscreen, blockClipboard]);\n\n // Leave fullscreen when the attempt ends (monitoring turned off, or the exam\n // unmounts after submit) so the learner gets their window back.\n useEffect(() => {\n const release = (): void => {\n if (!acquiredFullscreen.current) return;\n acquiredFullscreen.current = false;\n if (fullscreenElement() !== null && typeof document.exitFullscreen === 'function') {\n void document.exitFullscreen().catch(() => undefined);\n }\n };\n if (!active) release();\n return release;\n }, [active]);\n\n return { fullscreenSupported, isFullscreen, enterFullscreen };\n}\n"],"mappings":";;AAkCA,IAAM,IAAgB;AAEtB,SAAS,IAAoC;AAC3C,QAAO,OAAO,WAAa,MAAc,OAAO,SAAS;;AAG3D,SAAgB,EAAuB,EACrC,WACA,sBACA,mBACA,eACmC;CACnC,IAAM,IACJ,OAAO,WAAa,OACpB,SAAS,sBAAsB,MAC/B,OAAO,SAAS,gBAAgB,qBAAsB,YAClD,CAAC,GAAc,KAAmB,QAAwB,GAAmB,KAAK,KAAK,EACvF,IAAY,EAAO,EAAS;AAClC,SAAgB;AACd,IAAU,UAAU;IACnB,CAAC,EAAS,CAAC;CACd,IAAM,IAAe,EAAO,EAAE,EAExB,IAAqB,EAAO,GAAM,EAElC,IAAkB,EAAY,YAA8B;AAChE,MAAI,CAAC,EAAqB,QAAO;AACjC,MAAI;AAGF,UAFA,MAAM,SAAS,gBAAgB,mBAAmB,EAClD,EAAmB,UAAU,IACtB;UACD;AACN,UAAO;;IAER,CAAC,EAAoB,CAAC;AAsFzB,QApFA,QAAgB;AACd,MAAI,CAAC,EAAQ;EACb,IAAM,KACJ,GACA,MACS,EAAU,QAAQ,GAAQ,EAAO,EAKxC,GACE,UAAyB;AAE7B,GADI,MAAgB,KAAA,KAAW,OAAO,aAAa,EAAY,EAC/D,IAAc,KAAA;KAEV,UAA2B;AAC/B,GAAI,SAAS,oBAAoB,aAC/B,EAAa,UAAU,KAAK,KAAK,EACjC,GAAY,EACZ,EAAO,oBAAoB;KAGzB,UAAqB;AACrB,QAAK,KAAK,GAAG,EAAa,UAAU,KACpC,SAAS,oBAAoB,aACjC,GAAY,EACZ,IAAc,OAAO,iBAAiB;AACpC,QAAc,KAAA,GACV,SAAS,oBAAoB,aAC7B,KAAK,KAAK,GAAG,EAAa,UAAU,KACxC,EAAO,cAAc;MACpB,EAAc;KAEb,UAA2B;GAC/B,IAAM,IAAM,GAAmB,KAAK;AACpC,KAAgB,EAAI,EACf,KACL,EAAO,IAAM,qBAAqB,kBAAkB;KAEhD,KACH,OACA,MAAuB;AACjB,SACL,EAAM,gBAAgB,EACtB,EAAO,EAAO;KAEZ,IAAS,EAAU,eAAe,EAClC,IAAQ,EAAU,cAAc,EAChC,IAAU,EAAU,gBAAgB,EACpC,IAAgB,EAAU,eAAe;AAS/C,SAPA,SAAS,iBAAiB,oBAAoB,EAAa,EAC3D,OAAO,iBAAiB,QAAQ,EAAO,EACvC,SAAS,iBAAiB,oBAAoB,EAAa,EAC3D,SAAS,iBAAiB,QAAQ,EAAO,EACzC,SAAS,iBAAiB,OAAO,EAAM,EACvC,SAAS,iBAAiB,SAAS,EAAQ,EAC3C,SAAS,iBAAiB,eAAe,EAAc,QAC1C;AAQX,GAPA,GAAY,EACZ,SAAS,oBAAoB,oBAAoB,EAAa,EAC9D,OAAO,oBAAoB,QAAQ,EAAO,EAC1C,SAAS,oBAAoB,oBAAoB,EAAa,EAC9D,SAAS,oBAAoB,QAAQ,EAAO,EAC5C,SAAS,oBAAoB,OAAO,EAAM,EAC1C,SAAS,oBAAoB,SAAS,EAAQ,EAC9C,SAAS,oBAAoB,eAAe,EAAc;;IAE3D;EAAC;EAAQ;EAAmB;EAAe,CAAC,EAI/C,QAAgB;EACd,IAAM,UAAsB;AACrB,KAAmB,YACxB,EAAmB,UAAU,IACzB,GAAmB,KAAK,QAAQ,OAAO,SAAS,kBAAmB,cAChE,SAAS,gBAAgB,CAAC,YAAY,KAAA,EAAU;;AAIzD,SADK,KAAQ,GAAS,EACf;IACN,CAAC,EAAO,CAAC,EAEL;EAAE;EAAqB;EAAc;EAAiB"}
@@ -0,0 +1,18 @@
1
+ /** Below Tailwind's `sm` breakpoint (640px): the phone layouts apply. */
2
+ export declare const PHONE_MEDIA_QUERY = "(max-width: 639px)";
3
+ export interface PhoneLayout {
4
+ /** Viewport is phone-width (any platform). */
5
+ isPhone: boolean;
6
+ /**
7
+ * Installed app (Capacitor / standalone PWA) on a phone: the shell renders
8
+ * its bottom tab bar and native top bar instead of the desktop chrome.
9
+ */
10
+ nativeChrome: boolean;
11
+ }
12
+ /**
13
+ * The single source for "which phone layout am I in" across TechnoSpam pages,
14
+ * so the in-page section switcher, pull-to-refresh and phone-only defaults all
15
+ * agree with the shell's own tab-bar rule.
16
+ */
17
+ export declare function usePhoneLayout(): PhoneLayout;
18
+ //# sourceMappingURL=usePhoneLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePhoneLayout.d.ts","sourceRoot":"","sources":["../../../src/technospam/hooks/usePhoneLayout.ts"],"names":[],"mappings":"AAGA,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,WAAW,CAI5C"}
@@ -0,0 +1,14 @@
1
+ import { useMediaQuery as e } from "@burdenoff/fe-libs/shared/hooks";
2
+ import { useMobileTabBarVisible as t } from "@burdenoff/fe-libs/chrome/mobileTabBar/useMobileTabBarVisible";
3
+ //#region src/technospam/hooks/usePhoneLayout.ts
4
+ var n = "(max-width: 639px)";
5
+ function r() {
6
+ return {
7
+ isPhone: e(n),
8
+ nativeChrome: t()
9
+ };
10
+ }
11
+ //#endregion
12
+ export { r as usePhoneLayout };
13
+
14
+ //# sourceMappingURL=usePhoneLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePhoneLayout.js","names":[],"sources":["../../../src/technospam/hooks/usePhoneLayout.ts"],"sourcesContent":["import { useMediaQuery } from '@burdenoff/fe-libs/shared/hooks';\nimport { useMobileTabBarVisible } from '@burdenoff/fe-libs/chrome/mobileTabBar/useMobileTabBarVisible';\n\n/** Below Tailwind's `sm` breakpoint (640px): the phone layouts apply. */\nexport const PHONE_MEDIA_QUERY = '(max-width: 639px)';\n\nexport interface PhoneLayout {\n /** Viewport is phone-width (any platform). */\n isPhone: boolean;\n /**\n * Installed app (Capacitor / standalone PWA) on a phone: the shell renders\n * its bottom tab bar and native top bar instead of the desktop chrome.\n */\n nativeChrome: boolean;\n}\n\n/**\n * The single source for \"which phone layout am I in\" across TechnoSpam pages,\n * so the in-page section switcher, pull-to-refresh and phone-only defaults all\n * agree with the shell's own tab-bar rule.\n */\nexport function usePhoneLayout(): PhoneLayout {\n const isPhone = useMediaQuery(PHONE_MEDIA_QUERY);\n const nativeChrome = useMobileTabBarVisible();\n return { isPhone, nativeChrome };\n}\n"],"mappings":";;;AAIA,IAAa,IAAoB;AAiBjC,SAAgB,IAA8B;AAG5C,QAAO;EAAE,SAFO,EAAc,EAAkB;EAE9B,cADG,GAAwB;EACb"}
@@ -1 +1 @@
1
- {"version":3,"file":"AdminAdminDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/AdminAdminDashboardPagePage.tsx"],"names":[],"mappings":"AA+nDA,wBAAgB,2BAA2B,gCA+D1C"}
1
+ {"version":3,"file":"AdminAdminDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/AdminAdminDashboardPagePage.tsx"],"names":[],"mappings":"AAioDA,wBAAgB,2BAA2B,gCAoE1C"}