bank20baht-ui 0.10.0 → 0.12.0

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 (39) hide show
  1. package/README.md +21 -19
  2. package/dist/components/baht-form.js +1 -0
  3. package/dist/components/primitives/baht-datepicker.d.ts +4 -1
  4. package/dist/components/primitives/baht-datepicker.js +29 -26
  5. package/dist/components/primitives/baht-filter.d.ts +12 -1
  6. package/dist/components/primitives/baht-filter.js +42 -32
  7. package/dist/components/primitives/baht-modal.d.ts +14 -0
  8. package/dist/components/primitives/baht-modal.js +42 -37
  9. package/dist/components/primitives/baht-select.d.ts +4 -0
  10. package/dist/components/primitives/baht-select.js +50 -46
  11. package/dist/components/primitives/baht-swipe-row.d.ts +62 -0
  12. package/dist/components/primitives/baht-swipe-row.js +174 -0
  13. package/dist/components/primitives/baht-tags.d.ts +95 -0
  14. package/dist/components/primitives/baht-tags.js +319 -0
  15. package/dist/components/primitives/baht-typeahead.js +52 -54
  16. package/dist/components/primitives/index.d.ts +4 -2
  17. package/dist/components/primitives/listbox.d.ts +6 -0
  18. package/dist/components/primitives/listbox.js +16 -0
  19. package/dist/components/primitives/tag-map.js +2 -1
  20. package/dist/core/defaults.js +1 -0
  21. package/dist/core/display.js +1 -0
  22. package/dist/core/i18n.d.ts +6 -0
  23. package/dist/core/i18n.js +20 -8
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/core/types.d.ts +19 -2
  26. package/dist/core/validators.d.ts +15 -1
  27. package/dist/core/validators.js +86 -58
  28. package/dist/css/components.css +288 -0
  29. package/dist/custom-elements.json +1898 -248
  30. package/dist/engine.js +37 -36
  31. package/dist/index.js +175 -170
  32. package/dist/react.d.ts +11 -1
  33. package/dist/register.d.ts +2 -0
  34. package/dist/register.js +2 -0
  35. package/dist/ssr.js +4 -2
  36. package/dist/tokens.css +288 -0
  37. package/dist/vscode.html-custom-data.json +54 -5
  38. package/dist/web-types.json +118 -7
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -154,7 +154,7 @@ el.value = JSON.parse(localStorage.getItem(key) ?? '{}'); // restore on mount
154
154
  // clear the key after the server accepts the submission
155
155
  ```
156
156
 
157
- See `apps/web`'s render page for a working restore/discard/clear-on-submit flow.
157
+ The `/builder` page of the docs site (`bun run --filter docs dev`) shows the same flow live.
158
158
 
159
159
  ### i18n — Thai (or any) built-in strings
160
160
 
@@ -261,7 +261,7 @@ near-black ink (`#171717`) on white, hairline neutral borders, 44px controls, on
261
261
  (`#444FBF`), status as tint pills, hand-drawn koboyo spot illustrations for empty /
262
262
  error / upload states, IBM Plex Sans Thai / IBM Plex Mono (load the
263
263
  fonts yourself), borders-over-shadows, "paper" easing — plus an opt-in dark theme
264
- (`<html data-baht-theme="dark">`). 35+ components:
264
+ (`<html data-baht-theme="dark">`). 59 elements:
265
265
 
266
266
  ```html
267
267
  <baht-button variant="secondary" size="sm">Cancel</baht-button>
@@ -269,16 +269,19 @@ fonts yourself), borders-over-shadows, "paper" easing — plus an opt-in dark th
269
269
  <baht-table></baht-table> <baht-modal></baht-modal> <baht-toast></baht-toast> …
270
270
  ```
271
271
 
272
- - **Data (server-side paging)** — `baht-table` · `baht-list` · `baht-pagination` · `baht-filter`.
272
+ - **Data (server-side paging)** — `baht-table` · `baht-list` · `baht-swipe-row` · `baht-pagination` · `baht-filter`.
273
273
  `baht-table` ships a ServiceNow-style column picker with dot-walking into reference columns (`requester.department`) and emits `baht-columns-change` so the host can persist the layout.
274
274
  `items` is one page; `baht-page-change` asks your app to fetch the next one.
275
275
  - **Feedback** — `baht-modal` · `baht-toast` · `baht-alert` · `baht-tooltip` · `baht-progress` ·
276
276
  `baht-spinner` · `baht-skeleton`
277
277
  - **Inputs** — `baht-rating` · `baht-range` · `baht-otp` · `baht-typeahead` · `baht-reference-picker` ·
