@burdenoff/microfe-technospam 2026.1030.6 → 2026.1031.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"InteractiveWorkspace.js","names":[],"sources":["../../../../src/technospam/components/lesson/InteractiveWorkspace.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport {\n CheckCircle2,\n Clock,\n Code2,\n Loader2,\n Play,\n RotateCcw,\n Terminal,\n XCircle,\n} from 'lucide-react';\nimport { Button, Textarea } from '@burdenoff/fe-libs/ui';\nimport { IoBlock } from '../SubmissionViews';\nimport { humanizeEnum } from '../../utils/format';\nimport { cn } from '../../utils/cn';\nimport { useRunLessonCode, type TecLesson, type TecLessonCodeRun } from '../../api/technospamApi';\n\n/**\n * A cold code sandbox can take up to a few minutes to start. While the API\n * answers WARMING we call it again on this interval, giving up after the\n * budget below so the learner can retry by hand.\n */\nconst WARMING_RETRY_MS = 4_000;\nconst WARMING_BUDGET_MS = 4 * 60_000;\n\nfunction runSummary(run: TecLessonCodeRun): {\n tone: 'success' | 'error' | 'neutral';\n text: string;\n} {\n if (run.compileFailed) return { tone: 'error', text: 'Compile error' };\n if (run.timedOut) return { tone: 'error', text: 'Time limit exceeded' };\n if (run.memoryLimitExceeded) return { tone: 'error', text: 'Memory limit exceeded' };\n if (run.exitCode !== 0) return { tone: 'error', text: `Exited with code ${run.exitCode}` };\n if (run.matchedExpected === true)\n return { tone: 'success', text: 'Output matches the expected output' };\n if (run.matchedExpected === false) return { tone: 'error', text: 'Output does not match yet' };\n return { tone: 'neutral', text: 'Ran successfully' };\n}\n\n/**\n * Runnable workspace for an INTERACTIVE lesson (BOFF-4107). Mount it with\n * `key={lesson.id}` so moving to another lesson starts a fresh editor: an editor\n * pre-filled with the lesson's starter code, an optional stdin box, and the\n * run output. Code runs only in the platform sandbox via `tecRunLessonCode`;\n * when the lesson has an expected output, a matching run completes the lesson\n * server-side and `onCompleted` lets the page refresh progress.\n */\nexport function InteractiveWorkspace({\n lesson,\n onCompleted,\n}: {\n lesson: TecLesson;\n onCompleted: () => void;\n}) {\n const starter = lesson.starterCode ?? '';\n const [code, setCode] = useState(starter);\n const [stdin, setStdin] = useState(lesson.sampleInput ?? '');\n const [run, setRun] = useState<TecLessonCodeRun | null>(null);\n const [warming, setWarming] = useState(false);\n const [running, setRunning] = useState(false);\n const warmingSince = useRef<number | null>(null);\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const [runLessonCode] = useRunLessonCode();\n\n // The page mounts one workspace per lesson (keyed by lesson id), so the\n // editor starts fresh on every lesson; only the pending retry needs cleanup.\n useEffect(\n () => () => {\n if (timer.current) clearTimeout(timer.current);\n },\n []\n );\n\n const execute = async (): Promise<void> => {\n setRunning(true);\n try {\n const res = await runLessonCode({\n variables: {\n input: {\n lessonId: lesson.id,\n language: lesson.language ?? undefined,\n code,\n stdin,\n },\n },\n });\n const result = res.data?.tecRunLessonCode;\n if (!result) {\n // The mutation failed (for example a spent sandbox quota); the error\n // toast explains why, so stop any warm-up polling and re-enable Run.\n setWarming(false);\n warmingSince.current = null;\n return;\n }\n if (result.status === 'WARMING') {\n warmingSince.current ??= Date.now();\n if (Date.now() - warmingSince.current < WARMING_BUDGET_MS) {\n setWarming(true);\n timer.current = setTimeout(() => void execute(), WARMING_RETRY_MS);\n return;\n }\n setWarming(false);\n warmingSince.current = null;\n setRun({ ...result, status: 'UNAVAILABLE' });\n return;\n }\n setWarming(false);\n warmingSince.current = null;\n setRun(result);\n if (result.lessonCompleted) onCompleted();\n } finally {\n setRunning(false);\n }\n };\n\n const busy = running || warming;\n const summary = run && run.status === 'COMPLETED' ? runSummary(run) : null;\n\n return (\n <div className=\"space-y-4\">\n <div className=\"overflow-hidden rounded-xl border border-border-strong bg-bg-sunken shadow-sm\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam bg-bg-canvas/40 px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-error-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-warning-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-success-border\" />\n </div>\n <span className=\"inline-flex items-center gap-1.5 font-mono text-xs text-text-muted\">\n <Code2 size={13} /> {lesson.language ? humanizeEnum(lesson.language) : 'code'}\n </span>\n </div>\n <Textarea\n aria-label=\"Lesson code editor\"\n value={code}\n onChange={(e) => setCode(e.target.value)}\n placeholder=\"Write your code…\"\n className=\"min-h-[260px] resize-y rounded-none border-0 bg-transparent font-mono text-xs leading-relaxed focus-visible:ring-0 focus-visible:ring-offset-0\"\n spellCheck={false}\n />\n </div>\n\n <div>\n <label\n htmlFor={`tec-lesson-stdin-${lesson.id}`}\n className=\"mb-1 block text-xs font-medium uppercase tracking-wide text-text-muted\"\n >\n Input (stdin)\n </label>\n <Textarea\n id={`tec-lesson-stdin-${lesson.id}`}\n value={stdin}\n onChange={(e) => setStdin(e.target.value)}\n placeholder=\"Optional input your program reads\"\n className=\"min-h-[64px] font-mono text-xs\"\n spellCheck={false}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button onClick={() => void execute()} disabled={busy || !code.trim()}>\n {busy ? <Loader2 size={16} className=\"animate-spin\" /> : <Play size={16} />}\n {warming ? 'Starting sandbox…' : running ? 'Running…' : 'Run code'}\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => {\n setCode(starter);\n setRun(null);\n }}\n disabled={busy || code === starter}\n >\n <RotateCcw size={15} /> Reset to starter\n </Button>\n <span className=\"text-xs text-text-muted\">Runs in an isolated sandbox.</span>\n </div>\n\n {warming ? (\n <p\n role=\"status\"\n className=\"rounded-lg border border-border-seam bg-bg-sunken px-3 py-2 text-sm text-text-secondary\"\n >\n The code sandbox is starting up. Your code will run automatically as soon as it is ready.\n </p>\n ) : null}\n\n {run?.status === 'UNAVAILABLE' ? (\n <p\n role=\"alert\"\n className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n The code sandbox is not available right now. Please try again in a moment.\n </p>\n ) : null}\n\n {run && summary ? (\n <div\n className=\"space-y-3 rounded-xl border border-border-seam p-4\"\n data-testid=\"lesson-run-output\"\n >\n <div className=\"flex flex-wrap items-center gap-3\">\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-sm font-medium',\n summary.tone === 'success' && 'text-status-success-text',\n summary.tone === 'error' && 'text-status-error-text',\n summary.tone === 'neutral' && 'text-text-primary'\n )}\n >\n {summary.tone === 'error' ? <XCircle size={16} /> : <CheckCircle2 size={16} />}\n {summary.text}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {run.durationMs} ms\n </span>\n {run.lessonCompleted ? (\n <span className=\"rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 text-xs font-medium text-status-success-text\">\n Lesson completed\n </span>\n ) : null}\n </div>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <IoBlock label=\"Output\" value={run.stdout} />\n {lesson.expectedOutput ? (\n <IoBlock label=\"Expected output\" value={lesson.expectedOutput} />\n ) : null}\n </div>\n {run.stderr ? (\n <div>\n <p className=\"mb-1 flex items-center gap-1 text-xs font-medium uppercase tracking-wide text-text-muted\">\n <Terminal size={12} /> Errors\n </p>\n <pre className=\"overflow-auto rounded-lg bg-status-error-bg-subtle px-3 py-2 font-mono text-xs text-status-error-text\">\n {run.stderr}\n </pre>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"InteractiveWorkspace.js","names":[],"sources":["../../../../src/technospam/components/lesson/InteractiveWorkspace.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport {\n CheckCircle2,\n Clock,\n Code2,\n Loader2,\n Play,\n RotateCcw,\n Terminal,\n XCircle,\n} from 'lucide-react';\nimport { Button, Textarea } from '@burdenoff/fe-libs/ui';\nimport { IoBlock } from '../SubmissionViews';\nimport { humanizeEnum } from '../../utils/format';\nimport { cn } from '../../utils/cn';\nimport { userMessageFromError } from '../../lib/errorMessage';\nimport { useRunLessonCode, type TecLesson, type TecLessonCodeRun } from '../../api/technospamApi';\n\n/**\n * A cold code sandbox can take up to a few minutes to start. While the API\n * answers WARMING we call it again on this interval, giving up after the\n * budget below so the learner can retry by hand.\n */\nconst WARMING_RETRY_MS = 4_000;\nconst WARMING_BUDGET_MS = 4 * 60_000;\n\nfunction runSummary(run: TecLessonCodeRun): {\n tone: 'success' | 'error' | 'neutral';\n text: string;\n} {\n if (run.compileFailed) return { tone: 'error', text: 'Compile error' };\n if (run.timedOut) return { tone: 'error', text: 'Time limit exceeded' };\n if (run.memoryLimitExceeded) return { tone: 'error', text: 'Memory limit exceeded' };\n if (run.exitCode !== 0) return { tone: 'error', text: `Exited with code ${run.exitCode}` };\n if (run.matchedExpected === true)\n return { tone: 'success', text: 'Output matches the expected output' };\n if (run.matchedExpected === false) return { tone: 'error', text: 'Output does not match yet' };\n return { tone: 'neutral', text: 'Ran successfully' };\n}\n\n/**\n * Runnable workspace for an INTERACTIVE lesson (BOFF-4107). Mount it with\n * `key={lesson.id}` so moving to another lesson starts a fresh editor: an editor\n * pre-filled with the lesson's starter code, an optional stdin box, and the\n * run output. Code runs only in the platform sandbox via `tecRunLessonCode`;\n * when the lesson has an expected output, a matching run completes the lesson\n * server-side and `onCompleted` lets the page refresh progress.\n */\nexport function InteractiveWorkspace({\n lesson,\n onCompleted,\n}: {\n lesson: TecLesson;\n onCompleted: () => void;\n}) {\n const starter = lesson.starterCode ?? '';\n const [code, setCode] = useState(starter);\n const [stdin, setStdin] = useState(lesson.sampleInput ?? '');\n const [run, setRun] = useState<TecLessonCodeRun | null>(null);\n const [warming, setWarming] = useState(false);\n const [running, setRunning] = useState(false);\n /** Why the last run was refused (for example a spent sandbox quota), shown inline. */\n const [runError, setRunError] = useState<string | null>(null);\n const warmingSince = useRef<number | null>(null);\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const [runLessonCode] = useRunLessonCode();\n\n // The page mounts one workspace per lesson (keyed by lesson id), so the\n // editor starts fresh on every lesson; only the pending retry needs cleanup.\n useEffect(\n () => () => {\n if (timer.current) clearTimeout(timer.current);\n },\n []\n );\n\n const execute = async (): Promise<void> => {\n setRunning(true);\n setRunError(null);\n try {\n const res = await runLessonCode({\n variables: {\n input: {\n lessonId: lesson.id,\n language: lesson.language ?? undefined,\n code,\n stdin,\n },\n },\n });\n const result = res.data?.tecRunLessonCode;\n if (!result) {\n // The mutation failed (for example a spent sandbox quota). Stop any\n // warm-up polling, re-enable Run and say why next to the editor, where\n // the learner is looking, not only in a transient toast.\n setWarming(false);\n warmingSince.current = null;\n setRun(null);\n setRunError(\n userMessageFromError(res.error, 'Your code could not be run. Please try again.')\n );\n return;\n }\n if (result.status === 'WARMING') {\n warmingSince.current ??= Date.now();\n if (Date.now() - warmingSince.current < WARMING_BUDGET_MS) {\n setWarming(true);\n timer.current = setTimeout(() => void execute(), WARMING_RETRY_MS);\n return;\n }\n setWarming(false);\n warmingSince.current = null;\n setRun({ ...result, status: 'UNAVAILABLE' });\n return;\n }\n setWarming(false);\n warmingSince.current = null;\n setRun(result);\n if (result.lessonCompleted) onCompleted();\n } finally {\n setRunning(false);\n }\n };\n\n const busy = running || warming;\n const summary = run && run.status === 'COMPLETED' ? runSummary(run) : null;\n\n return (\n <div className=\"space-y-4\">\n <div className=\"overflow-hidden rounded-xl border border-border-strong bg-bg-sunken shadow-sm\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam bg-bg-canvas/40 px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-error-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-warning-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-success-border\" />\n </div>\n <span className=\"inline-flex items-center gap-1.5 font-mono text-xs text-text-muted\">\n <Code2 size={13} /> {lesson.language ? humanizeEnum(lesson.language) : 'code'}\n </span>\n </div>\n <Textarea\n aria-label=\"Lesson code editor\"\n value={code}\n onChange={(e) => setCode(e.target.value)}\n placeholder=\"Write your code…\"\n className=\"min-h-[260px] resize-y rounded-none border-0 bg-transparent font-mono text-xs leading-relaxed focus-visible:ring-0 focus-visible:ring-offset-0\"\n spellCheck={false}\n />\n </div>\n\n <div>\n <label\n htmlFor={`tec-lesson-stdin-${lesson.id}`}\n className=\"mb-1 block text-xs font-medium uppercase tracking-wide text-text-muted\"\n >\n Input (stdin)\n </label>\n <Textarea\n id={`tec-lesson-stdin-${lesson.id}`}\n value={stdin}\n onChange={(e) => setStdin(e.target.value)}\n placeholder=\"Optional input your program reads\"\n className=\"min-h-[64px] font-mono text-xs\"\n spellCheck={false}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button onClick={() => void execute()} disabled={busy || !code.trim()}>\n {busy ? <Loader2 size={16} className=\"animate-spin\" /> : <Play size={16} />}\n {warming ? 'Starting sandbox…' : running ? 'Running…' : 'Run code'}\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => {\n setCode(starter);\n setRun(null);\n setRunError(null);\n }}\n disabled={busy || code === starter}\n >\n <RotateCcw size={15} /> Reset to starter\n </Button>\n <span className=\"text-xs text-text-muted\">Runs in an isolated sandbox.</span>\n </div>\n\n {warming ? (\n <p\n role=\"status\"\n className=\"rounded-lg border border-border-seam bg-bg-sunken px-3 py-2 text-sm text-text-secondary\"\n >\n The code sandbox is starting up. Your code will run automatically as soon as it is ready.\n </p>\n ) : null}\n\n {runError ? (\n <p\n role=\"alert\"\n data-testid=\"lesson-run-error\"\n className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {runError}\n </p>\n ) : null}\n\n {run?.status === 'UNAVAILABLE' ? (\n <p\n role=\"alert\"\n className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n The code sandbox is not available right now. Please try again in a moment.\n </p>\n ) : null}\n\n {run && summary ? (\n <div\n className=\"space-y-3 rounded-xl border border-border-seam p-4\"\n data-testid=\"lesson-run-output\"\n >\n <div className=\"flex flex-wrap items-center gap-3\">\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-sm font-medium',\n summary.tone === 'success' && 'text-status-success-text',\n summary.tone === 'error' && 'text-status-error-text',\n summary.tone === 'neutral' && 'text-text-primary'\n )}\n >\n {summary.tone === 'error' ? <XCircle size={16} /> : <CheckCircle2 size={16} />}\n {summary.text}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {run.durationMs} ms\n </span>\n {run.lessonCompleted ? (\n <span className=\"rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 text-xs font-medium text-status-success-text\">\n Lesson completed\n </span>\n ) : null}\n </div>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <IoBlock label=\"Output\" value={run.stdout} />\n {lesson.expectedOutput ? (\n <IoBlock label=\"Expected output\" value={lesson.expectedOutput} />\n ) : null}\n </div>\n {run.stderr ? (\n <div>\n <p className=\"mb-1 flex items-center gap-1 text-xs font-medium uppercase tracking-wide text-text-muted\">\n <Terminal size={12} /> Errors\n </p>\n <pre className=\"overflow-auto rounded-lg bg-status-error-bg-subtle px-3 py-2 font-mono text-xs text-status-error-text\">\n {run.stderr}\n </pre>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The message to show a person for a failed GraphQL operation: the service's
3
+ * `userMessage` when there is one, then the GraphQL error message, then the
4
+ * transport error's own message, then `fallback`.
5
+ */
6
+ export declare function userMessageFromError(error: unknown, fallback?: string): string;
7
+ //# sourceMappingURL=errorMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorMessage.d.ts","sourceRoot":"","sources":["../../../src/technospam/lib/errorMessage.ts"],"names":[],"mappings":"AAiCA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAsB,GAAG,MAAM,CAS3F"}
@@ -0,0 +1,21 @@
1
+ import { CombinedGraphQLErrors as e, ServerError as t } from "@apollo/client";
2
+ //#region src/technospam/lib/errorMessage.ts
3
+ function n(e) {
4
+ if (!e) return null;
5
+ let t = e.extensions?.userMessage;
6
+ return typeof t == "string" && t.trim() ? t.trim() : typeof e.message == "string" && e.message.trim() ? e.message.trim() : null;
7
+ }
8
+ function r(e) {
9
+ try {
10
+ return n(JSON.parse(e)?.errors?.[0]);
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function i(i, a = "Please try again.") {
16
+ return e.is(i) ? n(i.errors[0]) ?? a : t.is(i) ? r(i.bodyText) ?? a : i instanceof Error && i.message.trim() ? i.message.trim() : a;
17
+ }
18
+ //#endregion
19
+ export { i as userMessageFromError };
20
+
21
+ //# sourceMappingURL=errorMessage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorMessage.js","names":[],"sources":["../../../src/technospam/lib/errorMessage.ts"],"sourcesContent":["import { CombinedGraphQLErrors, ServerError } from '@apollo/client';\n\n/** The fields a platform GraphQL error carries that are worth showing a learner. */\ninterface GraphQLErrorShape {\n readonly message?: unknown;\n readonly extensions?: { readonly userMessage?: unknown } | null;\n}\n\nfunction messageOf(error: GraphQLErrorShape | undefined): string | null {\n if (!error) return null;\n const userMessage = error.extensions?.userMessage;\n if (typeof userMessage === 'string' && userMessage.trim()) return userMessage.trim();\n if (typeof error.message === 'string' && error.message.trim()) return error.message.trim();\n return null;\n}\n\n/**\n * Read the first GraphQL error out of a raw response body. The workspace\n * gateway answers a quota block (`QUOTA_EXHAUSTED` / `QUOTA_NOT_PROVISIONED`)\n * with HTTP 429 and a normal GraphQL error body; Apollo Client surfaces that as\n * a `ServerError` whose message is only \"Response not successful: Received\n * status code 429\", so the real reason has to come from the body.\n */\nfunction messageFromBody(bodyText: string): string | null {\n try {\n // A GraphQL response body; anything that is not one falls through to null.\n const body = JSON.parse(bodyText) as { errors?: readonly GraphQLErrorShape[] | null } | null;\n return messageOf(body?.errors?.[0]);\n } catch {\n return null;\n }\n}\n\n/**\n * The message to show a person for a failed GraphQL operation: the service's\n * `userMessage` when there is one, then the GraphQL error message, then the\n * transport error's own message, then `fallback`.\n */\nexport function userMessageFromError(error: unknown, fallback = 'Please try again.'): string {\n if (CombinedGraphQLErrors.is(error)) {\n return messageOf(error.errors[0]) ?? fallback;\n }\n if (ServerError.is(error)) {\n return messageFromBody(error.bodyText) ?? fallback;\n }\n if (error instanceof Error && error.message.trim()) return error.message.trim();\n return fallback;\n}\n"],"mappings":";;AAQA,SAAS,EAAU,GAAqD;AACtE,KAAI,CAAC,EAAO,QAAO;CACnB,IAAM,IAAc,EAAM,YAAY;AAGtC,QAFI,OAAO,KAAgB,YAAY,EAAY,MAAM,GAAS,EAAY,MAAM,GAChF,OAAO,EAAM,WAAY,YAAY,EAAM,QAAQ,MAAM,GAAS,EAAM,QAAQ,MAAM,GACnF;;AAUT,SAAS,EAAgB,GAAiC;AACxD,KAAI;AAGF,SAAO,EADM,KAAK,MAAM,EAAS,EACV,SAAS,GAAG;SAC7B;AACN,SAAO;;;AASX,SAAgB,EAAqB,GAAgB,IAAW,qBAA6B;AAQ3F,QAPI,EAAsB,GAAG,EAAM,GAC1B,EAAU,EAAM,OAAO,GAAG,IAAI,IAEnC,EAAY,GAAG,EAAM,GAChB,EAAgB,EAAM,SAAS,IAAI,IAExC,aAAiB,SAAS,EAAM,QAAQ,MAAM,GAAS,EAAM,QAAQ,MAAM,GACxE"}
@@ -1,4 +1,4 @@
1
- import { ue as e } from "../../node_modules/sonner/dist/index.js";
1
+ import { toast as e } from "sonner";
2
2
  //#region src/technospam/lib/toast.ts
3
3
  var t = {
4
4
  success(t, n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-technospam",
3
- "version": "2026.1030.6",
3
+ "version": "2026.1031.1",
4
4
  "description": "Burdenoff TechnoSpam Microfrontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -59,7 +59,6 @@
59
59
  "react": "^19.0.0",
60
60
  "react-dom": "^19.0.0",
61
61
  "react-router": "^8.3.0",
62
- "sonner": "^1.7.4",
63
62
  "tailwind-merge": "^3.4.0",
64
63
  "zustand": "^5.0.3",
65
64
  "recharts": "^2.15.0",
@@ -95,6 +94,7 @@
95
94
  "lefthook": "^2.1.1",
96
95
  "postcss": "^8.4.49",
97
96
  "prettier": "^3.4.2",
97
+ "sonner": "^2.0.7",
98
98
  "storybook": "8.6.18",
99
99
  "tailwindcss": "^4.0.0",
100
100
  "typescript": "^5.9.0",
@@ -107,6 +107,7 @@
107
107
  "@apollo/client": "^4.0.11",
108
108
  "react": "^19.0.0",
109
109
  "react-dom": "^19.0.0",
110
+ "sonner": "^2.0.7",
110
111
  "zustand": "^5.0.0"
111
112
  },
112
113
  "publishConfig": {