bank20baht-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/dist/chunks/baht-toast-BlZBbneT.js +324 -0
  4. package/dist/components/baht-form.d.ts +41 -0
  5. package/dist/components/baht-workflow.d.ts +64 -0
  6. package/dist/components/base.d.ts +38 -0
  7. package/dist/components/primitives/baht-accordion.d.ts +26 -0
  8. package/dist/components/primitives/baht-alert.d.ts +17 -0
  9. package/dist/components/primitives/baht-badge.d.ts +19 -0
  10. package/dist/components/primitives/baht-breadcrumbs.d.ts +22 -0
  11. package/dist/components/primitives/baht-button.d.ts +31 -0
  12. package/dist/components/primitives/baht-card.d.ts +18 -0
  13. package/dist/components/primitives/baht-checkbox.d.ts +7 -0
  14. package/dist/components/primitives/baht-code.d.ts +18 -0
  15. package/dist/components/primitives/baht-datepicker.d.ts +6 -0
  16. package/dist/components/primitives/baht-fieldset.d.ts +17 -0
  17. package/dist/components/primitives/baht-file.d.ts +9 -0
  18. package/dist/components/primitives/baht-filter.d.ts +24 -0
  19. package/dist/components/primitives/baht-input.d.ts +6 -0
  20. package/dist/components/primitives/baht-link.d.ts +17 -0
  21. package/dist/components/primitives/baht-list.d.ts +28 -0
  22. package/dist/components/primitives/baht-modal.d.ts +32 -0
  23. package/dist/components/primitives/baht-multiselect.d.ts +8 -0
  24. package/dist/components/primitives/baht-number.d.ts +5 -0
  25. package/dist/components/primitives/baht-otp.d.ts +26 -0
  26. package/dist/components/primitives/baht-pagination.d.ts +21 -0
  27. package/dist/components/primitives/baht-progress.d.ts +16 -0
  28. package/dist/components/primitives/baht-radio.d.ts +5 -0
  29. package/dist/components/primitives/baht-range.d.ts +21 -0
  30. package/dist/components/primitives/baht-rating.d.ts +22 -0
  31. package/dist/components/primitives/baht-select.d.ts +5 -0
  32. package/dist/components/primitives/baht-skeleton.d.ts +16 -0
  33. package/dist/components/primitives/baht-spinner.d.ts +13 -0
  34. package/dist/components/primitives/baht-table.d.ts +44 -0
  35. package/dist/components/primitives/baht-text.d.ts +8 -0
  36. package/dist/components/primitives/baht-textarea.d.ts +5 -0
  37. package/dist/components/primitives/baht-timeline.d.ts +21 -0
  38. package/dist/components/primitives/baht-toast.d.ts +27 -0
  39. package/dist/components/primitives/baht-toggle.d.ts +6 -0
  40. package/dist/components/primitives/baht-tooltip.d.ts +29 -0
  41. package/dist/components/primitives/icons.d.ts +13 -0
  42. package/dist/components/primitives/index.d.ts +38 -0
  43. package/dist/core/approval.d.ts +9 -0
  44. package/dist/core/defaults.d.ts +6 -0
  45. package/dist/core/engine/conditions.d.ts +4 -0
  46. package/dist/core/engine/evaluate.d.ts +8 -0
  47. package/dist/core/engine/expressions.d.ts +3 -0
  48. package/dist/core/index.d.ts +7 -0
  49. package/dist/core/types.d.ts +166 -0
  50. package/dist/core/validators.d.ts +6 -0
  51. package/dist/custom-elements.json +7761 -0
  52. package/dist/engine.js +329 -0
  53. package/dist/index.d.ts +5 -0
  54. package/dist/index.js +1796 -0
  55. package/dist/overlay.d.ts +45 -0
  56. package/dist/overlay.js +121 -0
  57. package/dist/react.d.ts +191 -0
  58. package/dist/react.js +1 -0
  59. package/dist/tokens.css +936 -0
  60. package/dist/vscode.css-custom-data.json +6 -0
  61. package/dist/vscode.html-custom-data.json +479 -0
  62. package/dist/web-types.json +1226 -0
  63. package/package.json +94 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nattapong Promthong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # bahtui
