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:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-06-04 08:52:34 -04:00
1 parent f50d92043c
commit 23a16c8e89
12 files changed
+897

No files matched your search

+111
View File
@@ -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.
+49
View File
@@ -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.
+57
View File
@@ -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.
+64
View File
@@ -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.
+43
View File
@@ -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.
+43
View File
@@ -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.
+49
View File
@@ -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.
+39
View File
@@ -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.
+57
View File
@@ -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.
+45
View File
@@ -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.
+65
View File
@@ -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
View File
@@ -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 () => {