@hypersocial/decks-cli 0.9.9 → 0.9.11
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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/skills/hyperdecks/SKILL.md +95 -48
- package/skills/hyperdecks/chat.md +8 -1
- package/src/brand.mjs +57 -5
- package/src/checkRules.mjs +90 -2
- package/src/commands.mjs +39 -9
- package/src/draftFace.mjs +56 -0
- package/src/files.mjs +3 -0
- package/src/layouts.mjs +3 -2
- package/src/loadingBlock.mjs +22 -7
- package/src/narrate.mjs +6 -1
- package/src/opening.mjs +117 -0
- package/src/ops.mjs +25 -3
- package/src/play.mjs +2 -2
- package/src/presenter.mjs +73 -9
- package/src/release.mjs +8 -7
- package/src/runtime.mjs +1 -1
- package/src/scaffold.mjs +28 -7
- package/templates/layouts/_deck.html +144 -36
- package/templates/layouts/closer.html +4 -2
- package/templates/layouts/cover-band.html +44 -0
- package/templates/layouts/cover-poster.html +42 -0
- package/templates/layouts/cover-type.html +31 -0
- package/templates/layouts/hero.html +27 -12
- package/templates/layouts/phones.html +5 -5
- package/templates/layouts/split.html +4 -4
- package/templates/narrated-deck.html +74 -3
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ whoami [--json]
|
|
|
58
58
|
deck list [--json]
|
|
59
59
|
deck stats <slug> [--json]
|
|
60
60
|
deck open <slug>
|
|
61
|
+
deck link <slug> [--json] # the owner link: present or check your own deck, untracked
|
|
61
62
|
|
|
62
63
|
share new <slug> <recipient> [--email e] [--note n] [--require-email] [--expires iso] [--limit n] [--qa] [--no-notify]
|
|
63
64
|
share list <slug> [--json]
|
package/package.json
CHANGED
|
@@ -146,14 +146,22 @@ Hyperdecks server; use the CLI where you have a shell and the tool otherwise.
|
|
|
146
146
|
| PDF for a data room or attachment | `hyperdecks export <slug> --pdf [--notes] --out deck.pdf`, `hyperdecks export list` | `export_deck`, `list_exports` |
|
|
147
147
|
| Video (MP4) for YouTube, LinkedIn or an attachment | `hyperdecks export <slug\|deck-dir> --video [--size 1080p\|720p\|4k] [--shape 16:9\|9:16\|1:1] [--title-card] [--end-card] [--no-captions] [--no-avatar] [--upload] --out deck.mp4`: records the narrated player in real time (a 4-minute deck takes about 5), avatar, voice and captions, no player chrome; a slug records the released copy, a deck folder its working copy; `--upload` lists it with the deck's exports | `export_deck { kind: "mp4" }` renders it on our render worker (the dashboard's Export → Video); poll `check_export`, or `hyperdecks export status <exportId>` |
|
|
148
148
|
| Import only | `hyperdecks import <file> --slug <slug>` | `import_deck_from_url` |
|
|
149
|
-
| One more link | `hyperdecks share new <slug> "<name>" [--no-forward]` | `create_share` (`allowForward`) |
|
|
149
|
+
| One more link | `hyperdecks share new <slug> "<name>" [--no-forward] [--pace auto\|slide\|step]` | `create_share` (`allowForward`, `pace`) |
|
|
150
150
|
| Forwarding, one link | `hyperdecks share forward on\|off\|default <token>` | `set_share_forwarding` |
|
|
151
151
|
| Forwarding, whole deck | `hyperdecks deck forward on\|off <slug>` | `set_deck_forwarding` |
|
|
152
152
|
| Slides or scroll, viewer toggle, pace, AI-readable version | `hyperdecks deck view <slug> [slides\|scroll\|default] [--toggle on\|off] [--pace auto\|slide\|step\|default] [--ai-readable on\|off]` | `set_deck_view` |
|
|
153
|
+
| Present or check your own deck (owner link) | `hyperdecks deck link <slug>` (`deck open` opens it) | `ownerUrl` from `list_decks` or `check_deck` |
|
|
153
154
|
| Who watched | `hyperdecks deck stats <slug>` | `deck_stats` |
|
|
154
155
|
| All activity | `hyperdecks activity --deck <slug>` | `activity_overview` |
|
|
155
156
|
| What one person watched | `hyperdecks engagement "<name>" --deck <slug> --json` | `recipient_engagement` |
|
|
156
157
|
|
|
158
|
+
To present or check your own deck, use the owner link (`deck link`,
|
|
159
|
+
`ownerUrl`): the released deck as recipients get it, for the owner only
|
|
160
|
+
(signed in), with nothing tracked. Anyone else gets a sign-in notice, so a
|
|
161
|
+
leaked owner link shows nothing. Its toolbar's Present (or H) hides the owner
|
|
162
|
+
tools for a screen share. QA links are only for testing what a recipient sees;
|
|
163
|
+
never make one to present on a call.
|
|
164
|
+
|
|
157
165
|
Forwarding is on by default: a viewer can forward the deck to someone, which
|
|
158
166
|
makes a child link tracked on its own ("Dana via Peter"; `share list` shows it
|
|
159
167
|
indented under Peter's, `list_shares` gives `parentToken` and `forwardedBy`).
|
|
@@ -266,17 +274,27 @@ The deck contract (`check` enforces or warns on each line):
|
|
|
266
274
|
<!-- topic: presenter -->
|
|
267
275
|
#### Which presenter style
|
|
268
276
|
|
|
269
|
-
**
|
|
270
|
-
writes
|
|
271
|
-
`
|
|
272
|
-
narrator's
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
**The CLI writes the face into the tag; leave it alone.** `deck new --narrated`
|
|
278
|
+
writes the narrator's face on the player tag (`data-presenter="head"
|
|
279
|
+
data-avatar="…" data-presenter-from="narrator"`): the account's default
|
|
280
|
+
narrator's, else D.A.S.H.'s. `deck narrator <slug> <name>` and `narrate`
|
|
281
|
+
rewrite it to the narrator's face and print `presenter: …, written to
|
|
282
|
+
index.html`; `deck narrator <slug> none` (or `deck new --captions-only`)
|
|
283
|
+
writes captions only on purpose. So the file plays with its face however it is
|
|
284
|
+
opened, a static server included. `release` (and `release_deck`) follows the
|
|
285
|
+
narrator: a `data-presenter-from="narrator"` face is replaced by the current
|
|
286
|
+
narrator's (your own narrator's avatar, or D.A.S.H.'s live head for a deck in
|
|
287
|
+
D.A.S.H.'s voice). It says `presented by …` only when the published page has
|
|
288
|
+
that face; otherwise it warns `releasing captions only` with the reason. To pick
|
|
289
|
+
the face by hand, set both attributes and no `data-presenter-from`:
|
|
290
|
+
`data-presenter="head" data-avatar="…"`. D.A.S.H.'s is
|
|
276
291
|
`data-presenter="head" data-avatar="https://hyperdecks.ai/player/v1/dash-avatar.json"`
|
|
277
292
|
(it loads from any host). `data-presenter="head"` with no `data-avatar` plays
|
|
278
|
-
captions only, and so does a `data-avatar` beside `data-presenter="none"
|
|
279
|
-
|
|
293
|
+
captions only, and so does a `data-avatar` beside `data-presenter="none"`, and
|
|
294
|
+
so does a plain `data-presenter="none"` from an older scaffold.
|
|
295
|
+
`check` warns on all three, and `release` leaves a hand-set `data-avatar` alone.
|
|
296
|
+
A hosted draft (`new_deck`) stores the tag as the template wrote it; its draft
|
|
297
|
+
link and `release_deck` add the narrator's face, so `check_deck` does not warn on its plain none.
|
|
280
298
|
|
|
281
299
|
Set the deck's on the player tag (`data-presenter-style="stage"`) or in
|
|
282
300
|
script.md's front matter (`presenterStyle: stage`); override one slide with
|
|
@@ -290,6 +308,20 @@ its `##` heading.
|
|
|
290
308
|
| `glass` | dark or busy slides: head and captions on one frosted panel |
|
|
291
309
|
| `corner` | the slide is the point (a product shot, a chart, a demo): small head bottom-right, subtitles along the bottom |
|
|
292
310
|
|
|
311
|
+
**Loading and buffering.** A deck opens on its loading card (the title and
|
|
312
|
+
the presenter's still), and a line still loading shows as a small waiting
|
|
313
|
+
pill. Once the deck has spoken, a line that stalls on the network for more
|
|
314
|
+
than 0.3 s makes the slide play a short
|
|
315
|
+
"signal" glitch in the deck's accents (`--accent-2` / `--accent-3`, else
|
|
316
|
+
`--accent`; set `--hd-signal-1` / `--hd-signal-2` to choose others): the
|
|
317
|
+
title's colour splits, thin scanlines and a slice pass over the slide, and a
|
|
318
|
+
`SIGNAL // BUFFERING` tag sits in the top-left corner.
|
|
319
|
+
Body text never moves; it calms to scanlines after 6 s and stops the moment
|
|
320
|
+
sound plays. It never shows in thumbnails, exports, present mode or while the
|
|
321
|
+
owner presents, and reduced motion gets a still dim and the tag. Where it
|
|
322
|
+
doesn't fit the deck, turn it off with `data-loading="calm"` on the player tag
|
|
323
|
+
(or on `<html>`, or `"loading": "calm"` in the manifest).
|
|
324
|
+
|
|
293
325
|
Size it per slide with `data-presenter-size="s|m|l|xl"` on the `<section>`
|
|
294
326
|
(0.7×, 1×, 1.3×, 1.6× the style's head; on the player tag it is the deck's
|
|
295
327
|
default): `xl` on a slide with empty space, `s` on a busy product slide. Phones
|
|
@@ -413,8 +445,8 @@ slide that overflows a 390×844 phone.
|
|
|
413
445
|
|
|
414
446
|
#### House style (preferences)
|
|
415
447
|
|
|
416
|
-
|
|
417
|
-
|
|
448
|
+
The owner's house style, not rules: follow it by default; depart from it
|
|
449
|
+
when the brief asks.
|
|
418
450
|
|
|
419
451
|
**One idea per slide.** The title states the idea as a sentence a listener
|
|
420
452
|
could repeat ("Narration renders in two minutes", not "Speed"). Everything
|
|
@@ -433,16 +465,15 @@ new sizes:
|
|
|
433
465
|
| `--t-body` | 24px | 15px | list items, card text |
|
|
434
466
|
| `--t-label` | 17px | 12px | kickers, sources, captions (mono, uppercase) |
|
|
435
467
|
|
|
436
|
-
Nothing on a slide below 17px at stage size (a screenshot's frame chrome aside). A title over
|
|
437
|
-
`data-title-mobile
|
|
468
|
+
Nothing on a slide below 17px at stage size (a screenshot's frame chrome aside). A title over
|
|
469
|
+
28 characters needs `data-title-mobile`, written as a real sentence.
|
|
438
470
|
|
|
439
471
|
**Colour and contrast.** Three colours do the work: `--ink`, `--paper` and one
|
|
440
|
-
`--accent` (
|
|
441
|
-
|
|
472
|
+
`--accent` (`--accent-2` only for a chart's second series or a placeholder
|
|
473
|
+
face). Text is ink on paper or accent, or paper on ink: never accent
|
|
442
474
|
on paper, never grey on a colour. Alternate slide grounds (`on-accent`,
|
|
443
475
|
plain, `on-ink`) so the slide changes read, and keep every number, source and
|
|
444
|
-
label at full contrast.
|
|
445
|
-
layout reads them.
|
|
476
|
+
label at full contrast. Set the palette and fonts once, in `:root`.
|
|
446
477
|
|
|
447
478
|
**Start from the brand.** The account keeps a brand: colours, fonts, a logo,
|
|
448
479
|
a presenter style and a design.md (voice, do and don't). `deck new` and
|
|
@@ -450,16 +481,14 @@ a presenter style and a design.md (voice, do and don't). `deck new` and
|
|
|
450
481
|
it first (`hyperdecks brand show --json`, `get_brand`) and write in its voice.
|
|
451
482
|
An explicit request for this deck wins over the brand; `--no-brand`
|
|
452
483
|
(`brand: false`) starts from the template's look. No brand yet? Offer to
|
|
453
|
-
import it
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
Every brand save is kept in its history: `brand history` (`brand_history`)
|
|
462
|
-
lists the last changes and `brand undo [<version>]` (`undo_brand`) goes back.
|
|
484
|
+
import it: `hyperdecks brand import <url|file.pptx|file.pdf|deck-slug>`
|
|
485
|
+
(`import_brand`) reads the real colours, fonts and logo (a PowerPoint's theme
|
|
486
|
+
exactly) and returns a preview link of a sample slide and a token. Show them
|
|
487
|
+
the preview; save with `brand set --draft <token>` (`set_brand { draft }`)
|
|
488
|
+
only after they say yes.
|
|
489
|
+
To change it in words, `brand revise "<change>"` (`revise_brand`) gives a
|
|
490
|
+
draft and preview the same way; `brand history` (`brand_history`) lists past
|
|
491
|
+
saves and `brand undo [<version>]` (`undo_brand`) goes back.
|
|
463
492
|
|
|
464
493
|
**Fonts: pick a pairing.** Fonts load from Google Fonts. Use one of the
|
|
465
494
|
curated pairings below (display for titles, body for text, mono for kickers
|
|
@@ -471,13 +500,19 @@ saves one.
|
|
|
471
500
|
**Textures, icons, pictures:** the `look` topic (one texture, one icon set
|
|
472
501
|
from `search_icons` or `generate_icon_set`, a picture on every other slide).
|
|
473
502
|
|
|
474
|
-
**Pick the layout from what the slide says
|
|
475
|
-
`templates/layouts/` opens with its slot
|
|
476
|
-
|
|
503
|
+
**Pick the layout from what the slide says** (each file in
|
|
504
|
+
`templates/layouts/` opens with its slot, presenter and cues).
|
|
505
|
+
|
|
506
|
+
**The cover never puts text on the picture.** The title and lead sit on a
|
|
507
|
+
solid ground, never on a picture or one faded
|
|
508
|
+
with a gradient `mask-image` (`check` warns).
|
|
477
509
|
|
|
478
510
|
| Layout | Use it for | Presenter, moves |
|
|
479
511
|
|---|---|---|
|
|
480
|
-
| `hero` | the
|
|
512
|
+
| `hero` | the default cover, a section opener: title on the ground, the real subject framed beside it (no `.art`: the presenter is the visual) | stage; `[wave]`, `[look: h1]` |
|
|
513
|
+
| `cover-band` | the picture is the story: full bleed, title on a panel | as `hero` |
|
|
514
|
+
| `cover-poster` | a wide scene: picture on top, title below | as `hero` |
|
|
515
|
+
| `cover-type` | no real picture yet: type only, presenter large | as `hero` |
|
|
481
516
|
| `number` | one number with its source: proof, traction, a delta | stage; `[look: #<id>-figure]` as it is said |
|
|
482
517
|
| `product` | a screenshot in a browser frame (`.frame.card` for a phone) with the point beside it | stage; `[look: #<id>-frame]`, `[reveal: #<id>-callout]` |
|
|
483
518
|
| `split` | the workhorse: a real picture (phone, laptop, 16:9) beside sticker, title, three ticks | stage, bottom-left; `[look: #<id>-frame]`, `[go: #<id>-t1]` … `t3` |
|
|
@@ -492,7 +527,7 @@ suit it.
|
|
|
492
527
|
|
|
493
528
|
**Phones and backgrounds.** One phone: `product`'s `.frame.card`; a flow of
|
|
494
529
|
two to four: `phones` (`--tilt`, `--shadow` on a card). A few slides at most:
|
|
495
|
-
`bg-
|
|
530
|
+
`bg-stage` (never under text), a `.deco` of inked tiles.
|
|
496
531
|
|
|
497
532
|
Presenter style follows the deck (step 2): `stage` when the presenter carries
|
|
498
533
|
the story (the layouts keep the slot's corner clear); `corner` where a big
|
|
@@ -552,7 +587,8 @@ still.
|
|
|
552
587
|
- `[go]` when the narration walks through items (cards, milestones, people,
|
|
553
588
|
the action on the closer): one `go` per item at the word that starts it,
|
|
554
589
|
then `[go: slot]` before the slide ends. Never more than one `go` per
|
|
555
|
-
sentence; never a `go` on a slide in `corner` style.
|
|
590
|
+
sentence; never a `go` on a slide in `corner` style. A `go` keeps off
|
|
591
|
+
the picture; with no room it stays home.
|
|
556
592
|
- `[look]` for a glance: the title as it is read, a number as it is said, a
|
|
557
593
|
screenshot as the narration turns to it.
|
|
558
594
|
- `[reveal]` for the one detail that should not be read before it is said (a
|
|
@@ -564,8 +600,8 @@ still.
|
|
|
564
600
|
<!-- topic: look -->
|
|
565
601
|
#### Look: textures, icons, pictures
|
|
566
602
|
|
|
567
|
-
What the house decks do
|
|
568
|
-
|
|
603
|
+
What the house decks do. Decide these three once per deck, before writing
|
|
604
|
+
slides, and say them in your plan.
|
|
569
605
|
|
|
570
606
|
**1. A picture on at least every other slide**, the most real one you have:
|
|
571
607
|
|
|
@@ -579,18 +615,22 @@ once per deck, before writing slides, and say them in your plan.
|
|
|
579
615
|
No product to show (a plan, a strategy)? Make the slides visual with type
|
|
580
616
|
and the kit below: a `number` slide, stickers, ticks on a `panel`.
|
|
581
617
|
|
|
582
|
-
**2.
|
|
583
|
-
|
|
618
|
+
**2. Plain grounds; texture only behind objects.** Alternate plain,
|
|
619
|
+
`on-tint`, `on-tint-2`, `on-tint-3`, `on-accent`, `on-ink`. On the section:
|
|
584
620
|
|
|
585
621
|
| Class | Looks like | For |
|
|
586
622
|
|---|---|---|
|
|
587
|
-
| `bg-
|
|
588
|
-
| `bg-
|
|
589
|
-
| `bg-
|
|
590
|
-
| `bg-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
623
|
+
| `bg-pool` | a soft light behind the picture | any picture |
|
|
624
|
+
| `bg-corner` | a halftone quarter-disc, top-right | cover, closer |
|
|
625
|
+
| `bg-stage` | a dot plate under a mockup | product, phones |
|
|
626
|
+
| `bg-mark` | the slide number, huge, faint (`--mark:'04'`) | cover, opener, closer |
|
|
627
|
+
|
|
628
|
+
- One signature per deck (`bg-corner` or `bg-mark`), on three slides at
|
|
629
|
+
most: the cover, an opener or the ask, the closer.
|
|
630
|
+
- `bg-pool` and `bg-stage` are picture helpers: only beside an image.
|
|
631
|
+
- Nothing under text, a chart or a photo; the presenter's corner stays clear.
|
|
632
|
+
- Your own is fine in the deck's `<style>`: tokens only, behind the content
|
|
633
|
+
in an isolated slide, off with nothing to back.
|
|
594
634
|
|
|
595
635
|
**3. One icon set.** Pick one source and keep it for every icon in the deck:
|
|
596
636
|
Lucide (`search_icons`, same `strokeWidth` throughout); or one
|
|
@@ -608,9 +648,9 @@ an app icon or a face), `.ticks`, `.panel`, `.pill`, `.frame.card` (a phone),
|
|
|
608
648
|
`<concrete subject>, <setting>, <medium: e.g. editorial photo, soft daylight>,
|
|
609
649
|
palette <ink hex> <accent hex> <paper hex>, no text, no logos`
|
|
610
650
|
(`generate_image { brand: true }` adds the palette and the "no text" for
|
|
611
|
-
you). Generate at the slide's shape (
|
|
651
|
+
you). Generate at the slide's shape (16:9; near-square for the hero), in one batch.
|
|
612
652
|
|
|
613
|
-
**Slide recipes.** Opener: `on-accent` + `bg-
|
|
653
|
+
**Slide recipes.** Opener: `on-accent` + `bg-corner`, the logo
|
|
614
654
|
or a few app-icon stickers, one display line. Product or feature: `split`
|
|
615
655
|
with a real screen. Proof: `number` (huge, sourced) or `chart`. Plan:
|
|
616
656
|
`timeline` on a tint. People: `team` with real faces. Closer: `on-ink`, the
|
|
@@ -741,6 +781,8 @@ you do).
|
|
|
741
781
|
clip that says "Minimoji" but is transcribed as "Moji" needs
|
|
742
782
|
`normalize: moji=minimoji`, and with a second entry
|
|
743
783
|
`normalize: moji=minimoji, dash=D.A.S.H.` (one line, not a YAML list).
|
|
784
|
+
British and American spellings of a word (centre/center, colour/color,
|
|
785
|
+
organise/organize, travelled/traveled) already pass: no entry needed.
|
|
744
786
|
- Write a narrator's name as it is spoken when the two differ, or leave it to
|
|
745
787
|
the narrator's spoken name: `narrator list` shows it, and narrate reads
|
|
746
788
|
"D.A.S.H." as "Dash" (captions keep what you wrote).
|
|
@@ -802,7 +844,8 @@ with the player's `hyperdecks.ai` script tag. Messages and what to do:
|
|
|
802
844
|
| `forbidden phrase` | remove it from the page, script or narration |
|
|
803
845
|
| `slide <id>: needs a data-note` / ⚠ `no data-note` | add the source (required on a `data-kind="pitch"` deck and with `--strict`) |
|
|
804
846
|
| `slide <id>: title … needs a data-title-mobile` | add a shorter title |
|
|
805
|
-
| `slide <id>: the hero's art` / `the product screenshot` / `the chart … is still the layout's placeholder` | make the real file (`gen-image`, `screenshot <url>`, `chart <spec.json>`) and swap the element for the `<img>` the message names; or delete the hero's `<div class="art">` for no art |
|
|
847
|
+
| `slide <id>: the hero's art` / `the cover's picture` / `the product screenshot` / `the chart … is still the layout's placeholder` | make the real file (`gen-image`, `screenshot <url>`, `chart <spec.json>`) and swap the element for the `<img>` the message names; or delete the hero's `<div class="art">` for no art (on `cover-band` / `cover-poster`, switch to `cover-type` instead) |
|
|
848
|
+
| ⚠ `slide <id>: the … cover fades its picture into the ground with a mask-image` | the old hero's look: the title sits on picture and colour mixed. Move the deck's opening to a current cover (`hero`, `cover-band`, `cover-poster`, `cover-type`), or drop the mask and keep the title off the picture |
|
|
806
849
|
| `… presenter style "…" is not one of` | use stage, bubble, glass or corner |
|
|
807
850
|
| `script.md line N: [go: …] matches nothing in slide <id>` | fix the selector, or add the element to that slide |
|
|
808
851
|
| `… transition "…" is not one of` | use drift, cut, push, wipe, zoom `<selector>`, morph or fly |
|
|
@@ -840,6 +883,10 @@ browser (an agent reads the URL; `--json` prints it as one line).
|
|
|
840
883
|
`release --qa` is not a preview: it needs a commit, and it changes what every
|
|
841
884
|
link to this deck plays.
|
|
842
885
|
|
|
886
|
+
Watch a draft with `hyperdecks play`: it shows the face the deck will ship
|
|
887
|
+
with. A plain static server shows the file as written: the face named in the
|
|
888
|
+
tag (the narrator's, since `deck new`, `deck narrator` and `narrate` write it
|
|
889
|
+
there), or none for a tag that says `data-presenter="none"` (`check` warns).
|
|
843
890
|
For the layout pass, serve the folder (`npx serve decks/<slug>` or `python3 -m http.server -d decks/<slug>`)
|
|
844
891
|
and screenshot it with Playwright at 1440×900 and 390×844, every slide
|
|
845
892
|
(`#scene=<id>`). Look at both: nothing clipped, the presenter and its captions
|
|
@@ -46,9 +46,11 @@ makes the hello deck; continue from step 5 below.
|
|
|
46
46
|
`upload_deck_asset` for a picture the user gave you (a URL, or a file they
|
|
47
47
|
uploaded). Each returns a hosted URL: put that URL in the HTML. A hosted deck
|
|
48
48
|
has no folder, so a relative path like `img/a.png` is an error. The
|
|
49
|
-
layouts' stand-ins (
|
|
49
|
+
layouts' stand-ins (the covers' `<div class="art">`, product's
|
|
50
50
|
`<div class="shot">`, chart's placeholder `<svg>`) fail `check_deck` until
|
|
51
51
|
you swap each for an `<img>` of its real file, or delete the hero's art.
|
|
52
|
+
Open on a cover (`hero`, `cover-band`, `cover-poster` or `cover-type`,
|
|
53
|
+
deck_guide topic layouts) and never put the title or lead on the picture.
|
|
52
54
|
Icons: one set per deck (`look` topic): `search_icons { query }` for real
|
|
53
55
|
line icons as inline `<svg>` in `currentColor`, or one `generate_icon_set`.
|
|
54
56
|
For motion, `generate_video` (poll `check_video_job`): `loop: true` for a
|
|
@@ -90,6 +92,11 @@ makes the hello deck; continue from step 5 below.
|
|
|
90
92
|
only, when nobody chose that, is refused: hand the person its Add narrator
|
|
91
93
|
link, or offer D.A.S.H., and pass `captionsOnly: true` only if they say so.
|
|
92
94
|
The result's `presenter` names who presents.
|
|
95
|
+
To present or check your own deck (on a call, say), give the user its
|
|
96
|
+
owner link, `ownerUrl` from `list_decks` or `check_deck`: only they can
|
|
97
|
+
open it, signed in, and nothing is tracked; Present (or H) hides their
|
|
98
|
+
owner tools for a screen share. QA links are only for testing what a
|
|
99
|
+
recipient sees, not for presenting.
|
|
93
100
|
10. Later: `list_shares` and `deck_stats` (both show the card),
|
|
94
101
|
`activity_overview`; `email_embed { token }` for the HTML that pastes into an
|
|
95
102
|
email as the deck's moving preview (it needs a recipient's link, not a QA
|
package/src/brand.mjs
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// only the tokens here touch the deck.
|
|
9
9
|
|
|
10
10
|
// Weights each role loads: the templates set body text at 500, titles at 700
|
|
11
|
-
// and labels in mono at 400/700. Every family
|
|
11
|
+
// and labels in mono at 400/700. Every pairing family has these on Google Fonts;
|
|
12
|
+
// a display family outside the pairings loads what displayFace says it has.
|
|
12
13
|
const ROLE_WEIGHTS = { display: "600;700", body: "400;500;700", mono: "400;700" };
|
|
13
14
|
|
|
14
15
|
// 16 pairings, display + body + mono, each a family Google Fonts serves at the
|
|
@@ -32,8 +33,35 @@ export const FONT_PAIRINGS = [
|
|
|
32
33
|
{ id: "grotesk", display: "Schibsted Grotesk", body: "Work Sans", mono: "Roboto Mono", use: "Marketplaces and media brands; confident and newsy." },
|
|
33
34
|
];
|
|
34
35
|
|
|
35
|
-
// Weights for families that do not have every role weight (load what exists)
|
|
36
|
-
|
|
36
|
+
// Weights for families that do not have every role weight (load what exists),
|
|
37
|
+
// including single-weight display faces a brand may name (Google Fonts refuses
|
|
38
|
+
// a stylesheet that asks for a weight the family lacks).
|
|
39
|
+
const SINGLE_WEIGHT = ["Bangers", "Bebas Neue", "Anton", "Luckiest Guy", "Permanent Marker", "Pacifico", "Lobster", "Abril Fatface", "Alfa Slab One", "Righteous", "Press Start 2P", "Black Ops One", "Bungee", "Russo One", "Shrikhand", "Monoton", "Creepster", "Special Elite"];
|
|
40
|
+
const FAMILY_WEIGHTS = {
|
|
41
|
+
"Space Mono": "400;700", "DM Mono": "400;500", "Libre Baskerville": "400;700", "Courier Prime": "400;700", "Syne": "600;700",
|
|
42
|
+
...Object.fromEntries(SINGLE_WEIGHT.map((f) => [f, "400"])),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// The template's titles (700, -.045em) were tuned for the pairings' sans
|
|
46
|
+
// display faces. Any other display face gets tracking 0; a face with a known
|
|
47
|
+
// weight table is set at its heaviest weight. A face we know nothing about stays
|
|
48
|
+
// at 700 and loads 400;700: Google Fonts' css2 serves whichever of the asked
|
|
49
|
+
// weights exist (it fails only when none do), and the templates' font-synthesis:
|
|
50
|
+
// none means a face without a 700 draws its real 400, never a faked bold.
|
|
51
|
+
const SERIF_DISPLAY = new Set(["Fraunces", "Playfair Display", "Newsreader", "Libre Baskerville"]);
|
|
52
|
+
const TUNED_DISPLAY = new Set(FONT_PAIRINGS.map((p) => p.display).filter((f) => !SERIF_DISPLAY.has(f)));
|
|
53
|
+
// Any family a pairing names (display or body) has the display weights.
|
|
54
|
+
const PAIRED_DISPLAY = new Set(FONT_PAIRINGS.flatMap((p) => [p.display, p.body]));
|
|
55
|
+
|
|
56
|
+
// { weights, weight, tracking } for a display family: the weights to load, and
|
|
57
|
+
// the --display-weight / --display-tracking to write (null: the template's).
|
|
58
|
+
export function displayFace(family) {
|
|
59
|
+
const f = String(family ?? "").trim();
|
|
60
|
+
const weights = FAMILY_WEIGHTS[f] ?? (PAIRED_DISPLAY.has(f) ? ROLE_WEIGHTS.display : "400;700");
|
|
61
|
+
if (TUNED_DISPLAY.has(f)) return { weights, weight: null, tracking: null };
|
|
62
|
+
const heaviest = Math.max(...weights.split(";").map(Number));
|
|
63
|
+
return { weights, weight: heaviest === 700 ? null : String(heaviest), tracking: "0" };
|
|
64
|
+
}
|
|
37
65
|
|
|
38
66
|
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
39
67
|
// A Google Fonts family name: letters, digits and spaces (e.g. "Source Sans 3").
|
|
@@ -54,7 +82,7 @@ export function findPairing(id) {
|
|
|
54
82
|
// The Google Fonts stylesheet URL for one family at the weights its roles use
|
|
55
83
|
// (a family that is both display and body loads both roles' weights).
|
|
56
84
|
export function googleFontsUrl(family, roles = "body") {
|
|
57
|
-
const wanted = [roles].flat().flatMap((r) => (ROLE_WEIGHTS[r] ?? "400;700").split(";"));
|
|
85
|
+
const wanted = [roles].flat().flatMap((r) => (r === "display" ? displayFace(family).weights : ROLE_WEIGHTS[r] ?? "400;700").split(";"));
|
|
58
86
|
const weights = FAMILY_WEIGHTS[family] ?? [...new Set(wanted)].sort().join(";");
|
|
59
87
|
return `https://fonts.googleapis.com/css2?family=${family.trim().replace(/ /g, "+")}:wght@${weights}&display=swap`;
|
|
60
88
|
}
|
|
@@ -90,6 +118,21 @@ export function cleanBrand(brand) {
|
|
|
90
118
|
return { name, colors, fonts, logoUrl, presenterStyle, hasDesignMd };
|
|
91
119
|
}
|
|
92
120
|
|
|
121
|
+
// The templates' own ink, paper and accent (layouts/_deck.html, narrated-deck.html), for a brand that sets only some colours.
|
|
122
|
+
const TEMPLATE_INK = "#0b0b0f", TEMPLATE_PAPER = "#fafaf7", TEMPLATE_ACCENT = "#ffda1b";
|
|
123
|
+
// WCAG relative luminance and contrast ratio of two #rgb / #rrggbb colours.
|
|
124
|
+
function luminance(hex) {
|
|
125
|
+
const h = hex.replace("#", ""), full = h.length === 3 ? [...h].map((c) => c + c).join("") : h.slice(0, 6);
|
|
126
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
|
|
127
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
128
|
+
}
|
|
129
|
+
export const contrast = (a, b) => { const [x, y] = [luminance(a), luminance(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };
|
|
130
|
+
// Which of ink and paper the text on the accent is: ink unless it falls under 4.5:1 and paper reads better.
|
|
131
|
+
export function accentInk(accent, ink, paper) {
|
|
132
|
+
const onInk = contrast(accent, ink);
|
|
133
|
+
return onInk < 4.5 && contrast(accent, paper) > onInk ? "paper" : "ink";
|
|
134
|
+
}
|
|
135
|
+
|
|
93
136
|
const escapeAttr = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
94
137
|
const fontStack = (family, role) => `"${family}", ${FALLBACK[role]}`;
|
|
95
138
|
const LOGO_CSS = `
|
|
@@ -113,9 +156,18 @@ export function applyBrand(html, brand) {
|
|
|
113
156
|
block = decl.test(block) ? block.replace(decl, (_, k) => `${k}${value}`) : block.replace(/\s*\}$/, (end) => ` ${name}: ${value};${end}`);
|
|
114
157
|
};
|
|
115
158
|
for (const [k, v] of Object.entries(b.colors)) set(CSS_VAR[k], v);
|
|
159
|
+
// Text on an accent ground reads --accent-ink (ink by default): a dark accent
|
|
160
|
+
// that ink cannot be read on takes the paper instead, so a cover on the accent stays legible.
|
|
161
|
+
const { accent = TEMPLATE_ACCENT, ink = TEMPLATE_INK, paper = TEMPLATE_PAPER } = b.colors;
|
|
162
|
+
if ((b.colors.accent || b.colors.ink || b.colors.paper) && accentInk(accent, ink, paper) === "paper") set("--accent-ink", "var(--paper)");
|
|
116
163
|
if (b.fonts.body) set("--font", fontStack(b.fonts.body, "body"));
|
|
117
164
|
if (b.fonts.mono) set("--mono", fontStack(b.fonts.mono, "mono"));
|
|
118
|
-
if (b.fonts.display)
|
|
165
|
+
if (b.fonts.display) {
|
|
166
|
+
set("--display", fontStack(b.fonts.display, "display"));
|
|
167
|
+
const face = displayFace(b.fonts.display);
|
|
168
|
+
if (face.weight) set("--display-weight", face.weight);
|
|
169
|
+
if (face.tracking) set("--display-tracking", face.tracking);
|
|
170
|
+
}
|
|
119
171
|
let out = html.slice(0, root.index) + block + html.slice(root.index + root[0].length);
|
|
120
172
|
|
|
121
173
|
const families = FONT_ROLES.filter((r) => b.fonts[r]).map((r) => [b.fonts[r], r]);
|
package/src/checkRules.mjs
CHANGED
|
@@ -63,6 +63,7 @@ export const MESSAGES = {
|
|
|
63
63
|
gesture: (line, id) => `script.md line ${line}: [gesture: ${id}] is not one of the head's gestures: ${GESTURES.join(", ")}`,
|
|
64
64
|
presenterUnfilled: (mode) => `the player tag says data-presenter="${mode}" but names no face (${mode === "head" ? "data-avatar" : "data-presenter-image"}), so as written it plays captions only. release puts the narrator's face on it; to set one by hand, D.A.S.H. is data-presenter="head" data-avatar="${DASH_AVATAR}" (deck_guide topic presenter)`,
|
|
65
65
|
presenterKept: (mode) => `the player tag has a data-avatar but data-presenter="${mode ?? "none"}", so it plays captions only and release leaves it that way: set data-presenter="head" to show that face, or remove data-avatar so release adds the narrator's`,
|
|
66
|
+
presenterCaptions: () => `the player tag plays captions only as written (data-presenter="none" or none set), so the file served as is (npx serve, python3 -m http.server, the screenshot pass) shows no face: run \`hyperdecks deck narrator <slug> <name>\` (or narrate again) to write the narrator's face into index.html, or watch it with \`hyperdecks play <deck>\`; release adds the face either way. Captions only on purpose: \`hyperdecks deck narrator <slug> none\``,
|
|
66
67
|
presenterSize: (where, value) => `${where}: data-presenter-size "${value}" is not one of ${PRESENTER_SIZES.join(", ")}; the presenter keeps its default size`,
|
|
67
68
|
presenterStyle: (where, value) => `${where}: presenter style "${value}" is not one of ${PRESENTER_STYLES.join(", ")}`,
|
|
68
69
|
transition: (where, value) => `${where}: transition "${value}" is not one of ${TRANSITIONS.join(", ")} (zoom takes a selector: "zoom #row")`,
|
|
@@ -71,9 +72,13 @@ export const MESSAGES = {
|
|
|
71
72
|
transitionStale: (where, script, manifest) => `${where}: script.md says transition "${script}" but the manifest has "${manifest}": run \`hyperdecks narrate\` so the player gets it`,
|
|
72
73
|
flyTwice: (ids) => `fly is for the deck's one big moment, but slides ${ids.join(", ")} all use it`,
|
|
73
74
|
// Warnings from the look topic (deck_guide look): the deck passes, but a person should look.
|
|
75
|
+
coverFade: (where, layout) => `${where}: the ${layout} cover fades its picture into the ground with a mask-image, so the title sits on a mix of picture and colour that is hard to read: keep the text off the picture (layouts hero, cover-band, cover-poster or cover-type; deck_guide topic layouts)`,
|
|
74
76
|
fewPictures: (shown, total) => `only ${shown} of ${total} slides show a picture (a screenshot, a recording, a photo, a chart); aim for a real one on every other slide (deck_guide topic look)`,
|
|
75
77
|
presenterGutter: (where, decl) => `${where}: ${decl} keeps a right gutter for the presenter; leave that corner to the slide's .narration-slot so a deck without a presenter can use it (deck_guide topic look)`,
|
|
76
78
|
mixedIcons: (sets) => `the deck mixes icon sets (${sets.join(" and ")}); keep one set per deck (deck_guide topic look)`,
|
|
79
|
+
retiredTexture: (where, cls, instead, inlined) => `${where}: ${cls} is a whole-slide texture the kit no longer ships ${inlined ? "(this deck still carries its rule, so it runs under the text)" : "(it ran under the text), so it paints nothing"}; ${instead} (deck_guide topic look)`,
|
|
80
|
+
tooManySignatures: (n) => `${n} slides carry a signature background (bg-corner or bg-mark); keep it to three at most: the cover, one opener or the ask, the closer (deck_guide topic look)`,
|
|
81
|
+
textureOff: (where, cls, layout) => `${where}: ${cls} does nothing on ${layout} (the shell switches it off there: nothing to back); remove it, or use bg-pool behind the picture (deck_guide topic look)`,
|
|
77
82
|
// A layout's stand-in left in: `how` is the tool (hosted) or command (CLI) that makes the real file.
|
|
78
83
|
placeholder: (id, what, el, how, img) => `slide ${id}: ${what} is still the layout's placeholder (${el}); ${how}, then put it in as ${img}`,
|
|
79
84
|
// Warnings (check --layout): the deck still passes.
|
|
@@ -312,6 +317,19 @@ export function slides(html) {
|
|
|
312
317
|
});
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
// Each slide's titles written short for phones: { id, titles: [{ desktop, phone }] },
|
|
321
|
+
// only where the phone text differs. The player swaps data-title-mobile in below
|
|
322
|
+
// 650px, so review must not read the shorter phone title as cut off.
|
|
323
|
+
export function mobileTitles(html) {
|
|
324
|
+
return slides(html).map(({ id, body }) => ({
|
|
325
|
+
id,
|
|
326
|
+
titles: [...body.matchAll(/<([a-z][\w-]*)\b([^>]*\sdata-title-mobile\s*=[^>]*)>([\s\S]*?)<\/\1>/gi)]
|
|
327
|
+
.map(([, , attrs, inner]) => ({ desktop: textOf(inner), phone: attr(attrs, "data-title-mobile") }))
|
|
328
|
+
.filter((t) => t.phone != null && decode(t.phone).trim() && decode(t.phone).trim() !== t.desktop)
|
|
329
|
+
.map((t) => ({ desktop: t.desktop.slice(0, 200), phone: decode(t.phone).trim().slice(0, 200) })),
|
|
330
|
+
})).filter((s) => s.id && s.titles.length);
|
|
331
|
+
}
|
|
332
|
+
|
|
315
333
|
// The player tag also carries the deck's declarations: data-silent (no
|
|
316
334
|
// narration, on purpose) and data-kind="pitch" (every slide sources its facts).
|
|
317
335
|
// The check reads what the deck says, never guesses from its content.
|
|
@@ -531,6 +549,14 @@ const PLACEHOLDERS = [
|
|
|
531
549
|
layout: "l-hero", tag: "div", cls: "art", what: "the hero's art", img: '<img class="art" src="…" alt="">, or delete the div for no art (the presenter is the visual)',
|
|
532
550
|
how: { hosted: "make a picture of the real subject with generate_image", cli: "make a picture of the real subject with `hyperdecks gen-image \"<prompt>\"`" },
|
|
533
551
|
},
|
|
552
|
+
{
|
|
553
|
+
layout: "l-cover-band", tag: "div", cls: "art", what: "the cover's picture", img: '<img class="art" src="…" alt="">, or use the cover-type layout for no picture',
|
|
554
|
+
how: { hosted: "make a wide picture of the real subject with generate_image", cli: "make a wide picture of the real subject with `hyperdecks gen-image \"<prompt>\"`" },
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
layout: "l-cover-poster", tag: "div", cls: "art", what: "the cover's picture", img: '<img class="art" src="…" alt="">, or use the cover-type layout for no picture',
|
|
558
|
+
how: { hosted: "make a wide picture of the real subject with generate_image", cli: "make a wide picture of the real subject with `hyperdecks gen-image \"<prompt>\"`" },
|
|
559
|
+
},
|
|
534
560
|
{
|
|
535
561
|
layout: "l-product", tag: "div", cls: "shot", what: "the product screenshot", img: '<img class="shot" src="…" alt="…">',
|
|
536
562
|
how: { hosted: "capture the real page with capture_url", cli: "capture the real page with `hyperdecks screenshot <url>`" },
|
|
@@ -810,6 +836,62 @@ export function lookWarnings(html) {
|
|
|
810
836
|
if (iconImg(tag)) sets.add("image icons");
|
|
811
837
|
}
|
|
812
838
|
if (sets.size > 1) out.push(MESSAGES.mixedIcons([...sets].sort()));
|
|
839
|
+
out.push(...textureWarnings(all, html), ...coverFadeWarnings(all, html));
|
|
840
|
+
return out;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Covers never put text on the picture. Where the text overlaps a picture can only be
|
|
844
|
+
// measured by rendering, so this reads the one source pattern that always does: a cover
|
|
845
|
+
// layout whose CSS fades its picture with a gradient mask-image (the old hero's look,
|
|
846
|
+
// still inlined in decks made from it). A deck's own picture and text placement beyond
|
|
847
|
+
// that is left to the layouts and the review.
|
|
848
|
+
export const COVER_LAYOUTS = ["l-hero", "l-cover-band", "l-cover-poster", "l-cover-type"];
|
|
849
|
+
function coverFadeWarnings(all, html) {
|
|
850
|
+
const css = [...uncommented(html).matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1].replace(/\/\*[\s\S]*?\*\//g, "")).join("\n");
|
|
851
|
+
const faded = new Set();
|
|
852
|
+
for (const [, sel, body] of css.matchAll(/([^{}]*)\{([^{}]*)\}/g)) {
|
|
853
|
+
if (!/(?:^|;|\s)(?:-webkit-)?mask(?:-image)?\s*:[^;]*gradient\(/i.test(body)) continue;
|
|
854
|
+
for (const l of COVER_LAYOUTS) if (new RegExp(`\\.${l}(?![\\w-])`).test(sel)) faded.add(l);
|
|
855
|
+
}
|
|
856
|
+
if (!faded.size) return [];
|
|
857
|
+
return all.flatMap((s, i) => classes(s.tag).filter((c) => faded.has(c)).map((c) => MESSAGES.coverFade(`slide ${s.id || i + 1}`, c.slice(2))));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Backgrounds (the look topic): the kit's textures are confined to the object they
|
|
861
|
+
// back. The retired whole-slide textures warn rather than vanish silently (a deck made
|
|
862
|
+
// before keeps their rules in its own inlined shell, where they still paint). Read from each slide's class attribute; a texture on a layout whose shell
|
|
863
|
+
// rule switches it off (TEXTURE_OFF, pinned to _deck.html by lib/deckLayouts.test.ts) did nothing, and a custom
|
|
864
|
+
// slide (no l-* class) is never judged on that. Commented-out slides do not count.
|
|
865
|
+
const RETIRED_TEXTURES = {
|
|
866
|
+
"bg-rays": "use a plain ground, or bg-corner on the cover and the closer",
|
|
867
|
+
"bg-lines": "use a plain ground, or bg-pool behind the picture",
|
|
868
|
+
"bg-halftone": "use bg-corner on the cover and the closer",
|
|
869
|
+
"bg-dots": "use bg-stage under the mockup or the phones",
|
|
870
|
+
};
|
|
871
|
+
const SIGNATURES = ["bg-corner", "bg-mark"];
|
|
872
|
+
export const MAX_SIGNATURE_SLIDES = 3;
|
|
873
|
+
const BUSY_LAYOUTS = ["l-number", "l-features", "l-team", "l-timeline", "l-compare"]; // full-width content: nothing to back
|
|
874
|
+
const PICTURE_COVERS = ["l-cover-band", "l-cover-poster"]; // the picture is the ground: a texture would only sit under it
|
|
875
|
+
export const TEXTURE_OFF = {
|
|
876
|
+
"bg-corner": ["l-split", "l-product", "l-chart", "l-quote", ...BUSY_LAYOUTS, ...PICTURE_COVERS],
|
|
877
|
+
"bg-stage": ["l-split", "l-chart", "l-quote", "l-closer", ...BUSY_LAYOUTS, ...PICTURE_COVERS, "l-cover-type"],
|
|
878
|
+
"bg-mark": ["l-split", "l-product", "l-chart", "l-phones", ...BUSY_LAYOUTS, ...PICTURE_COVERS],
|
|
879
|
+
};
|
|
880
|
+
function textureWarnings(all, html) {
|
|
881
|
+
const css = [...uncommented(html).matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1].replace(/\/\*[\s\S]*?\*\//g, "")).join("\n");
|
|
882
|
+
const inlined = (c) => new RegExp(`\\.${c}(?![\\w-])[^{}]*\\{`).test(css);
|
|
883
|
+
const out = [];
|
|
884
|
+
let signatures = 0;
|
|
885
|
+
all.forEach((s, i) => {
|
|
886
|
+
const where = `slide ${s.id || i + 1}`;
|
|
887
|
+
const cls = classes(s.tag);
|
|
888
|
+
for (const [c, instead] of Object.entries(RETIRED_TEXTURES)) if (cls.includes(c)) out.push(MESSAGES.retiredTexture(where, c, instead, inlined(c)));
|
|
889
|
+
if (SIGNATURES.some((c) => cls.includes(c))) signatures++;
|
|
890
|
+
const layout = cls.find((c) => /^l-[\w-]+$/.test(c));
|
|
891
|
+
if (!layout) return;
|
|
892
|
+
for (const [c, off] of Object.entries(TEXTURE_OFF)) if (cls.includes(c) && off.includes(layout)) out.push(MESSAGES.textureOff(where, c, layout));
|
|
893
|
+
});
|
|
894
|
+
if (signatures > MAX_SIGNATURE_SLIDES) out.push(MESSAGES.tooManySignatures(signatures));
|
|
813
895
|
return out;
|
|
814
896
|
}
|
|
815
897
|
|
|
@@ -970,8 +1052,14 @@ export async function checkSources({ html, script = "" }, opts = {}, { fetch: fe
|
|
|
970
1052
|
}
|
|
971
1053
|
warnings.push(...transitionWarnings(html, script, manifest), ...presenterSizeWarnings(html));
|
|
972
1054
|
// A narrated deck's tag that shows no face and says otherwise (a silent deck has no presenter).
|
|
973
|
-
|
|
974
|
-
|
|
1055
|
+
// A hosted draft plays only through its draft link and release_deck, which add the face, so a plain none is no issue there.
|
|
1056
|
+
const found = silent ? null : presenterTagIssue(html);
|
|
1057
|
+
const tagIssue = found?.kind === "captions" && opts.hosted ? null : found;
|
|
1058
|
+
if (tagIssue) {
|
|
1059
|
+
warnings.push(tagIssue.kind === "kept" ? MESSAGES.presenterKept(tagIssue.presenter)
|
|
1060
|
+
: tagIssue.kind === "captions" ? MESSAGES.presenterCaptions()
|
|
1061
|
+
: MESSAGES.presenterUnfilled(tagIssue.presenter));
|
|
1062
|
+
}
|
|
975
1063
|
for (const t of walked.templates) warnings.push(MESSAGES.scriptTemplate(t.from, t.samples));
|
|
976
1064
|
for (const c of walked.computed) warnings.push(MESSAGES.scriptComputed(c.from, c.calls));
|
|
977
1065
|
if (opts.hosted) {
|