tessera-learn 0.0.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.
Files changed (71) hide show
  1. package/AGENTS.md +1228 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -0
  4. package/dist/plugin/index.d.ts +7 -0
  5. package/dist/plugin/index.d.ts.map +1 -0
  6. package/dist/plugin/index.js +1239 -0
  7. package/dist/plugin/index.js.map +1 -0
  8. package/package.json +77 -0
  9. package/src/archiver.d.ts +27 -0
  10. package/src/components/Accordion.svelte +32 -0
  11. package/src/components/AccordionItem.svelte +144 -0
  12. package/src/components/Audio.svelte +38 -0
  13. package/src/components/Callout.svelte +81 -0
  14. package/src/components/Carousel.svelte +194 -0
  15. package/src/components/CarouselSlide.svelte +32 -0
  16. package/src/components/DefaultLayout.svelte +108 -0
  17. package/src/components/FillInTheBlank.svelte +345 -0
  18. package/src/components/Image.svelte +47 -0
  19. package/src/components/Matching.svelte +513 -0
  20. package/src/components/MultipleChoice.svelte +363 -0
  21. package/src/components/Quiz.svelte +569 -0
  22. package/src/components/RevealModal.svelte +228 -0
  23. package/src/components/Sorting.svelte +663 -0
  24. package/src/components/Video.svelte +118 -0
  25. package/src/components/index.ts +15 -0
  26. package/src/components/quiz-payload.ts +71 -0
  27. package/src/components/util.ts +24 -0
  28. package/src/index.ts +56 -0
  29. package/src/plugin/export.ts +264 -0
  30. package/src/plugin/index.ts +464 -0
  31. package/src/plugin/layout.ts +55 -0
  32. package/src/plugin/manifest.ts +330 -0
  33. package/src/plugin/quiz.ts +65 -0
  34. package/src/plugin/validation.ts +838 -0
  35. package/src/runtime/App.svelte +435 -0
  36. package/src/runtime/ErrorPage.svelte +14 -0
  37. package/src/runtime/LoadingSkeleton.svelte +26 -0
  38. package/src/runtime/Sidebar.svelte +76 -0
  39. package/src/runtime/access.ts +55 -0
  40. package/src/runtime/adapters/cmi5.ts +341 -0
  41. package/src/runtime/adapters/discovery.ts +38 -0
  42. package/src/runtime/adapters/index.ts +99 -0
  43. package/src/runtime/adapters/retry.ts +284 -0
  44. package/src/runtime/adapters/scorm12.ts +172 -0
  45. package/src/runtime/adapters/scorm2004.ts +162 -0
  46. package/src/runtime/adapters/web.ts +62 -0
  47. package/src/runtime/contexts.ts +76 -0
  48. package/src/runtime/duration.ts +29 -0
  49. package/src/runtime/hooks.svelte.ts +543 -0
  50. package/src/runtime/interaction-format.ts +132 -0
  51. package/src/runtime/interaction.ts +96 -0
  52. package/src/runtime/navigation.svelte.ts +117 -0
  53. package/src/runtime/persistence.ts +56 -0
  54. package/src/runtime/progress.svelte.ts +168 -0
  55. package/src/runtime/quiz-policy.ts +227 -0
  56. package/src/runtime/slugify.ts +17 -0
  57. package/src/runtime/types.ts +92 -0
  58. package/src/runtime/xapi/agent-rules.ts +93 -0
  59. package/src/runtime/xapi/client.ts +133 -0
  60. package/src/runtime/xapi/derive-actor.ts +90 -0
  61. package/src/runtime/xapi/publisher.ts +604 -0
  62. package/src/runtime/xapi/registry.ts +38 -0
  63. package/src/runtime/xapi/setup.ts +250 -0
  64. package/src/runtime/xapi/types.ts +106 -0
  65. package/src/runtime/xapi/uuid.ts +21 -0
  66. package/src/runtime/xapi/validation.ts +71 -0
  67. package/src/runtime/xapi/version.ts +23 -0
  68. package/src/virtual.d.ts +16 -0
  69. package/styles/base.css +194 -0
  70. package/styles/layout.css +408 -0
  71. package/styles/theme.css +36 -0