278
- `baht-keyvalue-editor` (+ the 11 form field primitives). Typed fields debounce keystrokes (150 ms,
279
- `debounce` / `options.inputDebounce`); `dropdown` renders its own listbox, never the OS picker.
280
- - **Structure & nav** — `baht-card` · `baht-fieldset` · `baht-timeline` · `baht-accordion` ·
281
- `baht-link` · `baht-breadcrumbs`
278
+ `baht-keyvalue-editor` · `baht-upload` (+ the 15 form field primitives). Typed fields debounce keystrokes
279
+ (150 ms, `debounce`); `dropdown` renders its own listbox, never the OS picker.
280
+ - **Stats & charts** — `baht-stat` · `baht-stat-chart` · `baht-meter` · `baht-sparkline` · `baht-bar-chart` ·
281
+ `baht-line-chart` · `baht-donut-chart` · `baht-heatmap` · `baht-progress-ring`
282
+ - **Structure & nav** — `baht-card` · `baht-fieldset` · `baht-timeline` · `baht-accordion` · `baht-steps` ·
283
+ `baht-link` · `baht-breadcrumbs` · `baht-avatar` · `baht-code` · `baht-empty-state` · `baht-error-state` ·
284
+ `baht-loading-overlay`
282
285
 
283
286
  ## Theming — design tokens + light DOM
284
287
 
@@ -305,7 +308,7 @@ Full reference: [docs/theming.md](./docs/theming.md).
305
308
  // 1. per-element subpath — one module per tag, plus only what it imports
306
309
  import 'bank20baht-ui/components/button'; // <baht-button>
307
310
  import { BahtTable } from 'bank20baht-ui/components/table';
308
- import 'bank20baht-ui/components/form'; // <baht-form> + the 14 field primitives
311
+ import 'bank20baht-ui/components/form'; // <baht-form> + the 15 field primitives
309
312
  import 'bank20baht-ui/components/workflow';
310
313
 
311
314
  // 2. by tag list — each tag is a dynamic import, fetched on demand
@@ -344,7 +347,7 @@ no hand-written type declarations in your app:
344
347
  | **Angular** | template binding on properties/events | `CUSTOM_ELEMENTS_SCHEMA`, see below |
345
348
  | **Vue 3** | keep Vue from treating `baht-*` as components | `isCustomElement`, see below |
346
349
  | **Phoenix / Rails / plain HTML** | it's just HTML — works as-is | none |
