mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
docs: in-app "Read the docs" modal + per-mode help pages
Adds a 📖 Docs link to the sidebar footer (next to GitHub + artstr.org) that opens a Read-the-Docs-style two-pane modal: topic sidebar on the left, rendered markdown on the right. Content (src/help/*.md, 11 files): - index.md — overview + nav - template.md — Template tab explainer - designer.md — Designer tab explainer - cover.md — Case cover wrap - disc.md — Disc-label sheet - jewel.md — Jewel-case insert - customart.md — Custom art - slide.md — Slide - deck.md — Slide deck - pixelart.md — Pixel art - book.md — Book Designer (links out to the deeper BOOK_DESIGNER_TUTORIAL.md + spec on GitHub) Implementation: - _DOCS_TOPICS registry maps slug → { title, group, file }. Sidebar groups headings under "Start here", "App tabs", "Template-mode layouts". - loadDocsTopic(slug) fetches `help/<slug>.md` (force-cache), renders via the already-vendored marked, caches the rendered HTML for session-instant re-visits, and updates the URL hash via history.replaceState so deep links work. - #/help/<slug> hash route auto-opens the modal at that topic on page load + on hashchange. - Esc closes; backdrop click closes; internal `#/help/foo` links in page content load via the dispatcher (no page bounce); external http(s) links get target="_blank" + noopener. Styling: - Inherits the rest of the modal aesthetic (var(--line), var(--accent), system font for chrome, Georgia serif for body text). Dark-mode-aware backdrops on code / blockquote / table blocks. - Two-pane layout collapses to a narrower sidebar on <720px viewports. Files live in src/help/ (not docs/) because Vercel's outputDirectory is "src" — the production site only serves files under src/. The canonical deeper docs (BOOK_DESIGNER_TUTORIAL, FEATURE specs) stay in docs/ and are linked-out from the in-app pages. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
f50d92043c
commit
23a16c8e89
12 files changed
+897
No files matched your search
@@ -0,0 +1,111 @@
|
||||
# Book
|
||||
|
||||
Multi-page reflowable book with cover, designed pages, reflow
|
||||
chapters, masters, PDF export, and full-screen Reader Mode.
|
||||
|
||||
> Hands-on walkthrough: see [`BOOK_DESIGNER_TUTORIAL.md`](https://github.com/PR0M3TH3AN/Artstr-Studio/blob/main/docs/BOOK_DESIGNER_TUTORIAL.md).
|
||||
> Spec: see [`BOOK_DESIGNER_FEATURE.md`](https://github.com/PR0M3TH3AN/Artstr-Studio/blob/main/docs/BOOK_DESIGNER_FEATURE.md).
|
||||
|
||||
## Starting
|
||||
|
||||
When you enter Book mode with no pages, four starter cards appear
|
||||
(US Trade / A5 / A4 / Square). Clicking one seeds a default master
|
||||
with a text-frame, a starter chapter, and a Title page.
|
||||
|
||||
A 5th card appears when you have a slide deck loaded — **Convert
|
||||
deck to book** turns each slide into a designed page (letterbox-fit
|
||||
into the trim).
|
||||
|
||||
## The two main views
|
||||
|
||||
- **Pages overview** — every manifest entry as a tile (designed
|
||||
pages + reflow chapters). Drag to reorder. Hover for per-tile
|
||||
actions (proof PDF, duplicate, delete, master picker).
|
||||
- **Cover** — wraparound spread editor. Front / spine / back panels
|
||||
selected via the layer-target dropdown.
|
||||
|
||||
Plus two **per-entry** views:
|
||||
|
||||
- **Designed page editor** — same flat-canvas tools as Custom Art,
|
||||
scoped to one `book-page:<id>` target. Master layers render
|
||||
underneath at 55% opacity.
|
||||
- **Chapter editor** — markdown-on-left, paginated preview on right.
|
||||
CodeMirror with markdown syntax + smart Enter for list
|
||||
continuation.
|
||||
|
||||
## Masters
|
||||
|
||||
Reusable underlays. A master with a **text-frame** layer becomes a
|
||||
valid reflow target — the chapter's prose flows into that frame.
|
||||
Header / footer text on the master can use **tokens** that resolve
|
||||
per page:
|
||||
|
||||
- `{page-number}`, `{page-count}`, `{physical-page-count}`
|
||||
- `{book-title}`, `{book-author}`
|
||||
- `{chapter-title}`, `{chapter-number}`
|
||||
|
||||
## Markdown features in chapters
|
||||
|
||||
- GFM headings / lists / blockquotes / code / tables / task lists.
|
||||
- Nested lists (up to depth 6).
|
||||
- `[^N]` footnote refs + `[^N]: definition` for footnote bodies
|
||||
(renders as Unicode superscript + chapter-end Footnotes section).
|
||||
- `^[term]` invisible index markers.
|
||||
- `[link text](#chapter-3)` cross-references — auto-appended with
|
||||
` (p. N)` at render time.
|
||||
- Magic tokens on their own line:
|
||||
- `{toc}` — auto-generated table of contents with page numbers.
|
||||
- `{index}` — alphabetical index of every `^[term]` marker.
|
||||
|
||||
## Publishing
|
||||
|
||||
Sidecar-architecture: each surface publishes as its own Nostr event.
|
||||
|
||||
| Surface | Event | d-tag |
|
||||
|---------|-------|-------|
|
||||
| Designed page | kind-30078 `casewrap-page` (role: 'interior') | `book-page:<entry-id>` |
|
||||
| Master | kind-30078 `casewrap-page` (role: 'master') | `book-master:<master-id>` |
|
||||
| Cover | kind-30078 `casewrap-page` (role: 'cover-spread') | `book-cover:<title-slug>` |
|
||||
| Chapter (public) | kind-30023 NIP-23 article | `chapter:<title-slug>` |
|
||||
| Chapter (private) | kind-30078 encrypted envelope | `book-chapter:<entry-id>` |
|
||||
| Manifest | kind-30078 `casewrap-book` | `book:<title-slug>` |
|
||||
|
||||
The manifest references its sidecars via `{kind, pubkey, dTag}`
|
||||
coords + emits matching `link` tags so external clients can
|
||||
resolve them.
|
||||
|
||||
Publish each surface first (via the per-editor Publish buttons),
|
||||
then the manifest (top-bar Publish).
|
||||
|
||||
## Private path
|
||||
|
||||
Toggle the sidebar **🔒 Private** chip. Every subsequent publish
|
||||
(manifest + sidecars) goes through the encrypted envelope path
|
||||
(AES-256-GCM, key wrapped to your own pubkey via NIP-44 / NIP-04).
|
||||
Re-opens decrypt automatically.
|
||||
|
||||
## Reader Mode + embed
|
||||
|
||||
- **▶ Read** opens a full-screen page-flip viewer with TOC sidebar,
|
||||
keyboard nav, click-to-flip, and prefetch.
|
||||
- The published manifest's `naddr` works in the embed iframe via
|
||||
`?book=<naddr>` (cut-down Reader Mode without TOC).
|
||||
|
||||
## PDF export
|
||||
|
||||
The **Export PDF…** modal exposes:
|
||||
|
||||
- Page range (All / from–to).
|
||||
- Include cover spread.
|
||||
- Include bleed margin.
|
||||
- Crop marks (L-marks at trim corners).
|
||||
- Single vs split output (interior + cover separate).
|
||||
- Filename pattern.
|
||||
|
||||
Reflowed prose using Times / Helvetica / Courier-family fonts emits
|
||||
as real PDF text (vector, selectable, searchable). Other fonts fall
|
||||
back to vector raster + warning.
|
||||
|
||||
**📄 Proof PDF** in any editor toolbar exports just that single
|
||||
page / chapter — useful for a printer paper-and-colour proof
|
||||
before committing to the full book.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Case cover wrap
|
||||
|
||||
A printable wraparound for DVD, Blu-ray, softcover, or any
|
||||
back + spine + front case format.
|
||||
|
||||
## Geometry
|
||||
|
||||
- **Trim** — outer cut line.
|
||||
- **Bleed** — extends past the trim on each outer edge so the
|
||||
printer's cut tolerance doesn't reveal white.
|
||||
- **Spine** — middle strip whose width depends on the case style
|
||||
(DVD = 14 mm, Blu-ray = 11 mm, etc).
|
||||
- **Safe area** — interior margin you should keep text + key art
|
||||
inside.
|
||||
|
||||
Pick a preset in the sidebar (DVD / Blu-ray / U-Card / Custom) or
|
||||
enter your own panel width + trim height + spine + bleed under the
|
||||
**Custom** preset.
|
||||
|
||||
## Artwork modes
|
||||
|
||||
- **Combined wraparound** — one canvas spanning the full
|
||||
back+spine+front. Best for continuous artwork.
|
||||
- **Slots** — separate back / spine / front layers. Best for posters
|
||||
+ spine text + back-cover layout.
|
||||
|
||||
## Slots panel
|
||||
|
||||
When you're in slot mode, each panel exposes:
|
||||
|
||||
- A drop-zone for an image or a published cover design from Nostr.
|
||||
- Fit (`cover` / `contain` / `fill` / `none`), zoom, position offsets,
|
||||
rotation.
|
||||
- Show / hide for testing different layouts.
|
||||
|
||||
## Spine text
|
||||
|
||||
The **Spine label** input adds a vertical text caption along the
|
||||
spine. Family + weight + colour controls in the right options pane.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- The **show guides** checkbox toggles trim + bleed lines on the
|
||||
canvas; **show crops** adds the cut marks. Both stay off in the
|
||||
exported file unless you also enable the matching **print** options.
|
||||
- Switching presets resets panel widths to that preset's defaults —
|
||||
if you've customised, save first.
|
||||
- Publishing posts a `casewrap-cover` event with the same flat
|
||||
payload anyone can fork.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Custom art
|
||||
|
||||
A freeform flat canvas you can size to anything from a Twitter banner
|
||||
(1500×500) to a poster. The most general-purpose Artstr surface — if
|
||||
your design doesn't fit one of the other modes, start here.
|
||||
|
||||
## Sizing the canvas
|
||||
|
||||
In the sidebar:
|
||||
|
||||
- **Preset** — common social / print sizes.
|
||||
- **W × H (px)** — manual override.
|
||||
- **Background** — solid colour or `#ffffff` for transparent-on-export.
|
||||
|
||||
The canvas is rendered at 96 DPI; the PDF/PNG export upscales to your
|
||||
chosen DPI on the way out.
|
||||
|
||||
## Layer types
|
||||
|
||||
Custom Art accepts every layer kind in the app:
|
||||
|
||||
- **Text** — full HTML rich text with bold / italic / underline /
|
||||
strikethrough / link, inline-marks-aware.
|
||||
- **Image** — drag-drop or pick; fit (cover / contain / fill / none),
|
||||
rotate, opacity, optional clip.
|
||||
- **Shape** — rectangle / ellipse / polygon / star + Pen-tool path
|
||||
(full SVG-path support with anchors + handles).
|
||||
- **Color block** — solid fill.
|
||||
- **QR code** — input a string, choose ECC + style.
|
||||
- **Pixel art** — sub-grid pixel art layer, edited via the Pixel
|
||||
editor.
|
||||
- **Linked design** — embeds another author's published Artstr design
|
||||
as a live-resolving layer (Linked Designs feature). Right-click the
|
||||
layer to expand into the linked event's full layer list and edit
|
||||
pieces inline.
|
||||
|
||||
## Tools
|
||||
|
||||
The vertical tool palette has:
|
||||
- **Select (V)** — move / scale / rotate whole layers.
|
||||
- **Direct Selection (A)** — edit anchors + handles on a path.
|
||||
- **Pen (P)** — Illustrator-parity pen tool (auto-add/delete anchors,
|
||||
spacebar reposition, alt-drag asymmetric handles, Cmd+J join,
|
||||
Cmd+Alt+J average).
|
||||
- **Eyedropper (I)** — uses `window.EyeDropper` to sample any colour
|
||||
on screen.
|
||||
- **Pathfinder** — Unite / Subtract / Intersect / Exclude on selected
|
||||
vector shapes.
|
||||
- **Align / Distribute** — left/center/right + top/middle/bottom +
|
||||
even-distribute.
|
||||
- **Free Transform** — handles around the selection bbox for scale +
|
||||
rotate.
|
||||
|
||||
## Publish
|
||||
|
||||
Posts a `casewrap-customart` event. Public, private (encrypted), or
|
||||
premium (zap-gated) — selected in the publish-confirm modal.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Slide deck
|
||||
|
||||
An ordered list of slides published as a single event. Every slide's
|
||||
full layer data is embedded inline — Presenter Mode + previews never
|
||||
depend on additional relay fetches.
|
||||
|
||||
## Sorter view
|
||||
|
||||
The deck-sorter grid shows every slide as a thumbnail:
|
||||
|
||||
- **+ Add slide** — append a blank slide.
|
||||
- **Import PowerPoint (.pptx)** — imports `.pptx` files via a
|
||||
vendored parser. Shapes, text runs, images, charts, tables, theme
|
||||
fills, and slide notes all convert.
|
||||
- **→ Convert to book** — turn the deck into a Book Designer book
|
||||
(each slide → one designed page, letterbox-fit into the trim).
|
||||
- Click a thumbnail to edit that slide; the canvas opens with the
|
||||
slide's layers as canvas layers.
|
||||
- Drag thumbnails to reorder. Right-click for duplicate / delete.
|
||||
|
||||
## Theme
|
||||
|
||||
Deck-wide theme overrides under **Deck theme** in the sidebar:
|
||||
|
||||
- **Font family** — applied to every slide's text layers (unless
|
||||
the slide explicitly opts out via "Ignore deck theme").
|
||||
- **Background colour** — applied to every slide's canvas
|
||||
background. Same opt-out applies.
|
||||
|
||||
A slide can pin its own font + background by toggling **Ignore deck
|
||||
theme** in the slide-editor sidebar.
|
||||
|
||||
## Presenter Mode
|
||||
|
||||
Click **Present** to launch a full-screen reader:
|
||||
|
||||
- Arrow keys flip slides.
|
||||
- **S** toggles speaker-notes overlay.
|
||||
- **F** toggles fullscreen.
|
||||
- **Esc** exits.
|
||||
- Footer counter shows `N / total`.
|
||||
|
||||
## Embed
|
||||
|
||||
A published deck's `naddr` works in the embed iframe:
|
||||
|
||||
```html
|
||||
<iframe src="…/#/embed/<naddr>" width="1280" height="720"></iframe>
|
||||
```
|
||||
|
||||
The embed renders a paginated player with prev/next + a slide
|
||||
counter, plus fullscreen + reduced-motion handling.
|
||||
|
||||
## Publish
|
||||
|
||||
Posts a `casewrap-deck` event with every slide inlined. Forks land
|
||||
with the full deck intact; remixers can swap slides + republish.
|
||||
|
||||
## Tips
|
||||
|
||||
- For just one slide, the [Slide](#/help/slide) mode is lighter —
|
||||
standalone slides import into decks as forkable units later.
|
||||
- The deck's `deck.theme` overrides apply at render time only — a
|
||||
slide's own `fontFamily` / `background` stay in the event payload.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Designer tab
|
||||
|
||||
The **Designer** tab opens a dedicated single-disc canvas — one
|
||||
13.5 cm circular disc face, optimised for designing the kind of
|
||||
reusable disc artwork you'd drop into multiple disc-label sheets.
|
||||
|
||||
## When to use the Designer tab
|
||||
|
||||
- You want one disc artwork to publish + reuse across many disc-label
|
||||
sheets (Top / Bottom of a single sheet, or shared across multiple
|
||||
releases).
|
||||
- The geometry is a single circle, not a sheet. Bleed + safe-area
|
||||
rings render dedicated to the circular trim instead of the
|
||||
rectangular cover/jewel layout.
|
||||
|
||||
## Compared to Template > Disc-label sheet
|
||||
|
||||
- **Template / Disc** = a sheet with *two* disc slots side-by-side,
|
||||
printed on full-page label stock.
|
||||
- **Designer** = one disc design, publishable as `disc-design`. Import
|
||||
it later into a sheet via the Disc-label slot picker.
|
||||
|
||||
## Layers
|
||||
|
||||
Layers target `designer-disc`. The single disc surface scales them in
|
||||
its native pixel space (the same `discTemplate.discD` constant used
|
||||
by sheet rendering), so a disc design opened standalone vs dropped
|
||||
into a sheet slot renders identically.
|
||||
|
||||
## Publish
|
||||
|
||||
- Title + Category in **Project Metadata** are required.
|
||||
- The publish event type is `casewrap-disc-design`. Other authors can
|
||||
fork it or drop it into their own disc-label sheets via the sheet's
|
||||
slot picker → *Browse community*.
|
||||
|
||||
## Tips
|
||||
|
||||
- The **Spine label** input in the sidebar applies to Disc-label
|
||||
sheets, not Designer mode — leave it alone here.
|
||||
- Use **Print / Save as PDF** to get a circular vector cut sheet for
|
||||
test prints. Most consumer printers won't print full bleed on a
|
||||
disc; check your printer's safe area before mass printing.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Disc-label sheet
|
||||
|
||||
Two-up disc-label sheet, sized for full-page label stock with two disc
|
||||
circles arranged vertically (Disc 1 / Disc 2).
|
||||
|
||||
## Geometry
|
||||
|
||||
- **Sheet** — full-page (Letter / A4) with two disc cut-outs.
|
||||
- **Disc diameter** — standard CD/DVD ~118 mm with a centre hole.
|
||||
- **Safe area** — interior ring that printers and label stock
|
||||
tolerances tend to respect.
|
||||
|
||||
## Slots: Disc 1 + Disc 2
|
||||
|
||||
Each slot accepts:
|
||||
|
||||
- A local image (drop or pick).
|
||||
- A previously-published **disc design** from Nostr (the standalone
|
||||
`disc-design` from the Designer tab). Click **Browse community →**
|
||||
inside the slot.
|
||||
- Disc-level meta: title, IMDb id, language. Used for both publish
|
||||
tagging and the layout's text decoration.
|
||||
|
||||
You can also leave a slot empty — only the populated disc(s) emit
|
||||
output.
|
||||
|
||||
## Spine labels
|
||||
|
||||
The **Spine labels** inputs apply to a CD/DVD spine running between
|
||||
the two discs (some label stocks include a spine strip). Top label
|
||||
+ bottom label render in the small strip between the two discs.
|
||||
|
||||
## Publishing
|
||||
|
||||
Posts a `casewrap-disc` event with both disc slot images +
|
||||
spine-label text. Forks land with the full sheet preserved.
|
||||
|
||||
## Tips
|
||||
|
||||
- For ONE disc only, use the Designer tab instead — that's the
|
||||
standalone-publishable single-disc surface.
|
||||
- The Crop marks toggle in the Export fieldset adds printer cut
|
||||
marks at the trim corners of each disc.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Artstr Studio docs
|
||||
|
||||
Welcome. Artstr Studio is a single-page design tool that publishes
|
||||
everything to Nostr instead of a private server. Pick a topic below.
|
||||
|
||||
## The two app modes
|
||||
|
||||
- **[Template](#/help/template)** — print + screen artwork (cover wraps, disc
|
||||
labels, jewel inserts, custom-art canvases, slides, decks, pixel art,
|
||||
books). Every artboard publishes as one or more Nostr events that anyone
|
||||
can fork or remix.
|
||||
- **[Designer](#/help/designer)** — a reusable single-disc design surface.
|
||||
Disc designs publish independently, then drop into any disc slot of a
|
||||
disc-label sheet.
|
||||
|
||||
## Template-mode layouts
|
||||
|
||||
- **[Case cover wrap](#/help/cover)** — front + spine + back wraparound
|
||||
for DVD / Blu-ray / softcover printable cases.
|
||||
- **[Disc-label sheet](#/help/disc)** — two-up CD/DVD label sheet.
|
||||
- **[Jewel-case insert](#/help/jewel)** — front + tray jewel inserts.
|
||||
- **[Custom art](#/help/customart)** — any-aspect flat canvas.
|
||||
- **[Slide](#/help/slide)** — 16:9 standalone slide with speaker notes.
|
||||
- **[Slide deck](#/help/deck)** — ordered collection of slides.
|
||||
- **[Pixel art](#/help/pixelart)** — palette-indexed grid editor with
|
||||
multi-frame animation.
|
||||
- **[Book](#/help/book)** — multi-page reflowable book with cover,
|
||||
chapters, masters, PDF export, and Reader Mode.
|
||||
|
||||
## Working in Nostr
|
||||
|
||||
- Sign in with a browser extension (Alby, nos2x, etc) using the top-
|
||||
right widget. Without one you can still author and export locally;
|
||||
publishing + browsing the community requires a key.
|
||||
- Every Artstr event is replaceable (NIP-33). Re-publishing under the
|
||||
same d-tag updates the design in place.
|
||||
- Private publishing uses NIP-44 / NIP-04 to encrypt the payload to
|
||||
your own key — only you can decrypt on a fresh device.
|
||||
|
||||
## Tips
|
||||
|
||||
- Most modes autosave to localStorage as you edit, so you can close
|
||||
the tab and come back later. Publish to Nostr to share or to back
|
||||
up across devices.
|
||||
- Linked Designs let you reference another author's published design
|
||||
as a layer in yours. Their event is fetched and rendered live; the
|
||||
manifest p-tags the contributors for attribution.
|
||||
- The community browser (top-right Nostr widget) shows feeds you can
|
||||
filter and import from.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Jewel-case insert
|
||||
|
||||
Front-cover + tray inserts for a standard CD jewel case.
|
||||
|
||||
## Geometry
|
||||
|
||||
- **Jewel front** — 4.72 × 4.72 in trim, sits behind the clear plastic
|
||||
front cover.
|
||||
- **Jewel tray** — 5.91 × 4.72 in trim, sits under the disc tray (with
|
||||
vertical spine edges that wrap into the case sides).
|
||||
- **Bleed** + **safe area** mirror the standard CD/DVD design.
|
||||
|
||||
## Slots
|
||||
|
||||
Both inserts pick up artwork the same way:
|
||||
|
||||
- Drop in a local image, or
|
||||
- **Browse community →** to pick a published design from Nostr.
|
||||
|
||||
Per slot you can set fit / zoom / x / y / rotate independently.
|
||||
|
||||
## Spine labels
|
||||
|
||||
Left + right spine inputs feed the strips that wrap around the
|
||||
tray-card sides — typically your album / movie title text.
|
||||
|
||||
## Publishing
|
||||
|
||||
Posts a `casewrap-jewel` event. Both inserts' images + the spine
|
||||
labels live in the same event so a forker gets the complete jewel
|
||||
package.
|
||||
|
||||
## Tips
|
||||
|
||||
- Test print at 100% on plain paper first; trim by hand and check the
|
||||
fit before printing on label stock.
|
||||
- For double-sided printing, the tray usually needs a flipped layout
|
||||
on the back — print the same event twice with the front oriented
|
||||
correctly and you'll match most consumer printers.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Pixel art
|
||||
|
||||
Palette-indexed grid editor for sprite work + simple animation.
|
||||
|
||||
## Canvas
|
||||
|
||||
- Grid sizes from 8×8 up to 128×128.
|
||||
- Palette-indexed (not RGB) — every pixel is an index into the
|
||||
palette block, which means animations + colour-cycling are cheap.
|
||||
- Pencil / eraser / fill / eyedropper / line / rect / ellipse tools.
|
||||
- Mirror X / Y for symmetric sprites.
|
||||
- Flip / rotate / resize transforms.
|
||||
|
||||
## Animation
|
||||
|
||||
- Multi-frame timeline at the bottom of the canvas.
|
||||
- Per-frame duration (ms).
|
||||
- Playback preview with onion-skin of the previous frame.
|
||||
- Reorder frames by drag.
|
||||
|
||||
## Palette
|
||||
|
||||
- HSV picker inline.
|
||||
- **Eyedropper (I)** uses `window.EyeDropper` to sample any colour
|
||||
on screen.
|
||||
- Up to 256 palette entries; unused entries don't cost anything in
|
||||
the export.
|
||||
|
||||
## Import
|
||||
|
||||
- **Browse community** — pulls a published pixel-art design from
|
||||
Nostr as a layer.
|
||||
- **Import from file** — JSON design export.
|
||||
- **From image…** — pick any image, then the live side-by-side
|
||||
preview tunes target W/H, colour count, and fit mode (Cover /
|
||||
Fit / Stretch) before applying. Uses median-cut quantization.
|
||||
|
||||
## Export
|
||||
|
||||
- **PNG** at 1× / 2× / 4× / 8× / 16× / 32× (transparent or solid bg,
|
||||
optional grid lines).
|
||||
- **Sprite sheet PNG** — every frame tiled in a Grid or Row layout.
|
||||
- **Animated GIF** — inline GIF89a encoder, no vendored dependency
|
||||
(palette-indexed input is what GIF expects natively).
|
||||
|
||||
## Embed
|
||||
|
||||
Published pixel-art animations play back live in the embed iframe.
|
||||
Embed URL params: `?play=0` starts paused; `?fps=N` overrides
|
||||
frame rate; reduced-motion users get static start unless `?play=1`
|
||||
opts in.
|
||||
|
||||
## Publish
|
||||
|
||||
Posts a `casewrap-pixelart` event with the palette + per-frame
|
||||
pixel data (run-length encoded). Forks land with the full
|
||||
animation intact.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Slide
|
||||
|
||||
A 16:9 standalone slide. Same flat-canvas model as Custom Art but
|
||||
locked to 16:9 + carries a speaker-notes field.
|
||||
|
||||
## When to use
|
||||
|
||||
- A single one-off slide you want to share or embed (16:9 social
|
||||
card, lecture insert, recap card).
|
||||
- The slide is the artwork — you don't need a multi-slide deck shell.
|
||||
|
||||
## Geometry
|
||||
|
||||
Default 1920 × 1080 at 96 DPI. The aspect lock keeps you on 16:9; you
|
||||
can resize via Presets in the sidebar.
|
||||
|
||||
## Speaker notes
|
||||
|
||||
The **Notes** field in the sidebar attaches a freeform string to the
|
||||
slide event. Shown in:
|
||||
|
||||
- Presenter Mode (Deck mode only).
|
||||
- The published event's payload — anyone who forks gets your notes.
|
||||
|
||||
## Importing into a deck
|
||||
|
||||
A standalone Slide is also a Deck-importable unit:
|
||||
|
||||
1. In **Deck** mode, click **Import slide** on a slide tile.
|
||||
2. Pick from Nostr or paste a slide event id.
|
||||
3. The slide drops in as a new entry; can be edited inline.
|
||||
|
||||
## Publish
|
||||
|
||||
Posts a `casewrap-slide` event. The full layer state ships in
|
||||
`payload.slide.layers`; the canvas dimensions + background + notes
|
||||
live in `payload.slide`.
|
||||
|
||||
## Tips
|
||||
|
||||
- For multi-slide presentations, jump to [Slide deck](#/help/deck)
|
||||
instead. A deck owns its own ordered slide list; standalone
|
||||
slides import as forkable units.
|
||||
- Linked-design layers work in slides too — embed someone else's
|
||||
Custom Art / Pixel Art as a layer.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Template tab
|
||||
|
||||
The **Template** tab is where you author finished artwork — anything
|
||||
you'd hand off to a printer, post as a slide, or publish to a community
|
||||
feed. Each option below the tab switcher picks a different artboard.
|
||||
|
||||
## Choosing a layout
|
||||
|
||||
Click an item in the left sidebar's *Template type* picker. The
|
||||
canvas reshapes to that mode's geometry and the right-side options
|
||||
(layers, export) re-scope to it.
|
||||
|
||||
| Mode | When to use |
|
||||
|------|-------------|
|
||||
| **[Cover wrap](#/help/cover)** | DVD / Blu-ray / softcover case wraparound. |
|
||||
| **[Disc-label sheet](#/help/disc)** | Two-up CD/DVD label sheet for printing on full-page label stock. |
|
||||
| **[Jewel-case insert](#/help/jewel)** | Front + tray inserts for a CD jewel case. |
|
||||
| **[Custom art](#/help/customart)** | Any-aspect freeform canvas. Posters, social art, anything. |
|
||||
| **[Slide](#/help/slide)** | 16:9 single slide with speaker-notes field. |
|
||||
| **[Slide deck](#/help/deck)** | Ordered list of slides; publishes as one event with all slides inlined. |
|
||||
| **[Pixel art](#/help/pixelart)** | Palette-indexed grid editor up to 128×128 with multi-frame animation. |
|
||||
| **[Book](#/help/book)** | Multi-page reflowable book with cover, masters, chapters, PDF export. |
|
||||
|
||||
## Project metadata
|
||||
|
||||
Above the type picker is the **Project Metadata** block:
|
||||
|
||||
- **Title** — required for publish.
|
||||
- **Category** — required for publish; chooses the discovery facet
|
||||
(`movie`, `comic`, `book`, etc).
|
||||
- **IMDb / UPC / TVDB / MusicBrainz IDs** — optional external IDs;
|
||||
used to deduplicate against other community designs.
|
||||
- **Language** — affects search / browser filters.
|
||||
- **Custom tag** — free-form discovery tag.
|
||||
|
||||
## Layers panel
|
||||
|
||||
Once you add anything to the canvas, the **Layers** panel surfaces:
|
||||
|
||||
- Per-layer visibility / lock toggles.
|
||||
- Drag to reorder z.
|
||||
- Multi-select with shift/cmd, then use the **Align / Distribute**
|
||||
panel + the **Pathfinder** ops (Unite / Subtract / Intersect /
|
||||
Exclude) for vector shapes.
|
||||
- Layer-target dropdown picks which sub-surface a new layer belongs
|
||||
to (e.g. cover front vs spine vs back; jewel front vs tray).
|
||||
|
||||
## Export + publish
|
||||
|
||||
- **Save / Save as** — local JSON download of the project.
|
||||
- **Open** — load a previously-saved JSON file.
|
||||
- **Print / Save as PDF** — opens the browser print dialog with
|
||||
proper trim + bleed.
|
||||
- **Export JPEG / PNG** — direct raster export.
|
||||
- **Publish to Nostr** — opens a confirmation modal. Public, private
|
||||
(encrypted to self), or premium (zap-gated, watermarked preview).
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- The Layers panel respects the active layer-target. If you don't see
|
||||
a layer you just added, switch the dropdown to its surface.
|
||||
- "Custom" book size unlocks the trim inputs in Document settings.
|
||||
- Mode-specific guides + bleed lines toggle on the canvas via the
|
||||
show-guides / show-bleed / show-crops checkboxes in the right-side
|
||||
Export fieldset.
|
||||
+275
@@ -545,6 +545,119 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Docs modal — Read-the-Docs-style two-pane layout. */
|
||||
#docsModal .modalInner {
|
||||
max-width: 1100px; width: 92vw;
|
||||
max-height: 88vh; height: 720px;
|
||||
padding: 0; overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.docsHeader {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
||||
background: var(--bg);
|
||||
}
|
||||
.docsHeader h2 { margin: 0; font-size: 1rem; font-family: inherit; flex: 1; }
|
||||
.docsHeader .docsBreadcrumb {
|
||||
font-size: .76rem; color: var(--muted);
|
||||
}
|
||||
.docsBody {
|
||||
flex: 1; display: flex; min-height: 0;
|
||||
}
|
||||
.docsSidebar {
|
||||
width: 240px; flex-shrink: 0;
|
||||
border-right: 1px solid var(--line);
|
||||
overflow-y: auto;
|
||||
background: var(--bg);
|
||||
}
|
||||
.docsSidebarGroup { padding: 6px 0; }
|
||||
.docsSidebarGroup + .docsSidebarGroup { border-top: 1px solid var(--line); }
|
||||
.docsSidebarLabel {
|
||||
font-size: .68rem; text-transform: uppercase;
|
||||
letter-spacing: .08em; color: var(--muted);
|
||||
padding: 10px 14px 6px; font-weight: 700;
|
||||
}
|
||||
.docsSidebarItem {
|
||||
display: block; padding: 6px 14px;
|
||||
font-size: .85rem; color: var(--ink);
|
||||
cursor: pointer; text-decoration: none;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.docsSidebarItem:hover { background: rgba(0,0,0,.04); }
|
||||
.docsSidebarItem.current {
|
||||
background: rgba(0,0,0,.06);
|
||||
border-left-color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
body.dark .docsSidebarItem:hover { background: rgba(255,255,255,.05); }
|
||||
body.dark .docsSidebarItem.current { background: rgba(255,255,255,.08); }
|
||||
.docsContent {
|
||||
flex: 1; overflow-y: auto;
|
||||
padding: 22px 30px;
|
||||
font-family: Georgia, serif;
|
||||
line-height: 1.6; font-size: 15px;
|
||||
color: var(--ink); background: #fff;
|
||||
}
|
||||
body.dark .docsContent { background: #1a1a1a; }
|
||||
.docsContent h1 {
|
||||
font-family: inherit; font-size: 1.6rem;
|
||||
margin: 0 0 .4em; padding-bottom: .3em;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.docsContent h2 {
|
||||
font-family: inherit; font-size: 1.2rem;
|
||||
margin: 1.4em 0 .4em;
|
||||
}
|
||||
.docsContent h3 {
|
||||
font-family: inherit; font-size: 1rem;
|
||||
margin: 1.1em 0 .3em;
|
||||
}
|
||||
.docsContent p { margin: 0 0 .9em; }
|
||||
.docsContent ul, .docsContent ol { margin: 0 0 .9em; padding-left: 1.4em; }
|
||||
.docsContent li { margin-bottom: .2em; }
|
||||
.docsContent a { color: var(--accent); }
|
||||
.docsContent code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: .92em; padding: 1px 5px;
|
||||
background: rgba(0,0,0,.06); border-radius: 3px;
|
||||
}
|
||||
body.dark .docsContent code { background: rgba(255,255,255,.08); }
|
||||
.docsContent pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: .85em; padding: 10px 14px;
|
||||
background: rgba(0,0,0,.06); border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
body.dark .docsContent pre { background: rgba(255,255,255,.06); }
|
||||
.docsContent pre code { background: transparent; padding: 0; }
|
||||
.docsContent blockquote {
|
||||
margin: .9em 0; padding: 6px 14px;
|
||||
border-left: 3px solid var(--accent);
|
||||
background: rgba(0,0,0,.03); font-style: italic;
|
||||
}
|
||||
body.dark .docsContent blockquote { background: rgba(255,255,255,.04); }
|
||||
.docsContent table {
|
||||
border-collapse: collapse; margin: 1em 0;
|
||||
font-size: .9em;
|
||||
}
|
||||
.docsContent th, .docsContent td {
|
||||
border: 1px solid var(--line); padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.docsContent th { background: rgba(0,0,0,.04); font-weight: 700; }
|
||||
body.dark .docsContent th { background: rgba(255,255,255,.05); }
|
||||
.docsContent hr {
|
||||
border: none; border-top: 1px solid var(--line);
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
.docsLoading {
|
||||
color: var(--muted); font-style: italic;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.docsSidebar { width: 160px; }
|
||||
.docsContent { padding: 16px 18px; }
|
||||
}
|
||||
|
||||
.appModeTabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -4362,6 +4475,7 @@
|
||||
</fieldset>
|
||||
|
||||
<div class="sidebarFooter">
|
||||
<a href="#" id="docsLink" role="button">📖 Docs</a>
|
||||
<a href="https://github.com/PR0M3TH3AN/Artstr-Studio" target="_blank" rel="noopener noreferrer">GitHub Repository</a>
|
||||
<a href="https://artstr.org" target="_blank" rel="noopener noreferrer">artstr.org</a>
|
||||
</div>
|
||||
@@ -5524,6 +5638,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="docsModal" aria-hidden="true" style="display:none">
|
||||
<div class="modalInner">
|
||||
<div class="docsHeader">
|
||||
<h2>Artstr Studio docs</h2>
|
||||
<span class="docsBreadcrumb" id="docsBreadcrumb"></span>
|
||||
<button type="button" id="docsCloseBtn" class="welcomeCloseBtn" aria-label="Close" style="position:relative;top:auto;right:auto">×</button>
|
||||
</div>
|
||||
<div class="docsBody">
|
||||
<aside class="docsSidebar" id="docsSidebar"></aside>
|
||||
<article class="docsContent" id="docsContent"><p class="docsLoading">Loading…</p></article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="embedCodeModal" aria-hidden="true" style="display:none">
|
||||
<div class="modalInner" style="max-width:680px;position:relative">
|
||||
<button type="button" id="embedCloseBtn" class="welcomeCloseBtn" aria-label="Close">×</button>
|
||||
@@ -11912,6 +12040,131 @@
|
||||
// Minimal HTML escape + marked render. Sanitizes by routing
|
||||
// through marked.parse with `mangle:false, headerIds:false`
|
||||
// and post-strips any `<script>` tags as belt-and-braces.
|
||||
// ---- In-app docs ("Read the docs" modal) ----------------------------
|
||||
// Topic registry. Each entry maps slug → { title, group, file }.
|
||||
// Files live under docs/help/<slug>.md and load on demand via fetch.
|
||||
const _DOCS_TOPICS = [
|
||||
{ slug: 'index', title: 'Overview', group: 'Start here', file: 'help/index.md' },
|
||||
{ slug: 'template', title: 'Template tab', group: 'App tabs', file: 'help/template.md' },
|
||||
{ slug: 'designer', title: 'Designer tab', group: 'App tabs', file: 'help/designer.md' },
|
||||
{ slug: 'cover', title: 'Case cover wrap', group: 'Template-mode layouts', file: 'help/cover.md' },
|
||||
{ slug: 'disc', title: 'Disc-label sheet', group: 'Template-mode layouts', file: 'help/disc.md' },
|
||||
{ slug: 'jewel', title: 'Jewel-case insert', group: 'Template-mode layouts', file: 'help/jewel.md' },
|
||||
{ slug: 'customart', title: 'Custom art', group: 'Template-mode layouts', file: 'help/customart.md' },
|
||||
{ slug: 'slide', title: 'Slide', group: 'Template-mode layouts', file: 'help/slide.md' },
|
||||
{ slug: 'deck', title: 'Slide deck', group: 'Template-mode layouts', file: 'help/deck.md' },
|
||||
{ slug: 'pixelart', title: 'Pixel art', group: 'Template-mode layouts', file: 'help/pixelart.md' },
|
||||
{ slug: 'book', title: 'Book', group: 'Template-mode layouts', file: 'help/book.md' },
|
||||
];
|
||||
const _docsCache = new Map(); // slug → rendered HTML
|
||||
let _docsActiveSlug = 'index';
|
||||
|
||||
function _renderDocsSidebar() {
|
||||
const aside = document.getElementById('docsSidebar');
|
||||
if (!aside) return;
|
||||
const groups = new Map();
|
||||
for (const t of _DOCS_TOPICS) {
|
||||
if (!groups.has(t.group)) groups.set(t.group, []);
|
||||
groups.get(t.group).push(t);
|
||||
}
|
||||
const parts = [];
|
||||
for (const [groupName, items] of groups) {
|
||||
parts.push(`<div class="docsSidebarGroup"><div class="docsSidebarLabel">${escapeHtml(groupName)}</div>`);
|
||||
for (const t of items) {
|
||||
const cur = t.slug === _docsActiveSlug ? ' current' : '';
|
||||
parts.push(`<a class="docsSidebarItem${cur}" href="#/help/${t.slug}" data-slug="${t.slug}">${escapeHtml(t.title)}</a>`);
|
||||
}
|
||||
parts.push('</div>');
|
||||
}
|
||||
aside.innerHTML = parts.join('');
|
||||
aside.querySelectorAll('.docsSidebarItem').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
loadDocsTopic(el.dataset.slug);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function _fetchDocsMarkdown(file) {
|
||||
const res = await fetch(file, { cache: 'force-cache' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return await res.text();
|
||||
}
|
||||
async function loadDocsTopic(slug) {
|
||||
const topic = _DOCS_TOPICS.find(t => t.slug === slug) || _DOCS_TOPICS[0];
|
||||
_docsActiveSlug = topic.slug;
|
||||
const content = document.getElementById('docsContent');
|
||||
const crumb = document.getElementById('docsBreadcrumb');
|
||||
if (!content) return;
|
||||
_renderDocsSidebar();
|
||||
if (crumb) crumb.textContent = topic.group + ' · ' + topic.title;
|
||||
// Update hash without scrolling.
|
||||
if (location.hash !== `#/help/${topic.slug}`) {
|
||||
try { history.replaceState(null, '', `#/help/${topic.slug}`); } catch {}
|
||||
}
|
||||
if (_docsCache.has(topic.slug)) {
|
||||
content.innerHTML = _docsCache.get(topic.slug);
|
||||
_wireDocsInternalLinks(content);
|
||||
content.scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
content.innerHTML = '<p class="docsLoading">Loading…</p>';
|
||||
try {
|
||||
const md = await _fetchDocsMarkdown(topic.file);
|
||||
let html;
|
||||
if (typeof window.marked !== 'undefined' && window.marked.parse) {
|
||||
html = window.marked.parse(md, { mangle: false, headerIds: false, breaks: false, gfm: true });
|
||||
} else {
|
||||
html = '<pre style="white-space:pre-wrap">' + escapeHtml(md) + '</pre>';
|
||||
}
|
||||
// Safety scrub: strip <script> just in case.
|
||||
html = String(html).replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
_docsCache.set(topic.slug, html);
|
||||
content.innerHTML = html;
|
||||
_wireDocsInternalLinks(content);
|
||||
content.scrollTop = 0;
|
||||
} catch (err) {
|
||||
console.warn('docs load failed', topic.file, err);
|
||||
content.innerHTML = `<p style="color:#b91c1c">Couldn't load <code>${escapeHtml(topic.file)}</code>: ${escapeHtml(err.message || String(err))}</p>`;
|
||||
}
|
||||
}
|
||||
function _wireDocsInternalLinks(root) {
|
||||
// Rewrite any `<a href="#/help/foo">` to load via the topic
|
||||
// dispatcher instead of bouncing the page. External http(s)
|
||||
// links keep their default behaviour + open in a new tab.
|
||||
root.querySelectorAll('a').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
if (/^#\/help\//.test(href)) {
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const slug = href.replace(/^#\/help\//, '');
|
||||
loadDocsTopic(slug);
|
||||
});
|
||||
} else if (/^https?:\/\//.test(href)) {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
});
|
||||
}
|
||||
function openDocsModal(slug) {
|
||||
const modal = document.getElementById('docsModal');
|
||||
if (!modal) return;
|
||||
modal.style.display = 'flex';
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
if (typeof bringModalToFront === 'function') bringModalToFront(modal);
|
||||
loadDocsTopic(slug || _docsActiveSlug || 'index');
|
||||
}
|
||||
function closeDocsModal() {
|
||||
const modal = document.getElementById('docsModal');
|
||||
if (!modal) return;
|
||||
modal.style.display = 'none';
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
// Drop the #/help/ hash if we're closing.
|
||||
if (/^#\/help\//.test(location.hash)) {
|
||||
try { history.replaceState(null, '', location.pathname + location.search); } catch {}
|
||||
}
|
||||
}
|
||||
// ---- end docs ------------------------------------------------------
|
||||
|
||||
function markdownToSafeHtml(src) {
|
||||
if (typeof window.marked === 'undefined' || !window.marked.parse) {
|
||||
return '<p>(marked library failed to load)</p>';
|
||||
@@ -36650,6 +36903,28 @@
|
||||
});
|
||||
|
||||
// Book Designer — Document settings modal.
|
||||
// Docs modal — footer "📖 Docs" link + Esc + backdrop close +
|
||||
// hash-route auto-open. Topic slug comes from the URL hash:
|
||||
// #/help/<slug> opens directly to that page.
|
||||
on('docsLink', 'click', (e) => { e.preventDefault(); openDocsModal(); });
|
||||
on('docsCloseBtn', 'click', closeDocsModal);
|
||||
const _docsModal = document.getElementById('docsModal');
|
||||
if (_docsModal) _docsModal.addEventListener('click', (e) => {
|
||||
if (e.target === _docsModal) closeDocsModal();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && _docsModal && _docsModal.style.display === 'flex') {
|
||||
closeDocsModal();
|
||||
}
|
||||
});
|
||||
// Open the docs from a hash route (deep-link friendly).
|
||||
const _checkDocsHash = () => {
|
||||
const m = /^#\/help\/([\w-]+)/.exec(location.hash);
|
||||
if (m) openDocsModal(m[1]);
|
||||
};
|
||||
window.addEventListener('hashchange', _checkDocsHash);
|
||||
_checkDocsHash();
|
||||
|
||||
on('bookDocumentSettingsBtn', 'click', openBookDocumentSettings);
|
||||
on('bookVisibilityChip', 'click', toggleBookVisibility);
|
||||
on('bookResolveBtn', 'click', async () => {
|
||||
|
||||
Reference in new issue
Block a user