tessera-learn 0.0.10 → 0.0.13

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 (79) hide show
  1. package/README.md +1 -0
  2. package/dist/audit-BBJpQGqb.js +204 -0
  3. package/dist/audit-BBJpQGqb.js.map +1 -0
  4. package/dist/plugin/a11y-cli.d.ts +1 -0
  5. package/dist/plugin/a11y-cli.js +36 -0
  6. package/dist/plugin/a11y-cli.js.map +1 -0
  7. package/dist/plugin/cli.js +6 -3
  8. package/dist/plugin/cli.js.map +1 -1
  9. package/dist/plugin/index.d.ts +16 -1
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +171 -140
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
  14. package/dist/validation-B-xTvM9B.js.map +1 -0
  15. package/package.json +17 -2
  16. package/src/components/Accordion.svelte +3 -1
  17. package/src/components/AccordionItem.svelte +1 -5
  18. package/src/components/Audio.svelte +22 -5
  19. package/src/components/Callout.svelte +5 -1
  20. package/src/components/Carousel.svelte +24 -8
  21. package/src/components/DefaultLayout.svelte +41 -12
  22. package/src/components/FillInTheBlank.svelte +75 -103
  23. package/src/components/Image.svelte +14 -10
  24. package/src/components/LockedBanner.svelte +5 -5
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +81 -102
  28. package/src/components/Quiz.svelte +63 -21
  29. package/src/components/ResultIcon.svelte +20 -4
  30. package/src/components/RevealModal.svelte +25 -22
  31. package/src/components/Sorting.svelte +61 -26
  32. package/src/components/Transcript.svelte +37 -0
  33. package/src/components/Video.svelte +25 -20
  34. package/src/components/util.ts +4 -1
  35. package/src/components/video-embed.ts +25 -0
  36. package/src/index.ts +2 -7
  37. package/src/plugin/a11y/audit.ts +299 -0
  38. package/src/plugin/a11y/contrast.ts +67 -0
  39. package/src/plugin/a11y-cli.ts +35 -0
  40. package/src/plugin/cli.ts +6 -8
  41. package/src/plugin/export.ts +60 -50
  42. package/src/plugin/index.ts +244 -101
  43. package/src/plugin/layout.ts +6 -51
  44. package/src/plugin/manifest.ts +90 -24
  45. package/src/plugin/override-plugin.ts +68 -0
  46. package/src/plugin/quiz.ts +9 -54
  47. package/src/plugin/validation.ts +768 -183
  48. package/src/runtime/App.svelte +128 -64
  49. package/src/runtime/LoadingBar.svelte +12 -3
  50. package/src/runtime/Sidebar.svelte +24 -8
  51. package/src/runtime/access.ts +15 -3
  52. package/src/runtime/adapters/cmi5.ts +68 -116
  53. package/src/runtime/adapters/format.ts +67 -0
  54. package/src/runtime/adapters/index.ts +45 -34
  55. package/src/runtime/adapters/retry.ts +25 -84
  56. package/src/runtime/adapters/scorm-base.ts +19 -15
  57. package/src/runtime/adapters/scorm12.ts +8 -9
  58. package/src/runtime/adapters/scorm2004.ts +22 -30
  59. package/src/runtime/adapters/web.ts +1 -1
  60. package/src/runtime/hooks.svelte.ts +152 -328
  61. package/src/runtime/interaction-format.ts +30 -12
  62. package/src/runtime/interaction.ts +44 -11
  63. package/src/runtime/navigation.svelte.ts +29 -40
  64. package/src/runtime/persistence.ts +2 -2
  65. package/src/runtime/progress.svelte.ts +22 -9
  66. package/src/runtime/quiz-engine.svelte.ts +361 -0
  67. package/src/runtime/quiz-policy.ts +28 -179
  68. package/src/runtime/types.ts +24 -2
  69. package/src/runtime/xapi/agent-rules.ts +11 -3
  70. package/src/runtime/xapi/client.ts +5 -5
  71. package/src/runtime/xapi/derive-actor.ts +2 -2
  72. package/src/runtime/xapi/publisher.ts +33 -40
  73. package/src/runtime/xapi/setup.ts +18 -15
  74. package/src/runtime/xapi/validation.ts +15 -6
  75. package/src/virtual.d.ts +4 -1
  76. package/styles/base.css +32 -11
  77. package/styles/layout.css +39 -18
  78. package/styles/theme.css +15 -3
  79. package/dist/validation-BxWAMMnJ.js.map +0 -1