347
- | **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)) |
350
+ | **Tooling** (Storybook, docgen, custom) | machine-readable API of all 59 elements | read `dist/custom-elements.json` ([CEM standard](https://github.com/webcomponents/custom-elements-manifest)) |
348
351
 
349
352
  **VS Code** — `.vscode/settings.json`:
350
353
 
@@ -400,21 +403,20 @@ import 'bank20baht-ui';
400
403
  `@lit-labs/ssr` is an optional peer dependency (server only). Full guide, limitations
401
404
  (light-DOM children, ids, i18n locale) and troubleshooting: [docs/ssr.md](./docs/ssr.md).
402
405
 
403
- ## Docs, Storybook, examples
406
+ ## Docs
404
407
 
405
- - **Per-component docs**: [`docs/`](./docs/README.md) — one .md per component + framework
406
- guides (vanilla / React / Angular).
407
- - **Storybook**: `pnpm --filter storybook-bahtui dev` → http://localhost:6006.
408
- - **Copy-out starters**: `examples/vanilla`, `examples/react`, `examples/angular` at the
409
- repo root — same workflow JSON in three hosts.
408
+ - **Per-component docs**: [`docs/`](./docs/README.md) — one .md per element + framework
409
+ guides (vanilla / React / Angular) + theming + SSR.
410
+ - **Docs site with live specimens**: https://bahtui-docs.pages.dev (source: `apps/docs`).
411
+ - **For agents**: the `bahtui-mcp` server (`apps/mcp`) serves these docs and runs the engine on your JSON.
410
412
 
411
- ## Field types (13)
413
+ ## Field types (17)
412
414
 
413
- `text · textarea · number · email · password · dropdown · multiselect · checkbox · radio · toggle · date ·
414
- file (capture only, no upload) · label (static)`
415
+ `text · textarea · number · currency · email · password · dropdown · multiselect · tags · checkbox · radio ·
416
+ toggle · date · file · repeater (one level of rows) · label (static) · section (divider)`
415
417
 
416
418
  Each field descriptor: `{ order, type, key, label, placeholder, width: '1/4'|'1/2'|'3/4'|'full',
417
- value, disabled, required, options, max_length, min, max, validators[], formatter }`.
419
+ value, disabled, required, options | options_from, max_length, min, max, validators[], formatter, flag }`.
418
420
 
419
421
  ## Visual Builder
420
422
 
@@ -17,6 +17,7 @@ import "./primitives/baht-number.js";
17
17
  import "./primitives/baht-currency.js";
18
18
  import "./primitives/baht-select.js";
19
19
  import "./primitives/baht-multiselect.js";
20
+ import "./primitives/baht-tags.js";
20
21
  import "./primitives/baht-checkbox.js";
21
22
  import "./primitives/baht-radio.js";
22
23
  import "./primitives/baht-toggle.js";
@@ -15,7 +15,10 @@ import { BahtFieldBase } from '../field-base.js';
15
15
  * Give a clipping ancestor (`overflow: hidden`) room, or it hides the popup.
16
16
  *
17
17
  * A footer under the grid carries **Today** (picks today — greyed out when
18
- * min/max/disabled rules exclude it) and **Clear** (empties the value). Both
18
+ * min/max/disabled rules exclude it) and **Clear** (empties the value). With
19
+ * `field.enable_time` the first button reads **Now** (i18n key `now`) and picks
20
+ * today at the current time, to the minute (seconds dropped), under the same
21
+ * min/max/disabled rules; like Today it closes the calendar. Both
19
22
  * `await el.updateComplete` resolves only once the calendar is attached (the
20
23
  * lazy flatpickr load is folded into it), so hosts and tests need no extra wait.
21
24
  * Today / Clear go through flatpickr, so they emit exactly like a click on a day. `disableMobile`
@@ -1,18 +1,18 @@
1
- import { html as g } from "lit";
2
- import { customElement as D } from "lit/decorators.js";
3
- import { ifDefined as k } from "lit/directives/if-defined.js";
1
+ import { html as y } from "lit";
2
+ import { customElement as g } from "lit/decorators.js";
3
+ import { ifDefined as D } from "lit/directives/if-defined.js";
4
4
  import { BahtFieldBase as w } from "../field-base.js";
5
- import { formatIsoDateTime as u, formatIsoDate as c, dateModeOf as h, parseIsoDate as l, isoDaysOf as C, parseIsoDateTime as v, parseDisabledEntry as _ } from "../../core/dates.js";
5
+ import { formatIsoDateTime as u, formatIsoDate as c, dateModeOf as h, parseIsoDate as l, isoDaysOf as k, parseIsoDateTime as C, parseDisabledEntry as v } from "../../core/dates.js";
6
6
  import { t as p } from "../../core/i18n.js";
7
- import { renderIcon as I } from "./icons.js";
8
- var B = Object.getOwnPropertyDescriptor, O = (t, e, a, s) => {
9
- for (var i = s > 1 ? void 0 : s ? B(e, a) : e, n = t.length - 1, r; n >= 0; n--)
7
+ import { renderIcon as _ } from "./icons.js";
8
+ var I = Object.getOwnPropertyDescriptor, B = (t, e, a, s) => {
9
+ for (var i = s > 1 ? void 0 : s ? I(e, a) : e, n = t.length - 1, r; n >= 0; n--)
10
10
  (r = t[n]) && (i = r(i) || i);
11
11
  return i;
12
12
  };
13
- let f, x;
14
- function E() {
15
- return x ?? (x = import("flatpickr").then((t) => f = t.default));
13
+ let f, O;
14
+ function x() {
15
+ return O ?? (O = import("flatpickr").then((t) => f = t.default));
16
16
  }
17
17
  let m = class extends w {
18
18
  constructor() {
@@ -25,20 +25,20 @@ let m = class extends w {
25
25
  return t;
26
26
  }
27
27
  renderControl() {
28
- return g`<span class="baht-datepicker-field">
28
+ return y`<span class="baht-datepicker-field">
29
29
  <input
30
30
  class="baht-control baht-control--date"
31
31
  id=${this.controlId}
32
32
  type="text"
33
33
  autocomplete="off"
34
- placeholder=${k(this.field.placeholder)}
34
+ placeholder=${D(this.field.placeholder)}
35
35
  ?disabled=${this.disabled}
36
36
  ?readonly=${this.readonly}
37
37
  ?required=${this.required}
38
38
  aria-invalid=${this.invalid}
39
39
  aria-describedby=${this.describedBy}
40
40
  />
41
- <span class="baht-datepicker-field-icon" aria-hidden="true">${I("calendar")}</span>
41
+ <span class="baht-datepicker-field-icon" aria-hidden="true">${_("calendar")}</span>
42
42
  </span>`;
43
43
  }
