khalidadil
  • Joined on 2025-08-25

@khalidadil/sign-studio (0.4.0)

Published 2026-09-19 20:46:57 +00:00 by khalidadil

Installation

@khalidadil:registry=
npm install @khalidadil/sign-studio@0.4.0
"@khalidadil/sign-studio": "0.4.0"

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).

This is a standalone repo — git.kadil.dev/khalidadil/sign-studio-module — extracted from, and now consumed by, pattern-svg-builder (a separate repo), the module's first consumer and living reference host. apparel-designer is the next target host. A self-contained minimal host lives in examples/embed-host/.

The build is self-contained: npm install && npm run build needs no parent — devDependencies carry the toolchain (vite, @vitejs/plugin-react) and mirror every peer, so the module never free-rides a parent install. Do not reintroduce a dependency on a parent/hoisted node_modules; it breaks on clone, CI, and publish.

Install

Route the @khalidadil scope to the Gitea registry. This repo ships an .npmrc with the registry line; a consumer needs the same line, plus an auth token supplied out-of-band (a global ~/.npmrc or NODE_AUTH_TOKEN in CI — never commit the token):

@khalidadil:registry=https://git.kadil.dev/api/packages/khalidadil/npm/
// package.json — published, or a sibling checkout for fleet-local dev:
"@khalidadil/sign-studio": "^0.1.0-alpha.1"
// "@khalidadil/sign-studio": "file:../sign-studio-module"

Provide these peer dependencies (the module's lib build externalizes them and bundles none — the host supplies the one copy):

peer range notes
react, react-dom >=18 <20
fabric ^6 dynamic-imported; the host bundler code-splits it
@khalidadil/laser-geometry >=0.3 <1 single copy — palette live-bindings
express ^4.18 || ^5 server mount only

Your bundler MUST dedupe the shared singletons. The module can be consumed from source through a symlink whose realpath sits outside your project, which otherwise lets react/three/laser-geometry resolve to a second copy — a green build that then throws invalid-hook-call at runtime, forks three, or desyncs the palette. With Vite:

// vite.config.js
export default { resolve: { dedupe: ['react', 'react-dom', 'three'] } };

Keep @khalidadil/laser-geometry a single copy the same way (a single install, or one symlink both halves share). three reaches you transitively via the engine's lazy ./three 3D view; dedupe it even though it isn't a direct peer.

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.3 <1, must be a SINGLE copy in the bundle — live bindings). See Install for the required resolve.dedupe.
  • 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

From the repo root: npm install (self-contained — pulls the toolchain + peer mirrors), then npm run build emits dist/sign-studio.js + the concatenated scoped stylesheet (prepublishOnly runs the build for you). npm publish targets the Gitea registry (publishConfig); bump the version first — the registry rejects a re-publish of an existing version. dist/ is committed and listed in files, so consumers on file: links (no build step) get the bundle too — rebuild and commit it whenever src/ changes. Consumers add the @khalidadil registry line from Install above.

Dependencies

Dependencies

ID Version
@khalidadil/laser-geometry ^0.10.0
better-sqlite3 ^12.4.1
dotenv ^17.2.3
earcut ^3.0.2
multer ^2.0.2
opentype.js ^1.3.4
sharp ^0.35.4

Development Dependencies

ID Version
@khalidadil/laser-geometry ^0.8.0
@vitejs/plugin-react ^4.3.0
express ^4.22.2
fabric ^6.0.0
react ^18.3.0
react-dom ^18.3.0
vite ^5.4.0

Peer Dependencies

ID Version
@khalidadil/laser-geometry >=0.3.0 <1.0.0
express ^4.18.0 || ^5.0.0
fabric ^6.0.0
react >=18 <20
react-dom >=18 <20
Details
npm
2026-09-19 20:46:57 +00:00
7
9.5 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