@@ -23,15 +23,23 @@
23
23
  const groupId = `mc-${componentId}`;
24
24
 
25
25
  const q = useQuestion({
26
- get id() { return id ?? `mc-${slugFromQuestion(question)}`; },
27
- get weight() { return weight; },
28
- get maxRetries() { return maxRetries; },
26
+ get id() {
27
+ return id ?? `mc-${slugFromQuestion(question)}`;
28
+ },
29
+ get weight() {
30
+ return weight;
31
+ },
32
+ get maxRetries() {
33
+ return maxRetries;
34
+ },
29
35
  response: () => ({
30
36
  type: 'choice',
31
37
  response: selectedOption !== null ? [String(selectedOption)] : [],
32
38
  correct: [String(correct)],
33
39
  }),
34
- reset: () => { selectedOption = null; },
40
+ reset: () => {
41
+ selectedOption = null;
42
+ },
35
43
  });
36
44
 
37
45
  // `q.mode` is fixed for the lifetime of the widget; capture once.
@@ -58,62 +66,72 @@
58
66
 
59
67
  function getOptionClass(optIndex) {
60
68
  if (!q.feedbackVisible) return '';
61
- const answer = inQuiz ? q.answer : selectedOption;
62
69
  if (isCorrectOption(optIndex)) return 'correct';
63
- if (optIndex === answer && !isCorrectOption(optIndex)) return 'incorrect';
70
+ if (optIndex === selectedOption && !isCorrectOption(optIndex))
71
+ return 'incorrect';
64
72
  return '';
65
73
  }
66
74
  </script>
67
75
 