44
44
  connectedCallback() {
@@ -56,7 +56,7 @@ let m = class extends w {
56
56
  }
57
57
  if (((s = this.fp) == null ? void 0 : s.input) !== e || t.has("field") || t.has("disabled") || t.has("readonly")) {
58
58
  if (this.teardown(), !f) {
59
- this.pendingLoad ?? (this.pendingLoad = E().catch((i) => {
59
+ this.pendingLoad ?? (this.pendingLoad = x().catch((i) => {
60
60
  console.warn("[bahtui] <baht-datepicker> needs the `flatpickr` peer dependency", i);
61
61
  }).then(() => {
62
62
  this.pendingLoad = void 0, this.isConnected && this.requestUpdate();
@@ -119,7 +119,7 @@ let m = class extends w {
119
119
  const e = document.createElement("div");
120
120
  e.className = "baht-datepicker-footer";
121
121
  const a = document.createElement("button");
122
- a.type = "button", a.className = "baht-button baht-button--sm baht-datepicker-today", a.textContent = p("today"), a.addEventListener("click", () => {
122
+ a.type = "button", a.className = "baht-button baht-button--sm baht-datepicker-today", a.textContent = p(this.field.enable_time ? "now" : "today"), a.addEventListener("click", () => {
123
123
  this.pickToday();
124
124
  });
125
125
  const s = document.createElement("button");
@@ -136,18 +136,18 @@ let m = class extends w {
136
136
  }
137
137
  todayAllowed() {
138
138
  const t = o(/* @__PURE__ */ new Date()), e = l(this.field.min), a = l(this.field.max);
139
- return e && t < o(e) || a && t > o(a) ? !1 : !(b(this.field) ?? []).some((s) => F(s, t));
139
+ return e && t < o(e) || a && t > o(a) ? !1 : !(b(this.field) ?? []).some((s) => L(s, t));
140
140
  }
141
141
  pickToday() {
142
142
  const t = this.fp;
143
143
  if (!t || !this.todayAllowed()) return;
144
- const e = o(/* @__PURE__ */ new Date());
144
+ const e = this.field.enable_time ? E(/* @__PURE__ */ new Date()) : o(/* @__PURE__ */ new Date());
145
145
  switch (h(this.field)) {
146
146
  case "range":
147
147
  t.setDate([e, e], !0);
148
148
  break;
149
149
  case "multiple": {
150
- t.selectedDates.some((s) => y(s, e)) || t.setDate([...t.selectedDates, e], !0);
150
+ t.selectedDates.some((s) => this.sameInstants([s], [e])) || t.setDate([...t.selectedDates, e], !0);
151
151
  break;
152
152
  }
153
153
  case "single":
@@ -164,8 +164,8 @@ let m = class extends w {
164
164
  * a host may not always have a time to feed back (an edited record that
165
165
  * predates this field, for one). */
166
166
  datesOf(t) {
167
- const e = (a) => (this.field.enable_time ? v(a) : null) ?? l(a);
168
- return C(t).map(e).filter((a) => a !== null);
167
+ const e = (a) => (this.field.enable_time ? C(a) : null) ?? l(a);
168
+ return k(t).map(e).filter((a) => a !== null);
169
169
  }
170
170
  /** flatpickr's selection -> the value shape of the field's mode. */
171
171
  toValue(t) {
@@ -180,17 +180,20 @@ let m = class extends w {
180
180
  }
181
181
  }
182
182
  };
183
- m = O([
184
- D("baht-datepicker")
183
+ m = B([
184
+ g("baht-datepicker")
185
185
  ], m);
186
186
  function o(t) {
187
187
  return new Date(t.getFullYear(), t.getMonth(), t.getDate());
188
188
  }
189
- function y(t, e) {
190
- return c(t) === c(e);
189
+ function E(t) {
190
+ return new Date(t.getFullYear(), t.getMonth(), t.getDate(), t.getHours(), t.getMinutes());
191
191
  }
192
192
  function F(t, e) {
193
- if (t instanceof Date) return y(t, e);
193
+ return c(t) === c(e);
194
+ }
195
+ function L(t, e) {
196
+ if (t instanceof Date) return F(t, e);
194
197
  if (typeof t == "function") return t(e);
195
198
  if (typeof t == "object" && "from" in t) {
196
199
  const a = o(new Date(t.from)), s = o(new Date(t.to));
@@ -201,7 +204,7 @@ function F(t, e) {
201
204
  function b(t) {
202
205
  const e = [];
203
206
  for (const s of t.disabled_dates ?? []) {
204
- const i = _(s);
207
+ const i = v(s);
205
208
  i && e.push(i[0].getTime() === i[1].getTime() ? i[0] : { from: i[0], to: i[1] });
206
209
  }
207
210
  const a = t.disabled_weekdays;
@@ -1,6 +1,7 @@
1
1
  import { TemplateResult } from 'lit';
2
2
  import { BahtElement } from '../base.js';
3
3
  import { FieldOption } from '../../core/types.js';
4
+ export type FilterVariant = 'chips' | 'segmented';
4
5
  /**
5
6
  * Filter chip group. Controlled: emits `baht-change { selected }` (array of
6
7
  * option keys), host re-feeds `selected`. Typical use: above an <baht-table>,
@@ -10,11 +11,21 @@ import { FieldOption } from '../../core/types.js';
10
11
  * f.options = [{ key: 'running', label: 'Running' }, { key: 'failed', label: 'Failed' }];
11
12
  * f.selected = ['running'];
12
13
  * f.addEventListener('baht-change', (e) => refetch({ status: e.detail.selected }));
14
+ *
15
+ * `variant="segmented"` draws the same options as one joined segmented
16
+ * control (a single bordered group, active segment filled with the ink
17
+ * colour) — for a period switch like Week / Month / Year. Pair it with
18
+ * `multiple="false"`: a segmented control always holds one value, so clicking
19
+ * the active segment does nothing (no `baht-change`) instead of clearing it.
13
20
  */
14
21
  export declare class BahtFilter extends BahtElement {
15
22
  options: FieldOption[];
16
23
  selected: string[];
17
- /** false = radio behavior (max one active; clicking it again clears). */
24
+ /** Look: `'chips'` (default) = separate pill chips; `'segmented'` = one joined
25
+ * segmented control. With `multiple=false` a segmented control never clears. */
26
+ variant: FilterVariant;
27
+ /** false = radio behavior (max one active; clicking it again clears — except
28
+ * with `variant="segmented"`, where the active segment stays selected). */
18
29
  multiple: boolean;
19
30
  disabled: boolean;
20
31
  /** Accessible name for the chip group. */
@@ -1,22 +1,29 @@
1
1
  import { html as h } from "lit";
2
- import { property as p, customElement as c } from "lit/decorators.js";
3
- import { boolOff as d, BahtElement as b } from "../base.js";
4
- var u = Object.defineProperty, f = Object.getOwnPropertyDescriptor, i = (t, s, l, o) => {
5
- for (var e = o > 1 ? void 0 : o ? f(s, l) : s, a = t.length - 1, n; a >= 0; a--)
6
- (n = t[a]) && (e = (o ? n(s, l, e) : n(e)) || e);
7
- return o && e && u(s, l, e), e;
2
+ import { property as o, customElement as d } from "lit/decorators.js";
3
+ import { boolOff as c, BahtElement as b } from "../base.js";
4
+ var m = Object.defineProperty, u = Object.getOwnPropertyDescriptor, l = (s, t, r, a) => {
5
+ for (var e = a > 1 ? void 0 : a ? u(t, r) : t, p = s.length - 1, n; p >= 0; p--)
6
+ (n = s[p]) && (e = (a ? n(t, r, e) : n(e)) || e);
7
+ return a && e && m(t, r, e), e;
8
8
  };
9
- let r = class extends b {
9
+ let i = class extends b {
10
10
  constructor() {
11
- super(...arguments), this.options = [], this.selected = [], this.multiple = !0, this.disabled = !1, this.label = "Filters";
11
+ super(...arguments), this.options = [], this.selected = [], this.variant = "chips", this.multiple = !0, this.disabled = !1, this.label = "Filters";
12
12
  }
13
- toggle(t) {
14
- const s = this.selected.includes(t);
15
- let l;
16
- this.multiple ? (l = s ? this.selected.filter((e) => e !== t) : [...this.selected, t], l = this.options.map((e) => e.key).filter((e) => l.includes(e))) : l = s ? [] : [t], this.emit("baht-change", { selected: l });
13
+ toggle(s) {
14
+ const t = this.selected.includes(s);
15
+ let r;
16
+ if (this.multiple)
17
+ r = t ? this.selected.filter((e) => e !== s) : [...this.selected, s], r = this.options.map((e) => e.key).filter((e) => r.includes(e));
18
+ else {
19
+ if (t && this.variant === "segmented") return;
20
+ r = t ? [] : [s];
21
+ }
22
+ this.emit("baht-change", { selected: r });
17
23
  }
18
24
  render() {
19
- return h`<div class="baht-filter" role="group" aria-label=${this.label}>
25
+ const s = this.variant === "segmented" ? " baht-filter--segmented" : "";
26
+ return h`<div class="baht-filter${s}" role="group" aria-label=${this.label}>
20
27
  ${this.options.map(
21
28
  (t) => h`<button
22
29
  type="button"
@@ -30,24 +37,27 @@ let r = class extends b {
30
37
  </div>`;
31
38
  }
32
39
  };
33
- i([
34
- p({ attribute: !1 })
35
- ], r.prototype, "options", 2);
36
- i([
37
- p({ attribute: !1 })
38
- ], r.prototype, "selected", 2);
39
- i([
40
- p({ converter: d })
41
- ], r.prototype, "multiple", 2);
42
- i([
43
- p({ type: Boolean })
44
- ], r.prototype, "disabled", 2);
45
- i([
46
- p()
47
- ], r.prototype, "label", 2);
48
- r = i([
49
- c("baht-filter")
50
- ], r);
40
+ l([
41
+ o({ attribute: !1 })
42
+ ], i.prototype, "options", 2);
43
+ l([
44
+ o({ attribute: !1 })
45
+ ], i.prototype, "selected", 2);
46
+ l([
47
+ o()
48
+ ], i.prototype, "variant", 2);
49
+ l([
50
+ o({ converter: c })
51
+ ], i.prototype, "multiple", 2);
52
+ l([
53
+ o({ type: Boolean })
54
+ ], i.prototype, "disabled", 2);
55
+ l([
56
+ o()
57
+ ], i.prototype, "label", 2);
58
+ i = l([
59
+ d("baht-filter")
60
+ ], i);
51
61
  export {
52
- r as BahtFilter
62
+ i as BahtFilter
53
63
  };
@@ -1,5 +1,7 @@
1
1
  import { TemplateResult } from 'lit';
2
2
  import { BahtElement } from '../base.js';
3
+ /** Where the dialog panel sits. */
4
+ export type ModalPlacement = 'center' | 'sheet' | 'side';
3
5
  /**
4
6
  * Modal dialog (20baht: 0.4 ink scrim, no blur; float shadow; 20px radius).
5
7
  * Controlled: the host flips `open`. Children become the body.
@@ -9,6 +11,15 @@ import { BahtElement } from '../base.js';
9
11
  * </baht-modal>
10
12
  * modal.open = true;
11
13
  * modal.addEventListener('baht-close', (e) => (modal.open = false));
14
+ *
15
+ * `placement` moves the panel: `'center'` (default) is the dialog above;
16
+ * `'sheet'` docks it to the bottom as a bottom sheet below 600px viewport
17
+ * width (full width, rounded top corners, a grab handle, max 90dvh with the
18
+ * body scrolling, slides up) and is a centered dialog from 600px up; `'side'`
19
+ * is a full-height drawer at the inline end, `min(420px, 100vw)` wide, sliding
20
+ * in from that edge. Scrim, focus trap/restore and `baht-close` are the same in
21
+ * every placement. Modifier classes: `.baht-modal--sheet` / `.baht-modal--side`
22
+ * on the panel, `.baht-modal-scrim--sheet` / `.baht-modal-scrim--side` on the scrim.
12
23
  */
13
24
  export declare class BahtModal extends BahtElement {
14
25
  open: boolean;
@@ -16,6 +27,9 @@ export declare class BahtModal extends BahtElement {
16
27
  /** One muted line under the heading; also becomes the dialog's aria-describedby. */
17
28
  description: string;
18
29
  showClose: boolean;
30
+ /** `'center'` (default) dialog, `'sheet'` bottom sheet below 600px (centered
31
+ * from 600px up), or `'side'` full-height drawer at the inline end. */
32
+ placement: ModalPlacement;
19
33
  /**
20
34
  * Scrim click requests close. Off by default: a stray click outside the panel
21
35
  * must not throw away what the user typed — close via ×, Escape or your buttons.
@@ -1,13 +1,13 @@
1
- import { nothing as a, html as h } from "lit";
2
- import { property as c, query as f, state as m, customElement as b } from "lit/decorators.js";
1
+ import { nothing as r, html as h } from "lit";
2
+ import { property as c, query as m, state as f, customElement as b } from "lit/decorators.js";
3
3
  import { boolOff as y, BahtElement as v, hasDom as p, adoptLightChildren as u } from "../base.js";
4
4
  import { afterExit as g } from "../motion.js";
5
5
  import { t as $ } from "../../core/i18n.js";
6
6
  import { renderIllustration as k } from "./illustrations.js";
7
- var C = Object.defineProperty, S = Object.getOwnPropertyDescriptor, r = (t, i, e, n) => {
8
- for (var o = n > 1 ? void 0 : n ? S(i, e) : i, l = t.length - 1, d; l >= 0; l--)
9
- (d = t[l]) && (o = (n ? d(i, e, o) : d(o)) || o);
10
- return n && o && C(i, e, o), o;
7
+ var C = Object.defineProperty, S = Object.getOwnPropertyDescriptor, l = (t, o, e, n) => {
8
+ for (var i = n > 1 ? void 0 : n ? S(o, e) : o, a = t.length - 1, d; a >= 0; a--)
9
+ (d = t[a]) && (i = (n ? d(o, e, i) : d(i)) || i);
10
+ return n && i && C(o, e, i), i;
11
11
  };
12
12
  const E = {
13
13
  fromAttribute: (t) => t !== null && t !== "false",
@@ -15,7 +15,7 @@ const E = {
15
15
  };
16
16
  let s = class extends v {
17
17
  constructor() {
18
- super(...arguments), this.open = !1, this.heading = "", this.description = "", this.showClose = !0, this.closeOnScrim = !1, this.closing = !1, this.closeToken = 0, this.previousFocus = null, this.onKeydown = (t) => {
18
+ super(...arguments), this.open = !1, this.heading = "", this.description = "", this.showClose = !0, this.placement = "center", this.closeOnScrim = !1, this.closing = !1, this.closeToken = 0, this.previousFocus = null, this.onKeydown = (t) => {
19
19
  !this.open || s.openStack.at(-1) !== this || (t.key === "Escape" ? this.requestClose("escape") : t.key === "Tab" && this.trapFocus(t));
20
20
  };
21
21
  }
@@ -35,17 +35,17 @@ let s = class extends v {
35
35
  }
36
36
  /** Keep Tab cycling inside the dialog (no keyboard escape to the page behind). */
37
37
  trapFocus(t) {
38
- const i = this.focusables(), e = this.querySelector(".baht-modal");
39
- if (i.length === 0) {
38
+ const o = this.focusables(), e = this.querySelector(".baht-modal");
39
+ if (o.length === 0) {
40
40
  e == null || e.focus(), t.preventDefault();
41
41
  return;
42
42
  }
43
- const n = i[0], o = i[i.length - 1], l = document.activeElement;
44
- if (!this.contains(l)) {
43
+ const n = o[0], i = o[o.length - 1], a = document.activeElement;
44
+ if (!this.contains(a)) {
45
45
  n.focus(), t.preventDefault();
46
46
  return;
47
47
  }
48
- t.shiftKey && (l === n || l === e) ? (o.focus(), t.preventDefault()) : !t.shiftKey && l === o && (n.focus(), t.preventDefault());
48
+ t.shiftKey && (a === n || a === e) ? (i.focus(), t.preventDefault()) : !t.shiftKey && a === i && (n.focus(), t.preventDefault());
49
49
  }
50
50
  connectedCallback() {
51
51
  super.connectedCallback(), p() && document.addEventListener("keydown", this.onKeydown);
@@ -66,14 +66,14 @@ let s = class extends v {
66
66
  // already under `.baht-modal-body`) and keeps that content inside the body
67
67
  // instead of leaking it to render outside the modal entirely.
68
68
  updated(t) {
69
- var i;
69
+ var o;
70
70
  if (u(this, this.bodyEl), t.has("open") && (s.openStack = s.openStack.filter((e) => e !== this), this.open && s.openStack.push(this)), t.has("open") && !this.open && this.closing) {
71
- const e = ++this.closeToken, n = this.querySelector(".baht-modal-scrim"), o = () => {
71
+ const e = ++this.closeToken, n = this.querySelector(".baht-modal-scrim"), i = () => {
72
72
  e === this.closeToken && !this.open && (this.closing = !1);
73
73
  };
74
- n ? g(n, o) : o();
74
+ n ? g(n, i) : i();
75
75
  }
76
- if (t.has("open") && this.open && (this.previousFocus = document.activeElement, (i = this.querySelector(".baht-modal")) == null || i.focus()), t.has("open") && !this.open && t.get("open") === !0) {
76
+ if (t.has("open") && this.open && (this.previousFocus = document.activeElement, (o = this.querySelector(".baht-modal")) == null || o.focus()), t.has("open") && !this.open && t.get("open") === !0) {
77
77
  const e = this.previousFocus;
78
78
  this.previousFocus = null, e != null && e.isConnected && (!document.activeElement || document.activeElement === document.body || this.contains(document.activeElement)) && e.focus();
79
79
  }
@@ -83,59 +83,64 @@ let s = class extends v {
83
83
  this.emit("baht-close", { reason: t });
84
84
  }
85
85
  render() {
86
+ const t = this.placement === "sheet" || this.placement === "side" ? this.placement : void 0;
86
87
  return h`<div
87
- class="baht-modal-scrim${this.closing ? " baht-modal-scrim--closing" : ""}"
88
- style=${this.open || this.closing ? a : "display:none"}
89
- @click=${(t) => {
90
- this.closeOnScrim && t.target === t.currentTarget && this.requestClose("scrim");
88
+ class="baht-modal-scrim${t ? ` baht-modal-scrim--${t}` : ""}${this.closing ? " baht-modal-scrim--closing" : ""}"
89
+ style=${this.open || this.closing ? r : "display:none"}
90
+ @click=${(o) => {
91
+ this.closeOnScrim && o.target === o.currentTarget && this.requestClose("scrim");
91
92
  }}
92
93
  >
93
94
  <div
94
- class="baht-modal"
95
+ class="baht-modal${t ? ` baht-modal--${t}` : ""}"
95
96
  role="dialog"
96
97
  aria-modal="true"
97
- aria-labelledby=${this.heading ? this.titleId : a}
98
- aria-label=${this.heading ? a : "Dialog"}
99
- aria-describedby=${this.description ? this.descriptionId : a}
98
+ aria-labelledby=${this.heading ? this.titleId : r}
99
+ aria-label=${this.heading ? r : "Dialog"}
100
+ aria-describedby=${this.description ? this.descriptionId : r}
100
101
  tabindex="-1"
101
102
  >
103
+ ${t === "sheet" ? h`<div class="baht-modal-handle" aria-hidden="true"></div>` : r}
102
104
  ${this.heading || this.description || this.showClose ? h`<header class="baht-modal-header">
103
105
  <div class="baht-modal-heading">
104
106
  <div class="baht-modal-title" id=${this.titleId}>${this.heading}</div>
105
- ${this.description ? h`<p class="baht-modal-description" id=${this.descriptionId}>${this.description}</p>` : a}
107
+ ${this.description ? h`<p class="baht-modal-description" id=${this.descriptionId}>${this.description}</p>` : r}
106
108
  </div>
107
109
  ${this.showClose ? h`<button type="button" class="baht-modal-close" aria-label=${$("close")} @click=${() => {
108
110
  this.requestClose("button");
109
- }}>${k("cross-box")}</button>` : a}
110
- </header>` : a}
111
+ }}>${k("cross-box")}</button>` : r}
112
+ </header>` : r}
111
113
  <div class="baht-modal-body"></div>
112
114
  </div>
113
115
  </div>`;
114
116
  }
115
117
  };
116
118
  s.openStack = [];
117
- r([
119
+ l([
118
120
  c({ type: Boolean, reflect: !0 })
119
121
  ], s.prototype, "open", 2);
120
- r([
122
+ l([
121
123
  c()
122
124
  ], s.prototype, "heading", 2);
123
- r([
125
+ l([
124
126
  c()
125
127
  ], s.prototype, "description", 2);
126
- r([
128
+ l([
127
129
  c({ attribute: "show-close", converter: y })
128
130
  ], s.prototype, "showClose", 2);
129
- r([
131
+ l([
132
+ c()
133
+ ], s.prototype, "placement", 2);
134
+ l([
130
135
  c({ attribute: "close-on-scrim", converter: E })
131
136
  ], s.prototype, "closeOnScrim", 2);
132
- r([
133
- f(".baht-modal-body")
137
+ l([
138
+ m(".baht-modal-body")
134
139
  ], s.prototype, "bodyEl", 2);
135
- r([
136
- m()
140
+ l([
141
+ f()
137
142
  ], s.prototype, "closing", 2);
138
- s = r([
143
+ s = l([
139
144
  b("baht-modal")
140
145
  ], s);
141
146
  export {
@@ -13,6 +13,10 @@ import { BahtFieldBase } from '../field-base.js';
13
13
  * and moves on, typing jumps to the first option starting with those letters
14
14
  * (while closed it picks it directly, like a native select).
15
15
  *
16
+ * An option's `description` renders as a muted second line under its label in
17
+ * the list, and after the label on the closed trigger while it is selected
18
+ * (e.g. label "Food", description "฿580 left").
19
+ *
16
20
  * Value contract unchanged: option `key`, or `null` for the placeholder row.
17
21
  */
18
22
  export declare class BahtSelect extends BahtFieldBase {