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.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/chunks/baht-toast-BlZBbneT.js +324 -0
- package/dist/components/baht-form.d.ts +41 -0
- package/dist/components/baht-workflow.d.ts +64 -0
- package/dist/components/base.d.ts +38 -0
- package/dist/components/primitives/baht-accordion.d.ts +26 -0
- package/dist/components/primitives/baht-alert.d.ts +17 -0
- package/dist/components/primitives/baht-badge.d.ts +19 -0
- package/dist/components/primitives/baht-breadcrumbs.d.ts +22 -0
- package/dist/components/primitives/baht-button.d.ts +31 -0
- package/dist/components/primitives/baht-card.d.ts +18 -0
- package/dist/components/primitives/baht-checkbox.d.ts +7 -0
- package/dist/components/primitives/baht-code.d.ts +18 -0
- package/dist/components/primitives/baht-datepicker.d.ts +6 -0
- package/dist/components/primitives/baht-fieldset.d.ts +17 -0
- package/dist/components/primitives/baht-file.d.ts +9 -0
- package/dist/components/primitives/baht-filter.d.ts +24 -0
- package/dist/components/primitives/baht-input.d.ts +6 -0
- package/dist/components/primitives/baht-link.d.ts +17 -0
- package/dist/components/primitives/baht-list.d.ts +28 -0
- package/dist/components/primitives/baht-modal.d.ts +32 -0
- package/dist/components/primitives/baht-multiselect.d.ts +8 -0
- package/dist/components/primitives/baht-number.d.ts +5 -0
- package/dist/components/primitives/baht-otp.d.ts +26 -0
- package/dist/components/primitives/baht-pagination.d.ts +21 -0
- package/dist/components/primitives/baht-progress.d.ts +16 -0
- package/dist/components/primitives/baht-radio.d.ts +5 -0
- package/dist/components/primitives/baht-range.d.ts +21 -0
- package/dist/components/primitives/baht-rating.d.ts +22 -0
- package/dist/components/primitives/baht-select.d.ts +5 -0
- package/dist/components/primitives/baht-skeleton.d.ts +16 -0
- package/dist/components/primitives/baht-spinner.d.ts +13 -0
- package/dist/components/primitives/baht-table.d.ts +44 -0
- package/dist/components/primitives/baht-text.d.ts +8 -0
- package/dist/components/primitives/baht-textarea.d.ts +5 -0
- package/dist/components/primitives/baht-timeline.d.ts +21 -0
- package/dist/components/primitives/baht-toast.d.ts +27 -0
- package/dist/components/primitives/baht-toggle.d.ts +6 -0
- package/dist/components/primitives/baht-tooltip.d.ts +29 -0
- package/dist/components/primitives/icons.d.ts +13 -0
- package/dist/components/primitives/index.d.ts +38 -0
- package/dist/core/approval.d.ts +9 -0
- package/dist/core/defaults.d.ts +6 -0
- package/dist/core/engine/conditions.d.ts +4 -0
- package/dist/core/engine/evaluate.d.ts +8 -0
- package/dist/core/engine/expressions.d.ts +3 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/types.d.ts +166 -0
- package/dist/core/validators.d.ts +6 -0
- package/dist/custom-elements.json +7761 -0
- package/dist/engine.js +329 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1796 -0
- package/dist/overlay.d.ts +45 -0
- package/dist/overlay.js +121 -0
- package/dist/react.d.ts +191 -0
- package/dist/react.js +1 -0
- package/dist/tokens.css +936 -0
- package/dist/vscode.css-custom-data.json +6 -0
- package/dist/vscode.html-custom-data.json +479 -0
- package/dist/web-types.json +1226 -0
- 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
|
+
}
|