@@ -0,0 +1,250 @@
1
+ import type { CourseConfig, XAPIConfig, XAPIExplicitConfig } from '../types.js';
2
+ import type { PersistenceAdapter } from '../persistence.js';
3
+ import type { XAPIAgent } from './types.js';
4
+ import { XAPIPublisher } from './publisher.js';
5
+ import { XAPIClient } from './client.js';
6
+ import {
7
+ synthesizeSCORM12Actor,
8
+ synthesizeSCORM2004Actor,
9
+ } from './derive-actor.js';
10
+ import { CMI5Adapter } from '../adapters/cmi5.js';
11
+ import { SCORM12Adapter } from '../adapters/scorm12.js';
12
+ import { SCORM2004Adapter } from '../adapters/scorm2004.js';
13
+
14
+ /**
15
+ * Wraps a value that the runtime knows how to materialize into an
16
+ * `XAPIPublisher`. Either a fresh publisher constructed for an explicit
17
+ * destination, or a reference to the cmi5 adapter's existing publisher
18
+ * (for the `endpoint: 'lms'` sentinel — same instance, shared queue).
19
+ */
20
+ type DestinationSource =
21
+ | { kind: 'lms-shared'; adapter: CMI5Adapter }
22
+ | { kind: 'explicit'; publisher: XAPIPublisher };
23
+
24
+ /**
25
+ * Throws synchronously when `endpoint: 'lms'` appears under cmi5 export
26
+ * but the runtime was constructed without cmi5 launch parameters (i.e.,
27
+ * running locally outside an LMS). Surfaced through every
28
+ * `sendStatement` call rather than silently no-oping — the alternative
29
+ * produces the "works in dev, silently broken in prod" footgun.
30
+ */
31
+ class XAPIDevFallbackError extends Error {
32
+ constructor() {
33
+ super(
34
+ "Tessera xAPI: xapi.endpoint is 'lms' but no cmi5 launch parameters " +
35
+ '(fetch / endpoint / activityId / actor) were present on the URL. ' +
36
+ 'Either launch this course from a real LMS / SCORM Cloud, or ' +
37
+ "temporarily change xapi.endpoint to an explicit URL pointed at a " +
38
+ 'local LRS (e.g. http://localhost:8080/data/xAPI/) for dev work.'
39
+ );
40
+ this.name = 'XAPIDevFallbackError';
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Build a stub publisher whose sends reject with `error`. Used for both
46
+ * dev-fallback paths: cmi5 `endpoint: 'lms'` with no launch params, and
47
+ * SCORM explicit endpoints that depend on a learner identity the dev
48
+ * fallback can't synthesize. The placeholder publisher carries a static
49
+ * actor so its constructor invariants hold and `XAPIClient.buildStatement`
50
+ * can run without throwing — only the network-bound methods reject.
51
+ */
52
+ function makeRejectingPublisher(error: () => Error): XAPIPublisher {
53
+ const pub = new XAPIPublisher({
54
+ endpoint: 'http://localhost/__tessera_dev_fallback__/',
55
+ auth: '',
56
+ actor: { mbox: 'mailto:nobody@example.invalid', objectType: 'Agent' },
57
+ activityId: 'http://localhost/__tessera_dev_fallback__',
58
+ });
59
+ // The static actor is cached at construction so getActor()/buildStatement
60
+ // work without a separate init() call. We only override the methods that
61
+ // would otherwise hit the network so author code surfaces the explicit
62
+ // error rather than silently no-oping.
63
+ const reject = (): Promise<never> => Promise.reject(error());
64
+ (pub as any).sendStatement = reject;
65
+ (pub as any).enqueueBuilt = reject;
66
+ return pub;
67
+ }
68
+
69
+ function makeDevFallbackPublisher(): XAPIPublisher {
70
+ return makeRejectingPublisher(() => new XAPIDevFallbackError());
71
+ }
72
+
73
+ class XAPISCORMDevFallbackError extends Error {
74
+ constructor(standard: 'scorm12' | 'scorm2004') {
75
+ const label = standard === 'scorm12' ? 'SCORM 1.2' : 'SCORM 2004';
76
+ super(
77
+ `Tessera xAPI: ${label} learner identity is unavailable in dev (no LMS API found, ` +
78
+ 'falling back to localStorage). The runtime cannot synthesize an actor for this xapi ' +
79
+ 'destination. Either supply xapi.actor explicitly in course.config.js, or launch from ' +
80
+ 'a real LMS / SCORM Cloud where ' +
81
+ (standard === 'scorm12' ? 'cmi.core.student_id' : 'cmi.learner_id') +
82
+ ' is populated.'
83
+ );
84
+ this.name = 'XAPISCORMDevFallbackError';
85
+ }
86
+ }
87
+
88
+ function makeSCORMDevFallbackPublisher(
89
+ standard: 'scorm12' | 'scorm2004'
90
+ ): XAPIPublisher {
91
+ return makeRejectingPublisher(() => new XAPISCORMDevFallbackError(standard));
92
+ }
93
+
94
+ /**
95
+ * Resolve a single `XAPIConfig` entry into a destination source. Returns
96
+ * null when the entry can't materialize (e.g., 'lms' with no cmi5
97
+ * adapter present in non-cmi5 export modes — the validator should have
98
+ * caught this at build time).
99
+ */
100
+ function resolveDestination(
101
+ entry: XAPIConfig,
102
+ config: CourseConfig,
103
+ adapter: PersistenceAdapter | null
104
+ ): DestinationSource | null {
105
+ if (entry.endpoint === 'lms') {
106
+ if (config.export?.standard !== 'cmi5') {
107
+ // Build-time validator should reject this; defense in depth at runtime.
108
+ console.warn(
109
+ "Tessera xAPI: ignoring xapi entry with endpoint: 'lms' under non-cmi5 export."
110
+ );
111
+ return null;
112
+ }
113
+ if (adapter instanceof CMI5Adapter) {
114
+ return { kind: 'lms-shared', adapter };
115
+ }
116
+ // Dev fallback — cmi5 launch params absent, adapter is the WebAdapter
117
+ // fallback. Materialize a publisher whose sends reject with an
118
+ // explicit error so author code surfaces the dev/prod gap.
119
+ return { kind: 'explicit', publisher: makeDevFallbackPublisher() };
120
+ }
121
+
122
+ // Explicit endpoint.
123
+ const explicit = entry as XAPIExplicitConfig;
124
+ const actorOrResolver = resolveExplicitActor(explicit, config, adapter);
125
+ if (actorOrResolver === null) return null;
126
+ if (
127
+ typeof actorOrResolver === 'object' &&
128
+ (actorOrResolver as { __scormDevFallback?: 'scorm12' | 'scorm2004' })
129
+ .__scormDevFallback
130
+ ) {
131
+ const std = (actorOrResolver as { __scormDevFallback: 'scorm12' | 'scorm2004' })
132
+ .__scormDevFallback;
133
+ return { kind: 'explicit', publisher: makeSCORMDevFallbackPublisher(std) };
134
+ }
135
+ const publisher = new XAPIPublisher({
136
+ endpoint: explicit.endpoint,
137
+ auth: explicit.auth,
138
+ actor: actorOrResolver as XAPIAgent | (() => XAPIAgent | Promise<XAPIAgent>),
139
+ activityId: explicit.activityId,
140
+ registration: explicit.registration,
141
+ });
142
+ return { kind: 'explicit', publisher };
143
+ }
144
+
145
+ /**
146
+ * Pick an actor (object or resolver function) for an explicit destination,
147
+ * applying the priority order: author-supplied > cmi5 launch actor >
148
+ * SCORM-derived actor > error. Returns null if no actor can be resolved
149
+ * (web export with no `xapi.actor` — build-time validator should have
150
+ * caught this; runtime returns null and the publisher is skipped).
151
+ */
152
+ function resolveExplicitActor(
153
+ explicit: XAPIExplicitConfig,
154
+ config: CourseConfig,
155
+ adapter: PersistenceAdapter | null
156
+ ):
157
+ | XAPIAgent
158
+ | (() => XAPIAgent | Promise<XAPIAgent>)
159
+ | { __scormDevFallback: 'scorm12' | 'scorm2004' }
160
+ | null {
161
+ if (explicit.actor !== undefined) return explicit.actor;
162
+ // No author-supplied actor — try mode-specific derivation.
163
+ if (config.export?.standard === 'cmi5' && adapter instanceof CMI5Adapter) {
164
+ const inner = adapter.getPublisher();
165
+ if (inner) {
166
+ // The cmi5 adapter's publisher has the launch actor cached.
167
+ try {
168
+ return inner.getActor();
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+ if (config.export?.standard === 'scorm12') {
176
+ if (adapter instanceof SCORM12Adapter) {
177
+ return synthesizeSCORM12Actor(
178
+ adapter.getAPI(),
179
+ explicit.activityId,
180
+ explicit.actorAccountHomePage
181
+ );
182
+ }
183
+ // Adapter is the WebAdapter dev fallback. Mirror the cmi5 'lms'
184
+ // dev-fallback path: install a stub publisher that surfaces an
185
+ // explicit error rather than silently no-oping. Authors get the
186
+ // same dev/prod parity in SCORM that they get in cmi5.
187
+ return { __scormDevFallback: 'scorm12' };
188
+ }
189
+ if (config.export?.standard === 'scorm2004') {
190
+ if (adapter instanceof SCORM2004Adapter) {
191
+ return synthesizeSCORM2004Actor(
192
+ adapter.getAPI(),
193
+ explicit.activityId,
194
+ explicit.actorAccountHomePage
195
+ );
196
+ }
197
+ return { __scormDevFallback: 'scorm2004' };
198
+ }
199
+ // Web export with no actor — build-time validator should have errored.
200
+ console.warn(
201
+ 'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.'
202
+ );
203
+ return null;
204
+ }
205
+
206
+ /**
207
+ * Construct an `XAPIClient` from a course's `config.xapi`. Returns null
208
+ * when xapi is unset, or when no destinations could be resolved.
209
+ *
210
+ * The returned client must have `init()` awaited before being registered
211
+ * so author code calling `useXAPI()` sees a fully initialized client
212
+ * (in particular, `getActor()` is safe to call sync).
213
+ */
214
+ export async function buildXAPIClient(
215
+ config: CourseConfig,
216
+ adapter: PersistenceAdapter | null
217
+ ): Promise<XAPIClient | null> {
218
+ const raw = config.xapi;
219
+ if (raw === undefined || raw === null) return null;
220
+ const entries: XAPIConfig[] = Array.isArray(raw) ? raw : [raw];
221
+ const sources: DestinationSource[] = [];
222
+ for (const entry of entries) {
223
+ const src = resolveDestination(entry, config, adapter);
224
+ if (src) sources.push(src);
225
+ }
226
+ if (sources.length === 0) return null;
227
+
228
+ // For each destination, get the publisher (either freshly constructed
229
+ // for an explicit entry, or the cmi5 adapter's existing instance for
230
+ // 'lms') and ensure it's initialized.
231
+ const publishers: XAPIPublisher[] = [];
232
+ for (const src of sources) {
233
+ if (src.kind === 'lms-shared') {
234
+ const inner = src.adapter.getPublisher();
235
+ if (inner) publishers.push(inner);
236
+ } else {
237
+ try {
238
+ await src.publisher.init();
239
+ publishers.push(src.publisher);
240
+ } catch (err) {
241
+ console.warn(
242
+ 'Tessera xAPI: failed to initialize an explicit destination — skipping.',
243
+ err
244
+ );
245
+ }
246
+ }
247
+ }
248
+ if (publishers.length === 0) return null;
249
+ return new XAPIClient(publishers);
250
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * xAPI types used by the publisher and registry. These mirror the relevant
3
+ * subset of the xAPI 1.0.3 spec — the publisher only models Agents (not
4
+ * Groups) for v1, and only the statement fields actually exercised by
5
+ * Tessera or surfaced to authors.
6
+ */
7
+
8
+ /**
9
+ * Identified xAPI Agent. Exactly one of `mbox` / `mbox_sha1sum` / `openid` /
10
+ * `account` must be present (the IFI rule). The publisher validates this on
11
+ * any actor it resolves; values that fail produce a runtime error rather
12
+ * than a silent LRS 400.
13
+ */
14
+ export interface XAPIAgent {
15
+ name?: string;
16
+ mbox?: string;
17
+ mbox_sha1sum?: string;
18
+ openid?: string;
19
+ account?: { homePage: string; name: string };
20
+ objectType?: 'Agent';
21
+ // Group support is non-goal for v1; field exists so a Group passed by an
22
+ // author surfaces a friendly validation error instead of a TS mismatch.
23
+ member?: unknown;
24
+ }
25
+
26
+ export interface XAPIVerb {
27
+ id: string;
28
+ display?: Record<string, string>;
29
+ }
30
+
31
+ export interface XAPIObject {
32
+ id: string;
33
+ objectType?: string;
34
+ definition?: Record<string, unknown>;
35
+ }
36
+
37
+ export interface XAPIContext {
38
+ registration?: string;
39
+ contextActivities?: {
40
+ parent?: Array<{ id: string }>;
41
+ grouping?: Array<{ id: string }>;
42
+ category?: Array<{ id: string }>;
43
+ other?: Array<{ id: string }>;
44
+ };
45
+ extensions?: Record<string, unknown>;
46
+ }
47
+
48
+ export interface XAPIResult {
49
+ success?: boolean;
50
+ completion?: boolean;
51
+ duration?: string;
52
+ score?: { scaled?: number; raw?: number; min?: number; max?: number };
53
+ response?: string;
54
+ extensions?: Record<string, unknown>;
55
+ }
56
+
57
+ /**
58
+ * Minimal partial-statement shape authors pass to `sendStatement`. The
59
+ * publisher fills in actor, timestamp, registration, grouping, sessionid
60
+ * extension, and statement id.
61
+ */
62
+ export interface PartialStatement {
63
+ verb: XAPIVerb;
64
+ object?: XAPIObject;
65
+ result?: XAPIResult;
66
+ context?: XAPIContext;
67
+ attachments?: unknown[];
68
+ }
69
+
70
+ /**
71
+ * Fully-formed statement after the publisher has filled in its automatic
72
+ * fields. Returned in `sendStatement`'s resolved value so authors can log
73
+ * or assert on what was actually sent.
74
+ */
75
+ export interface Statement {
76
+ id: string;
77
+ actor: XAPIAgent;
78
+ verb: XAPIVerb;
79
+ object: XAPIObject;
80
+ result?: XAPIResult;
81
+ context?: XAPIContext;
82
+ timestamp: string;
83
+ attachments?: unknown[];
84
+ }
85
+
86
+ export interface DestinationOutcome {
87
+ endpoint: string;
88
+ ok: boolean;
89
+ status?: number;
90
+ error?: Error;
91
+ }
92
+
93
+ export interface SendStatementResult {
94
+ statementId: string;
95
+ statement: Statement;
96
+ destinations: DestinationOutcome[];
97
+ }
98
+
99
+ export interface SendStatementOptions {
100
+ /**
101
+ * When false, the publisher sends one attempt and reports the outcome
102
+ * regardless of failure. Useful for high-volume telemetry where a missing
103
+ * statement is harmless. Default: true (retry on 5xx/network).
104
+ */
105
+ retry?: boolean;
106
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * RFC 4122 v4 UUID. Prefer `crypto.randomUUID` (modern browsers, Node 14.17+);
3
+ * fall back to a getRandomValues-based generator for legacy LMS shells.
4
+ */
5
+ export function uuidv4(): string {
6
+ const c: Crypto | undefined = globalThis.crypto;
7
+ if (c?.randomUUID) return c.randomUUID();
8
+ const bytes = new Uint8Array(16);
9
+ if (c?.getRandomValues) c.getRandomValues(bytes);
10
+ else for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
11
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
12
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
13
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0'));
14
+ return (
15
+ `${hex.slice(0, 4).join('')}-` +
16
+ `${hex.slice(4, 6).join('')}-` +
17
+ `${hex.slice(6, 8).join('')}-` +
18
+ `${hex.slice(8, 10).join('')}-` +
19
+ `${hex.slice(10, 16).join('')}`
20
+ );
21
+ }
@@ -0,0 +1,71 @@
1
+ import type { PartialStatement } from './types.js';
2
+ export { validateAgent, validateAuthCredential } from './agent-rules.js';
3
+
4
+ /** Thrown for runtime-validation failures (auth/actor resolver misuse). */
5
+ export class XAPIConfigError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = 'XAPIConfigError';
9
+ }
10
+ }
11
+
12
+ /** Thrown synchronously by `sendStatement` for partial-statement misuse. */
13
+ export class XAPIStatementError extends Error {
14
+ statement: PartialStatement;
15
+ constructor(message: string, statement: PartialStatement) {
16
+ super(message);
17
+ this.name = 'XAPIStatementError';
18
+ this.statement = statement;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Validate a partial statement at the boundary. Three checks —
24
+ * verb.id, object.id when supplied, score.scaled when supplied. Anything
25
+ * else passes through; the LRS gives clearer errors than we can.
26
+ *
27
+ * Called from both the client (so a fan-out send fails once before any
28
+ * destination's `buildStatement` runs) and the publisher (so a single-
29
+ * destination caller bypassing the client is still validated).
30
+ */
31
+ export function validatePartialStatement(partial: PartialStatement): void {
32
+ if (!partial || typeof partial !== 'object') {
33
+ throw new XAPIStatementError(
34
+ 'sendStatement: partial statement must be an object',
35
+ partial
36
+ );
37
+ }
38
+ if (
39
+ !partial.verb ||
40
+ typeof partial.verb !== 'object' ||
41
+ typeof partial.verb.id !== 'string' ||
42
+ !partial.verb.id
43
+ ) {
44
+ throw new XAPIStatementError(
45
+ 'sendStatement: verb.id is required and must be a non-empty string',
46
+ partial
47
+ );
48
+ }
49
+ if (partial.object !== undefined) {
50
+ if (
51
+ !partial.object ||
52
+ typeof partial.object !== 'object' ||
53
+ typeof partial.object.id !== 'string' ||
54
+ !partial.object.id
55
+ ) {
56
+ throw new XAPIStatementError(
57
+ 'sendStatement: object.id must be a non-empty string when object is supplied',
58
+ partial
59
+ );
60
+ }
61
+ }
62
+ const scaled = partial.result?.score?.scaled;
63
+ if (scaled !== undefined) {
64
+ if (typeof scaled !== 'number' || !Number.isFinite(scaled) || scaled < -1 || scaled > 1) {
65
+ throw new XAPIStatementError(
66
+ `sendStatement: result.score.scaled must be a number in [-1, 1], got ${scaled}`,
67
+ partial
68
+ );
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * xAPI version negotiated by Tessera. Sent as the `X-Experience-API-Version`
3
+ * header on every Statement / State API request.
4
+ *
5
+ * Pinned to ADL xAPI 1.0.3 — NOT IEEE 9274.1.1 (xAPI 2.0) — because:
6
+ *
7
+ * 1. cmi5 v1.0 (the LMS-launch profile we implement) is normatively bound
8
+ * to xAPI 1.0.x: §3 / §11 require LRS communication at 1.0.x and the
9
+ * `X-Experience-API-Version: 1.0.x` header. A conformant cmi5 LMS will
10
+ * reject a `2.0.0` header on its launch endpoint.
11
+ * 2. ADL has not yet published a cmi5 revision rebased on IEEE 9274. Until
12
+ * they do, every cmi5 launch in the wild expects 1.0.x.
13
+ * 3. None of the 2.0 additions (typed extensions, attachments-by-reference,
14
+ * profile registry) are features the runtime exercises — the wire format
15
+ * for the statement / state / registration / sessionid extension we use
16
+ * is unchanged.
17
+ *
18
+ * The right time to bump is when ADL releases cmi5 v2; that work will need
19
+ * proper version negotiation (the LRS announces its supported versions and
20
+ * the AU picks the highest mutually-supported one), not just a constant
21
+ * change here.
22
+ */
23
+ export const X_API_VERSION = '1.0.3';
@@ -0,0 +1,16 @@
1
+ declare module 'virtual:tessera-layout' {
2
+ import type { Component } from 'svelte';
3
+ const layout: Component<{ page: import('svelte').Snippet }> | null;
4
+ export default layout;
5
+ }
6
+
7
+ interface ImportMetaEnv {
8
+ readonly DEV: boolean;
9
+ readonly PROD: boolean;
10
+ readonly MODE: string;
11
+ readonly SSR: boolean;
12
+ }
13
+
14
+ interface ImportMeta {
15
+ readonly env: ImportMetaEnv;
16
+ }
@@ -0,0 +1,194 @@
1
+ /* ---- Reset ---- */
2
+ *,
3
+ *::before,
4
+ *::after {
5
+ box-sizing: border-box;
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ html {
11
+ -webkit-text-size-adjust: 100%;
12
+ -moz-tab-size: 4;
13
+ tab-size: 4;
14
+ line-height: 1.15;
15
+ }
16
+
17
+ body {
18
+ font-family: var(--tessera-font-family);
19
+ font-size: var(--tessera-font-size-base);
20
+ line-height: var(--tessera-line-height);
21
+ color: var(--tessera-text);
22
+ background-color: var(--tessera-bg);
23
+ -webkit-font-smoothing: antialiased;
24
+ -moz-osx-font-smoothing: grayscale;
25
+ }
26
+
27
+ button,
28
+ input,
29
+ select,
30
+ textarea {
31
+ font-family: inherit;
32
+ font-size: inherit;
33
+ line-height: inherit;
34
+ color: inherit;
35
+ }
36
+
37
+ img,
38
+ svg,
39
+ video,
40
+ canvas,
41
+ audio,
42
+ iframe,
43
+ embed,
44
+ object {
45
+ display: block;
46
+ max-width: 100%;
47
+ }
48
+
49
+ /* ---- Typography ---- */
50
+ h1, h2, h3, h4, h5, h6 {
51
+ font-weight: 700;
52
+ line-height: 1.25;
53
+ color: var(--tessera-text);
54
+ margin-top: var(--tessera-spacing-xl);
55
+ margin-bottom: var(--tessera-spacing-md);
56
+ }
57
+
58
+ h1 { font-size: 2rem; }
59
+ h2 { font-size: 1.625rem; }
60
+ h3 { font-size: 1.375rem; }
61
+ h4 { font-size: 1.125rem; }
62
+ h5 { font-size: 1rem; }
63
+ h6 { font-size: 0.875rem; }
64
+
65
+ h1:first-child,
66
+ h2:first-child,
67
+ h3:first-child {
68
+ margin-top: 0;
69
+ }
70
+
71
+ p {
72
+ margin-bottom: var(--tessera-spacing-md);
73
+ }
74
+
75
+ a {
76
+ color: var(--tessera-primary);
77
+ text-decoration: underline;
78
+ text-decoration-thickness: 1px;
79
+ text-underline-offset: 2px;
80
+ transition: color var(--tessera-transition-fast);
81
+ }
82
+
83
+ a:hover {
84
+ color: var(--tessera-primary-dark);
85
+ }
86
+
87
+ a:focus-visible {
88
+ outline: none;
89
+ box-shadow: var(--tessera-focus-ring);
90
+ border-radius: 2px;
91
+ }
92
+
93
+ strong, b {
94
+ font-weight: 700;
95
+ }
96
+
97
+ em, i {
98
+ font-style: italic;
99
+ }
100
+
101
+ /* ---- Lists ---- */
102
+ ul, ol {
103
+ margin-bottom: var(--tessera-spacing-md);
104
+ padding-left: var(--tessera-spacing-xl);
105
+ }
106
+
107
+ li {
108
+ margin-bottom: var(--tessera-spacing-sm);
109
+ }
110
+
111
+ li > ul,
112
+ li > ol {
113
+ margin-top: var(--tessera-spacing-sm);
114
+ margin-bottom: 0;
115
+ }
116
+
117
+ /* ---- Code ---- */
118
+ code {
119
+ font-family: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace;
120
+ font-size: 0.875em;
121
+ background-color: var(--tessera-bg-secondary);
122
+ border: 1px solid var(--tessera-border);
123
+ border-radius: 4px;
124
+ padding: 0.15em 0.4em;
125
+ }
126
+
127
+ pre {
128
+ margin-bottom: var(--tessera-spacing-md);
129
+ padding: var(--tessera-spacing-md);
130
+ background-color: var(--tessera-bg-secondary);
131
+ border: 1px solid var(--tessera-border);
132
+ border-radius: 8px;
133
+ overflow-x: auto;
134
+ }
135
+
136
+ pre code {
137
+ background: none;
138
+ border: none;
139
+ padding: 0;
140
+ font-size: 0.875rem;
141
+ }
142
+
143
+ /* ---- Blockquote ---- */
144
+ blockquote {
145
+ margin-bottom: var(--tessera-spacing-md);
146
+ padding: var(--tessera-spacing-md) var(--tessera-spacing-lg);
147
+ border-left: 4px solid var(--tessera-primary);
148
+ background-color: var(--tessera-bg-secondary);
149
+ border-radius: 0 8px 8px 0;
150
+ color: var(--tessera-text-light);
151
+ font-style: italic;
152
+ }
153
+
154
+ blockquote p:last-child {
155
+ margin-bottom: 0;
156
+ }
157
+
158
+ /* ---- Horizontal Rule ---- */
159
+ hr {
160
+ border: none;
161
+ border-top: 1px solid var(--tessera-border);
162
+ margin: var(--tessera-spacing-xl) 0;
163
+ }
164
+
165
+ /* ---- Table ---- */
166
+ table {
167
+ width: 100%;
168
+ border-collapse: collapse;
169
+ margin-bottom: var(--tessera-spacing-md);
170
+ font-size: 0.9375rem;
171
+ }
172
+
173
+ th, td {
174
+ padding: var(--tessera-spacing-sm) var(--tessera-spacing-md);
175
+ border: 1px solid var(--tessera-border);
176
+ text-align: left;
177
+ }
178
+
179
+ th {
180
+ background-color: var(--tessera-bg-secondary);
181
+ font-weight: 600;
182
+ }
183
+
184
+ /* ---- Focus Visible ---- */
185
+ :focus-visible {
186
+ outline: none;
187
+ box-shadow: var(--tessera-focus-ring);
188
+ }
189
+
190
+ /* ---- Selection ---- */
191
+ ::selection {
192
+ background-color: var(--tessera-primary-light);
193
+ color: var(--tessera-text);
194
+ }