khalidadil
  • Joined on 2025-08-25

@khalidadil/sign-studio (0.1.0-alpha.1)

Published 2026-09-03 03:45:18 +00:00 by khalidadil

Installation

@khalidadil:registry=
npm install @khalidadil/sign-studio@0.1.0-alpha.1
"@khalidadil/sign-studio": "0.1.0-alpha.1"

About this package

@khalidadil/sign-studio

Embeddable sign/pattern design studio: a single-project authoring editor (React) plus an Express mount (better-sqlite3 + @khalidadil/laser-geometry), extracted from pattern-svg-builder. The standalone builder app in this repo is the module's first consumer and living reference host; a runnable minimal host lives in examples/embed-host/ (node server.mjs + npx vite).

Server

import { mountSignStudioApi } from '@khalidadil/sign-studio/server';

const studio = await mountSignStudioApi(app, {
  basePath: '/api/signs',
  dataDir: '/var/data/sign-studio',        // sqlite + fonts/patterns/exports nest here
  auth: { isEnabled: () => true, isValid: (token) => ... },  // omit → no guard (guard app-level yourself)
  features: { patternBundles: false },     // zip routes need `zip`/`unzip` binaries
  uploads: { maxFileSizeMb: 50 },
  logger,
});
// → { db, router, ensureProject(data?) }
  • Explicit init, no import side effects. First mount on a dataDir seeds 28 fonts + 398 pre-measured patterns by COPY from the module's own assets/; against an existing pre-UUID database the mount runs the PK migration (with a .bak snapshot) — treat first-mount-after-upgrade as a migration event.
  • One project per host product: call studio.ensureProject() once and store the returned UUID on your product record.
  • Auth must be cookie-compatible for three GET families the browser loads natively (no headers possible): fonts/:id/file, patterns/:id/file, exports/*. The built-in guard exempts them; your own must too.
  • Geometry (sync Clipper2/earcut) is serialized on two lanes (interactive previews vs exports) so an export can't freeze typing previews. The native Clipper2 binding ships darwin-arm64 prebuilt; Linux images build it with a Rust toolchain (see the repo Dockerfile, glibc not musl).
  • POST :basePath/projects/:id/restore atomically reconciles a project tree to a client snapshot (the studio's undo/redo persistence).

Client

import { SignStudio } from '@khalidadil/sign-studio';
import '@khalidadil/sign-studio/styles.css';   // scoped under .sign-studio

<SignStudio
  projectId={product.builderProjectId}
  api={{ baseUrl: '/api/signs' }}              // + getAuthHeaders / onAuthError
  palette={hostPalette}                        // FULL palette, getPalette() shape
  scenes={[{ name, label, wallImageUrl }]}     // optional room-wall backdrops
  resolveTextureUrl={(imageUrl) => ...}        // palette colour textures → loadable URLs
  features={{ patternBundles: false }}
  singleArtboard
  productName="Name sign"                      // omit onRename → read-only title
  onSaveStatus={(s) => ...}
  onThumbnail={(png) => ...}                   // host persists cover images
  onPublish={async (projectId) => ...}         // saves are flushed first
  onSelectionChange={({ patternId }) => ...}   // deep-link support
  pendingPatternId={fromUrl}
  ref={studioRef}                              // { flushSaves, cancelSaves, undo, redo, fitAll, refreshPatterns, refreshFonts }
/>

Host rules:

  • One studio per page (module-level transport config + save-status bus).
  • Pass the full palette, never a per-product offered subset — setPaletteFromServer state is global in @khalidadil/laser-geometry/render and shared with any storefront ArtSvg on the page. Offers filtering is host UI.
  • Wood-photo texture: colours without an imageUrl fall back to the engine's /textures/oak-photo.jpg — serve that asset at your origin or give every wood colour an explicit texture, or the wood face renders as a flat tone.
  • Peers: react/react-dom (>=18 <20), fabric ^6 (dynamic-imported — your bundler code-splits it), @khalidadil/laser-geometry (^0.2, must be a SINGLE copy in the bundle — live bindings).
  • Uploaded fonts register as namespaced sign-<id> FontFaces (no collisions with the host page's fonts); style previews with previewFamily().

Customer view (the dual-view embed)

The same design serves both surfaces with no publish step between them: the admin edits it in <SignStudio>; the storefront renders it through a write-free public surface and substitutes the customer's text on the fly.

// server — mount OUTSIDE the admin auth boundary:
await mountSignCustomerApi(app, { basePath: '/api/signs-public', maxTextLength: 24 });
import { SignCustomizer } from '@khalidadil/sign-studio';

<SignCustomizer
  designId={product.builderProjectId}
  api={{ baseUrl: '/api/signs-public' }}
  palette={hostPalette} offers={product.offers}
  onChange={({ text, material, colorId, backerColorId, twoTone, widthMm, heightMm }) => ...}
/>
  • Which text the customer can edit: elements the admin flags with “Customer can edit this text” (TextToolbar); with no flags, a design whose only text element it is stays editable automatically. Unflagged text is fixed decor and renders as-authored.
  • The render endpoint accepts ONLY { text, backer? } (length-capped; Two-Tone honored only when the design offers it) — customers can never post geometry. Elements with auto-fit re-converge their font size so long names shrink to fit. Nothing is ever written.
  • examples/embed-host/customize.html is the runnable storefront demo (node server.mjs + npx vite, then /customize.html).

Publishing

npm run build (in module/) emits dist/sign-studio.js + the concatenated scoped stylesheet; npm publish targets the Gitea registry (publishConfig). Consumers add @khalidadil:registry=https://git.kadil.dev/api/packages/khalidadil/npm/.

Dependencies

Dependencies

ID Version
better-sqlite3 ^12.4.1
dotenv ^17.2.3
earcut ^3.0.2
multer ^2.0.2
opentype.js ^1.3.4

Peer Dependencies

ID Version
@khalidadil/laser-geometry ^0.2.0
express ^4.18.0 || ^5.0.0
fabric ^6.0.0
react >=18 <20
react-dom >=18 <20
Details
npm
2026-09-03 03:45:18 +00:00
0
9.3 MiB
Assets (1)
Versions (9) View all
0.5.1 2026-10-14
0.5.0 2026-10-14
0.4.0 2026-09-19
0.3.0 2026-09-18
0.2.0 2026-09-18