68
- {#if !inQuiz}
69
- <div class="tessera-mc" role="radiogroup" aria-labelledby="{groupId}-label">
70
- <p class="tessera-mc-question" id="{groupId}-label">{question}</p>
71
-
72
- <div class="tessera-mc-options">
73
- {#each options as option, i}
74
- {@const optionId = `${groupId}-opt-${i}`}
75
- {@const isSelected = selectedOption === i}
76
- {@const stateClass = getOptionClass(i)}
77
- <label
78
- class="tessera-mc-option {stateClass}"
79
- class:selected={isSelected}
80
- for={optionId}
81
- >
82
- <input
83
- type="radio"
84
- id={optionId}
85
- name={groupId}
86
- value={i}
87
- checked={isSelected}
88
- disabled={q.submitted}
89
- onchange={() => handleSelect(i)}
90
- />
91
- <span class="tessera-mc-radio-custom"></span>
92
- <span class="tessera-mc-option-text">{option}</span>
93
-
94
- {#if q.submitted}
95
- {#if stateClass === 'correct' && (correctFeedback || optionFeedback[i])}
96
- <span class="tessera-mc-feedback correct">{optionFeedback[i] || correctFeedback}</span>
97
- {:else if stateClass === 'incorrect' && (incorrectFeedback || optionFeedback[i])}
98
- <span class="tessera-mc-feedback incorrect">{optionFeedback[i] || incorrectFeedback}</span>
99
- {:else if optionFeedback[i]}
100
- <span class="tessera-mc-feedback">{optionFeedback[i]}</span>
101
- {/if}
76
+ {#snippet mcContent()}
77
+ <p class="tessera-mc-question" id="{groupId}-label">{question}</p>
78
+
79
+ <div class="tessera-mc-options">
80
+ {#each options as option, i (i)}
81
+ {@const optionId = `${groupId}-opt-${i}`}
82
+ {@const isSelected = selectedOption === i}
83
+ {@const stateClass = getOptionClass(i)}
84
+ <label
85
+ class="tessera-mc-option {stateClass}"
86
+ class:selected={isSelected}
87
+ for={optionId}
88
+ >
89
+ <input
90
+ type="radio"
91
+ id={optionId}
92
+ name={groupId}
93
+ value={i}
94
+ checked={isSelected}
95
+ disabled={q.locked}
96
+ onchange={() => handleSelect(i)}
97
+ />
98
+ <span class="tessera-mc-radio-custom"></span>
99
+ <span class="tessera-mc-option-text">{option}</span>
100
+
101
+ {#if q.feedbackVisible}
102
+ {#if stateClass === 'correct' && (correctFeedback || optionFeedback[i])}
103
+ <span class="tessera-mc-feedback correct"
104
+ >{optionFeedback[i] || correctFeedback}</span
105
+ >
106
+ {:else if stateClass === 'incorrect' && (incorrectFeedback || optionFeedback[i])}
107
+ <span class="tessera-mc-feedback incorrect"
108
+ >{optionFeedback[i] || incorrectFeedback}</span
109
+ >
110
+ {:else if optionFeedback[i]}
111
+ <span class="tessera-mc-feedback">{optionFeedback[i]}</span>
102
112
  {/if}
103
- </label>
104
- {/each}
105
- </div>
106
-
107
- {#if q.submitted}
108
- {#if selectedOption === correct && correctFeedback && !optionFeedback[selectedOption]}
109
- <div class="tessera-mc-overall-feedback correct">{correctFeedback}</div>
110
- {:else if selectedOption !== correct && incorrectFeedback && !optionFeedback[selectedOption]}
111
- <div class="tessera-mc-overall-feedback incorrect">{incorrectFeedback}</div>
112
- {/if}
113
- {#if q.canRetry}
114
- <RetryButton onclick={() => q.retry()} />
115
- {/if}
113
+ {/if}
114
+ </label>
115
+ {/each}
116
+ </div>
117
+
118
+ {#if q.feedbackVisible}
119
+ {#if selectedOption === correct && correctFeedback && !optionFeedback[selectedOption]}
120
+ <div class="tessera-mc-overall-feedback correct">{correctFeedback}</div>
121
+ {:else if selectedOption !== correct && incorrectFeedback && !optionFeedback[selectedOption]}
122
+ <div class="tessera-mc-overall-feedback incorrect">
123
+ {incorrectFeedback}
124
+ </div>
125
+ {/if}
126
+ {#if !inQuiz && q.canRetry}
127
+ <RetryButton onclick={() => q.retry()} />
116
128
  {/if}
129
+ {/if}
130
+ {/snippet}
131
+
132
+ {#if !inQuiz}
133
+ <div class="tessera-mc" role="radiogroup" aria-labelledby="{groupId}-label">
134
+ {@render mcContent()}
117
135
  </div>
118
136
  {/if}
119
137
 
@@ -122,51 +140,7 @@
122
140
  {#if q.isLockedCorrect}
123
141
  <LockedBanner />
124
142
  {/if}
125
- <p class="tessera-mc-question" id="{groupId}-label">{question}</p>
126
-
127
- <div class="tessera-mc-options">
128
- {#each options as option, i}
129
- {@const optionId = `${groupId}-opt-${i}`}
130
- {@const isSelected = (q.locked ? q.answer : selectedOption) === i}
131
- {@const stateClass = getOptionClass(i)}
132
- <label
133
- class="tessera-mc-option {stateClass}"
134
- class:selected={isSelected}
135
- for={optionId}
136
- >
137
- <input
138
- type="radio"
139
- id={optionId}
140
- name={groupId}
141
- value={i}
142
- checked={isSelected}
143
- disabled={q.locked}
144
- onchange={() => handleSelect(i)}
145
- />
146
- <span class="tessera-mc-radio-custom"></span>
147
- <span class="tessera-mc-option-text">{option}</span>
148
-
149
- {#if q.feedbackVisible}
150
- {#if stateClass === 'correct' && (correctFeedback || optionFeedback[i])}
151
- <span class="tessera-mc-feedback correct">{optionFeedback[i] || correctFeedback}</span>
152
- {:else if stateClass === 'incorrect' && (incorrectFeedback || optionFeedback[i])}
153
- <span class="tessera-mc-feedback incorrect">{optionFeedback[i] || incorrectFeedback}</span>
154
- {:else if optionFeedback[i]}
155
- <span class="tessera-mc-feedback">{optionFeedback[i]}</span>
156
- {/if}
157
- {/if}
158
- </label>
159
- {/each}
160
- </div>
161
-
162
- {#if q.feedbackVisible}
163
- {@const answer = q.answer}
164
- {#if answer === correct && correctFeedback && !optionFeedback[answer]}
165
- <div class="tessera-mc-overall-feedback correct">{correctFeedback}</div>
166
- {:else if answer !== correct && incorrectFeedback && !optionFeedback[answer]}
167
- <div class="tessera-mc-overall-feedback incorrect">{incorrectFeedback}</div>
168
- {/if}
169
- {/if}
143
+ {@render mcContent()}
170
144
  </div>
171
145
  {/snippet}
172
146
 
@@ -196,7 +170,9 @@
196
170
  border: 2px solid var(--tessera-border);
197
171
  border-radius: 8px;
198
172
  cursor: pointer;
199
- transition: border-color 0.2s, background 0.2s;
173
+ transition:
174
+ border-color 0.2s,
175
+ background 0.2s;
200
176
  flex-wrap: wrap;
201
177
  min-height: 44px;
202
178
  }
@@ -221,7 +197,7 @@
221
197
  background: var(--tessera-error-bg);
222
198
  }
223
199
 
224
- .tessera-mc-option input[type="radio"] {
200
+ .tessera-mc-option input[type='radio'] {
225
201
  position: absolute;
226
202
  opacity: 0;
227
203
  width: 0;
@@ -235,7 +211,9 @@
235
211
  border: 2px solid var(--tessera-border);
236
212
  border-radius: 50%;
237
213
  margin-top: 2px;
238
- transition: border-color 0.2s, background 0.2s;
214
+ transition:
215
+ border-color 0.2s,
216
+ background 0.2s;
239
217
  position: relative;
240
218
  }
241
219
 
@@ -279,7 +257,8 @@
279
257
  .tessera-mc-feedback {
280
258
  width: 100%;
281
259
  font-size: 0.875rem;
282
- padding: var(--tessera-spacing-sm) 0 0 calc(20px + var(--tessera-spacing-md));
260
+ padding: var(--tessera-spacing-sm) 0 0
261
+ calc(20px + var(--tessera-spacing-md));
283
262
  line-height: 1.4;
284
263
  }
285
264
 
@@ -1,26 +1,28 @@
1
1
  <script>
2
2
  import { getContext } from 'svelte';
3
3
  import { useQuiz } from '../runtime/hooks.svelte.js';
4
+ import { TESSERA_PAGE } from '../runtime/contexts.js';
4
5
 
5
6
  let { children } = $props();
6
7
  let quizElement = $state(null);
7
8
 
8
9
  const handle = useQuiz({ element: () => quizElement });
9
10
 
10
- const pageCtx = getContext('tessera-page');
11
+ const pageCtx = getContext(TESSERA_PAGE);
11
12
  let quizConfig = $derived(pageCtx?.quiz ?? {});
12
13
  let feedbackDisabled = $derived(quizConfig.feedbackMode === 'never');
13
14
  let maxAttempts = $derived(quizConfig.maxAttempts ?? Infinity);
14
- let isImmediateMode = $derived(!feedbackDisabled && quizConfig.feedbackMode === 'immediate');
15
+ let isImmediateMode = $derived(
16
+ !feedbackDisabled && quizConfig.feedbackMode === 'immediate',
17
+ );
15
18
 
16
19
  let currentQuestionIndex = $state(0);
17
20
  let reviewIndex = $state(0);
18
21
 
19
22
  let totalQuestions = $derived(handle.questions.length);
20
23
  let currentQuestion = $derived(handle.questions[currentQuestionIndex]);
21
- let reviewQuestion = $derived(handle.questions[reviewIndex]);
22
24
  let correctCount = $derived(
23
- handle.questions.reduce((sum, q) => sum + (q.correct ? 1 : 0), 0)
25
+ handle.questions.reduce((sum, q) => sum + (q.correct ? 1 : 0), 0),
24
26
  );
25
27
  let passed = $derived(handle.score >= handle.passingScore);
26
28
 
@@ -31,7 +33,12 @@
31
33
 
32
34
  function needsReveal(q) {
33
35
  if (!q) return false;
34
- return isImmediateMode && isAnswered(q) && !q.isLockedCorrect && !q.feedbackVisible;
36
+ return (
37
+ isImmediateMode &&
38
+ isAnswered(q) &&
39
+ !q.isLockedCorrect &&
40
+ !q.feedbackVisible
41
+ );
35
42
  }
36
43
 
37
44
  function goNextQuestion() {
@@ -74,25 +81,40 @@
74
81
  }
75
82
  </script>
76
83
 
77
- <div class="tessera-quiz" bind:this={quizElement} role="region" aria-label="Quiz">
84
+ <div
85
+ class="tessera-quiz"
86
+ bind:this={quizElement}
87
+ role="region"
88
+ aria-label="Quiz"
89
+ >
78
90
  {#if handle.state === 'answering'}
79
91
  <!-- Question phase -->
80
92
  <div class="tessera-quiz-progress" aria-live="polite">
81
93
  <span class="tessera-quiz-progress-text">
82
- <span class="tessera-quiz-progress-desktop">Question {currentQuestionIndex + 1} of {totalQuestions}</span>
83
- <span class="tessera-quiz-progress-mobile">{currentQuestionIndex + 1}/{totalQuestions}</span>
94
+ <span class="tessera-quiz-progress-desktop"
95
+ >Question {currentQuestionIndex + 1} of {totalQuestions}</span
96
+ >
97
+ <span class="tessera-quiz-progress-mobile"
98
+ >{currentQuestionIndex + 1}/{totalQuestions}</span
99
+ >
84
100
  </span>
85
101
  <div class="tessera-progress-track">
86
102
  <div
87
103
  class="tessera-progress-fill"
88
- style="width: {totalQuestions > 0 ? ((currentQuestionIndex + 1) / totalQuestions) * 100 : 0}%"
104
+ style="width: {totalQuestions > 0
105
+ ? ((currentQuestionIndex + 1) / totalQuestions) * 100
106
+ : 0}%"
89
107
  ></div>
90
108
  </div>
91
109
  </div>
92
110
 
93
111
  <div class="tessera-quiz-questions">
94
112
  {#each handle.questions as q, i (q.id)}
95
- <div class="tessera-quiz-question-wrapper" class:active={i === currentQuestionIndex} aria-hidden={i !== currentQuestionIndex}>
113
+ <div
114
+ class="tessera-quiz-question-wrapper"
115
+ class:active={i === currentQuestionIndex}
116
+ aria-hidden={i !== currentQuestionIndex}
117
+ >
96
118
  {#if q.render}
97
119
  {@render q.render()}
98
120
  {/if}
@@ -114,7 +136,9 @@
114
136
  disabled={!isAnswered(currentQuestion)}
115
137
  onclick={goNextQuestion}
116
138
  >
117
- {currentQuestion?.feedbackVisible && isImmediateMode ? 'Continue' : 'Next'}
139
+ {currentQuestion?.feedbackVisible && isImmediateMode
140
+ ? 'Continue'
141
+ : 'Next'}
118
142
  </button>
119
143
  {:else if needsReveal(currentQuestion)}
120
144
  <button
@@ -133,19 +157,26 @@
133
157
  </button>
134
158
  {/if}
135
159
  </div>
136
-
137
160
  {:else if handle.state === 'reviewing'}
138
161
  <!-- Review phase -->
139
162
  <div class="tessera-quiz-progress" aria-live="polite">
140
163
  <span class="tessera-quiz-progress-text">
141
- <span class="tessera-quiz-progress-desktop">Review: Question {reviewIndex + 1} of {totalQuestions}</span>
142
- <span class="tessera-quiz-progress-mobile">Review: {reviewIndex + 1}/{totalQuestions}</span>
164
+ <span class="tessera-quiz-progress-desktop"
165
+ >Review: Question {reviewIndex + 1} of {totalQuestions}</span
166
+ >
167
+ <span class="tessera-quiz-progress-mobile"
168
+ >Review: {reviewIndex + 1}/{totalQuestions}</span
169
+ >
143
170
  </span>
144
171
  </div>
145
172
 
146
173
  <div class="tessera-quiz-questions">
147
174
  {#each handle.questions as q, i (q.id)}
148
- <div class="tessera-quiz-question-wrapper" class:active={i === reviewIndex} aria-hidden={i !== reviewIndex}>
175
+ <div
176
+ class="tessera-quiz-question-wrapper"
177
+ class:active={i === reviewIndex}
178
+ aria-hidden={i !== reviewIndex}
179
+ >
149
180
  {#if q.render}
150
181
  {@render q.render()}
151
182
  {/if}
@@ -177,14 +208,17 @@
177
208
  </button>
178
209
  {/if}
179
210
  </div>
180
-
181
211
  {:else}
182
212
  <!-- Results phase -->
183
213
  <div class="tessera-quiz-results" role="status" aria-live="polite">
184
214
  <h2 class="tessera-quiz-results-title">Quiz Results</h2>
185
215
  <div class="tessera-quiz-score">
186
216
  <span class="tessera-quiz-score-value">{handle.score}%</span>
187
- <span class="tessera-quiz-score-label" class:passed class:failed={!passed}>
217
+ <span
218
+ class="tessera-quiz-score-label"
219
+ class:passed
220
+ class:failed={!passed}
221
+ >
188
222
  {passed ? 'Passed' : 'Not Passed'}
189
223
  </span>
190
224
  </div>
@@ -210,7 +244,9 @@
210
244
  </button>
211
245
  {/if}
212
246
  {#if maxAttempts !== Infinity && handle.attemptCount >= maxAttempts}
213
- <p class="tessera-quiz-attempts-exhausted">All attempts used ({handle.attemptCount}/{maxAttempts})</p>
247
+ <p class="tessera-quiz-attempts-exhausted">
248
+ All attempts used ({handle.attemptCount}/{maxAttempts})
249
+ </p>
214
250
  {/if}
215
251
  </div>
216
252
  </div>
@@ -271,7 +307,9 @@
271
307
  font-size: 0.9375rem;
272
308
  font-weight: 500;
273
309
  cursor: pointer;
274
- transition: background 0.2s, opacity 0.2s;
310
+ transition:
311
+ background 0.2s,
312
+ opacity 0.2s;
275
313
  min-height: 44px;
276
314
  min-width: 44px;
277
315
  }
@@ -352,8 +390,12 @@
352
390
 
353
391
  /* Mobile */
354
392
  @media (max-width: 640px) {
355
- .tessera-quiz-progress-desktop { display: none; }
356
- .tessera-quiz-progress-mobile { display: inline; }
393
+ .tessera-quiz-progress-desktop {
394
+ display: none;
395
+ }
396
+ .tessera-quiz-progress-mobile {
397
+ display: inline;
398
+ }
357
399
 
358
400
  .tessera-quiz-nav {
359
401
  position: sticky;
@@ -3,11 +3,27 @@
3
3
  </script>
4
4
 
5
5
  {#if kind === 'correct'}
6
- <svg viewBox="0 0 16 16" fill="currentColor" width={size} height={size} aria-hidden="true">
7
- <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
6
+ <svg
7
+ viewBox="0 0 16 16"
8
+ fill="currentColor"
9
+ width={size}
10
+ height={size}
11
+ aria-hidden="true"
12
+ >
13
+ <path
14
+ d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
15
+ />
8
16
  </svg>
9
17
  {:else}
10
- <svg viewBox="0 0 16 16" fill="currentColor" width={size} height={size} aria-hidden="true">
11
- <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
18
+ <svg
19
+ viewBox="0 0 16 16"
20
+ fill="currentColor"
21
+ width={size}
22
+ height={size}
23
+ aria-hidden="true"
24
+ >
25
+ <path
26
+ d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
27
+ />
12
28
  </svg>
13
29
  {/if}
@@ -7,8 +7,6 @@
7
7
  * @prop {import('svelte').Snippet} trigger - Trigger content snippet
8
8
  * @prop {import('svelte').Snippet} content - Modal body snippet
9
9
  */
10
- import { onMount } from 'svelte';
11
-
12
10
  let { trigger, content, title = '' } = $props();
13
11
  let open = $state(false);
14
12
  let modalRef = $state(null);
@@ -26,13 +24,6 @@
26
24
  }
27
25
  }
28
26
 
29
- function handleTriggerKey(e) {
30
- if (e.key === 'Enter' || e.key === ' ') {
31
- e.preventDefault();
32
- openModal();
33
- }
34
- }
35
-
36
27
  function handleOverlayClick(e) {
37
28
  if (e.target === e.currentTarget) {
38
29
  closeModal();
@@ -48,7 +39,7 @@
48
39
  // Focus trap
49
40
  if (e.key === 'Tab' && modalRef) {
50
41
  const focusable = modalRef.querySelectorAll(
51
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
42
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
52
43
  );
53
44
  if (focusable.length === 0) return;
54
45
 
@@ -76,7 +67,7 @@
76
67
  queueMicrotask(() => {
77
68
  if (modalRef) {
78
69
  const firstFocusable = modalRef.querySelector(
79
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
70
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
80
71
  );
81
72
  if (firstFocusable) firstFocusable.focus();
82
73
  else modalRef.focus();
@@ -90,10 +81,7 @@
90
81
 
91
82
  <!-- svelte-ignore a11y_click_events_have_key_events -->
92
83
  <!-- svelte-ignore a11y_no_static_element_interactions -->
93
- <div
94
- class="tessera-reveal-trigger"
95
- onclick={openModal}
96
- >
84
+ <div class="tessera-reveal-trigger" onclick={openModal}>
97
85
  {@render trigger()}
98
86
  </div>
99
87
 
@@ -118,7 +106,11 @@
118
106
  <div class="tessera-modal-body">
119
107
  {@render content()}
120
108
  </div>
121
- <button class="tessera-modal-close" onclick={closeModal} aria-label="Close modal">
109
+ <button
110
+ class="tessera-modal-close"
111
+ onclick={closeModal}
112
+ aria-label="Close modal"
113
+ >
122
114
 
123
115
  </button>
124
116
  </div>
@@ -194,8 +186,9 @@
194
186
  color: var(--tessera-text-light);
195
187
  cursor: pointer;
196
188
  border-radius: 6px;
197
- transition: background-color var(--tessera-transition-fast),
198
- color var(--tessera-transition-fast);
189
+ transition:
190
+ background-color var(--tessera-transition-fast),
191
+ color var(--tessera-transition-fast);
199
192
  }
200
193
 
201
194
  .tessera-modal-close:hover {
@@ -209,13 +202,23 @@
209
202
  }
210
203
 
211
204
  @keyframes tessera-modal-fade-in {
212
- from { opacity: 0; }
213
- to { opacity: 1; }
205
+ from {
206
+ opacity: 0;
207
+ }
208
+ to {
209
+ opacity: 1;
210
+ }
214
211
  }
215
212
 
216
213
  @keyframes tessera-modal-slide-in {
217
- from { transform: translateY(10px); opacity: 0; }
218
- to { transform: translateY(0); opacity: 1; }
214
+ from {
215
+ transform: translateY(10px);
216
+ opacity: 0;
217
+ }
218
+ to {
219
+ transform: translateY(0);
220
+ opacity: 1;
221
+ }
219
222
  }
220
223
 
221
224
  @media (max-width: 640px) {