@khalidadil/sign-studio (0.1.0-alpha.0)
Installation
@khalidadil:registry=npm install @khalidadil/sign-studio@0.1.0-alpha.0"@khalidadil/sign-studio": "0.1.0-alpha.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),
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
dataDirseeds 28 fonts + 398 pre-measured patterns by COPY from the module's ownassets/; against an existing pre-UUID database the mount runs the PK migration (with a.baksnapshot) — 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/restoreatomically 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 —
setPaletteFromServerstate is global in@khalidadil/laser-geometry/renderand shared with any storefrontArtSvgon the page. Offers filtering is host UI. - Wood-photo texture: colours without an
imageUrlfall 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 withpreviewFamily().
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.htmlis 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 |