live-quiz 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +44 -26
  2. package/dist/live-quiz.css +1 -1
  3. package/dist/live-quiz.js +1363 -1259
  4. package/dist/live-quiz.umd.js +73 -6
  5. package/dist/participant/index.d.ts +3 -23
  6. package/dist/participant/index.d.ts.map +1 -1
  7. package/dist/participant/selectors.d.ts +17 -0
  8. package/dist/participant/selectors.d.ts.map +1 -0
  9. package/dist/participant.css +1 -1
  10. package/dist/participant.js +723 -647
  11. package/dist/participant.umd.js +1 -1
  12. package/dist/src/dom/html.d.ts +10 -0
  13. package/dist/src/dom/html.d.ts.map +1 -0
  14. package/dist/src/dom/render-qr.d.ts +7 -0
  15. package/dist/src/dom/render-qr.d.ts.map +1 -1
  16. package/dist/src/dom/render-question.d.ts.map +1 -1
  17. package/dist/src/dom/render-results.d.ts.map +1 -1
  18. package/dist/src/dom/render-wordcloud.d.ts.map +1 -1
  19. package/dist/src/dom/selectors.d.ts +30 -0
  20. package/dist/src/dom/selectors.d.ts.map +1 -0
  21. package/dist/src/index.d.ts +3 -1
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/plugin.d.ts +1 -1
  24. package/dist/src/plugin.d.ts.map +1 -1
  25. package/dist/src/quiz-manager.d.ts +3 -8
  26. package/dist/src/quiz-manager.d.ts.map +1 -1
  27. package/dist/src/quiz-types.d.ts +94 -0
  28. package/dist/src/quiz-types.d.ts.map +1 -1
  29. package/functions/netlify/quiz-answer.mts +2 -2
  30. package/functions/netlify/quiz-sync.mts +2 -2
  31. package/functions/netlify/shared.mts +24 -2
  32. package/functions/vercel/quiz-answer.ts +2 -2
  33. package/functions/vercel/quiz-sync.ts +2 -2
  34. package/functions/vercel/shared.ts +24 -2
  35. package/package.json +8 -3
  36. package/src/live-quiz-components.css +276 -0
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # live-quiz
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/live-quiz)](https://www.npmjs.com/package/live-quiz)
4
+
3
5
  Add live audience quizzes to your [Reveal.js](https://revealjs.com) presentations. Powered by [AnyCable](https://anycable.io).
4
6
 
5
7
  **[Live Demo](https://livequizdemo.netlify.app/)** — open the presenter view in one tab and the [audience page](https://livequizdemo.netlify.app/quiz.html) on your phone.
@@ -8,9 +10,10 @@ Add live audience quizzes to your [Reveal.js](https://revealjs.com) presentation
8
10
 
9
11
  You build a Reveal.js deck with quiz slides, deploy it to the web, and present it. When you land on a quiz slide, your audience sees a QR code, scans it on their phones, and votes — results animate on your slides in real time.
10
12
 
11
- - **Multiple-choice questions** with up to 4 options
13
+ - **Multiple-choice questions** with up to 4 options and live bar charts
14
+ - **Free-text questions** with live word cloud results
12
15
  - **QR code** auto-generated on each quiz slide so the audience can join instantly
13
- - **Live bar chart** that updates as votes come in (sub-second via WebSockets)
16
+ - **Live results** that update as votes come in (sub-second via WebSockets)
14
17
  - **Participant counter** showing how many people are connected
15
18
  - **Mobile-friendly voting page** — no app install, just a browser
16
19
  - **Automatic question sync** — define questions once on your slides, the audience page receives them automatically
@@ -24,7 +27,7 @@ Your presentation needs to be **deployed to the web** (not just opened locally)
24
27
 
25
28
  2. **Your presentation** — a static site (HTML + JS) deployed to **Netlify** or **Vercel**. The plugin adds quiz UI to your slides automatically.
26
29
 
27
- 3. **Serverless functions** — 3 small files (~60 lines total) that run on Netlify or Vercel. They receive votes from the audience and broadcast results via AnyCable. Secrets stay in environment variables, never in your code.
30
+ 3. **Serverless functions** — 3 small files that run on Netlify or Vercel. They receive answers from the audience and broadcast results via AnyCable. Secrets stay in environment variables, never in your code.
28
31
 
29
32
  ```
30
33
  Presenter's slides AnyCable Audience phones
@@ -35,7 +38,7 @@ Presenter's slides AnyCable Audience phones
35
38
  │ │ │
36
39
  │ │◄──── submit vote ──────┤
37
40
  │◄── broadcast results ─────┤ (serverless fn) │
38
- │ update bar chart │ │
41
+ │ update results │ │
39
42
  ```
40
43
 
41
44
  Questions are defined once — as `data-quiz-*` attributes on your slides. The presenter broadcasts them to the audience page via the sync channel, so the participant widget doesn't need its own copy.
@@ -109,7 +112,7 @@ Reveal.initialize({
109
112
  Add data attributes to your slides — the plugin injects all the UI automatically:
110
113
 
111
114
  ```html
112
- <!-- Quiz question slide -->
115
+ <!-- Multiple-choice question -->
113
116
  <section data-quiz-id="q1"
114
117
  data-quiz-question="Where are you joining from?"
115
118
  data-quiz-options='[
@@ -130,8 +133,19 @@ Add data attributes to your slides — the plugin injects all the UI automatical
130
133
  {"label":"D","text":"Elsewhere"}
131
134
  ]'>
132
135
  </section>
136
+
137
+ <!-- Free-text question (word cloud results) -->
138
+ <section data-quiz-id="q2" data-quiz-type="text"
139
+ data-quiz-question="What's your favorite framework?">
140
+ </section>
141
+
142
+ <section data-quiz-results="q2" data-quiz-type="text"
143
+ data-quiz-question="What's your favorite framework?">
144
+ </section>
133
145
  ```
134
146
 
147
+ `data-quiz-type` defaults to `"choice"` when omitted, so existing slides work without changes.
148
+
135
149
  #### 5. Create the audience page
136
150
 
137
151
  The audience needs a separate page to vote from their phones. Create a `quiz.html` and a script that mounts the participant widget:
@@ -154,9 +168,10 @@ Your presentation must be deployed — the audience needs to reach it from their
154
168
 
155
169
  Copy the serverless functions from `functions/netlify/` or `functions/vercel/` into your project and set one environment variable:
156
170
 
157
- | Variable | Description |
158
- |---|---|
159
- | `ANYCABLE_BROADCAST_URL` | Broadcast URL from step 1 |
171
+ | Variable | Required | Description |
172
+ |---|---|---|
173
+ | `ANYCABLE_BROADCAST_URL` | Yes | Broadcast URL from step 1 |
174
+ | `ANYCABLE_BROADCAST_KEY` | No | Broadcast key (if your AnyCable app uses one) |
160
175
 
161
176
  See [functions/README.md](./functions/README.md) for step-by-step deploy instructions for each platform.
162
177
 
@@ -184,6 +199,7 @@ If your votes are confidential or you need to restrict who can participate, see
184
199
  | `quizGroupId` | `string` | Yes | Unique ID grouping quizzes in this talk |
185
200
  | `quizUrl` | `string` | No | Audience page URL (shown as QR code) |
186
201
  | `endpoints` | `object` | No | Custom endpoint paths (default: `/.netlify/functions/*`) |
202
+ | `titleText` | `string` | No | Title shown on question slides (default: `"Pop quiz!"`) |
187
203
 
188
204
  ### Custom Endpoints
189
205
 
@@ -200,22 +216,22 @@ liveQuiz: {
200
216
 
201
217
  ## Theming
202
218
 
203
- The plugin inherits your Reveal.js theme's fonts and colors via CSS custom properties. Override `--lq-*` variables to fine-tune:
204
-
205
- ```css
206
- :root {
207
- --lq-accent: #e11d48;
208
- --lq-text-muted: #a1a1aa;
209
- --lq-font: "Inter", sans-serif;
210
- --lq-mono: "JetBrains Mono", monospace;
211
- --lq-bar-fill: #52525b;
212
- --lq-bar-correct: #22c55e;
213
- --lq-bar-track: rgba(255, 255, 255, 0.1);
214
- --lq-border-radius: 0.25rem;
215
- }
216
- ```
219
+ The plugin inherits your Reveal.js theme's fonts and colors automatically via `--r-*` custom properties. Override `--lq-*` variables to fine-tune:
220
+
221
+ | Variable | Default | Description |
222
+ |---|---|---|
223
+ | `--lq-accent` | `var(--r-link-color, #f59e0b)` | Accent color (bar highlights, word cloud top word) |
224
+ | `--lq-text` | `var(--r-main-color, inherit)` | Main text color |
225
+ | `--lq-text-muted` | 50% of `--lq-text` | Secondary text |
226
+ | `--lq-font` | `var(--r-main-font, inherit)` | Body font |
227
+ | `--lq-heading-font` | `var(--r-heading-font, inherit)` | Heading font |
228
+ | `--lq-mono` | `var(--r-code-font, ...)` | Monospace font |
229
+ | `--lq-bar-fill` | 35% of `--lq-text` | Bar fill color |
230
+ | `--lq-bar-correct` | `var(--lq-accent)` | Correct answer bar color |
231
+ | `--lq-bar-track` | 10% of `--lq-text` | Bar track background |
232
+ | `--lq-border-radius` | `0.5rem` | Border radius |
217
233
 
218
- Participant widget uses `--lq-p-*` variables — see `participant/participant.css` for the full list.
234
+ Participant widget uses `--lq-p-*` variables — see `participant/participant.css` for the full list. The participant accent (`--lq-p-accent`) defaults to `var(--lq-accent)`, so setting `--lq-accent` once themes both presenter and participant.
219
235
 
220
236
  ## Data Attributes Reference
221
237
 
@@ -225,7 +241,8 @@ Participant widget uses `--lq-p-*` variables — see `participant/participant.cs
225
241
  |---|---|
226
242
  | `data-quiz-id` | Unique quiz identifier |
227
243
  | `data-quiz-question` | Question text |
228
- | `data-quiz-options` | JSON array of `{label, text, correct?}` |
244
+ | `data-quiz-type` | `"choice"` (default) or `"text"` |
245
+ | `data-quiz-options` | JSON array of `{label, text, correct?}` (choice only) |
229
246
 
230
247
  ### Results Slide
231
248
 
@@ -233,11 +250,12 @@ Participant widget uses `--lq-p-*` variables — see `participant/participant.cs
233
250
  |---|---|
234
251
  | `data-quiz-results` | Quiz ID to show results for |
235
252
  | `data-quiz-question` | Question text (shown as title) |
236
- | `data-quiz-options` | JSON array of `{label, text, correct?}` |
253
+ | `data-quiz-type` | `"choice"` (default) or `"text"` |
254
+ | `data-quiz-options` | JSON array of `{label, text, correct?}` (choice only) |
237
255
 
238
256
  ## Limitations
239
257
 
240
- - **Multiple choice only** — up to 4 options per question. No free text, ratings, or word clouds (yet).
258
+ - **Two question types** — multiple choice (up to 4 options) and free text (word cloud). No ratings or scales yet.
241
259
  - **Requires deployment** — the audience connects over the internet, so the presentation must be hosted, not served locally.
242
260
  - **AnyCable free tier** — supports up to 2,000 concurrent connections. For larger audiences, upgrade to a paid AnyCable Plus plan.
243
261
  - **No persistent storage** — quiz results live in memory during the presentation. Once the presenter closes the tab, results are gone.
@@ -1 +1 @@
1
- :root{--lq-accent: var(--r-link-color, #f59e0b);--lq-text: var(--r-main-color, inherit);--lq-text-muted: color-mix(in srgb, var(--lq-text) 50%, transparent);--lq-heading-font: var(--r-heading-font, inherit);--lq-font: var(--r-main-font, inherit);--lq-mono: var(--r-code-font, ui-monospace, "Cascadia Code", "Fira Code", monospace);--lq-bar-track: color-mix(in srgb, var(--lq-text) 10%, transparent);--lq-bar-fill: color-mix(in srgb, var(--lq-text) 35%, transparent);--lq-bar-correct: var(--lq-accent);--lq-border-radius: .5rem;--lq-option-border: color-mix(in srgb, var(--lq-text) 30%, transparent)}.reveal .lq-question{display:flex;flex-direction:column;align-items:center;width:100%;height:100%;padding:1rem 0;gap:1.5rem;color:var(--lq-text)}.reveal .lq-question__title{font-family:var(--lq-heading-font);font-size:clamp(2rem,5vw,4rem);font-weight:var(--r-heading-font-weight, 800);line-height:var(--r-heading-line-height, 1);letter-spacing:var(--r-heading-letter-spacing, -.02em);text-transform:var(--r-heading-text-transform, none);text-align:center}.reveal .lq-question__body{display:flex;align-items:flex-start;gap:3rem;width:100%}.reveal .lq-question__qr-side{display:flex;flex-direction:column;align-items:center;gap:.75rem;flex-shrink:0}.reveal .lq-qr{border-radius:var(--lq-border-radius);image-rendering:crisp-edges}.reveal .lq-question__url{font-family:var(--lq-mono);font-size:clamp(.6rem,1.2vw,.85rem);color:var(--lq-text-muted);text-align:center;word-break:break-all}.reveal .lq-question__content{display:flex;flex-direction:column;gap:1.5rem;flex:1}.reveal .lq-question__text{font-family:var(--lq-font);font-size:clamp(1rem,2.2vw,1.5rem);font-weight:600;line-height:1.3}.reveal .lq-question__options{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}.reveal .lq-question__option{display:flex;align-items:center;gap:.75rem;padding:.6em 1em;border:1px solid var(--lq-option-border);border-radius:var(--lq-border-radius);font-family:var(--lq-font);font-size:clamp(.8rem,1.6vw,1.1rem)}.reveal .lq-question__option-label{font-family:var(--lq-mono);font-weight:700;font-size:.85em;color:var(--lq-accent);flex-shrink:0}.reveal .lq-question__option-text{font-weight:500}.reveal .lq-question__counter{font-family:var(--lq-mono);font-size:clamp(.9rem,1.8vw,1.2rem);color:var(--lq-text-muted);font-variant-numeric:tabular-nums}.reveal .lq-answered{color:var(--lq-accent);font-weight:700}.reveal .lq-results{display:flex;flex-direction:column;gap:2rem;width:100%;color:var(--lq-text)}.reveal .lq-results__title{font-family:var(--lq-heading-font);font-size:clamp(1.2rem,2.5vw,1.8rem);font-weight:600;line-height:1.3;text-align:center}.reveal .lq-results__bars{display:flex;flex-direction:column;gap:1rem;width:100%}.reveal .lq-result-bar{display:grid;grid-template-columns:12rem 1fr 5rem;align-items:center;gap:1rem}.reveal .lq-result-bar__label{display:flex;align-items:center;gap:.75rem}.reveal .lq-result-bar__letter{font-family:var(--lq-mono);font-weight:700;font-size:clamp(.8rem,1.4vw,1rem);color:var(--lq-text-muted);flex-shrink:0}.reveal .lq-result-bar--correct .lq-result-bar__letter{color:var(--lq-accent)}.reveal .lq-result-bar__text{font-family:var(--lq-font);font-size:clamp(.85rem,1.6vw,1.1rem);font-weight:500}.reveal .lq-result-bar--correct .lq-result-bar__text{font-weight:700;color:var(--lq-accent)}.reveal .lq-result-bar__track{height:2.2rem;background:var(--lq-bar-track);border-radius:.35rem;overflow:hidden;position:relative}.reveal .lq-result-bar__fill{height:100%;border-radius:.35rem;background:var(--lq-bar-fill);transition:width 1.2s cubic-bezier(.4,0,.2,1)}.reveal .lq-result-bar--correct .lq-result-bar__fill{background:var(--lq-bar-correct)}.reveal .lq-result-bar__stats{display:flex;gap:.5rem;align-items:baseline;font-family:var(--lq-mono);font-variant-numeric:tabular-nums;font-size:clamp(.7rem,1.2vw,.9rem)}.reveal .lq-result-bar__pct{font-weight:700}.reveal .lq-result-bar--correct .lq-result-bar__pct{color:var(--lq-accent)}.reveal .lq-result-bar__count{color:var(--lq-text-muted)}.reveal .lq-question__hint{font-family:var(--lq-font);font-size:clamp(.9rem,1.8vw,1.2rem);font-style:italic;color:var(--lq-text-muted)}.reveal .lq-wordcloud{display:flex;flex-direction:column;gap:2rem;width:100%;color:var(--lq-text)}.reveal .lq-wordcloud__title{font-family:var(--lq-heading-font);font-size:clamp(1.2rem,2.5vw,1.8rem);font-weight:600;line-height:1.3;text-align:center}.reveal .lq-wordcloud__cloud{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem 1rem;width:100%;min-height:4rem}.reveal .lq-wordcloud__word{font-family:var(--lq-font);font-weight:500;transition:font-size .6s cubic-bezier(.4,0,.2,1),opacity .6s ease}.reveal .lq-wordcloud__word--top{color:var(--lq-accent);font-weight:800}@media(prefers-reduced-motion:reduce){.reveal .lq-result-bar__fill,.reveal .lq-wordcloud__word{transition:none}}
1
+ .lq-question{display:flex;flex-direction:column;align-items:center;width:100%;height:100%;padding:1rem 0;gap:1.5rem;color:var(--lq-text)}.lq-question__title{font-family:var(--lq-heading-font);font-size:clamp(2rem,5vw,4rem);font-weight:var(--lq-heading-font-weight, 800);line-height:var(--lq-heading-line-height, 1);letter-spacing:var(--lq-heading-letter-spacing, -.02em);text-transform:var(--lq-heading-text-transform, none);text-align:center}.lq-question__body{display:flex;align-items:flex-start;gap:3rem;width:100%}.lq-question__qr-side{display:flex;flex-direction:column;align-items:center;gap:.75rem;flex-shrink:0}.lq-qr{border-radius:var(--lq-border-radius);image-rendering:crisp-edges}.lq-question__url{font-family:var(--lq-mono);font-size:clamp(.6rem,1.2vw,.85rem);color:var(--lq-text-muted);text-align:center;word-break:break-all}.lq-question__content{display:flex;flex-direction:column;gap:1.5rem;flex:1}.lq-question__text{font-family:var(--lq-font);font-size:clamp(1rem,2.2vw,1.5rem);font-weight:600;line-height:1.3}.lq-question__options{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}.lq-question__option{display:flex;align-items:center;gap:.75rem;padding:.6em 1em;border:1px solid var(--lq-option-border);border-radius:var(--lq-border-radius);font-family:var(--lq-font);font-size:clamp(.8rem,1.6vw,1.1rem)}.lq-question__option-label{font-family:var(--lq-mono);font-weight:700;font-size:.85em;color:var(--lq-accent);flex-shrink:0}.lq-question__option-text{font-weight:500}.lq-question__counter{font-family:var(--lq-mono);font-size:clamp(.9rem,1.8vw,1.2rem);color:var(--lq-text-muted);font-variant-numeric:tabular-nums}.lq-answered{color:var(--lq-accent);font-weight:700}.lq-results{display:flex;flex-direction:column;gap:2rem;width:100%;color:var(--lq-text)}.lq-results__title{font-family:var(--lq-heading-font);font-size:clamp(1.2rem,2.5vw,1.8rem);font-weight:600;line-height:1.3;text-align:center}.lq-results__bars{display:flex;flex-direction:column;gap:1rem;width:100%}.lq-result-bar{display:grid;grid-template-columns:12rem 1fr 5rem;align-items:center;gap:1rem}.lq-result-bar__label{display:flex;align-items:center;gap:.75rem}.lq-result-bar__letter{font-family:var(--lq-mono);font-weight:700;font-size:clamp(.8rem,1.4vw,1rem);color:var(--lq-text-muted);flex-shrink:0}.lq-result-bar--correct .lq-result-bar__letter{color:var(--lq-accent)}.lq-result-bar__text{font-family:var(--lq-font);font-size:clamp(.85rem,1.6vw,1.1rem);font-weight:500}.lq-result-bar--correct .lq-result-bar__text{font-weight:700;color:var(--lq-accent)}.lq-result-bar__track{height:2.2rem;background:var(--lq-bar-track);border-radius:.35rem;overflow:hidden;position:relative}.lq-result-bar__fill{height:100%;border-radius:.35rem;background:var(--lq-bar-fill);transition:width 1.2s cubic-bezier(.4,0,.2,1)}.lq-result-bar--correct .lq-result-bar__fill{background:var(--lq-bar-correct)}.lq-result-bar__stats{display:flex;gap:.5rem;align-items:baseline;font-family:var(--lq-mono);font-variant-numeric:tabular-nums;font-size:clamp(.7rem,1.2vw,.9rem)}.lq-result-bar__pct{font-weight:700}.lq-result-bar--correct .lq-result-bar__pct{color:var(--lq-accent)}.lq-result-bar__count{color:var(--lq-text-muted)}.lq-question__hint{font-family:var(--lq-font);font-size:clamp(.9rem,1.8vw,1.2rem);font-style:italic;color:var(--lq-text-muted)}.lq-wordcloud{display:flex;flex-direction:column;gap:2rem;width:100%;color:var(--lq-text)}.lq-wordcloud__title{font-family:var(--lq-heading-font);font-size:clamp(1.2rem,2.5vw,1.8rem);font-weight:600;line-height:1.3;text-align:center}.lq-wordcloud__cloud{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem 1rem;width:100%;min-height:4rem}.lq-wordcloud__word{font-family:var(--lq-font);font-weight:500;transition:font-size .6s cubic-bezier(.4,0,.2,1),opacity .6s ease}.lq-wordcloud__word--top{color:var(--lq-accent);font-weight:800}@media(prefers-reduced-motion:reduce){.lq-result-bar__fill,.lq-wordcloud__word{transition:none}}:root{--lq-accent: var(--r-link-color, #f59e0b);--lq-text: var(--r-main-color, inherit);--lq-text-muted: color-mix(in srgb, var(--lq-text) 50%, transparent);--lq-heading-font: var(--r-heading-font, inherit);--lq-font: var(--r-main-font, inherit);--lq-mono: var(--r-code-font, ui-monospace, "Cascadia Code", "Fira Code", monospace);--lq-heading-font-weight: var(--r-heading-font-weight, 800);--lq-heading-line-height: var(--r-heading-line-height, 1);--lq-heading-letter-spacing: var(--r-heading-letter-spacing, -.02em);--lq-heading-text-transform: var(--r-heading-text-transform, none);--lq-bar-track: color-mix(in srgb, var(--lq-text) 10%, transparent);--lq-bar-fill: color-mix(in srgb, var(--lq-text) 35%, transparent);--lq-bar-correct: var(--lq-accent);--lq-border-radius: .5rem;--lq-option-border: color-mix(in srgb, var(--lq-text) 30%, transparent)}