@hypersocial/decks-cli 0.9.6 → 0.9.8
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 -1
- package/package.json +1 -1
- package/skills/hyperdecks/SKILL.md +104 -17
- package/skills/hyperdecks/chat.md +10 -7
- package/src/checkRules.mjs +107 -0
- package/src/commands.mjs +42 -25
- package/src/files.mjs +1 -0
- package/src/guide.mjs +2 -2
- package/src/init.mjs +8 -4
- package/src/layouts.mjs +3 -3
- package/src/loadingBlock.mjs +43 -12
- package/src/ops.mjs +46 -15
- package/src/presenter.mjs +102 -16
- package/src/preview.mjs +2 -2
- package/src/release.mjs +11 -7
- package/src/runtime.mjs +62 -2
- package/templates/layouts/_deck.html +51 -0
- package/templates/layouts/chart.html +5 -0
- package/templates/layouts/closer.html +5 -0
- package/templates/layouts/compare.html +5 -0
- package/templates/layouts/features.html +6 -0
- package/templates/layouts/hero.html +5 -0
- package/templates/layouts/number.html +5 -0
- package/templates/layouts/phones.html +5 -0
- package/templates/layouts/product.html +5 -0
- package/templates/layouts/quote.html +6 -0
- package/templates/layouts/split.html +53 -0
- package/templates/layouts/team.html +6 -0
- package/templates/layouts/timeline.html +5 -0
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ share list <slug> [--json]
|
|
|
64
64
|
share on|off <token>
|
|
65
65
|
share mute|unmute <token>
|
|
66
66
|
share rm <token>
|
|
67
|
-
share url <token>
|
|
67
|
+
share url <token> [--backup]
|
|
68
68
|
|
|
69
69
|
recipient archive|unarchive <name>
|
|
70
70
|
import <html-or-zip-or-url> --slug <slug> [--title title] [--status draft|live|archived]
|
package/package.json
CHANGED
|
@@ -149,7 +149,7 @@ Hyperdecks server; use the CLI where you have a shell and the tool otherwise.
|
|
|
149
149
|
| One more link | `hyperdecks share new <slug> "<name>" [--no-forward]` | `create_share` (`allowForward`) |
|
|
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
|
-
| Slides or scroll, viewer toggle | `hyperdecks deck view <slug> [slides\|scroll\|default] [--toggle on\|off]` | `set_deck_view` |
|
|
152
|
+
| Slides or scroll, viewer toggle, pace | `hyperdecks deck view <slug> [slides\|scroll\|default] [--toggle on\|off] [--pace auto\|slide\|step\|default]` | `set_deck_view` |
|
|
153
153
|
| Who watched | `hyperdecks deck stats <slug>` | `deck_stats` |
|
|
154
154
|
| All activity | `hyperdecks activity --deck <slug>` | `activity_overview` |
|
|
155
155
|
| What one person watched | `hyperdecks engagement "<name>" --deck <slug> --json` | `recipient_engagement` |
|
|
@@ -169,7 +169,15 @@ HTML says. Viewers get a slides/scroll toggle in the player, remembered per
|
|
|
169
169
|
link, unless you turn it off (`--toggle off`, `viewToggle: false`). A viewer
|
|
170
170
|
who already switched keeps their own choice while the toggle is on; turn the
|
|
171
171
|
toggle off to make every viewer see the deck's view.
|
|
172
|
-
|
|
172
|
+
Pace is the third: `--pace auto` (`pace: "auto"`, the default) plays itself,
|
|
173
|
+
`slide` waits for Next (the Continue pill, →, a swipe) after each slide's
|
|
174
|
+
narration, and `step` also stops the narration before each `[reveal]` cue,
|
|
175
|
+
so a slide's reveals are the steps the viewer clicks through: group what
|
|
176
|
+
belongs together at one word, so each stop is a meaningful step. Viewers can
|
|
177
|
+
switch pace (and slides/scroll) in the player's Preferences, in its top bar
|
|
178
|
+
(remembered per link); a video export always plays straight through. Pace
|
|
179
|
+
waits apply side to side (slides) only: scroll keeps its own flow.
|
|
180
|
+
`deck list --json` / `list_decks` and `deck stats` / `deck_stats` show all three.
|
|
173
181
|
|
|
174
182
|
### 1. Ground it in the user's own material
|
|
175
183
|
|
|
@@ -258,6 +266,18 @@ The deck contract (`check` enforces or warns on each line):
|
|
|
258
266
|
<!-- topic: presenter -->
|
|
259
267
|
#### Which presenter style
|
|
260
268
|
|
|
269
|
+
**Who presents is set at release; leave the tag alone.** `deck new --narrated`
|
|
270
|
+
writes `data-presenter="none"` on the player tag, and `release` (and
|
|
271
|
+
`release_deck`) puts the narrator's face on the copy it publishes: your own
|
|
272
|
+
narrator's avatar, or D.A.S.H.'s live head for a deck in D.A.S.H.'s voice. It
|
|
273
|
+
says `presented by …` only when the published page has that face; otherwise
|
|
274
|
+
it warns `releasing captions only` with the reason. To pick the face by hand,
|
|
275
|
+
set both attributes: `data-presenter="head" data-avatar="…"`. D.A.S.H.'s is
|
|
276
|
+
`data-presenter="head" data-avatar="https://hyperdecks.ai/player/v1/dash-avatar.json"`
|
|
277
|
+
(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
|
+
`check` warns on both, and `release` leaves a hand-set `data-avatar` alone.
|
|
280
|
+
|
|
261
281
|
Set the deck's on the player tag (`data-presenter-style="stage"`) or in
|
|
262
282
|
script.md's front matter (`presenterStyle: stage`); override one slide with
|
|
263
283
|
`data-presenter-style` on its `<section>` or `presenter-style: corner` under
|
|
@@ -448,11 +468,8 @@ saves one.
|
|
|
448
468
|
|
|
449
469
|
<!-- font pairings -->
|
|
450
470
|
|
|
451
|
-
**
|
|
452
|
-
|
|
453
|
-
the deck's colours: paste the `<svg>` in place of a features card's icon. Use
|
|
454
|
-
`icons <subject...> --style` (`generate_icon_set`) only for illustrated icons
|
|
455
|
-
in a style of your own.
|
|
471
|
+
**Textures, icons, pictures:** the `look` topic (one texture, one icon set
|
|
472
|
+
from `search_icons` or `generate_icon_set`, a picture on every other slide).
|
|
456
473
|
|
|
457
474
|
**Pick the layout from what the slide says.** Each layout's file in
|
|
458
475
|
`templates/layouts/` opens with its slot side, presenter and the cues that
|
|
@@ -463,6 +480,7 @@ suit it.
|
|
|
463
480
|
| `hero` | the opening, a section opener; the presenter is the visual, no `.art` unless it shows the real subject (check fails while the placeholder `<div class="art">` is in: swap it for an `<img class="art">` or delete it) | stage, bottom-right; `[wave]`, `[look: h1]` |
|
|
464
481
|
| `number` | one number with its source: proof, traction, a delta | stage; `[look: #<id>-figure]` as it is said |
|
|
465
482
|
| `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
|
+
| `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` |
|
|
466
484
|
| `phones` | a phone app's flow: a fanned row of phone cards | stage, bottom-left; `[go: #<id>-p1]` … `p3`, `[go: slot]` |
|
|
467
485
|
| `quote` | a customer or user in their own words, quoted exactly | stage, bottom-left; `[look: #<id>-quote]` |
|
|
468
486
|
| `compare` | before/after, them/us, three parallel lines each | stage; `[look: #<id>-before]`, `[go: #<id>-after]` |
|
|
@@ -476,12 +494,10 @@ suit it.
|
|
|
476
494
|
two to four: `phones` (`--tilt`, `--shadow` on a card). A few slides at most:
|
|
477
495
|
`bg-dots` (never under text), a `.deco` of inked tiles.
|
|
478
496
|
|
|
479
|
-
Presenter style follows the deck (step 2
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
picture. Three layouts (`quote`, `phones`, `closer`) sit left; put a `slot-left` on others
|
|
484
|
-
where there is room so slide changes move the presenter.
|
|
497
|
+
Presenter style follows the deck (step 2): `stage` when the presenter carries
|
|
498
|
+
the story (the layouts keep the slot's corner clear); `corner` where a big
|
|
499
|
+
visual is the point; `glass` on a dark or busy picture. `quote`, `split`,
|
|
500
|
+
`phones` and `closer` sit left; add `slot-left` elsewhere where there is room.
|
|
485
501
|
|
|
486
502
|
**Visuals: real first, generated second.**
|
|
487
503
|
|
|
@@ -495,10 +511,12 @@ where there is room so slide changes move the presenter.
|
|
|
495
511
|
floating shapes, abstract "tech" art, clip-art metaphors) makes a deck look
|
|
496
512
|
cheap. A visual earns its place only if it shows something real from the
|
|
497
513
|
story: the product, the data, a concrete situation or object. When in doubt,
|
|
498
|
-
leave it out: one big statement in type beats a picture of nothing.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
514
|
+
leave it out: one big statement in type beats a picture of nothing. But a
|
|
515
|
+
deck of nothing but type is not the answer: reach for the real things
|
|
516
|
+
first (`look` topic). The presenter is the hero of the opening slide;
|
|
517
|
+
don't put generated art behind it.
|
|
518
|
+
- Icons: from the deck's one set (`look` topic), labelling things or in a
|
|
519
|
+
`.sticker`; not one per sentence. Bullets are `.ticks`.
|
|
502
520
|
- Images: `gen-image` (`generate_image`) only for a concrete subject the
|
|
503
521
|
deck is about (a product in use, a place, a real object), never a metaphor.
|
|
504
522
|
One style per deck, described the same way every time (subject, medium,
|
|
@@ -543,6 +561,75 @@ still.
|
|
|
543
561
|
- No CSS animation on slides beyond `.reveal`; the player animates the slide
|
|
544
562
|
change.
|
|
545
563
|
|
|
564
|
+
<!-- topic: look -->
|
|
565
|
+
#### Look: textures, icons, pictures
|
|
566
|
+
|
|
567
|
+
What the house decks do that a plain agent deck doesn't. Decide these three
|
|
568
|
+
once per deck, before writing slides, and say them in your plan.
|
|
569
|
+
|
|
570
|
+
**1. A picture on at least every other slide**, the most real one you have:
|
|
571
|
+
|
|
572
|
+
1. the product itself: `capture_url` (phone: width 390, height 844), or a
|
|
573
|
+
screen recording or photo the person gives you (`upload_deck_asset`);
|
|
574
|
+
2. their people, logos and app icons (with permission);
|
|
575
|
+
3. real data drawn with `render_chart`;
|
|
576
|
+
4. only then `generate_image`, of a concrete subject (a place, an object,
|
|
577
|
+
the product in use), never a metaphor or abstract "tech" art.
|
|
578
|
+
|
|
579
|
+
No product to show (a plan, a strategy)? Make the slides visual with type
|
|
580
|
+
and the kit below: a `number` slide, stickers, ticks on a `panel`.
|
|
581
|
+
|
|
582
|
+
**2. One texture**, on two or three slides (an opener, a closer, a slide
|
|
583
|
+
with a visual), never under body text. On the section:
|
|
584
|
+
|
|
585
|
+
| Class | Looks like | For |
|
|
586
|
+
|---|---|---|
|
|
587
|
+
| `bg-lines` | diagonal speed lines fading toward `--fade-at` | product slides, beside a picture |
|
|
588
|
+
| `bg-halftone` | a corner of halftone dots | openers, numbers |
|
|
589
|
+
| `bg-rays` | rays from `--rays-at` | an opener or a closer |
|
|
590
|
+
| `bg-dots` | a fading dot grid | a phones row |
|
|
591
|
+
|
|
592
|
+
Grounds carry the rest: alternate `on-accent`, plain, `on-ink` and the soft
|
|
593
|
+
washes `on-tint`, `on-tint-2`, `on-tint-3` so each slide change reads.
|
|
594
|
+
|
|
595
|
+
**3. One icon set.** Pick one source and keep it for every icon in the deck:
|
|
596
|
+
Lucide (`search_icons`, same `strokeWidth` throughout); or one
|
|
597
|
+
`generate_icon_set` call with every subject you need and one `preset`
|
|
598
|
+
(`line`, `duotone`, `sticker`, `clay`; the palette defaults to the brand's);
|
|
599
|
+
or the product's own app icons and logos. Never mix two.
|
|
600
|
+
Put an icon or logo in a `.sticker` (tilted tile, hard shadow) on openers
|
|
601
|
+
and product slides; use `.ticks` (accent squares) for lists, not icons.
|
|
602
|
+
|
|
603
|
+
**Kit (in every layout deck's shell):** `.sticker` (`--tilt`; `.photo` for
|
|
604
|
+
an app icon or a face), `.ticks`, `.panel`, `.pill`, `.frame.card` (a phone),
|
|
605
|
+
`.frame.laptop` (a desktop shot). The `split` layout is image plus copy.
|
|
606
|
+
|
|
607
|
+
**Image prompts.** One recipe per deck, the same every time:
|
|
608
|
+
`<concrete subject>, <setting>, <medium: e.g. editorial photo, soft daylight>,
|
|
609
|
+
palette <ink hex> <accent hex> <paper hex>, no text, no logos`
|
|
610
|
+
(`generate_image { brand: true }` adds the palette and the "no text" for
|
|
611
|
+
you). Generate at the slide's shape (wide for a hero), in one batch.
|
|
612
|
+
|
|
613
|
+
**Slide recipes.** Opener: `on-accent` + `bg-rays` or `bg-halftone`, the logo
|
|
614
|
+
or a few app-icon stickers, one display line. Product or feature: `split`
|
|
615
|
+
with a real screen. Proof: `number` (huge, sourced) or `chart`. Plan:
|
|
616
|
+
`timeline` on a tint. People: `team` with real faces. Closer: `on-ink`, the
|
|
617
|
+
one action, stickers again.
|
|
618
|
+
|
|
619
|
+
**No presenter.** When nothing stands in the corner, no face (presenter
|
|
620
|
+
`none`) *and* no captions (`data-silent`, or an export with captions off), or
|
|
621
|
+
when a host calls `window.hyperdecks.setPresenterOff(true)`, the player puts
|
|
622
|
+
`hd-no-presenter` on `<html>` and hides every `.narration-slot`. Each layout
|
|
623
|
+
has a variant for that in its own `<style>` (`deck_guide layouts` says what
|
|
624
|
+
each does): compare and chart widen; features, team and timeline centre
|
|
625
|
+
vertically; split, product and phones centre their copy; number, quote,
|
|
626
|
+
closer and a hero without art re-centre. Type sizes stay. A deck with
|
|
627
|
+
captions keeps the slot (the bubble lives there). For a custom slide, keep it
|
|
628
|
+
compatible: the presenter's corner is the slide's `.narration-slot`, never a
|
|
629
|
+
hard-coded gutter (`padding-right: 400px`); if it needs one, override it in a
|
|
630
|
+
`:root.hd-no-presenter .l-yours …` rule (check warns on a right gutter until
|
|
631
|
+
you do).
|
|
632
|
+
|
|
546
633
|
<!-- topic: script -->
|
|
547
634
|
### 3. Write slides and script
|
|
548
635
|
|
|
@@ -4,7 +4,7 @@ You are building a narrated deck through the Hyperdecks connector: no terminal,
|
|
|
4
4
|
no git, no folder. The deck's files (`index.html`, `script.md`, `evidence.md`)
|
|
5
5
|
live on Hyperdecks as a draft; you read and edit them with tools, and nothing
|
|
6
6
|
reaches a recipient until `release_deck`. Topics: call `deck_guide` with
|
|
7
|
-
`format`, `script`, `scripts`, `design`, `layouts`, `presenter`, `transitions` or `check`
|
|
7
|
+
`format`, `script`, `scripts`, `design`, `look`, `layouts`, `presenter`, `transitions` or `check`
|
|
8
8
|
before the step that needs it.
|
|
9
9
|
|
|
10
10
|
## First time here?
|
|
@@ -21,7 +21,8 @@ makes the hello deck; continue from step 5 below.
|
|
|
21
21
|
|
|
22
22
|
## The loop
|
|
23
23
|
|
|
24
|
-
1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides
|
|
24
|
+
1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides
|
|
25
|
+
and `deck_guide { topic: "look" }` to pick the deck's texture, icon set and pictures.
|
|
25
26
|
`get_brand` for the person's saved brand: write in its designMd's voice.
|
|
26
27
|
None yet? Offer to import it from what they have: `import_brand` with
|
|
27
28
|
their website (`url`), a PowerPoint or PDF they attach (`file`) or one of
|
|
@@ -46,8 +47,8 @@ makes the hello deck; continue from step 5 below.
|
|
|
46
47
|
layouts' stand-ins (hero's `<div class="art">`, product's
|
|
47
48
|
`<div class="shot">`, chart's placeholder `<svg>`) fail `check_deck` until
|
|
48
49
|
you swap each for an `<img>` of its real file, or delete the hero's art.
|
|
49
|
-
Icons: `search_icons { query }`
|
|
50
|
-
|
|
50
|
+
Icons: one set per deck (`look` topic): `search_icons { query }` for real
|
|
51
|
+
line icons as inline `<svg>` in `currentColor`, or one `generate_icon_set`.
|
|
51
52
|
For motion, `generate_video` (poll `check_video_job`): `loop: true` for a
|
|
52
53
|
clip that repeats with no jump, `transparent: true` for a moving cut-out
|
|
53
54
|
over the slide. Paste the finished job's `markup` as is (a transparent
|
|
@@ -90,9 +91,11 @@ makes the hello deck; continue from step 5 below.
|
|
|
90
91
|
10. Later: `list_shares` and `deck_stats` (both show the card),
|
|
91
92
|
`activity_overview`; `email_embed { token }` for the HTML that pastes into an
|
|
92
93
|
email as the deck's moving preview (it needs a recipient's link, not a QA
|
|
93
|
-
one); `set_deck_view { slug, view: "scroll" | "slides" | null, viewToggle }`
|
|
94
|
-
for how the deck opens
|
|
95
|
-
|
|
94
|
+
one); `set_deck_view { slug, view: "scroll" | "slides" | null, viewToggle, pace }`
|
|
95
|
+
for how the deck opens, whether viewers may switch, and its pace (`auto`
|
|
96
|
+
plays itself, `slide` waits for Next after each slide, `step` also stops
|
|
97
|
+
before each `[reveal]`, so group a slide's reveals into meaningful steps)
|
|
98
|
+
(every link, next load, no re-release); `set_share_notify` to mute a link's "opened" alerts;
|
|
96
99
|
`set_recipient_archived` to tidy the activity feed; `credits` for the
|
|
97
100
|
balance. `remove_deck` deletes a deck and breaks every link to it: confirm
|
|
98
101
|
with the user first.
|
package/src/checkRules.mjs
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// the deck's index.html and script.md as text, with the manifest and clip fetches
|
|
6
6
|
// injected.
|
|
7
7
|
|
|
8
|
+
import { DASH_AVATAR, presenterTagIssue } from "./presenter.mjs";
|
|
9
|
+
|
|
8
10
|
export const PLAYER_V1 = "https://hyperdecks.ai/player/v1/player.js";
|
|
9
11
|
// Decks released before the app moved to hyperdecks.ai load the same player
|
|
10
12
|
// from the old host, which serves it forever; both pass.
|
|
@@ -58,6 +60,8 @@ export const MESSAGES = {
|
|
|
58
60
|
selector: (line, cue, selector, id) => `script.md line ${line}: [${cue}: ${selector}] matches nothing in slide ${id}`,
|
|
59
61
|
gestureColon: (line, id) => `script.md line ${line}: [gesture ${id}] needs a colon: [gesture: ${id}]`,
|
|
60
62
|
gesture: (line, id) => `script.md line ${line}: [gesture: ${id}] is not one of the head's gestures: ${GESTURES.join(", ")}`,
|
|
63
|
+
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)`,
|
|
64
|
+
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`,
|
|
61
65
|
presenterSize: (where, value) => `${where}: data-presenter-size "${value}" is not one of ${PRESENTER_SIZES.join(", ")}; the presenter keeps its default size`,
|
|
62
66
|
presenterStyle: (where, value) => `${where}: presenter style "${value}" is not one of ${PRESENTER_STYLES.join(", ")}`,
|
|
63
67
|
transition: (where, value) => `${where}: transition "${value}" is not one of ${TRANSITIONS.join(", ")} (zoom takes a selector: "zoom #row")`,
|
|
@@ -65,6 +69,10 @@ export const MESSAGES = {
|
|
|
65
69
|
morphPair: (id, prev) => `slide ${id}: morph needs an element with the same data-morph="<name>" on this slide and the one before it (${prev})`,
|
|
66
70
|
transitionStale: (where, script, manifest) => `${where}: script.md says transition "${script}" but the manifest has "${manifest}": run \`hyperdecks narrate\` so the player gets it`,
|
|
67
71
|
flyTwice: (ids) => `fly is for the deck's one big moment, but slides ${ids.join(", ")} all use it`,
|
|
72
|
+
// Warnings from the look topic (deck_guide look): the deck passes, but a person should look.
|
|
73
|
+
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)`,
|
|
74
|
+
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)`,
|
|
75
|
+
mixedIcons: (sets) => `the deck mixes icon sets (${sets.join(" and ")}); keep one set per deck (deck_guide topic look)`,
|
|
68
76
|
// A layout's stand-in left in: `how` is the tool (hosted) or command (CLI) that makes the real file.
|
|
69
77
|
placeholder: (id, what, el, how, img) => `slide ${id}: ${what} is still the layout's placeholder (${el}); ${how}, then put it in as ${img}`,
|
|
70
78
|
// Warnings (check --layout): the deck still passes.
|
|
@@ -526,6 +534,10 @@ const PLACEHOLDERS = [
|
|
|
526
534
|
layout: "l-product", tag: "div", cls: "shot", what: "the product screenshot", img: '<img class="shot" src="…" alt="…">',
|
|
527
535
|
how: { hosted: "capture the real page with capture_url", cli: "capture the real page with `hyperdecks screenshot <url>`" },
|
|
528
536
|
},
|
|
537
|
+
{
|
|
538
|
+
layout: "l-split", tag: "div", cls: "shot", what: "the picture", img: '<img class="shot" src="…" alt="…"> (or <video class="shot" src="…" autoplay muted loop playsinline>)',
|
|
539
|
+
how: { hosted: "capture the app at phone size with capture_url (width 390, height 844), or upload a screen recording or photo with upload_deck_asset", cli: "capture the app at phone size with `hyperdecks screenshot <url> --width 390 --height 844`, or add a screen recording or photo with `hyperdecks asset add`" },
|
|
540
|
+
},
|
|
529
541
|
{
|
|
530
542
|
layout: "l-phones", tag: "div", cls: "shot", what: "each phone screen", img: '<img class="shot" src="…" alt="…"> (or <video class="shot" src="…" autoplay muted loop playsinline>) in each card',
|
|
531
543
|
how: { hosted: "capture the app at phone size with capture_url (width 390, height 844), or upload a screen recording with upload_deck_asset", cli: "capture the app at phone size with `hyperdecks screenshot <url> --width 390 --height 844`, or add a screen recording with `hyperdecks asset add`" },
|
|
@@ -754,6 +766,97 @@ const isHostedAsset = (url) => {
|
|
|
754
766
|
}
|
|
755
767
|
};
|
|
756
768
|
|
|
769
|
+
// The look topic as warnings. A slide shows a picture when it holds an <img>
|
|
770
|
+
// (not the brand logo or an icon), <video>, <canvas>, <iframe>, a chart <svg> or an
|
|
771
|
+
// inline-style url() background (not a stylesheet's: that is usually a texture); a deck of three or more slides with fewer than a third that do
|
|
772
|
+
// reads as all type (the topic aims for every other slide; a plan with no
|
|
773
|
+
// product to show may have fewer). Icons count as one set each: inline Lucide <svg> (by stroke width),
|
|
774
|
+
// and <img> icons (class icon, or a generated icon-* asset).
|
|
775
|
+
const iconImg = (tag) => /\bicon\b/.test(attr(tag, "class") ?? "") || /\/icon-[\w-]+\.(png|webp|svg)\b/i.test(attr(tag, "src") ?? "");
|
|
776
|
+
export function lookWarnings(html) {
|
|
777
|
+
const all = slides(html);
|
|
778
|
+
const out = [];
|
|
779
|
+
const picture = (body) =>
|
|
780
|
+
/<(video|canvas|iframe)\b/i.test(body) ||
|
|
781
|
+
/<svg\b[^>]*class=["'][^"']*\bchart\b/i.test(body) ||
|
|
782
|
+
[...body.matchAll(/\bstyle=("[^"]*"|'[^']*')/gi)].some(([, v]) => /\burl\(/i.test(v)) ||
|
|
783
|
+
[...body.matchAll(/<img\b[^>]*>/gi)].some(([tag]) => !/\bbrand-logo\b/.test(attr(tag, "class") ?? "") && !iconImg(tag));
|
|
784
|
+
const shown = all.filter((s) => picture(uncommented(s.body)) || /\burl\(/i.test(attr(s.tag, "style") ?? "")).length;
|
|
785
|
+
if (all.length >= 3 && shown * 3 < all.length) out.push(MESSAGES.fewPictures(shown, all.length));
|
|
786
|
+
const body = uncommented(all.map((s) => s.body).join("\n"));
|
|
787
|
+
const sets = new Set();
|
|
788
|
+
for (const [tag] of body.matchAll(/<svg\b[^>]*>/gi)) {
|
|
789
|
+
if (/\blucide/.test(attr(tag, "class") ?? "")) sets.add(`Lucide at stroke-width ${attr(tag, "stroke-width") ?? "2"}`);
|
|
790
|
+
}
|
|
791
|
+
for (const [tag] of body.matchAll(/<img\b[^>]*>/gi)) {
|
|
792
|
+
if (iconImg(tag)) sets.add("image icons");
|
|
793
|
+
}
|
|
794
|
+
if (sets.size > 1) out.push(MESSAGES.mixedIcons([...sets].sort()));
|
|
795
|
+
return out;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// A wide right padding or margin (PRESENTER_GUTTER px or more, and wider than
|
|
799
|
+
// the left: a centred column is not a gutter) is a gutter held for the
|
|
800
|
+
// presenter outside its .narration-slot: without a presenter
|
|
801
|
+
// (:root.hd-no-presenter) it stays empty. Read from inline styles on a slide
|
|
802
|
+
// and its elements, and from the deck's <style> rules (by selector). Skipped:
|
|
803
|
+
// rules in a phone media query (max-width, no min-width), in @keyframes and
|
|
804
|
+
// the like, on the slot itself, the no-presenter rules, and a selector whose
|
|
805
|
+
// gutter a :root.hd-no-presenter rule overrides (the variant the guide asks for).
|
|
806
|
+
// One linear pass over the text: a malformed paste must not stall check.
|
|
807
|
+
export const PRESENTER_GUTTER = 240;
|
|
808
|
+
const sides = (prop, value) => {
|
|
809
|
+
const px = (v) => { const m = /^(\d+(?:\.\d+)?)(px|rem|em)$/i.exec(v ?? ""); return m ? Number(m[1]) * (m[2].toLowerCase() === "px" ? 1 : 16) : 0; };
|
|
810
|
+
const parts = value.trim().split(/\s+/);
|
|
811
|
+
if (/-(right|inline-end)$/i.test(prop)) return { right: px(parts[0]), left: 0 };
|
|
812
|
+
return { right: px(parts[1] ?? parts[0]), left: px(parts[3] ?? parts[1] ?? parts[0]) };
|
|
813
|
+
};
|
|
814
|
+
const gutters = (decls) => decls.split(";").map((d) => /^\s*((?:padding|margin)(?:-right|-inline-end)?)\s*:\s*([^!]+)/i.exec(d)).filter(Boolean)
|
|
815
|
+
.filter(([, prop, value]) => { const { right, left } = sides(prop, value); return right >= PRESENTER_GUTTER && right > left; })
|
|
816
|
+
.map(([, prop, value]) => `${prop.toLowerCase()}: ${value.trim()}`);
|
|
817
|
+
// Each style rule of a stylesheet: { selector, decls, at } where `at` lists the
|
|
818
|
+
// @-rule preludes around it. Comments drop out; braces nest.
|
|
819
|
+
function cssRules(css) {
|
|
820
|
+
const out = [], stack = [];
|
|
821
|
+
let start = 0, i = 0;
|
|
822
|
+
let text = "";
|
|
823
|
+
while (i < css.length) {
|
|
824
|
+
if (css.startsWith("/*", i)) { const end = css.indexOf("*/", i + 2); text += css.slice(start, i); i = start = end === -1 ? css.length : end + 2; continue; }
|
|
825
|
+
const ch = css[i];
|
|
826
|
+
if (ch === "{" || ch === "}") {
|
|
827
|
+
text += css.slice(start, i);
|
|
828
|
+
if (ch === "{") stack.push(text.trim());
|
|
829
|
+
else {
|
|
830
|
+
const head = stack.pop();
|
|
831
|
+
if (head !== undefined && !head.startsWith("@")) out.push({ selector: head, decls: text, at: stack.filter((h) => h.startsWith("@")) });
|
|
832
|
+
}
|
|
833
|
+
text = "";
|
|
834
|
+
start = i + 1;
|
|
835
|
+
}
|
|
836
|
+
i++;
|
|
837
|
+
}
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
840
|
+
const phoneOnly = (at) => at.some((h) => /^@media\b/i.test(h) && /max-width/i.test(h) && !/min-width/i.test(h));
|
|
841
|
+
const NO_PRESENTER = /^:root\.hd-no-presenter\s+/;
|
|
842
|
+
export function presenterGutterWarnings(html) {
|
|
843
|
+
const out = [];
|
|
844
|
+
for (const [i, s] of slides(html).entries()) {
|
|
845
|
+
const where = `slide ${s.id || i + 1}`;
|
|
846
|
+
for (const [, value] of `${s.tag}\n${uncommented(s.body)}`.matchAll(/\sstyle\s*=\s*("[^"]*"|'[^']*')/gi)) {
|
|
847
|
+
for (const decl of gutters(value.slice(1, -1))) out.push(MESSAGES.presenterGutter(where, decl));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const rules = [...uncommented(html).matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].flatMap(([, css]) => cssRules(css));
|
|
851
|
+
const overridden = new Set(rules.filter((r) => NO_PRESENTER.test(r.selector) && /(padding|margin)/i.test(r.decls)).map((r) => r.selector.replace(NO_PRESENTER, "")));
|
|
852
|
+
for (const r of rules) {
|
|
853
|
+
if (r.at.some((h) => !/^@(media|supports|layer|container)\b/i.test(h)) || phoneOnly(r.at)) continue;
|
|
854
|
+
if (NO_PRESENTER.test(r.selector) || /\.narration-slot\b/.test(r.selector) || overridden.has(r.selector)) continue;
|
|
855
|
+
for (const decl of gutters(r.decls)) out.push(MESSAGES.presenterGutter(`rule ${r.selector}`, decl));
|
|
856
|
+
}
|
|
857
|
+
return out;
|
|
858
|
+
}
|
|
859
|
+
|
|
757
860
|
// Every rule on the deck's text. `exists(ref)` answers whether a local reference
|
|
758
861
|
// resolves (true, false or "outside"); `hosted: true` means there is no folder,
|
|
759
862
|
// so every local reference is an error naming upload_deck_asset, a hotlinked
|
|
@@ -773,6 +876,7 @@ export async function checkSources({ html, script = "" }, opts = {}, { fetch: fe
|
|
|
773
876
|
const errors = htmlErrors(html, { exists, forbid, script, hosted: !!opts.hosted, strict });
|
|
774
877
|
const warnings = [];
|
|
775
878
|
if (!notesRequired(html, { strict })) for (const id of slidesWithoutNotes(html)) warnings.push(MESSAGES.noteSoft(id));
|
|
879
|
+
warnings.push(...lookWarnings(html), ...presenterGutterWarnings(html));
|
|
776
880
|
// Without a folder (hosted, or a caller that passes no reader) only the inline scripts are read.
|
|
777
881
|
const walked = deckFiles(html, opts.hosted ? {} : { pathExists: opts.pathExists, read: opts.read, isDir: opts.isDir });
|
|
778
882
|
for (const m of walked.missing) errors.push(MESSAGES.scriptRef(m.ref, m.from));
|
|
@@ -836,6 +940,9 @@ export async function checkSources({ html, script = "" }, opts = {}, { fetch: fe
|
|
|
836
940
|
}
|
|
837
941
|
}
|
|
838
942
|
warnings.push(...transitionWarnings(html, script, manifest), ...presenterSizeWarnings(html));
|
|
943
|
+
// A narrated deck's tag that shows no face and says otherwise (a silent deck has no presenter).
|
|
944
|
+
const tagIssue = silent ? null : presenterTagIssue(html);
|
|
945
|
+
if (tagIssue) warnings.push(tagIssue.kind === "kept" ? MESSAGES.presenterKept(tagIssue.presenter) : MESSAGES.presenterUnfilled(tagIssue.presenter));
|
|
839
946
|
for (const t of walked.templates) warnings.push(MESSAGES.scriptTemplate(t.from, t.samples));
|
|
840
947
|
for (const c of walked.computed) warnings.push(MESSAGES.scriptComputed(c.from, c.calls));
|
|
841
948
|
if (opts.hosted) {
|
package/src/commands.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
|
+
backupLinkUrl,
|
|
4
5
|
installHyperdecksSkill,
|
|
5
6
|
login,
|
|
6
7
|
logout,
|
|
7
8
|
openBrowser,
|
|
9
|
+
linkBase,
|
|
8
10
|
parse,
|
|
9
11
|
target,
|
|
10
12
|
VERSION,
|
|
@@ -60,7 +62,7 @@ async function deckStats(pos, opts) {
|
|
|
60
62
|
console.log(` unique viewers: ${s.viewers}`);
|
|
61
63
|
console.log(` avg engaged: ${fmtMs(s.avgMs)}`);
|
|
62
64
|
console.log(` total engaged: ${fmtMs(s.totalMs)}`);
|
|
63
|
-
if (s.view !== undefined) console.log(` plays as: ${s.view ?? "as its HTML says"}${s.viewToggle === false ? " (viewer toggle off)" : ""}${s.usesPlayer === false ? " (won't apply: the deck doesn't load player/v1)" : ""}`);
|
|
65
|
+
if (s.view !== undefined) console.log(` plays as: ${s.view ?? "as its HTML says"}${s.viewToggle === false ? " (viewer toggle off)" : ""}${s.pace ? `, pace ${s.pace}` : ""}${s.usesPlayer === false ? " (won't apply: the deck doesn't load player/v1)" : ""}`);
|
|
64
66
|
if (s.recent?.length) {
|
|
65
67
|
console.log(" recent:");
|
|
66
68
|
for (const r of s.recent) {
|
|
@@ -85,10 +87,6 @@ async function deckRemove(pos, opts) {
|
|
|
85
87
|
console.log(`OK removed deck ${slug}`);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
function shareBase(opts) {
|
|
89
|
-
return target(opts).shareBase;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
90
|
async function shareNew(pos, opts) {
|
|
93
91
|
const [slug, recipient] = pos;
|
|
94
92
|
if (!slug || !recipient) throw new Error("usage: share new <slug> <recipient> [--email e] [--note n] [--require-email] [--expires iso] [--limit n] [--qa] [--no-notify] [--allow-forward|--no-forward]");
|
|
@@ -105,9 +103,13 @@ async function shareNew(pos, opts) {
|
|
|
105
103
|
expires: opts.expires,
|
|
106
104
|
limit: opts.limit,
|
|
107
105
|
});
|
|
108
|
-
const url = `${
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
const url = `${await linkBase(opts)}/v/${token}`;
|
|
107
|
+
const backupUrl = backupLinkUrl(url);
|
|
108
|
+
if (opts.json) console.log(JSON.stringify({ token, url, backupUrl }, null, 2));
|
|
109
|
+
else {
|
|
110
|
+
console.log(`OK share for "${recipient}" -> ${url}`);
|
|
111
|
+
if (backupUrl) console.log(` backup link (same link, other domain, if their network blocks it): ${backupUrl}`);
|
|
112
|
+
}
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
async function shareList(pos, opts) {
|
|
@@ -156,23 +158,28 @@ async function deckForward(pos, opts) {
|
|
|
156
158
|
console.log(`OK forwarding ${mode} for deck ${slug} (links with their own setting keep it)`);
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
const VIEW_USAGE = "usage: deck view <slug> [slides|scroll|default] [--toggle on|off]";
|
|
161
|
+
const VIEW_USAGE = "usage: deck view <slug> [slides|scroll|default] [--toggle on|off] [--pace auto|slide|step|default]";
|
|
160
162
|
const VIEWS = { slides: "slides", scroll: "scroll", default: null };
|
|
163
|
+
const PACES = { auto: "auto", slide: "slide", step: "step", default: null };
|
|
161
164
|
async function deckView(pos, opts) {
|
|
162
165
|
const [slug, mode] = pos;
|
|
163
166
|
const toggle = opts.toggle;
|
|
164
|
-
|
|
167
|
+
const pace = opts.pace;
|
|
168
|
+
if (!slug || (mode === undefined && !Object.hasOwn(opts, "toggle") && !Object.hasOwn(opts, "pace"))) throw new Error(VIEW_USAGE);
|
|
165
169
|
if (mode !== undefined && !Object.hasOwn(VIEWS, mode)) throw new Error(VIEW_USAGE);
|
|
166
170
|
// A trailing --toggle with no value is a mistake, not "leave it".
|
|
167
171
|
if (Object.hasOwn(opts, "toggle") && toggle !== "on" && toggle !== "off") throw new Error(VIEW_USAGE);
|
|
172
|
+
if (Object.hasOwn(opts, "pace") && !Object.hasOwn(PACES, pace)) throw new Error(VIEW_USAGE);
|
|
168
173
|
const out = await ops.setDeckView(opts, slug, {
|
|
169
174
|
view: mode === undefined ? undefined : VIEWS[mode],
|
|
170
175
|
viewToggle: toggle === undefined ? undefined : toggle === "on",
|
|
176
|
+
pace: pace === undefined ? undefined : PACES[pace],
|
|
171
177
|
});
|
|
172
178
|
if (opts.json) return void console.log(JSON.stringify(out, null, 2));
|
|
173
179
|
const said = [];
|
|
174
180
|
if (mode !== undefined) said.push(mode === "default" ? "opens as its HTML says" : `opens as ${mode}`);
|
|
175
181
|
if (toggle !== undefined) said.push(`viewer toggle ${toggle}`);
|
|
182
|
+
if (pace !== undefined) said.push(pace === "default" ? "paces as its HTML says" : `pace ${pace}`);
|
|
176
183
|
if (out.applies === false) {
|
|
177
184
|
console.log(`OK deck ${slug} ${said.join(", ")} (saved)`);
|
|
178
185
|
console.warn(`warning: ${out.warning}`);
|
|
@@ -181,10 +188,14 @@ async function deckView(pos, opts) {
|
|
|
181
188
|
console.log(`OK deck ${slug} ${said.join(", ")} (every link, on its next load)`);
|
|
182
189
|
}
|
|
183
190
|
|
|
184
|
-
function shareUrl(pos, opts) {
|
|
191
|
+
async function shareUrl(pos, opts) {
|
|
185
192
|
const [token] = pos;
|
|
186
|
-
if (!token) throw new Error("usage: share url <token>");
|
|
187
|
-
|
|
193
|
+
if (!token) throw new Error("usage: share url <token> [--backup]");
|
|
194
|
+
const url = `${await linkBase(opts)}/v/${token}`;
|
|
195
|
+
console.log(url);
|
|
196
|
+
// Second line: the same link on the other domain (stdout stays one URL per line).
|
|
197
|
+
const backupUrl = backupLinkUrl(url);
|
|
198
|
+
if (backupUrl && opts.backup) console.log(backupUrl);
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
async function recipientArchive(pos, opts, archived) {
|
|
@@ -336,7 +347,7 @@ commands:
|
|
|
336
347
|
deck list|stats|open|rm|forward|view
|
|
337
348
|
deck forward on|off <slug>
|
|
338
349
|
whether viewers can forward the deck's links (on by default; a link's own setting wins)
|
|
339
|
-
deck view <slug> [slides|scroll|default] [--toggle on|off]
|
|
350
|
+
deck view <slug> [slides|scroll|default] [--toggle on|off] [--pace auto|slide|step|default]
|
|
340
351
|
how the deck opens in the player (default: as its HTML says, data-view or slides) and
|
|
341
352
|
whether viewers get the slides/scroll toggle (on by default). Every link, next load; no re-release.
|
|
342
353
|
deck new <slug> "<title>" --narrated [--layouts hero,product,number,closer] [--presenter-style stage|bubble|glass|corner] [--no-brand] [--dir <path>]
|
|
@@ -376,8 +387,9 @@ commands:
|
|
|
376
387
|
engagement <recipient|token> [--deck <slug>] [--json]
|
|
377
388
|
what one recipient watched per deck: narration heard, replays, drop-off, visits
|
|
378
389
|
gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--transparent] [--image <url>] [--size <s>]
|
|
379
|
-
[--quality <q>] [--count <n>] [--seed <n>] [--slug <deck>] [--download <file>]
|
|
380
|
-
default model gpt-image-2.5; --seed applies to nano-banana-2.1
|
|
390
|
+
[--quality <q>] [--count <n>] [--seed <n>] [--brand] [--slug <deck>] [--download <file>]
|
|
391
|
+
default model gpt-image-2.5; --seed applies to nano-banana-2.1;
|
|
392
|
+
--brand adds your brand's colours and "no text, no logos" to the prompt
|
|
381
393
|
image-job <jobId> [--json]
|
|
382
394
|
gen-video "<prompt>" [--model h3-max|kling-v3] [--image <url>] [--seconds 5] [--aspect 16:9|9:16] [--loop] [--transparent] [--slug <deck>] [--name <name>] [--download <file>]
|
|
383
395
|
a 3–10s clip via fal (default MiniMax H3 Max); --image animates that still and keeps its aspect
|
|
@@ -390,8 +402,9 @@ commands:
|
|
|
390
402
|
a branded SVG: { "kind": "bar|line|donut|number", "labels", "series", "values", "value", "palette" }
|
|
391
403
|
icons search <word...> [--limit 8] [--stroke-width 2] [--json]
|
|
392
404
|
real icons from the Lucide library as inline SVG in currentColor (free, offline)
|
|
393
|
-
icons <subject...> --style "<style>" [--palette "#hex,#hex"] [--slug <deck>] [--name <prefix>]
|
|
394
|
-
a style-consistent set of generated (illustrated) icons, one per subject (max 12)
|
|
405
|
+
icons <subject...> --preset line|duotone|sticker|clay | --style "<style>" [--palette "#hex,#hex"] [--slug <deck>] [--name <prefix>]
|
|
406
|
+
a style-consistent set of generated (illustrated) icons, one per subject (max 12);
|
|
407
|
+
palette defaults to your brand's colours
|
|
395
408
|
brand [show] your saved brand (colours, fonts, logo, design.md) that every new deck starts from
|
|
396
409
|
brand set [--accent "#hex"] [--ink|--paper|--accent-2|--card "#hex"] [--fonts <pairing>]
|
|
397
410
|
[--display|--body|--mono "<Google Fonts family>"] [--logo <url|file>] [--no-logo]
|
|
@@ -439,7 +452,7 @@ commands:
|
|
|
439
452
|
|
|
440
453
|
narrate <deck-dir> [--slide <id>] [--voice <name|id>] [--slug <slug>] [--dry-run] [--json]
|
|
441
454
|
script.md → voiced clips, timelines and a manifest, rendered server-side; only changed slides re-render
|
|
442
|
-
voice list [--json] your voices and their status
|
|
455
|
+
voice list [--json] your voices and their status, then the built-in voices anyone can narrate with
|
|
443
456
|
voice clone <file...> --name <name>
|
|
444
457
|
clone your voice from 1-3 mp3/m4a/wav recordings (30 s to 3 min in total, 10 MB each);
|
|
445
458
|
asks you to type "I consent" before anything is uploaded
|
|
@@ -477,7 +490,7 @@ async function genImage(pos, opts) {
|
|
|
477
490
|
const prompt = pos.join(" ").trim() || opts.prompt;
|
|
478
491
|
if (!prompt) {
|
|
479
492
|
throw new Error(
|
|
480
|
-
'usage: gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--image <url>] [--transparent] [--size <s>] [--quality <q>] [--count <n>] [--format png|jpeg|webp] [--seed <n>] [--slug <deck>] [--name <name>] [--download <file>]',
|
|
493
|
+
'usage: gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--image <url>] [--transparent] [--size <s>] [--quality <q>] [--count <n>] [--format png|jpeg|webp] [--seed <n>] [--brand] [--slug <deck>] [--name <name>] [--download <file>]',
|
|
481
494
|
);
|
|
482
495
|
}
|
|
483
496
|
let out = await ops.generateImage(opts, {
|
|
@@ -490,6 +503,7 @@ async function genImage(pos, opts) {
|
|
|
490
503
|
format: opts.format,
|
|
491
504
|
model: opts.model,
|
|
492
505
|
seed: opts.seed,
|
|
506
|
+
brand: opts.brand,
|
|
493
507
|
slug: opts.slug,
|
|
494
508
|
name: opts.name,
|
|
495
509
|
});
|
|
@@ -628,12 +642,13 @@ async function chart(pos, opts) {
|
|
|
628
642
|
}
|
|
629
643
|
|
|
630
644
|
async function icons(pos, opts) {
|
|
631
|
-
if (!pos.length || !opts.style) {
|
|
632
|
-
throw new Error('usage: icons <subject...> --style "<style>" [--palette "#hex,#hex"] [--model nano-banana-2.1|gpt-image-2.5|gpt-image-2] [--slug <deck>] [--name <prefix>]');
|
|
645
|
+
if (!pos.length || (!opts.style && !opts.preset)) {
|
|
646
|
+
throw new Error('usage: icons <subject...> --preset line|duotone|sticker|clay | --style "<style>" [--palette "#hex,#hex"] [--model nano-banana-2.1|gpt-image-2.5|gpt-image-2] [--slug <deck>] [--name <prefix>]');
|
|
633
647
|
}
|
|
634
648
|
const queued = await ops.generateIconSet(opts, {
|
|
635
649
|
subjects: pos,
|
|
636
650
|
style: opts.style,
|
|
651
|
+
preset: opts.preset,
|
|
637
652
|
palette: opts.palette ? String(opts.palette).split(",").map((c) => c.trim()).filter(Boolean) : undefined,
|
|
638
653
|
model: opts.model,
|
|
639
654
|
slug: opts.slug,
|
|
@@ -847,10 +862,12 @@ function commandTable(pos, rest, opts) {
|
|
|
847
862
|
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
848
863
|
},
|
|
849
864
|
"voice list": async () => {
|
|
850
|
-
const voices = await ops.
|
|
865
|
+
const voices = await ops.listAllVoices(opts);
|
|
851
866
|
if (opts.json) return void console.log(JSON.stringify(voices, null, 2));
|
|
852
|
-
|
|
853
|
-
|
|
867
|
+
const own = voices.filter((x) => !x.builtin);
|
|
868
|
+
for (const x of own) console.log(`${x.name}\t${x.status}\t${x.handle ?? "-"}\t${x.id}${x.error ? `\t${x.error}` : ""}`);
|
|
869
|
+
if (!own.length) console.log("(no voices of your own; voice clone <file...> --name <name> makes one)");
|
|
870
|
+
for (const x of voices.filter((x) => x.builtin)) console.log(`${x.name}\tbuilt in\t${x.description}\t${x.id}`);
|
|
854
871
|
},
|
|
855
872
|
"voice clone": async () => {
|
|
856
873
|
const { voiceClone } = await import("./voice.mjs");
|
package/src/files.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const FILES = {
|
|
|
17
17
|
"templates/layouts/number.html": () => new URL("../templates/layouts/number.html", import.meta.url),
|
|
18
18
|
"templates/layouts/product.html": () => new URL("../templates/layouts/product.html", import.meta.url),
|
|
19
19
|
"templates/layouts/phones.html": () => new URL("../templates/layouts/phones.html", import.meta.url),
|
|
20
|
+
"templates/layouts/split.html": () => new URL("../templates/layouts/split.html", import.meta.url),
|
|
20
21
|
"templates/layouts/quote.html": () => new URL("../templates/layouts/quote.html", import.meta.url),
|
|
21
22
|
"templates/layouts/team.html": () => new URL("../templates/layouts/team.html", import.meta.url),
|
|
22
23
|
"templates/layouts/timeline.html": () => new URL("../templates/layouts/timeline.html", import.meta.url),
|
package/src/guide.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { packageText } from "./files.mjs";
|
|
|
8
8
|
import { LAYOUTS, readLayout } from "./layouts.mjs";
|
|
9
9
|
import { fontPairingsMarkdown } from "./brand.mjs";
|
|
10
10
|
|
|
11
|
-
export const GUIDE_TOPICS = ["workflow", "format", "script", "scripts", "design", "layouts", "presenter", "transitions", "check"];
|
|
11
|
+
export const GUIDE_TOPICS = ["workflow", "format", "script", "scripts", "design", "look", "layouts", "presenter", "transitions", "check"];
|
|
12
12
|
const CLI_NOTE = "_Written for the CLI. On the hosted MCP there is no folder: use the tool names in `deck_guide` (workflow), hosted URLs for every file, and write_deck_source for edits._\n\n";
|
|
13
13
|
|
|
14
14
|
// One <!-- topic: name --> section of SKILL.md: its heading and body, up to the
|
|
@@ -29,7 +29,7 @@ export function skillSection(skill, topic) {
|
|
|
29
29
|
function layoutsTopic() {
|
|
30
30
|
const rows = LAYOUTS.map((name) => {
|
|
31
31
|
const { meta } = readLayout(name);
|
|
32
|
-
return [`### ${name}`, meta.summary, meta.use && `Use: ${meta.use}`, meta.slot && `Slots: ${meta.slot}`, meta.cues && `Cues: ${meta.cues}`].filter(Boolean).join("\n");
|
|
32
|
+
return [`### ${name}`, meta.summary, meta.use && `Use: ${meta.use}`, meta.slot && `Slots: ${meta.slot}`, meta.nopresenter && `Without a presenter: ${meta.nopresenter}`, meta.cues && `Cues: ${meta.cues}`].filter(Boolean).join("\n");
|
|
33
33
|
});
|
|
34
34
|
return `# Layouts\n\nPass any of these, in order, as new_deck's \`layouts\` (repeat one for more of the same: number, number gives slides number and number-2).\n\n${rows.join("\n\n")}`;
|
|
35
35
|
}
|