2
+
3
+ **JSON in, interactive form out.** A framework-neutral form + workflow engine shipped as Web
4
+ Components (Lit, light DOM) with a pure-TypeScript rules core.
5
+
6
+ ```
7
+ npm i bank20baht-ui
8
+ ```
9
+
10
+ ## Quick start — one form (Level 1)
11
+
12
+ ```html
13
+ <script type="module">
14
+ import 'bank20baht-ui'; // registers <baht-form> / <baht-workflow>
15
+ </script>
16
+ <link rel="stylesheet" href="bank20baht-ui/tokens.css" />
17
+
18
+ <baht-form id="f"></baht-form>
19
+ <script>
20
+ const el = document.getElementById('f');
21
+ el.form = {
22
+ formId: 'f1',
23
+ formName: 'Person',
24
+ field_data: [
25
+ { order: 1, type: 'text', key: 'firstName', label: 'First name', width: '1/2', required: true, value: '' },
26
+ { order: 2, type: 'text', key: 'lastName', label: 'Last name', width: '1/2', required: true, value: '' },
27
+ { order: 3, type: 'number', key: 'age', label: 'Age', width: '1/4', min: 0, value: null }
28
+ ]
29
+ };
30
+ el.addEventListener('baht-change', (e) => console.log(e.detail.data));
31
+ // el.validate() / el.getData() / el.setValue({...}) / el.reset()
32
+ </script>
33
+ ```
34
+
35
+ `<baht-form>` is a **dumb controlled component**: props in → UI → `baht-change` out. No buttons, no
36
+ cross-field logic, no fetch. Your app owns the state (mutate the JSON, feed it back).
37
+
38
+ ## Quick start — wizard with rules (Level 2)
39
+
40
+ ```js
41
+ document.querySelector('baht-workflow').workflow = {
42
+ workflowId: 'wf-1',
43
+ forms: [/* {formId, formName, order, field_data}, ... */],
44
+ rules: [
45
+ { id: 'skip-person',
46
+ when: { field: 'Intro.kind', op: 'eq', value: 'company' },
47
+ then: [{ action: 'skipForm', target: { form: 'Person' } }] },
48
+ { id: 'vat',
49
+ when: { field: 'Company.hasVat', op: 'eq', value: true },
50
+ then: [{ action: 'show', target: { form: 'Company', field: 'vatId' } },
51
+ { action: 'required', target: { form: 'Company', field: 'vatId' } }],
52
+ else: [{ action: 'hide', target: { form: 'Company', field: 'vatId' } }] }
53
+ ]
54
+ };
55
+ ```
56
+
57
+ `<baht-workflow>` walks the forms in `order`, evaluates the rules on every change
58
+ (show/hide/enable/disable/readonly/required/setValue/skipForm…), renders the single
59
+ Next→Submit button, and emits `baht-next` / `baht-submit` / `baht-complete` / `baht-data-change` /
60
+ `baht-step-change`. **The runtime navigates; your app acts.**
61
+
62
+ ## Headless core (any framework, no DOM)
63
+
64
+ ```ts
65
+ import { evaluate, validateField } from 'bank20baht-ui/engine';
66
+
67
+ const derived = evaluate(workflow, { Intro: { kind: 'company' } });
68
+ // derived.forms -> { Person: { visible: false }, ... }
69
+ // derived.fields -> { 'Company.vatId': { visible, enabled, readonly, required, computed } }
70
+ // derived.data -> nested output (hidden/skipped values cleared + excluded)
71
+ ```
72
+
73
+ The engine is a pure function with fixpoint evaluation (cycle-guarded), per-rule `priority`
74
+ conflict resolution, and clear-on-hide/skip semantics. Build a React/Vue renderer on top of it
75
+ without touching the Web Components.
76
+
77
+ ## Functions never travel in JSON
78
+
79
+ Formatters and custom validators are referenced **by name** and resolved from a registry you
80
+ pass as a prop:
81
+
82
+ ```js
83
+ el.registry = {
84
+ formatters: { uppercase: (v) => String(v).toUpperCase() },
85
+ validators: { idCardTH: (v) => isThaiId(v) ? null : 'บัตรประชาชนไม่ถูกต้อง' }
86
+ };
87
+ // JSON side: { formatter: 'uppercase', validators: [{ type: 'custom', fn: 'idCardTH' }] }
88
+ ```
89
+
90
+ ## Design system — 20baht
91
+
92
+ Ships themed as the **20baht design system**: banknote green (`#1F7A4E`), cream
93
+ paper background, Bricolage Grotesque / Geist / Geist Mono, borders-over-shadows,
94
+ "paper" easing. 35+ components:
95
+
96
+ ```html
97
+ <baht-button variant="secondary" size="sm">Cancel</baht-button>
98
+ <baht-badge tone="success" label="Running"></baht-badge>
99
+ <baht-table></baht-table> <baht-modal></baht-modal> <baht-toast></baht-toast> …
100
+ ```
101
+
102
+ - **Data (server-side paging)** — `baht-table` · `baht-list` · `baht-pagination` · `baht-filter`.
103
+ `items` is one page; `baht-page-change` asks your app to fetch the next one.
104
+ - **Feedback** — `baht-modal` · `baht-toast` · `baht-alert` · `baht-tooltip` · `baht-progress` ·
105
+ `baht-spinner` · `baht-skeleton`
106
+ - **Inputs** — `baht-rating` · `baht-range` · `baht-otp` (+ the 11 form field primitives)
107
+ - **Structure & nav** — `baht-card` · `baht-fieldset` · `baht-timeline` · `baht-accordion` ·
108
+ `baht-link` · `baht-breadcrumbs`
109
+
110
+ ## Theming — design tokens + light DOM
111
+
112
+ Everything renders in light DOM and reads `--baht-*` CSS custom properties — override
113
+ at build time, at runtime, or inject your own sheet entirely:
114
+
115
+ ```css
116
+ .my-app {
117
+ --baht-color-primary: #047857;
118
+ --baht-radius-md: 6px;
119
+ }
120
+ .my-app .baht-label { text-transform: uppercase; } /* your CSS reaches every node */
121
+ ```
122
+
123
+ Styling API: `.baht-form`, `.baht-field`, `.baht-field--w-1-2`, `.baht-field--hidden`, `.baht-label`,
124
+ `.baht-control`, `.baht-error`, `.baht-button`, `.baht-badge` (+ the token list in `tokens.css`).
125
+ Full reference: [docs/theming.md](./docs/theming.md).
126
+
127
+ ## One install, typed everywhere
128
+
129
+ `npm i bank20baht-ui` ships the runtime **and** the metadata every editor/framework needs —
130
+ no hand-written type declarations in your app:
131
+
132
+ | You use | What you get | Setup |
133
+ |---|---|---|
134
+ | **JetBrains / WebStorm** (any framework) | autocomplete + docs for every `<baht-*>` tag | none — `web-types` is auto-detected |
135
+ | **VS Code** (HTML / Angular templates) | tag + attribute completion | one line, see below |
136
+ | **React (TSX)** | full JSX typings, typed props per element | `import 'bank20baht-ui/react'` once |
137
+ | **Angular** | template binding on properties/events | `CUSTOM_ELEMENTS_SCHEMA`, see below |
138
+ | **Vue 3** | keep Vue from treating `baht-*` as components | `isCustomElement`, see below |
139
+ | **Phoenix / Rails / plain HTML** | it's just HTML — works as-is | none |
140
+ | **Tooling** (Storybook, docgen, custom) | machine-readable API of all 36 elements | read `dist/custom-elements.json` ([CEM standard](https://github.com/webcomponents/custom-elements-manifest)) |
141
+
142
+ **VS Code** — `.vscode/settings.json`:
143
+
144
+ ```json
145
+ { "html.customData": ["./node_modules/bahtui/dist/vscode.html-custom-data.json"] }
146
+ ```
147
+
148
+ **React** — once, in your app entry (or load it globally without a runtime import via
149
+ `"types": ["bank20baht-ui/react"]` in `tsconfig.json`):
150
+
151
+ ```ts
152
+ import 'bank20baht-ui/react'; // JSX typings for every <baht-*> element
153
+ ```
154
+
155
+ React 19+ sets non-string JSX props as DOM properties, so object props (`form`,
156
+ `workflow`, `items`, …) work inline; on React 18 set those via a ref.
157
+
158
+ **Angular** — the compiler must be told `baht-*` tags are custom elements:
159
+
160
+ ```ts
161
+ @Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA], /* … */ })
162
+ ```
163
+
164
+ **Vue 3** — `vite.config.ts`:
165
+
166
+ ```ts
167
+ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('baht-') } } })
168
+ ```
169
+
170
+ ## Docs, Storybook, examples
171
+
172
+ - **Per-component docs**: [`docs/`](./docs/README.md) — one .md per component + framework
173
+ guides (vanilla / React / Angular).
174
+ - **Storybook**: `pnpm --filter storybook-bahtui dev` → http://localhost:6006.
175
+ - **Copy-out starters**: `examples/vanilla`, `examples/react`, `examples/angular` at the
176
+ repo root — same workflow JSON in three hosts.
177
+
178
+ ## Field types (12)
179
+
180
+ `text · textarea · number · email · dropdown · multiselect · checkbox · radio · toggle · date ·
181
+ file (capture only, no upload) · label (static)`
182
+
183
+ Each field descriptor: `{ order, type, key, label, placeholder, width: '1/4'|'1/2'|'3/4'|'full',
184
+ value, disabled, required, options, maxLength, min, max, validators[], formatter }`.
185
+
186
+ ## Visual Builder
187
+
188
+ The playground app ships a two-mode Visual Builder (Form mode: palette/WYSIWYG canvas/properties;
189
+ Workflow mode: sequence + rules with a live preview running the real `<baht-workflow>`), with
190
+ Import/Export JSON. Run it from the monorepo: `pnpm --filter playground dev` → `/builder`.
@@ -0,0 +1,324 @@
1
+ import { LitElement as E, nothing as h, html as d, svg as p } from "lit";
2
+ import { property as a, query as C, customElement as $, state as _ } from "lit/decorators.js";
3
+ var B = Object.defineProperty, f = (t, e, i, o) => {
4
+ for (var s = void 0, r = t.length - 1, n; r >= 0; r--)
5
+ (n = t[r]) && (s = n(e, i, s) || s);
6
+ return s && B(e, i, s), s;
7
+ };
8
+ class v extends E {
9
+ createRenderRoot() {
10
+ return this;
11
+ }
12
+ }
13
+ function w(t, e) {
14
+ let i = e;
15
+ for (; i.parentNode && i.parentNode !== t; ) i = i.parentNode;
16
+ const o = Array.from(t.childNodes).filter(
17
+ (s) => s !== i && s.nodeType !== Node.COMMENT_NODE
18
+ );
19
+ o.length > 0 && e.append(...o);
20
+ }
21
+ class m extends v {
22
+ constructor() {
23
+ super(...arguments), this.value = null, this.disabled = !1, this.readonly = !1, this.required = !1, this.error = null;
24
+ }
25
+ /** Emit the user's edit upward (composed so it crosses any boundary). */
26
+ emitInput(e) {
27
+ this.dispatchEvent(
28
+ new CustomEvent("baht-input", {
29
+ detail: { key: this.field.key, value: e },
30
+ bubbles: !0,
31
+ composed: !0
32
+ })
33
+ );
34
+ }
35
+ renderLabel() {
36
+ return this.field.label ? d`<label class="baht-label" id=${this.labelId} for=${this.controlId}>
37
+ ${this.field.label}${this.required ? d`<span class="baht-required-mark" aria-hidden="true">*</span>` : h}
38
+ </label>` : h;
39
+ }
40
+ get controlId() {
41
+ return `baht-${this.field.key}`;
42
+ }
43
+ get labelId() {
44
+ return `${this.controlId}-label`;
45
+ }
46
+ get errorId() {
47
+ return `${this.controlId}-error`;
48
+ }
49
+ get helpId() {
50
+ return `${this.controlId}-help`;
51
+ }
52
+ /** id list for the control's aria-describedby: error first, then help. */
53
+ get describedBy() {
54
+ const e = [this.error ? this.errorId : "", this.field.help ? this.helpId : ""].filter(
55
+ Boolean
56
+ );
57
+ return e.length > 0 ? e.join(" ") : h;
58
+ }
59
+ /** 'true' while the field is in error, else absent (styles hook on [aria-invalid]). */
60
+ get invalid() {
61
+ return this.error ? "true" : h;
62
+ }
63
+ render() {
64
+ return d`
65
+ ${this.renderLabel()}
66
+ ${this.renderControl()}
67
+ ${this.field.help ? d`<div class="baht-help" id=${this.helpId}>${this.field.help}</div>` : h}
68
+ ${this.error ? d`<div class="baht-error" id=${this.errorId} role="alert">${this.error}</div>` : h}
69
+ `;
70
+ }
71
+ }
72
+ f([
73
+ a({ attribute: !1 })
74
+ ], m.prototype, "field");
75
+ f([
76
+ a({ attribute: !1 })
77
+ ], m.prototype, "value");
78
+ f([
79
+ a({ type: Boolean })
80
+ ], m.prototype, "disabled");
81
+ f([
82
+ a({ type: Boolean })
83
+ ], m.prototype, "readonly");
84
+ f([
85
+ a({ type: Boolean })
86
+ ], m.prototype, "required");
87
+ f([
88
+ a({ attribute: !1 })
89
+ ], m.prototype, "error");
90
+ const O = {
91
+ copy: p`<rect x="6" y="6" width="8" height="8" rx="1.5"/><path d="M10 6V4.5A1.5 1.5 0 0 0 8.5 3H4.5A1.5 1.5 0 0 0 3 4.5v4A1.5 1.5 0 0 0 4.5 10H6"/>`,
92
+ check: p`<path d="M3.5 8.5 6.5 11.5 12.5 4.5"/>`,
93
+ download: p`<path d="M8 3v7m0 0 3-3m-3 3L5 7"/><path d="M3 12h10"/>`,
94
+ upload: p`<path d="M8 12V5m0 0 3 3M8 5 5 8"/><path d="M3 4h10"/>`,
95
+ plus: p`<path d="M8 3.5v9M3.5 8h9"/>`,
96
+ external: p`<path d="M6 3H3.5v9.5h9.5V10"/><path d="M9 3h4v4"/><path d="M13 3 7.5 8.5"/>`,
97
+ close: p`<path d="M4 4l8 8M12 4l-8 8"/>`,
98
+ play: p`<path d="M5 3.5v9l7-4.5z"/>`
99
+ };
100
+ function M(t) {
101
+ return d`<svg
102
+ class="baht-icon"
103
+ width="16"
104
+ height="16"
105
+ viewBox="0 0 16 16"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ stroke-width="1.5"
109
+ stroke-linecap="round"
110
+ stroke-linejoin="round"
111
+ aria-hidden="true"
112
+ >${O[t]}</svg>`;
113
+ }
114
+ function I(t) {
115
+ return t in O;
116
+ }
117
+ var P = Object.defineProperty, S = Object.getOwnPropertyDescriptor, u = (t, e, i, o) => {
118
+ for (var s = o > 1 ? void 0 : o ? S(e, i) : e, r = t.length - 1, n; r >= 0; r--)
119
+ (n = t[r]) && (s = (o ? n(e, i, s) : n(s)) || s);
120
+ return o && s && P(e, i, s), s;
121
+ };
122
+ let c = class extends v {
123
+ constructor() {
124
+ super(...arguments), this.variant = "primary", this.size = "md", this.disabled = !1, this.type = "button", this.label = "", this.icon = "", this.iconOnly = !1;
125
+ }
126
+ firstUpdated() {
127
+ this.iconOnly || w(this, this.buttonEl);
128
+ }
129
+ render() {
130
+ const t = this.variant === "primary" ? "" : ` baht-button--${this.variant}`, e = this.size === "md" ? "" : ` baht-button--${this.size}`, i = this.iconOnly ? " baht-button--icon" : "", o = this.icon && I(this.icon) ? M(this.icon) : h;
131
+ return d`<button
132
+ class="baht-button${t}${e}${i}"
133
+ type=${this.type}
134
+ aria-label=${this.iconOnly && this.label ? this.label : h}
135
+ ?disabled=${this.disabled}
136
+ >${o}${this.iconOnly ? h : this.label}</button>`;
137
+ }
138
+ };
139
+ u([
140
+ a()
141
+ ], c.prototype, "variant", 2);
142
+ u([
143
+ a()
144
+ ], c.prototype, "size", 2);
145
+ u([
146
+ a({ type: Boolean })
147
+ ], c.prototype, "disabled", 2);
148
+ u([
149
+ a()
150
+ ], c.prototype, "type", 2);
151
+ u([
152
+ a()
153
+ ], c.prototype, "label", 2);
154
+ u([
155
+ a()
156
+ ], c.prototype, "icon", 2);
157
+ u([
158
+ a({ type: Boolean, attribute: "icon-only" })
159
+ ], c.prototype, "iconOnly", 2);
160
+ u([
161
+ C("button")
162
+ ], c.prototype, "buttonEl", 2);
163
+ c = u([
164
+ $("baht-button")
165
+ ], c);
166
+ var k = Object.defineProperty, x = Object.getOwnPropertyDescriptor, y = (t, e, i, o) => {
167
+ for (var s = o > 1 ? void 0 : o ? x(e, i) : e, r = t.length - 1, n; r >= 0; r--)
168
+ (n = t[r]) && (s = (o ? n(e, i, s) : n(s)) || s);
169
+ return o && s && k(e, i, s), s;
170
+ };
171
+ let l = class extends v {
172
+ constructor() {
173
+ super(...arguments), this.open = !1, this.heading = "", this.showClose = !0, this.closeOnScrim = !0, this.previousFocus = null, this.onKeydown = (t) => {
174
+ !this.open || l.openStack.at(-1) !== this || (t.key === "Escape" ? this.requestClose("escape") : t.key === "Tab" && this.trapFocus(t));
175
+ };
176
+ }
177
+ focusables() {
178
+ const t = this.querySelector(".baht-modal");
179
+ return t ? Array.from(
180
+ t.querySelectorAll(
181
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
182
+ )
183
+ ) : [];
184
+ }
185
+ /** Keep Tab cycling inside the dialog (no keyboard escape to the page behind). */
186
+ trapFocus(t) {
187
+ const e = this.focusables(), i = this.querySelector(".baht-modal");
188
+ if (e.length === 0) {
189
+ i == null || i.focus(), t.preventDefault();
190
+ return;
191
+ }
192
+ const o = e[0], s = e[e.length - 1], r = document.activeElement;
193
+ if (!this.contains(r)) {
194
+ o.focus(), t.preventDefault();
195
+ return;
196
+ }
197
+ t.shiftKey && (r === o || r === i) ? (s.focus(), t.preventDefault()) : !t.shiftKey && r === s && (o.focus(), t.preventDefault());
198
+ }
199
+ connectedCallback() {
200
+ super.connectedCallback(), document.addEventListener("keydown", this.onKeydown);
201
+ }
202
+ disconnectedCallback() {
203
+ document.removeEventListener("keydown", this.onKeydown), l.openStack = l.openStack.filter((t) => t !== this), super.disconnectedCallback();
204
+ }
205
+ firstUpdated() {
206
+ w(this, this.bodyEl);
207
+ }
208
+ updated(t) {
209
+ var e, i;
210
+ if (t.has("open") && (l.openStack = l.openStack.filter((o) => o !== this), this.open && l.openStack.push(this)), t.has("open") && this.open && (this.previousFocus = document.activeElement, (e = this.querySelector(".baht-modal")) == null || e.focus()), t.has("open") && !this.open && t.get("open") === !0) {
211
+ const o = this.previousFocus;
212
+ this.previousFocus = null, o != null && o.isConnected && (!document.activeElement || document.activeElement === document.body || this.contains(document.activeElement)) && ((i = o.focus) == null || i.call(o));
213
+ }
214
+ }
215
+ /** Ask the host to close (the host owns `open`). */
216
+ requestClose(t = "button") {
217
+ this.dispatchEvent(
218
+ new CustomEvent("baht-close", { detail: { reason: t }, bubbles: !0, composed: !0 })
219
+ );
220
+ }
221
+ render() {
222
+ return d`<div
223
+ class="baht-modal-scrim"
224
+ style=${this.open ? h : "display:none"}
225
+ @click=${(t) => this.closeOnScrim && t.target === t.currentTarget && this.requestClose("scrim")}
226
+ >
227
+ <div class="baht-modal" role="dialog" aria-modal="true" aria-label=${this.heading || "Dialog"} tabindex="-1">
228
+ ${this.heading || this.showClose ? d`<header class="baht-modal-header">
229
+ <div class="baht-modal-title">${this.heading}</div>
230
+ ${this.showClose ? d`<button type="button" class="baht-modal-close" aria-label="Close" @click=${() => this.requestClose("button")}>×</button>` : h}
231
+ </header>` : h}
232
+ <div class="baht-modal-body"></div>
233
+ </div>
234
+ </div>`;
235
+ }
236
+ };
237
+ l.openStack = [];
238
+ y([
239
+ a({ type: Boolean, reflect: !0 })
240
+ ], l.prototype, "open", 2);
241
+ y([
242
+ a()
243
+ ], l.prototype, "heading", 2);
244
+ y([
245
+ a({ type: Boolean, attribute: "show-close" })
246
+ ], l.prototype, "showClose", 2);
247
+ y([
248
+ a({ type: Boolean, attribute: "close-on-scrim" })
249
+ ], l.prototype, "closeOnScrim", 2);
250
+ y([
251
+ C(".baht-modal-body")
252
+ ], l.prototype, "bodyEl", 2);
253
+ l = y([
254
+ $("baht-modal")
255
+ ], l);
256
+ var q = Object.defineProperty, D = Object.getOwnPropertyDescriptor, g = (t, e, i, o) => {
257
+ for (var s = o > 1 ? void 0 : o ? D(e, i) : e, r = t.length - 1, n; r >= 0; r--)
258
+ (n = t[r]) && (s = (o ? n(e, i, s) : n(s)) || s);
259
+ return o && s && q(e, i, s), s;
260
+ };
261
+ let b = class extends v {
262
+ constructor() {
263
+ super(...arguments), this.duration = 4e3, this.entries = [], this.timers = /* @__PURE__ */ new Map();
264
+ }
265
+ show(t) {
266
+ const e = {
267
+ id: ++b.counter,
268
+ message: t.message,
269
+ tone: t.tone ?? "neutral",
270
+ duration: t.duration ?? this.duration
271
+ };
272
+ return this.entries = [...this.entries, e], e.duration > 0 && this.timers.set(
273
+ e.id,
274
+ setTimeout(() => this.dismiss(e.id), e.duration)
275
+ ), e.id;
276
+ }
277
+ dismiss(t) {
278
+ const e = this.entries.find((i) => i.id === t);
279
+ e && (clearTimeout(this.timers.get(t)), this.timers.delete(t), this.entries = this.entries.filter((i) => i.id !== t), this.dispatchEvent(
280
+ new CustomEvent("baht-dismiss", {
281
+ detail: { id: t, message: e.message },
282
+ bubbles: !0,
283
+ composed: !0
284
+ })
285
+ ));
286
+ }
287
+ disconnectedCallback() {
288
+ this.timers.forEach((t) => clearTimeout(t)), this.timers.clear(), super.disconnectedCallback();
289
+ }
290
+ render() {
291
+ return d`<div class="baht-toaster" role="status" aria-live="polite">
292
+ ${this.entries.map(
293
+ // Danger/warning interrupt (role=alert); the rest ride the polite container.
294
+ (t) => d`<div
295
+ class="baht-toast ${t.tone === "neutral" ? "" : `baht-toast--${t.tone}`}"
296
+ role=${t.tone === "danger" || t.tone === "warning" ? "alert" : h}
297
+ >
298
+ <span class="baht-toast-message">${t.message}</span>
299
+ <button type="button" class="baht-toast-dismiss" aria-label="Dismiss" @click=${() => this.dismiss(t.id)}>×</button>
300
+ </div>`
301
+ )}
302
+ </div>`;
303
+ }
304
+ };
305
+ b.counter = 0;
306
+ g([
307
+ a({ type: Number })
308
+ ], b.prototype, "duration", 2);
309
+ g([
310
+ _()
311
+ ], b.prototype, "entries", 2);
312
+ b = g([
313
+ $("baht-toast")
314
+ ], b);
315
+ export {
316
+ v as B,
317
+ w as a,
318
+ m as b,
319
+ c,
320
+ l as d,
321
+ b as e,
322
+ I as i,
323
+ M as r
324
+ };
@@ -0,0 +1,41 @@
1
+ import { TemplateResult } from 'lit';
2
+ import { BahtElement } from './base.js';
3
+ import { DerivedFieldState, FormDefinition, Registry, ValidationError } from '../core/types.js';
4
+ export interface BahtFormOptions {
5
+ validateOn?: 'change' | 'blur' | 'submit';
6
+ }
7
+ /**
8
+ * <baht-form> — dumb controlled form renderer (spec 01).
9
+ * JSON in (form + value + registry) -> UI out; emits `baht-change` / `baht-validity`.
10
+ * No buttons, no cross-field logic — that's <baht-workflow> or the host.
11
+ */
12
+ export declare class BahtForm extends BahtElement {
13
+ form: FormDefinition;
14
+ value: Record<string, unknown>;
15
+ registry: Registry;
16
+ options: BahtFormOptions;
17
+ /** L2 (workflow) derived per-field UI state, keyed by field key. Optional. */
18
+ fieldStates: Record<string, DerivedFieldState>;
19
+ /** Edits the user made since the host last set `value` (controlled overlay). */
20
+ private edits;
21
+ private errors;
22
+ protected willUpdate(changed: Map<PropertyKey, unknown>): void;
23
+ private get sortedFields();
24
+ private stateFor;
25
+ private valueFor;
26
+ getData(): Record<string, unknown>;
27
+ setValue(patch: Record<string, unknown>): void;
28
+ validate(): {
29
+ valid: boolean;
30
+ errors: ValidationError[];
31
+ };
32
+ reset(): void;
33
+ private onFieldInput;
34
+ private renderField;
35
+ protected render(): TemplateResult;
36
+ }
37
+ declare global {
38
+ interface HTMLElementTagNameMap {
39
+ 'baht-form': BahtForm;
40
+ }
41
+ }
@@ -0,0 +1,64 @@
1
+ import { TemplateResult } from 'lit';
2
+ import { BahtElement } from './base.js';
3
+ import { NestedData, Registry, WorkflowDefinition } from '../core/types.js';
4
+ /**
5
+ * <baht-workflow> — the optional brain (spec 03). Wraps the current form in an
6
+ * internal <baht-form>, evaluates rules on every change, walks the visible forms
7
+ * in linear `order`, and owns the single Next/Submit button.
8
+ * Forms with kind 'approval' render a decision step instead (spec 03 §4):
9
+ * a summary of earlier answers plus Approve/Reject (+ optional comment); the
10
+ * decision lands in data as `<formName>.decision` so rules can branch on it.
11
+ * The runtime navigates; the host acts (on baht-next / baht-submit / baht-approve /
12
+ * baht-reject / baht-complete).
13
+ */
14
+ export declare class BahtWorkflow extends BahtElement {
15
+ workflow: WorkflowDefinition;
16
+ value: NestedData;
17
+ registry: Registry;
18
+ validateGate: boolean;
19
+ nextLabel: string;
20
+ submitLabel: string;
21
+ backLabel: string;
22
+ showBack: boolean;
23
+ private data;
24
+ private derived;
25
+ private current;
26
+ private approvalError;
27
+ private history;
28
+ get currentFormName(): string | null;
29
+ protected willUpdate(changed: Map<PropertyKey, unknown>): void;
30
+ private reevaluate;
31
+ /** Forms in ascending order whose derived visibility is true. */
32
+ private get visibleForms();
33
+ private get currentForm();
34
+ private get isLast();
35
+ private fieldStatesFor;
36
+ private get formEl();
37
+ getData(): NestedData;
38
+ next(): void;
39
+ /** Advance past the current form: submit+complete on the last one, else step forward. */
40
+ private advance;
41
+ /** Approve the current approval step (same as pressing its Approve button). */
42
+ approve(): void;
43
+ /** Reject the current approval step (same as pressing its Reject button). */
44
+ reject(): void;
45
+ private decide;
46
+ /** Jump back to a visible form, truncating the history to that point. */
47
+ private gotoForm;
48
+ back(): void;
49
+ gotoStart(): void;
50
+ private onFormChange;
51
+ private onCommentInput;
52
+ private emit;
53
+ /** Human-readable value for the summary: option labels, Yes/No, em-dash for empty. */
54
+ private displayValue;
55
+ /** Forms the approval step summarizes: configured list, else every earlier plain form. */
56
+ private summaryForms;
57
+ private renderApproval;
58
+ protected render(): TemplateResult;
59
+ }
60
+ declare global {
61
+ interface HTMLElementTagNameMap {
62
+ 'baht-workflow': BahtWorkflow;
63
+ }
64
+ }
@@ -0,0 +1,38 @@
1
+ import { LitElement, nothing, TemplateResult } from 'lit';
2
+ import { FieldDescriptor } from '../core/types.js';
3
+ /** Light-DOM Lit base: consumer CSS reaches every internal node (spec 01 §6). */
4
+ export declare class BahtElement extends LitElement {
5
+ protected createRenderRoot(): HTMLElement;
6
+ }
7
+ /**
8
+ * Light DOM has no <slot>. Call from firstUpdated() to move author-supplied
9
+ * children into `slot` — everything that isn't the rendered `root` or a Lit
10
+ * comment marker. (During HTML parsing children can land after
11
+ * connectedCallback fires, so sweeping at first render is the reliable point.)
12
+ */
13
+ export declare function adoptLightChildren(host: HTMLElement, slot: HTMLElement): void;
14
+ /**
15
+ * Shared shape of every field primitive: descriptor + value in, `baht-input` out.
16
+ * Dumb and controlled — never mutates its own `value` prop; the host re-feeds it.
17
+ */
18
+ export declare abstract class BahtFieldBase extends BahtElement {
19
+ field: FieldDescriptor;
20
+ value: unknown;
21
+ disabled: boolean;
22
+ readonly: boolean;
23
+ required: boolean;
24
+ error: string | null;
25
+ /** Emit the user's edit upward (composed so it crosses any boundary). */
26
+ protected emitInput(value: unknown): void;
27
+ protected abstract renderControl(): TemplateResult;
28
+ protected renderLabel(): TemplateResult | typeof nothing;
29
+ protected get controlId(): string;
30
+ protected get labelId(): string;
31
+ protected get errorId(): string;
32
+ protected get helpId(): string;
33
+ /** id list for the control's aria-describedby: error first, then help. */
34
+ protected get describedBy(): string | typeof nothing;
35
+ /** 'true' while the field is in error, else absent (styles hook on [aria-invalid]). */
36
+ protected get invalid(): string | typeof nothing;
37
+ protected render(): TemplateResult;
38
+ }