{"_id":"@archduck/gst-core","_rev":"2-64c7a9b103a2f726c88922c0c73f0d27","name":"@archduck/gst-core","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@archduck/gst-core","version":"0.1.0","keywords":["forms","engine","configuration","compositional","reactive"],"author":{"name":"Alan Aydelott"},"license":"MIT","_id":"@archduck/gst-core@0.1.0","maintainers":[{"name":"archduck","email":"alan.aydelott@gmail.com"}],"homepage":"https://gitlab.com/Aydelott/gst-core","bugs":{"url":"https://gitlab.com/Aydelott/gst-core/-/issues"},"dist":{"shasum":"ee299affbdba0109cfed4cbb56d6608c72b12418","tarball":"https://registry.npmjs.org/@archduck/gst-core/-/gst-core-0.1.0.tgz","fileCount":27,"integrity":"sha512-/e8lwwCuatol3z3iDK1Z3GwCy6bPjbcfZPHWSBuO5LYskCyST1Bt7Awni8HwvMB6ZJli7Fwswpwpw/mk6fvrIw==","signatures":[{"sig":"MEYCIQDjFoJIU2UMMoCJLybZG3rO8gZXIP2XjZQk7qDVuhJzOwIhAMV23LdRmfRN6V3jQ/stwLi6PhZbGiEe/MHhhlauIcH8","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":908732},"main":"./dist/index.cjs","type":"module","module":"./dist/index.js","exports":{".":{"import":"./dist/index.js","require":"./dist/index.cjs"},"./core":{"import":"./dist/core.js","require":"./dist/core.cjs"},"./loaders":{"import":"./dist/loaders.js","require":"./dist/loaders.cjs"}},"_npmUser":{"name":"archduck","email":"alan.aydelott@gmail.com"},"repository":{"url":"git+https://gitlab.com/Aydelott/gst-core.git","type":"git"},"_npmVersion":"11.14.1","description":"Compositional config-driven rendering engine","directories":{},"_nodeVersion":"22.22.2","dependencies":{"@vue/reactivity":"^3.5.32"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/gst-core_0.1.0_1780100494545_0.6498769208147648","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@archduck/gst-core","version":"0.1.1","type":"module","description":"Compositional config-driven rendering engine","keywords":["forms","engine","configuration","compositional","reactive"],"author":{"name":"Alan Aydelott"},"license":"MIT","repository":{"type":"git","url":"git+https://gitlab.com/Aydelott/gst-core.git"},"homepage":"https://gitlab.com/Aydelott/gst-core","bugs":{"url":"https://gitlab.com/Aydelott/gst-core/-/issues"},"main":"./dist/index.cjs","module":"./dist/index.js","exports":{".":{"import":"./dist/index.js","require":"./dist/index.cjs"},"./core":{"import":"./dist/core.js","require":"./dist/core.cjs"},"./loaders":{"import":"./dist/loaders.js","require":"./dist/loaders.cjs"}},"scripts":{"build":"vite build","dev":"vite build --watch","test":"vitest","test:run":"vitest run","test:coverage":"vitest run --coverage"},"dependencies":{"@vue/reactivity":"^3.5.32"},"devDependencies":{"vite":"^6.0.0"},"gitHead":"2b215dbe4d2f769dbef09cb33731e761cb45175c","_id":"@archduck/gst-core@0.1.1","_nodeVersion":"22.22.2","_npmVersion":"11.14.1","dist":{"integrity":"sha512-0DjVxk9SXwjA2i62VgF0eJKWQxO22BFKrKDd+bHcaFUBBgzU32vuPcx9rbuQW7IHejMBVPMKchaReldMpJEG6A==","shasum":"eed7b42177ee417ca7e77cd4e97a6b8906c55b58","tarball":"https://registry.npmjs.org/@archduck/gst-core/-/gst-core-0.1.1.tgz","fileCount":27,"unpackedSize":908973,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGVBzjLhxf8ZA+t4fNu8mE3wXZWL5eFcIxKQoSFUN9onAiBY8pSHxfDAIXREspFFoqAPYqMuOIA5ETTyWP3cB9Q+6w=="}]},"_npmUser":{"name":"archduck","email":"alan.aydelott@gmail.com"},"directories":{},"maintainers":[{"name":"archduck","email":"alan.aydelott@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/gst-core_0.1.1_1780353797627_0.9276352955195268"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-30T00:21:34.387Z","modified":"2026-06-01T22:43:17.927Z","0.1.0":"2026-05-30T00:21:34.801Z","0.1.1":"2026-06-01T22:43:17.825Z"},"bugs":{"url":"https://gitlab.com/Aydelott/gst-core/-/issues"},"author":{"name":"Alan Aydelott"},"license":"MIT","homepage":"https://gitlab.com/Aydelott/gst-core","keywords":["forms","engine","configuration","compositional","reactive"],"repository":{"type":"git","url":"git+https://gitlab.com/Aydelott/gst-core.git"},"description":"Compositional config-driven rendering engine","maintainers":[{"name":"archduck","email":"alan.aydelott@gmail.com"}],"readme":"# gst-core\n\nConfig-driven rendering engine. Describe UI as plain objects -- in JavaScript, JSON, or both -- and get reactive DOM with automatic updates, dynamic properties, scoped styles, and layered configuration loading.\n\n**gst** stands for **Grand Schema Things** -- a play on \"grand scheme of things.\" Forms, pages, and UIs are defined as schemas (JSON configurations) rather than code. The name captures the idea that these config-driven \"things\" participate in a larger system of data, interaction, and workflow.\n\nGeneric by design. No opinions about forms, pages, or any domain. Domain behavior is injected through options; `gst-forms` is one such plugin.\n\nBuilt on `@vue/reactivity` and `gst-compose`.\n\n```\nnpm install @archduck/gst-core\n```\n\n---\n\n## Quick start\n\n```js\nimport { mount } from '@archduck/gst-core'\n\nmount(document.getElementById('root'), {\n  layout: [\n    { component: 'h1', innerText: 'Hello' },\n    { component: 'input', name: 'email', type: 'email' },\n    { component: 'p', innerText: '{{greeting}}' },\n  ],\n  registries: {\n    functions: {\n      greeting(context) {\n        return context.record.data.email\n          ? `You entered: ${context.record.data.email}`\n          : 'Type an email above'\n      }\n    }\n  }\n}, {\n  record: { key: null, data: { email: '' } }\n})\n```\n\nThat's a reactive UI from a plain object. The `<input>` is two-way bound to `record.data.email`. The paragraph re-renders whenever the email changes, because `greeting` reads from the reactive record. No JSX, no templates, no build step required for the config itself.\n\n---\n\n## The config object\n\nEverything converges to a single shape before rendering:\n\n```js\n{\n  meta: {},\n  layout: [],\n  registries: { fields, buttons, components, functions, optionSets },\n  styles: null,\n  actions: {}\n}\n```\n\nHow you build this object is up to you.\n\n**Write it directly in JS** when you want full control and type checking:\n\n```js\nconst form = {\n  layout: [\n    { component: 'h1', innerText: 'Hello' },\n    { component: 'input', name: 'email' },\n  ],\n  registries: {\n    fields: {\n      email: { component: 'input', type: 'email', label: 'Email' }\n    },\n    functions: {\n      greeting(context) { return `Hi, ${context.record.data.email}` }\n    }\n  }\n}\n\nmount(document.getElementById('root'), form)\n```\n\n**Load it from JSON files** when the config needs to live outside code:\n\n```js\nimport { loadJsonConfig } from '@archduck/gst-core'\n\nconst form = await loadJsonConfig('./UserView', { patches: ['uchealth'], schema })\nmount(document.getElementById('root'), form)\n```\n\n**Mix both** -- load structure from JSON, define functions in JS:\n\n```js\nconst form = await loadJsonConfig('./UserView', { schema })\nform.registries.functions = {\n  ...form.registries.functions,\n  customValidation(context) { /* ... */ }\n}\nmount(document.getElementById('root'), form)\n```\n\n---\n\n## Dynamic properties\n\nAny property can be a static value or a `{{functionName}}` reference. Functions receive `(context, event)` with `this` bound to an element-specific stage object. They re-evaluate automatically when reactive dependencies change.\n\n```js\n{\n  show: '{{isAdmin}}',\n  label: '{{getLabel}}',\n  disabled: '{{isLocked}}',\n  innerText: '{{summary}}'\n}\n```\n\nThere's no special list of \"properties that can be dynamic.\" If you can set it statically, you can compute it with a function.\n\n`show` conditionally renders an element. This works on fields, groups, buttons -- anything in the layout.\n\nPass arguments with `{{functionName:arg}}`:\n\n```js\n{\n  show: '{{hasRole:editor}}',\n  className: '{{getStepClass:3}}'\n}\n```\n\nThe arg is always a string. Parse it in the function if you need another type.\n\n---\n\n## Layout\n\nLayout is an array of groups. Groups contain rows; rows contain field names (looked up in `registries.fields`) or inline element definitions:\n\n```js\nlayout: [\n  {\n    title: 'Contact Info',\n    rows: [\n      ['firstName', 'lastName'],    // side by side\n      ['email'],                     // full width\n    ]\n  },\n  {\n    title: 'Preferences',\n    show: '{{hasAccount}}',\n    rows: [['theme', 'language']]\n  }\n]\n```\n\nBare arrays are sugar for `{ rows: [...] }`. String items in rows are registry lookups; object items render inline.\n\n---\n\n## Registries and spread\n\nRegistries are named definition collections. Definitions inherit from each other using `gst-compose` spread syntax:\n\n```js\nregistries: {\n  fields: {\n    email: { component: 'input', type: 'email', placeholder: 'Email' },\n    workEmail: { '...': 'email', placeholder: 'Work email' },\n    personalEmail: { '...': 'email', placeholder: 'Personal email' },\n  }\n}\n```\n\n`workEmail` spreads everything from `email`, then overrides `placeholder`. Cross-registry references work too: `\"...\": \"fields.email\"` from inside a different registry.\n\n---\n\n## Config loading\n\nLoad config from a directory of JSON/JS files. Files are discovered by naming convention and merged using `gst-compose`:\n\n```js\nconst form = await loadJsonConfig('./UserView', { schema })\n\n// With variant overlays (each variant stacks on top)\nconst form = await loadJsonConfig('./UserView', { patches: ['uchealth', 'admin'], schema })\n```\n\nFile naming: `property[-variant].{json|js}`. A form directory might contain:\n\n```\nUserView/\n  fields.json            # base field definitions\n  fields-uchealth.json   # uchealth variant (merges with base)\n  layout.json            # layout structure\n  functions.js           # custom functions (JS, not JSON)\n  styles.json            # scoped styles\n```\n\nThe loader finds all files, resolves variants in order, applies `gst-compose` spread semantics, and returns the unified config object.\n\n---\n\n## LiveConfig\n\nPatch configuration at runtime without reloading:\n\n```js\nimport { LiveConfig } from '@archduck/gst-core'\n\nconst liveConfig = new LiveConfig({ schema })\nawait liveConfig.load('./UserView')\n\nliveConfig.patch('fields', { email: { required: true } })\nconst form = liveConfig.get()\n```\n\nPatches cascade through dependencies. If `workEmail` spreads from `email`, patching `email` re-resolves `workEmail` too.\n\n---\n\n## Scoped styles\n\n```js\nstyles: {\n  scoped: true,\n  rules: {\n    '&': { maxWidth: '48em', marginInline: 'auto' },\n    '& input': { padding: '0.5em', border: '1px solid #ccc' },\n  }\n}\n```\n\n`&` refers to the mount container. Styles are written in JS object syntax (camelCase properties), injected as a `<style>` element, and cleaned up on unmount. Multiple mount instances on the same page don't collide.\n\n---\n\n## Extending gst-core\n\n### Custom components\n\nRegister renderers in the component registry:\n\n```js\nregistries: {\n  components: {\n    StarRating: {\n      _adapter: {\n        mount(container, { def, store }) {\n          // create DOM, wire events, return { unmount() {} }\n        }\n      }\n    }\n  }\n}\n```\n\nComponents are referenced by name in definitions: `{ component: 'StarRating', max: 5 }`.\n\n### Define your own domain vocabulary\n\n`createTLPSchema` lets you declare what top-level properties exist, which are registries, and how they depend on each other:\n\n```js\nimport { createTLPSchema } from '@archduck/gst-core'\n\nconst schema = createTLPSchema({\n  functions:   { registry: true, dependencies: [] },\n  widgets:     { registry: true, dependencies: ['functions'] },\n  dataSources: { registry: true, dependencies: [] },\n  layout:      { dependencies: ['widgets', 'dataSources'], default: [] },\n  settings:    { dependencies: [] },\n})\n```\n\nThe `dependencies` array drives resolution order via topological sort. Config loading, variant layering, spread syntax, and cross-references all work with your vocabulary. `gst-forms` defines its own schema (fields, buttons, actions, hooks); you could define one for a dashboard engine, page builder, or anything else.\n\n### Inject domain behavior\n\n`mount()` is a generic renderer. It turns config into reactive DOM but has no opinion about what that DOM means. Domain-specific behavior goes through options:\n\n```js\nmount(container, config, {\n  record: { key: null, data: {} },\n  components: {},\n  initialState: {},\n  effects: [(store) => effect(() => { /* ... */ })],\n  setupActions: (store, options) => { /* ... */ },\n  onMount: (store, controller) => {},\n  onUnmount: (store) => {},\n  onHide: (def, store) => {},\n})\n```\n\n`gst-forms` injects dirty tracking, field errors, action hooks, and confirmation dialogs through this interface. A different plugin could inject drag-and-drop, real-time collaboration, or analytics.\n\n---\n\n## API reference\n\n### `mount(container, config, options?) -> controller`\n\nRender config into a DOM container. Returns a controller:\n\n- `controller.update(record)` -- update record data reactively\n- `controller.executeAction(name, extra)` -- trigger an action\n- `controller.unmount()` -- clean up and remove from DOM\n- `controller.store` -- the reactive store (read-only access)\n\n### `createTLPSchema(declarations) -> schema`\n\nBuild a schema from top-level property declarations. Returns an object with `resolutionOrder`, `dependencies`, `registryMapping`, `validRegistries`, and `getDefault(property)`.\n\n### `loadJsonConfig(path, options?) -> Promise<config>`\n\nLoad and merge config files from a namespace directory. Options include `schema` (required), `patches` (variant array), `scope`, `registries`, `functions`.\n\n### `LiveConfig`\n\nStateful config manager with runtime patching.\n\n- `new LiveConfig({ schema })`\n- `liveConfig.load(path, options?)` -- load from files\n- `liveConfig.from(config, options?)` -- initialize from existing config\n- `liveConfig.get()` -- get resolved config (cached)\n- `liveConfig.patch(property, patch)` -- merge patch, re-resolve dependents\n- `liveConfig.set(property, value)` -- replace property, re-resolve dependents\n\n### Config merging utilities\n\n- `mergeConfigs(...configs)` -- deep merge with array spread support\n- `mergeRegistries(...registries)` -- merge registry objects\n- `applyMap(config, map)` -- apply string replacement map (i18n)\n- `mergeMaps(...maps)` -- merge translation maps\n\n### Reactivity (re-exported from @vue/reactivity)\n\n`reactive`, `effect`, `computed`, `ref`, `toRaw`, `stop`, `pauseTracking`, `enableTracking`\n\n### Style processing\n\n- `processStyles(styles, scopeId)` -- convert JS style objects to scoped CSS\n- `injectStyles(css, id)` / `removeStyles(id)` -- manage `<style>` elements\n- `generateScopeId()` -- generate unique scope ID\n","readmeFilename":"README.md"}