@mindees/atlas 0.27.2 → 0.29.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.
package/dist/a11y.d.ts CHANGED
@@ -50,6 +50,15 @@ interface A11yProps {
50
50
  * (the renderer re-applies them via `setProp`), so accessibility tracks state changes.
51
51
  */
52
52
  declare function toA11yProps(a11y: A11yProps): Record<string, unknown>;
53
+ /** Politeness for {@link announce} — `'polite'` waits for a pause; `'assertive'` interrupts. */
54
+ type Announce = 'polite' | 'assertive';
55
+ /**
56
+ * Imperatively announce `message` to screen readers (programmatic, not tied to a rendered node) — for
57
+ * results that aren't otherwise voiced ("3 results found", "Saved", validation errors). Writes into a
58
+ * persistent visually-hidden `aria-live` region (one per politeness), clearing first so the SAME message
59
+ * re-announces. SSR/native-safe: a no-op without a DOM.
60
+ */
61
+ declare function announce(message: string, politeness?: Announce): void;
53
62
  //#endregion
54
- export { A11yProps, A11yState, Role, toA11yProps };
63
+ export { A11yProps, A11yState, Announce, Role, announce, toA11yProps };
55
64
  //# sourceMappingURL=a11y.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"a11y.d.ts","names":[],"sources":["../src/a11y.ts"],"mappings":";;AAUA;;;;AAAgB;AAuBhB;;;KAvBY,IAAA;;UAuBK,SAAA;EACf,QAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;AAAA;;UAee,SAAA;EAER;EAAP,IAAA,GAAO,IAAA;EAcoB;EAZ3B,KAAA;EAYoC;EAVpC,UAAA;EAJO;EAMP,WAAA;EAFA;EAIA,IAAA;EAAA;;;;;EAMA,KAAA,GAAQ,SAAA,UAAmB,SAAA;EAM3B;EAJA,QAAA;EAIQ;EAFR,QAAA;EAUyB;EARzB,QAAA;AAAA;;;;;AAQkD;iBAApC,WAAA,CAAY,IAAA,EAAM,SAAA,GAAY,MAAM"}
1
+ {"version":3,"file":"a11y.d.ts","names":[],"sources":["../src/a11y.ts"],"mappings":";;AAUA;;;;AAAgB;AAuBhB;;;KAvBY,IAAA;;UAuBK,SAAA;EACf,QAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;AAAA;;UAee,SAAA;EAER;EAAP,IAAA,GAAO,IAAA;EAcoB;EAZ3B,KAAA;EAYoC;EAVpC,UAAA;EAJO;EAMP,WAAA;EAFA;EAIA,IAAA;EAAA;;;;;EAMA,KAAA,GAAQ,SAAA,UAAmB,SAAA;EAM3B;EAJA,QAAA;EAIQ;EAFR,QAAA;EAUyB;EARzB,QAAA;AAAA;;;;;AAQkD;iBAApC,WAAA,CAAY,IAAA,EAAM,SAAA,GAAY,MAAM;;KAkCxC,QAAA;;AAAQ;AAoBpB;;;;iBAAgB,QAAA,CAAS,OAAA,UAAiB,UAAA,GAAY,QAAmB"}
package/dist/a11y.js CHANGED
@@ -41,7 +41,34 @@ function toA11yProps(a11y) {
41
41
  else if (vn !== void 0) out["aria-valuenow"] = String(vn);
42
42
  return out;
43
43
  }
44
+ const liveRegions = {};
45
+ /**
46
+ * Imperatively announce `message` to screen readers (programmatic, not tied to a rendered node) — for
47
+ * results that aren't otherwise voiced ("3 results found", "Saved", validation errors). Writes into a
48
+ * persistent visually-hidden `aria-live` region (one per politeness), clearing first so the SAME message
49
+ * re-announces. SSR/native-safe: a no-op without a DOM.
50
+ */
51
+ function announce(message, politeness = "polite") {
52
+ const doc = globalThis.document;
53
+ if (!doc || typeof doc.createElement !== "function" || !doc.body) return;
54
+ let region = liveRegions[politeness];
55
+ if (!region) {
56
+ const el = doc.createElement("div");
57
+ el.setAttribute("aria-live", politeness);
58
+ el.setAttribute("aria-atomic", "true");
59
+ el.setAttribute("role", politeness === "assertive" ? "alert" : "status");
60
+ el.style.cssText = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0";
61
+ doc.body.appendChild(el);
62
+ region = el;
63
+ liveRegions[politeness] = el;
64
+ }
65
+ region.textContent = "";
66
+ const r = region;
67
+ (globalThis.requestAnimationFrame ?? ((cb) => setTimeout(cb, 16)))(() => {
68
+ r.textContent = message;
69
+ });
70
+ }
44
71
  //#endregion
45
- export { toA11yProps };
72
+ export { announce, toA11yProps };
46
73
 
47
74
  //# sourceMappingURL=a11y.js.map
package/dist/a11y.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"a11y.js","names":[],"sources":["../src/a11y.ts"],"sourcesContent":["/**\n * Atlas accessibility — a single typed `A11yProps` surface lowered to `role` + `aria-*`\n * attribute props. The DOM backend passes these through verbatim (`setAttribute`), so web a11y\n * is real; native hosts receive them as serialized props (interpretation is a 🔬 research-track\n * host concern — carried, never silently dropped). See `docs/adr/0022-atlas-primitives.md`.\n *\n * @module\n */\n\n/** A WAI-ARIA-ish role, mapped straight to the host `role` attribute on web. */\nexport type Role =\n | 'button'\n | 'link'\n | 'image'\n | 'heading'\n | 'list'\n | 'listitem'\n | 'text'\n | 'textbox'\n | 'checkbox'\n | 'switch'\n | 'radio'\n | 'tab'\n | 'tabpanel'\n | 'dialog'\n | 'alert'\n | 'status'\n | 'separator'\n | 'progressbar'\n | 'none'\n | 'presentation'\n\n/** Accessibility state, lowered to the matching `aria-*` attributes. */\nexport interface A11yState {\n disabled?: boolean\n selected?: boolean\n checked?: boolean\n pressed?: boolean\n expanded?: boolean\n busy?: boolean\n hidden?: boolean\n}\n\n/** The boolean state keys, paired with their `aria-*` attribute. */\nconst STATE_ARIA = [\n ['disabled', 'aria-disabled'],\n ['selected', 'aria-selected'],\n ['checked', 'aria-checked'],\n ['pressed', 'aria-pressed'],\n ['expanded', 'aria-expanded'],\n ['busy', 'aria-busy'],\n ['hidden', 'aria-hidden'],\n] as const\n\n/** The accessibility surface every Atlas primitive accepts. */\nexport interface A11yProps {\n /** ARIA role (web `role`). */\n role?: Role\n /** Accessible name (`aria-label`). */\n label?: string\n /** Id(s) of the element(s) labelling this one (`aria-labelledby`). */\n labelledBy?: string\n /** Id(s) of the element(s) describing this one (`aria-describedby`). */\n describedBy?: string\n /** Live-region politeness (`aria-live`). */\n live?: 'off' | 'polite' | 'assertive'\n /**\n * Accessibility state → `aria-*`. Pass an **accessor** (`() => ({ checked: on() })`) to make the\n * `aria-*` attributes reactive — a static object bakes them once, so a screen reader never hears\n * a toggle change. Reactive keys are those present on the first read (stable shape).\n */\n state?: A11yState | (() => A11yState)\n /** Current value of a range widget (`aria-valuenow`); accessor → reactive. */\n valueNow?: number | (() => number)\n /** Minimum of a range widget (`aria-valuemin`). */\n valueMin?: number\n /** Maximum of a range widget (`aria-valuemax`). */\n valueMax?: number\n}\n\n/**\n * Lower {@link A11yProps} to a host prop bag of `role` + `aria-*` (only defined keys, so omitted\n * props stay omitted). Accessor-valued `state`/`valueNow` lower to **reactive** attribute bindings\n * (the renderer re-applies them via `setProp`), so accessibility tracks state changes.\n */\nexport function toA11yProps(a11y: A11yProps): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n if (a11y.role !== undefined) out.role = a11y.role\n if (a11y.label !== undefined) out['aria-label'] = a11y.label\n if (a11y.labelledBy !== undefined) out['aria-labelledby'] = a11y.labelledBy\n if (a11y.describedBy !== undefined) out['aria-describedby'] = a11y.describedBy\n if (a11y.live !== undefined) out['aria-live'] = a11y.live\n\n const s = a11y.state\n if (typeof s === 'function') {\n const initial = s()\n for (const [key, attr] of STATE_ARIA) {\n if (initial[key] === undefined) continue\n out[attr] = () => {\n const v = s()[key]\n return v === undefined ? undefined : String(v)\n }\n }\n } else if (s) {\n for (const [key, attr] of STATE_ARIA) {\n if (s[key] !== undefined) out[attr] = String(s[key])\n }\n }\n\n if (a11y.valueMin !== undefined) out['aria-valuemin'] = String(a11y.valueMin)\n if (a11y.valueMax !== undefined) out['aria-valuemax'] = String(a11y.valueMax)\n const vn = a11y.valueNow\n if (typeof vn === 'function') out['aria-valuenow'] = () => String(vn())\n else if (vn !== undefined) out['aria-valuenow'] = String(vn)\n\n return out\n}\n"],"mappings":";;AA4CA,MAAM,aAAa;CACjB,CAAC,YAAY,eAAe;CAC5B,CAAC,YAAY,eAAe;CAC5B,CAAC,WAAW,cAAc;CAC1B,CAAC,WAAW,cAAc;CAC1B,CAAC,YAAY,eAAe;CAC5B,CAAC,QAAQ,WAAW;CACpB,CAAC,UAAU,aAAa;AAC1B;;;;;;AAiCA,SAAgB,YAAY,MAA0C;CACpE,MAAM,MAA+B,CAAC;CACtC,IAAI,KAAK,SAAS,KAAA,GAAW,IAAI,OAAO,KAAK;CAC7C,IAAI,KAAK,UAAU,KAAA,GAAW,IAAI,gBAAgB,KAAK;CACvD,IAAI,KAAK,eAAe,KAAA,GAAW,IAAI,qBAAqB,KAAK;CACjE,IAAI,KAAK,gBAAgB,KAAA,GAAW,IAAI,sBAAsB,KAAK;CACnE,IAAI,KAAK,SAAS,KAAA,GAAW,IAAI,eAAe,KAAK;CAErD,MAAM,IAAI,KAAK;CACf,IAAI,OAAO,MAAM,YAAY;EAC3B,MAAM,UAAU,EAAE;EAClB,KAAK,MAAM,CAAC,KAAK,SAAS,YAAY;GACpC,IAAI,QAAQ,SAAS,KAAA,GAAW;GAChC,IAAI,cAAc;IAChB,MAAM,IAAI,EAAE,EAAE;IACd,OAAO,MAAM,KAAA,IAAY,KAAA,IAAY,OAAO,CAAC;GAC/C;EACF;CACF,OAAO,IAAI;OACJ,MAAM,CAAC,KAAK,SAAS,YACxB,IAAI,EAAE,SAAS,KAAA,GAAW,IAAI,QAAQ,OAAO,EAAE,IAAI;CAAA;CAIvD,IAAI,KAAK,aAAa,KAAA,GAAW,IAAI,mBAAmB,OAAO,KAAK,QAAQ;CAC5E,IAAI,KAAK,aAAa,KAAA,GAAW,IAAI,mBAAmB,OAAO,KAAK,QAAQ;CAC5E,MAAM,KAAK,KAAK;CAChB,IAAI,OAAO,OAAO,YAAY,IAAI,yBAAyB,OAAO,GAAG,CAAC;MACjE,IAAI,OAAO,KAAA,GAAW,IAAI,mBAAmB,OAAO,EAAE;CAE3D,OAAO;AACT"}
1
+ {"version":3,"file":"a11y.js","names":[],"sources":["../src/a11y.ts"],"sourcesContent":["/**\n * Atlas accessibility — a single typed `A11yProps` surface lowered to `role` + `aria-*`\n * attribute props. The DOM backend passes these through verbatim (`setAttribute`), so web a11y\n * is real; native hosts receive them as serialized props (interpretation is a 🔬 research-track\n * host concern — carried, never silently dropped). See `docs/adr/0022-atlas-primitives.md`.\n *\n * @module\n */\n\n/** A WAI-ARIA-ish role, mapped straight to the host `role` attribute on web. */\nexport type Role =\n | 'button'\n | 'link'\n | 'image'\n | 'heading'\n | 'list'\n | 'listitem'\n | 'text'\n | 'textbox'\n | 'checkbox'\n | 'switch'\n | 'radio'\n | 'tab'\n | 'tabpanel'\n | 'dialog'\n | 'alert'\n | 'status'\n | 'separator'\n | 'progressbar'\n | 'none'\n | 'presentation'\n\n/** Accessibility state, lowered to the matching `aria-*` attributes. */\nexport interface A11yState {\n disabled?: boolean\n selected?: boolean\n checked?: boolean\n pressed?: boolean\n expanded?: boolean\n busy?: boolean\n hidden?: boolean\n}\n\n/** The boolean state keys, paired with their `aria-*` attribute. */\nconst STATE_ARIA = [\n ['disabled', 'aria-disabled'],\n ['selected', 'aria-selected'],\n ['checked', 'aria-checked'],\n ['pressed', 'aria-pressed'],\n ['expanded', 'aria-expanded'],\n ['busy', 'aria-busy'],\n ['hidden', 'aria-hidden'],\n] as const\n\n/** The accessibility surface every Atlas primitive accepts. */\nexport interface A11yProps {\n /** ARIA role (web `role`). */\n role?: Role\n /** Accessible name (`aria-label`). */\n label?: string\n /** Id(s) of the element(s) labelling this one (`aria-labelledby`). */\n labelledBy?: string\n /** Id(s) of the element(s) describing this one (`aria-describedby`). */\n describedBy?: string\n /** Live-region politeness (`aria-live`). */\n live?: 'off' | 'polite' | 'assertive'\n /**\n * Accessibility state → `aria-*`. Pass an **accessor** (`() => ({ checked: on() })`) to make the\n * `aria-*` attributes reactive — a static object bakes them once, so a screen reader never hears\n * a toggle change. Reactive keys are those present on the first read (stable shape).\n */\n state?: A11yState | (() => A11yState)\n /** Current value of a range widget (`aria-valuenow`); accessor → reactive. */\n valueNow?: number | (() => number)\n /** Minimum of a range widget (`aria-valuemin`). */\n valueMin?: number\n /** Maximum of a range widget (`aria-valuemax`). */\n valueMax?: number\n}\n\n/**\n * Lower {@link A11yProps} to a host prop bag of `role` + `aria-*` (only defined keys, so omitted\n * props stay omitted). Accessor-valued `state`/`valueNow` lower to **reactive** attribute bindings\n * (the renderer re-applies them via `setProp`), so accessibility tracks state changes.\n */\nexport function toA11yProps(a11y: A11yProps): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n if (a11y.role !== undefined) out.role = a11y.role\n if (a11y.label !== undefined) out['aria-label'] = a11y.label\n if (a11y.labelledBy !== undefined) out['aria-labelledby'] = a11y.labelledBy\n if (a11y.describedBy !== undefined) out['aria-describedby'] = a11y.describedBy\n if (a11y.live !== undefined) out['aria-live'] = a11y.live\n\n const s = a11y.state\n if (typeof s === 'function') {\n const initial = s()\n for (const [key, attr] of STATE_ARIA) {\n if (initial[key] === undefined) continue\n out[attr] = () => {\n const v = s()[key]\n return v === undefined ? undefined : String(v)\n }\n }\n } else if (s) {\n for (const [key, attr] of STATE_ARIA) {\n if (s[key] !== undefined) out[attr] = String(s[key])\n }\n }\n\n if (a11y.valueMin !== undefined) out['aria-valuemin'] = String(a11y.valueMin)\n if (a11y.valueMax !== undefined) out['aria-valuemax'] = String(a11y.valueMax)\n const vn = a11y.valueNow\n if (typeof vn === 'function') out['aria-valuenow'] = () => String(vn())\n else if (vn !== undefined) out['aria-valuenow'] = String(vn)\n\n return out\n}\n\n/** Politeness for {@link announce} — `'polite'` waits for a pause; `'assertive'` interrupts. */\nexport type Announce = 'polite' | 'assertive'\n\n// One persistent visually-hidden live region per politeness, reused across calls (created lazily).\nconst liveRegions: Partial<Record<Announce, { textContent: string }>> = {}\n\ninterface DocLike {\n createElement(tag: string): {\n setAttribute(name: string, value: string): void\n style: { cssText: string }\n textContent: string\n }\n body: { appendChild(node: unknown): void } | null\n}\n\n/**\n * Imperatively announce `message` to screen readers (programmatic, not tied to a rendered node) — for\n * results that aren't otherwise voiced (\"3 results found\", \"Saved\", validation errors). Writes into a\n * persistent visually-hidden `aria-live` region (one per politeness), clearing first so the SAME message\n * re-announces. SSR/native-safe: a no-op without a DOM.\n */\nexport function announce(message: string, politeness: Announce = 'polite'): void {\n const doc = (globalThis as unknown as { document?: DocLike }).document\n if (!doc || typeof doc.createElement !== 'function' || !doc.body) return\n let region = liveRegions[politeness]\n if (!region) {\n const el = doc.createElement('div')\n el.setAttribute('aria-live', politeness)\n el.setAttribute('aria-atomic', 'true')\n el.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status')\n // Visually hidden but still announced (the standard sr-only clip pattern).\n el.style.cssText =\n 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0'\n doc.body.appendChild(el)\n region = el\n liveRegions[politeness] = el\n }\n // Clear then set on the next tick so a repeated identical message is still re-announced.\n region.textContent = ''\n const r = region\n const schedule =\n (globalThis as { requestAnimationFrame?: (cb: () => void) => void }).requestAnimationFrame ??\n ((cb: () => void) => setTimeout(cb, 16))\n schedule(() => {\n r.textContent = message\n })\n}\n"],"mappings":";;AA4CA,MAAM,aAAa;CACjB,CAAC,YAAY,eAAe;CAC5B,CAAC,YAAY,eAAe;CAC5B,CAAC,WAAW,cAAc;CAC1B,CAAC,WAAW,cAAc;CAC1B,CAAC,YAAY,eAAe;CAC5B,CAAC,QAAQ,WAAW;CACpB,CAAC,UAAU,aAAa;AAC1B;;;;;;AAiCA,SAAgB,YAAY,MAA0C;CACpE,MAAM,MAA+B,CAAC;CACtC,IAAI,KAAK,SAAS,KAAA,GAAW,IAAI,OAAO,KAAK;CAC7C,IAAI,KAAK,UAAU,KAAA,GAAW,IAAI,gBAAgB,KAAK;CACvD,IAAI,KAAK,eAAe,KAAA,GAAW,IAAI,qBAAqB,KAAK;CACjE,IAAI,KAAK,gBAAgB,KAAA,GAAW,IAAI,sBAAsB,KAAK;CACnE,IAAI,KAAK,SAAS,KAAA,GAAW,IAAI,eAAe,KAAK;CAErD,MAAM,IAAI,KAAK;CACf,IAAI,OAAO,MAAM,YAAY;EAC3B,MAAM,UAAU,EAAE;EAClB,KAAK,MAAM,CAAC,KAAK,SAAS,YAAY;GACpC,IAAI,QAAQ,SAAS,KAAA,GAAW;GAChC,IAAI,cAAc;IAChB,MAAM,IAAI,EAAE,EAAE;IACd,OAAO,MAAM,KAAA,IAAY,KAAA,IAAY,OAAO,CAAC;GAC/C;EACF;CACF,OAAO,IAAI;OACJ,MAAM,CAAC,KAAK,SAAS,YACxB,IAAI,EAAE,SAAS,KAAA,GAAW,IAAI,QAAQ,OAAO,EAAE,IAAI;CAAA;CAIvD,IAAI,KAAK,aAAa,KAAA,GAAW,IAAI,mBAAmB,OAAO,KAAK,QAAQ;CAC5E,IAAI,KAAK,aAAa,KAAA,GAAW,IAAI,mBAAmB,OAAO,KAAK,QAAQ;CAC5E,MAAM,KAAK,KAAK;CAChB,IAAI,OAAO,OAAO,YAAY,IAAI,yBAAyB,OAAO,GAAG,CAAC;MACjE,IAAI,OAAO,KAAA,GAAW,IAAI,mBAAmB,OAAO,EAAE;CAE3D,OAAO;AACT;AAMA,MAAM,cAAkE,CAAC;;;;;;;AAiBzE,SAAgB,SAAS,SAAiB,aAAuB,UAAgB;CAC/E,MAAM,MAAO,WAAiD;CAC9D,IAAI,CAAC,OAAO,OAAO,IAAI,kBAAkB,cAAc,CAAC,IAAI,MAAM;CAClE,IAAI,SAAS,YAAY;CACzB,IAAI,CAAC,QAAQ;EACX,MAAM,KAAK,IAAI,cAAc,KAAK;EAClC,GAAG,aAAa,aAAa,UAAU;EACvC,GAAG,aAAa,eAAe,MAAM;EACrC,GAAG,aAAa,QAAQ,eAAe,cAAc,UAAU,QAAQ;EAEvE,GAAG,MAAM,UACP;EACF,IAAI,KAAK,YAAY,EAAE;EACvB,SAAS;EACT,YAAY,cAAc;CAC5B;CAEA,OAAO,cAAc;CACrB,MAAM,IAAI;CAIV,CAFG,WAAoE,2BACnE,OAAmB,WAAW,IAAI,EAAE,UACzB;EACb,EAAE,cAAc;CAClB,CAAC;AACH"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A11yProps, A11yState, Role, toA11yProps } from "./a11y.js";
1
+ import { A11yProps, A11yState, Announce, Role, announce, toA11yProps } from "./a11y.js";
2
2
  import { StyleInput, StyleObject, StyleValue, flattenStyle } from "./style.js";
3
3
  import { BaseProps, Reactive, resolveStyle, toHostProps } from "./host.js";
4
4
  import { Accordion, AccordionProps, AccordionSection, ActivityIndicator, ActivityIndicatorProps, Avatar, AvatarProps, Badge, BadgeProps, Card, CardProps, Checkbox, CheckboxProps, Chip, ChipProps, Divider, DividerProps, KeyboardAvoidingView, KeyboardAvoidingViewProps, ProgressBar, ProgressBarProps, RadioGroup, RadioGroupProps, RadioOption, SafeAreaView, SafeAreaViewProps, Segment, SegmentedControl, SegmentedControlProps, Skeleton, SkeletonProps, Stepper, StepperProps, Switch, SwitchProps, TabItem, Tabs, TabsProps } from "./components.js";
@@ -19,7 +19,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
19
19
  /** The npm package name. */
20
20
  declare const name = "@mindees/atlas";
21
21
  /** The package version. All `@mindees/*` packages share one locked version line. */
22
- declare const VERSION = "0.27.2";
22
+ declare const VERSION = "0.29.0";
23
23
  /** Current maturity of this package. See the repository `STATUS.md`. */
24
24
  declare const maturity: Maturity;
25
25
  /**
@@ -29,5 +29,5 @@ declare const maturity: Maturity;
29
29
  */
30
30
  declare const info: PackageInfo;
31
31
  //#endregion
32
- export { type A11yProps, type A11yState, Accordion, type AccordionProps, type AccordionSection, ActivityIndicator, type ActivityIndicatorProps, type AsyncState, type AttachableGesture, Avatar, type AvatarProps, Badge, type BadgeProps, type BaseProps, Button, type ButtonProps, Card, type CardProps, Checkbox, type CheckboxProps, Chip, type ChipProps, type ColorScheme, Column, type Counter, Divider, type DividerProps, ErrorBoundary, type ErrorBoundaryProps, type Field, FocusScope, type FocusScopeProps, type FormApi, GestureView, type GestureViewProps, Image, type ImageProps, type InteractionState, KeyboardAvoidingView, type KeyboardAvoidingViewProps, type KeyboardState, type Maturity, Modal, type ModalProps, NotImplementedError, type PackageInfo, type PersistentSignalOptions, type PlatformEnvironment, Pressable, type PressableProps, ProgressBar, type ProgressBarProps, RadioGroup, type RadioGroupProps, type RadioOption, type Reactive, type Role, Row, type SafeAreaInsets, SafeAreaView, type SafeAreaViewProps, ScrollView, type ScrollViewProps, type Segment, SegmentedControl, type SegmentedControlProps, Show, type ShowProps, type SignalStorage, Skeleton, type SkeletonProps, Spacer, type SpacerProps, Stack, type StackProps, Stepper, type StepperProps, type StyleInput, type StyleObject, type StyleValue, Switch, type SwitchProps, type TabItem, Tabs, type TabsProps, Text, TextInput, type TextInputProps, type TextProps, type Theme, type ThemeColors, Toast, type ToastProps, type Toggle, type UseFormOptions, VERSION, View, type ViewProps, type WebEnvWindow, type WindowDimensions, animateTo, connectWebEnvironment, duration, easing, flattenStyle, fontSize, fontWeight, getEnvironment, getTheme, info, lineHeight, maturity, motion, name, notImplemented, palette, radius, resolveStyle, setEnvironment, space, toA11yProps, toHostProps, tokens, useAsync, useColorScheme, useCounter, useDebounce, useForm, useInterval, useKeyboard, usePersistentSignal, usePressable, usePrevious, useReducedMotion, useReducer, useSafeAreaInsets, useTheme, useTimeout, useToggle, useWindowDimensions };
32
+ export { type A11yProps, type A11yState, Accordion, type AccordionProps, type AccordionSection, ActivityIndicator, type ActivityIndicatorProps, type Announce, type AsyncState, type AttachableGesture, Avatar, type AvatarProps, Badge, type BadgeProps, type BaseProps, Button, type ButtonProps, Card, type CardProps, Checkbox, type CheckboxProps, Chip, type ChipProps, type ColorScheme, Column, type Counter, Divider, type DividerProps, ErrorBoundary, type ErrorBoundaryProps, type Field, FocusScope, type FocusScopeProps, type FormApi, GestureView, type GestureViewProps, Image, type ImageProps, type InteractionState, KeyboardAvoidingView, type KeyboardAvoidingViewProps, type KeyboardState, type Maturity, Modal, type ModalProps, NotImplementedError, type PackageInfo, type PersistentSignalOptions, type PlatformEnvironment, Pressable, type PressableProps, ProgressBar, type ProgressBarProps, RadioGroup, type RadioGroupProps, type RadioOption, type Reactive, type Role, Row, type SafeAreaInsets, SafeAreaView, type SafeAreaViewProps, ScrollView, type ScrollViewProps, type Segment, SegmentedControl, type SegmentedControlProps, Show, type ShowProps, type SignalStorage, Skeleton, type SkeletonProps, Spacer, type SpacerProps, Stack, type StackProps, Stepper, type StepperProps, type StyleInput, type StyleObject, type StyleValue, Switch, type SwitchProps, type TabItem, Tabs, type TabsProps, Text, TextInput, type TextInputProps, type TextProps, type Theme, type ThemeColors, Toast, type ToastProps, type Toggle, type UseFormOptions, VERSION, View, type ViewProps, type WebEnvWindow, type WindowDimensions, animateTo, announce, connectWebEnvironment, duration, easing, flattenStyle, fontSize, fontWeight, getEnvironment, getTheme, info, lineHeight, maturity, motion, name, notImplemented, palette, radius, resolveStyle, setEnvironment, space, toA11yProps, toHostProps, tokens, useAsync, useColorScheme, useCounter, useDebounce, useForm, useInterval, useKeyboard, usePersistentSignal, usePressable, usePrevious, useReducedMotion, useReducer, useSafeAreaInsets, useTheme, useTimeout, useToggle, useWindowDimensions };
33
33
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { toA11yProps } from "./a11y.js";
1
+ import { announce, toA11yProps } from "./a11y.js";
2
2
  import { getEnvironment, setEnvironment, useColorScheme, useKeyboard, useReducedMotion, useSafeAreaInsets, useWindowDimensions } from "./environment.js";
3
3
  import { flattenStyle } from "./style.js";
4
4
  import { resolveStyle, toHostProps } from "./host.js";
@@ -18,7 +18,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
18
18
  /** The npm package name. */
19
19
  const name = "@mindees/atlas";
20
20
  /** The package version. All `@mindees/*` packages share one locked version line. */
21
- const VERSION = "0.27.2";
21
+ const VERSION = "0.29.0";
22
22
  /** Current maturity of this package. See the repository `STATUS.md`. */
23
23
  const maturity = "experimental";
24
24
  /**
@@ -32,6 +32,6 @@ const info = Object.freeze({
32
32
  maturity
33
33
  });
34
34
  //#endregion
35
- export { Accordion, ActivityIndicator, Avatar, Badge, Button, Card, Checkbox, Chip, Column, Divider, ErrorBoundary, FocusScope, GestureView, Image, KeyboardAvoidingView, Modal, NotImplementedError, Pressable, ProgressBar, RadioGroup, Row, SafeAreaView, ScrollView, SegmentedControl, Show, Skeleton, Spacer, Stack, Stepper, Switch, Tabs, Text, TextInput, Toast, VERSION, View, animateTo, connectWebEnvironment, duration, easing, flattenStyle, fontSize, fontWeight, getEnvironment, getTheme, info, lineHeight, maturity, motion, name, notImplemented, palette, radius, resolveStyle, setEnvironment, space, toA11yProps, toHostProps, tokens, useAsync, useColorScheme, useCounter, useDebounce, useForm, useInterval, useKeyboard, usePersistentSignal, usePressable, usePrevious, useReducedMotion, useReducer, useSafeAreaInsets, useTheme, useTimeout, useToggle, useWindowDimensions };
35
+ export { Accordion, ActivityIndicator, Avatar, Badge, Button, Card, Checkbox, Chip, Column, Divider, ErrorBoundary, FocusScope, GestureView, Image, KeyboardAvoidingView, Modal, NotImplementedError, Pressable, ProgressBar, RadioGroup, Row, SafeAreaView, ScrollView, SegmentedControl, Show, Skeleton, Spacer, Stack, Stepper, Switch, Tabs, Text, TextInput, Toast, VERSION, View, animateTo, announce, connectWebEnvironment, duration, easing, flattenStyle, fontSize, fontWeight, getEnvironment, getTheme, info, lineHeight, maturity, motion, name, notImplemented, palette, radius, resolveStyle, setEnvironment, space, toA11yProps, toHostProps, tokens, useAsync, useColorScheme, useCounter, useDebounce, useForm, useInterval, useKeyboard, usePersistentSignal, usePressable, usePrevious, useReducedMotion, useReducer, useSafeAreaInsets, useTheme, useTimeout, useToggle, useWindowDimensions };
36
36
 
37
37
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/atlas` (Atlas) — accessible, signals-native UI primitives. Function components\n * over `@mindees/core`'s `createElement` that return renderer-agnostic `MindeesNode` trees:\n * web rendering is real via the Helix DOM backend; native is a labeled 🔬 research track (the\n * same serializable tree, interpreted by a native host later). A curated cross-platform\n * `StyleObject`, typed accessibility, and design-token theming (`useTheme`/`tokens`, on the main entry).\n * The virtualized recycling `List` is on the `@mindees/atlas/list` subpath.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/atlas'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.27.2'\n\n/** Current maturity of this package. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport { type A11yProps, type A11yState, type Role, toA11yProps } from './a11y'\nexport {\n Accordion,\n type AccordionProps,\n type AccordionSection,\n ActivityIndicator,\n type ActivityIndicatorProps,\n Avatar,\n type AvatarProps,\n Badge,\n type BadgeProps,\n Card,\n type CardProps,\n Checkbox,\n type CheckboxProps,\n Chip,\n type ChipProps,\n Divider,\n type DividerProps,\n KeyboardAvoidingView,\n type KeyboardAvoidingViewProps,\n ProgressBar,\n type ProgressBarProps,\n RadioGroup,\n type RadioGroupProps,\n type RadioOption,\n SafeAreaView,\n type SafeAreaViewProps,\n type Segment,\n SegmentedControl,\n type SegmentedControlProps,\n Skeleton,\n type SkeletonProps,\n Stepper,\n type StepperProps,\n Switch,\n type SwitchProps,\n type TabItem,\n Tabs,\n type TabsProps,\n} from './components'\nexport {\n type ColorScheme,\n getEnvironment,\n type KeyboardState,\n type PlatformEnvironment,\n type SafeAreaInsets,\n setEnvironment,\n useColorScheme,\n useKeyboard,\n useReducedMotion,\n useSafeAreaInsets,\n useWindowDimensions,\n type WindowDimensions,\n} from './environment'\nexport { ErrorBoundary, type ErrorBoundaryProps } from './error-boundary'\nexport { type Field, type FormApi, type UseFormOptions, useForm } from './form'\nexport { type AttachableGesture, GestureView, type GestureViewProps } from './gesture'\nexport {\n type AsyncState,\n type Counter,\n type PersistentSignalOptions,\n type SignalStorage,\n type Toggle,\n useAsync,\n useCounter,\n useDebounce,\n useInterval,\n usePersistentSignal,\n usePrevious,\n useReducer,\n useTimeout,\n useToggle,\n} from './hooks'\nexport { type BaseProps, type Reactive, resolveStyle, toHostProps } from './host'\nexport { animateTo, motion } from './motion'\nexport {\n FocusScope,\n type FocusScopeProps,\n Modal,\n type ModalProps,\n Toast,\n type ToastProps,\n} from './overlay'\nexport {\n Button,\n type ButtonProps,\n Column,\n Image,\n type ImageProps,\n type InteractionState,\n Pressable,\n type PressableProps,\n Row,\n ScrollView,\n type ScrollViewProps,\n Spacer,\n type SpacerProps,\n Stack,\n type StackProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextProps,\n usePressable,\n View,\n type ViewProps,\n} from './primitives'\nexport { Show, type ShowProps } from './show'\nexport { flattenStyle, type StyleInput, type StyleObject, type StyleValue } from './style'\nexport {\n duration,\n easing,\n fontSize,\n fontWeight,\n getTheme,\n lineHeight,\n palette,\n radius,\n space,\n type Theme,\n type ThemeColors,\n tokens,\n useTheme,\n} from './tokens'\nexport { connectWebEnvironment, type WebEnvWindow } from './web-environment'\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;;;;;;;AAeA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/atlas` (Atlas) — accessible, signals-native UI primitives. Function components\n * over `@mindees/core`'s `createElement` that return renderer-agnostic `MindeesNode` trees:\n * web rendering is real via the Helix DOM backend; native is a labeled 🔬 research track (the\n * same serializable tree, interpreted by a native host later). A curated cross-platform\n * `StyleObject`, typed accessibility, and design-token theming (`useTheme`/`tokens`, on the main entry).\n * The virtualized recycling `List` is on the `@mindees/atlas/list` subpath.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/atlas'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.29.0'\n\n/** Current maturity of this package. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport {\n type A11yProps,\n type A11yState,\n type Announce,\n announce,\n type Role,\n toA11yProps,\n} from './a11y'\nexport {\n Accordion,\n type AccordionProps,\n type AccordionSection,\n ActivityIndicator,\n type ActivityIndicatorProps,\n Avatar,\n type AvatarProps,\n Badge,\n type BadgeProps,\n Card,\n type CardProps,\n Checkbox,\n type CheckboxProps,\n Chip,\n type ChipProps,\n Divider,\n type DividerProps,\n KeyboardAvoidingView,\n type KeyboardAvoidingViewProps,\n ProgressBar,\n type ProgressBarProps,\n RadioGroup,\n type RadioGroupProps,\n type RadioOption,\n SafeAreaView,\n type SafeAreaViewProps,\n type Segment,\n SegmentedControl,\n type SegmentedControlProps,\n Skeleton,\n type SkeletonProps,\n Stepper,\n type StepperProps,\n Switch,\n type SwitchProps,\n type TabItem,\n Tabs,\n type TabsProps,\n} from './components'\nexport {\n type ColorScheme,\n getEnvironment,\n type KeyboardState,\n type PlatformEnvironment,\n type SafeAreaInsets,\n setEnvironment,\n useColorScheme,\n useKeyboard,\n useReducedMotion,\n useSafeAreaInsets,\n useWindowDimensions,\n type WindowDimensions,\n} from './environment'\nexport { ErrorBoundary, type ErrorBoundaryProps } from './error-boundary'\nexport { type Field, type FormApi, type UseFormOptions, useForm } from './form'\nexport { type AttachableGesture, GestureView, type GestureViewProps } from './gesture'\nexport {\n type AsyncState,\n type Counter,\n type PersistentSignalOptions,\n type SignalStorage,\n type Toggle,\n useAsync,\n useCounter,\n useDebounce,\n useInterval,\n usePersistentSignal,\n usePrevious,\n useReducer,\n useTimeout,\n useToggle,\n} from './hooks'\nexport { type BaseProps, type Reactive, resolveStyle, toHostProps } from './host'\nexport { animateTo, motion } from './motion'\nexport {\n FocusScope,\n type FocusScopeProps,\n Modal,\n type ModalProps,\n Toast,\n type ToastProps,\n} from './overlay'\nexport {\n Button,\n type ButtonProps,\n Column,\n Image,\n type ImageProps,\n type InteractionState,\n Pressable,\n type PressableProps,\n Row,\n ScrollView,\n type ScrollViewProps,\n Spacer,\n type SpacerProps,\n Stack,\n type StackProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextProps,\n usePressable,\n View,\n type ViewProps,\n} from './primitives'\nexport { Show, type ShowProps } from './show'\nexport { flattenStyle, type StyleInput, type StyleObject, type StyleValue } from './style'\nexport {\n duration,\n easing,\n fontSize,\n fontWeight,\n getTheme,\n lineHeight,\n palette,\n radius,\n space,\n type Theme,\n type ThemeColors,\n tokens,\n useTheme,\n} from './tokens'\nexport { connectWebEnvironment, type WebEnvWindow } from './web-environment'\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;;;;;;;AAeA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
package/dist/overlay.d.ts CHANGED
@@ -20,7 +20,7 @@ interface FocusScopeProps {
20
20
  /** Extra style on the scope container. */
21
21
  readonly style?: Reactive<StyleInput>;
22
22
  }
23
- /** A focus-scoped, `aria-modal` container. Captures + restores focus on web; declarative elsewhere. */
23
+ /** A focus-scoped, `aria-modal` container. Captures + restores focus AND traps Tab on web; declarative elsewhere. */
24
24
  declare const FocusScope: Component<FocusScopeProps>;
25
25
  /** Props for {@link Modal}. */
26
26
  interface ModalProps extends A11yProps {
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.d.ts","names":[],"sources":["../src/overlay.ts"],"mappings":";;;;;;;UAqCiB,eAAA;EAAA,SACN,QAAA,GAAW,WAAA;EAYX;EAAA,SAVA,SAAA;EAUiB;EAAA,SARjB,YAAA;EAQ2B;EAAA,SAN3B,QAAA;EAoDV;EAAA,SAlDU,IAAA,GAAO,IAAA;EAQO;EAAA,SANd,KAAA;EAmDM;EAAA,SAjDN,KAAA,GAAQ,QAAA,CAAS,UAAA;AAAA;;cAIf,UAAA,EAAY,SAAS,CAAC,eAAA;;UA6ClB,UAAA,SAAmB,SAAA;EAAA;EAAA,SAEzB,OAAA,EAAS,QAAA;EAFyB;EAAA,SAIlC,cAAA;EAAA,SACA,QAAA,GAAW,WAAA;EAHF;EAAA,SAKT,eAAA;EAFA;EAAA,SAIA,QAAA,GAAW,QAAA,CAAS,UAAA;EAFpB;EAAA,SAIA,KAAA;AAAA;;cAIE,KAAA,EAAO,SAAS,CAAC,UAAA;;UA0Db,UAAA;EA1DJ;EAAA,SA4DF,OAAA,EAAS,QAAA;;WAET,OAAA,GAAU,WAAA;EAAA,SACV,QAAA,GAAW,WAAA;EALL;EAAA,SAON,QAAA;EAAA,SACA,SAAA;EANS;EAAA,SAQT,QAAA;EALW;EAAA,SAOX,IAAA,GAAO,IAAA;EAAI;EAAA,SAEX,KAAA;AAAA;;;;;cAOE,KAAA,EAAO,SAAS,CAAC,UAAA"}
1
+ {"version":3,"file":"overlay.d.ts","names":[],"sources":["../src/overlay.ts"],"mappings":";;;;;;;UAqCiB,eAAA;EAAA,SACN,QAAA,GAAW,WAAA;EAYX;EAAA,SAVA,SAAA;EAUiB;EAAA,SARjB,YAAA;EAQ2B;EAAA,SAN3B,QAAA;EAsFV;EAAA,SApFU,IAAA,GAAO,IAAA;EAkBO;EAAA,SAhBd,KAAA;EAqFM;EAAA,SAnFN,KAAA,GAAQ,QAAA,CAAS,UAAA;AAAA;;cAcf,UAAA,EAAY,SAAS,CAAC,eAAA;;UAqElB,UAAA,SAAmB,SAAA;EAAA;EAAA,SAEzB,OAAA,EAAS,QAAA;EAFyB;EAAA,SAIlC,cAAA;EAAA,SACA,QAAA,GAAW,WAAA;EAHF;EAAA,SAKT,eAAA;EAFA;EAAA,SAIA,QAAA,GAAW,QAAA,CAAS,UAAA;EAFpB;EAAA,SAIA,KAAA;AAAA;;cAIE,KAAA,EAAO,SAAS,CAAC,UAAA;;UA0Db,UAAA;EA1DJ;EAAA,SA4DF,OAAA,EAAS,QAAA;;WAET,OAAA,GAAU,WAAA;EAAA,SACV,QAAA,GAAW,WAAA;EALL;EAAA,SAON,QAAA;EAAA,SACA,SAAA;EANS;EAAA,SAQT,QAAA;EALW;EAAA,SAOX,IAAA,GAAO,IAAA;EAAI;EAAA,SAEX,KAAA;AAAA;;;;;cAOE,KAAA,EAAO,SAAS,CAAC,UAAA"}
package/dist/overlay.js CHANGED
@@ -21,18 +21,42 @@ import { createElement, effect, onCleanup, portal } from "@mindees/core";
21
21
  function toAccessor(value, fallback) {
22
22
  return typeof value === "function" ? value : () => value === void 0 ? fallback : value;
23
23
  }
24
- /** A focus-scoped, `aria-modal` container. Captures + restores focus on web; declarative elsewhere. */
24
+ /** Tabbable descendants of a focus scope (the standard focusable set, minus `tabindex="-1"`). */
25
+ const FOCUSABLE_SELECTOR = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])";
26
+ /** A focus-scoped, `aria-modal` container. Captures + restores focus AND traps Tab on web; declarative elsewhere. */
25
27
  const FocusScope = (props) => {
26
28
  let restore = null;
29
+ let scopeNode = null;
27
30
  const captureAndFocus = (host) => {
28
31
  if (typeof document === "undefined") return;
29
32
  const node = host;
33
+ scopeNode = node;
30
34
  const previous = document.activeElement ?? null;
31
35
  if (props.restoreFocus !== false && previous && typeof previous.focus === "function") restore = () => previous.focus?.();
32
36
  if (props.autoFocus !== false && node && typeof node.focus === "function") queueMicrotask(() => {
33
37
  if (node.isConnected) node.focus?.();
34
38
  });
35
39
  };
40
+ const trapTab = (e) => {
41
+ const node = scopeNode;
42
+ if (typeof document === "undefined" || !node?.querySelectorAll) return;
43
+ const all = Array.from(node.querySelectorAll(FOCUSABLE_SELECTOR));
44
+ if (all.length === 0) {
45
+ e.preventDefault?.();
46
+ node.focus?.();
47
+ return;
48
+ }
49
+ const first = all[0];
50
+ const last = all[all.length - 1];
51
+ const active = document.activeElement;
52
+ if (e.shiftKey && active === first) {
53
+ e.preventDefault?.();
54
+ last?.focus?.();
55
+ } else if (!e.shiftKey && active === last) {
56
+ e.preventDefault?.();
57
+ first?.focus?.();
58
+ }
59
+ };
36
60
  onCleanup(() => restore?.());
37
61
  const callerStyle = props.style;
38
62
  const style = () => flattenStyle([{ position: "relative" }, typeof callerStyle === "function" ? callerStyle() : callerStyle ?? {}]);
@@ -44,8 +68,10 @@ const FocusScope = (props) => {
44
68
  style
45
69
  };
46
70
  if (props.label !== void 0) hostProps["aria-label"] = props.label;
47
- if (props.onEscape !== void 0) hostProps.onKeyDown = (e) => {
48
- if (e.key === "Escape") props.onEscape?.();
71
+ hostProps.onKeyDown = (e) => {
72
+ const ev = e;
73
+ if (ev.key === "Escape") props.onEscape?.();
74
+ else if (ev.key === "Tab") trapTab(ev);
49
75
  };
50
76
  return createElement("view", hostProps, props.children);
51
77
  };
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.js","names":["doc"],"sources":["../src/overlay.ts"],"sourcesContent":["/**\n * Atlas overlays — `Modal` + `FocusScope`, built on core's `portal`.\n *\n * `Modal` gates a portal by a reactive `visible`: on open it relocates a scrim + a focus-scoped\n * dialog to the renderer's overlay layer (above the tree); on close the gating region re-runs,\n * firing the portal's cleanup (unmount) and the FocusScope's focus-restore. `FocusScope` captures\n * the previously-focused element, auto-focuses its container, and restores focus on unmount —\n * DOM-feature-detected, so it no-ops on native/headless (the dialog markup + a11y still serialize).\n *\n * v1 scope: web is fully interactive (scrim dismiss, Escape, focus capture/restore). Native is\n * declarative — `role=\"dialog\"` + `aria-modal` are carried to the host; a true focus trap +\n * tab-cycling and back-button handling are a host follow-up (see the portal-modal ADR). Tab\n * cycling within the scope is also deferred (needs a descendant query).\n *\n * @module\n */\n\nimport {\n type Component,\n createElement,\n effect,\n type MindeesNode,\n onCleanup,\n portal,\n} from '@mindees/core'\nimport type { A11yProps, Role } from './a11y'\nimport type { Reactive } from './host'\nimport { Pressable, Text, View } from './primitives'\nimport { flattenStyle, type StyleInput } from './style'\n\nfunction toAccessor<T>(value: Reactive<T>, fallback: T): () => T {\n return typeof value === 'function'\n ? (value as () => T)\n : () => (value === undefined ? fallback : value)\n}\n\n/** Props for {@link FocusScope}. */\nexport interface FocusScopeProps {\n readonly children?: MindeesNode\n /** Focus the scope container on mount (default true). */\n readonly autoFocus?: boolean\n /** Restore focus to the previously-focused element on unmount (default true). */\n readonly restoreFocus?: boolean\n /** Called when Escape is pressed inside the scope. */\n readonly onEscape?: () => void\n /** Dialog role (default `'dialog'`). */\n readonly role?: Role\n /** Accessible name (`aria-label`). */\n readonly label?: string\n /** Extra style on the scope container. */\n readonly style?: Reactive<StyleInput>\n}\n\n/** A focus-scoped, `aria-modal` container. Captures + restores focus on web; declarative elsewhere. */\nexport const FocusScope: Component<FocusScopeProps> = (props) => {\n let restore: (() => void) | null = null\n const captureAndFocus = (host: unknown): void => {\n if (typeof document === 'undefined') return // native/headless: declarative only\n const node = host as { focus?: () => void } | null\n const doc = document as unknown as { activeElement?: { focus?: () => void } | null }\n const previous = doc.activeElement ?? null\n if (props.restoreFocus !== false && previous && typeof previous.focus === 'function') {\n restore = () => previous.focus?.()\n }\n if (props.autoFocus !== false && node && typeof node.focus === 'function') {\n // Defer: `ref` fires before the portal subtree is connected to the document, so a synchronous\n // focus() would no-op. By the microtask the subtree is mounted; skip if it closed first.\n queueMicrotask(() => {\n if ((node as { isConnected?: boolean }).isConnected) node.focus?.()\n })\n }\n }\n onCleanup(() => restore?.())\n\n const callerStyle = props.style\n const style: Reactive<StyleInput> = () =>\n flattenStyle([\n { position: 'relative' },\n typeof callerStyle === 'function' ? callerStyle() : (callerStyle ?? {}),\n ])\n\n const hostProps: Record<string, unknown> = {\n role: props.role ?? 'dialog',\n 'aria-modal': 'true',\n tabindex: -1,\n ref: captureAndFocus,\n style,\n }\n if (props.label !== undefined) hostProps['aria-label'] = props.label\n if (props.onEscape !== undefined) {\n hostProps.onKeyDown = (e: unknown) => {\n if ((e as { key?: string }).key === 'Escape') props.onEscape?.()\n }\n }\n // A RAW host `view` (not the curated View primitive) so ref/tabindex/onKeyDown/aria-modal pass through.\n return createElement('view', hostProps, props.children)\n}\n\n/** Props for {@link Modal}. */\nexport interface ModalProps extends A11yProps {\n /** Whether the modal is open (static or reactive). */\n readonly visible: Reactive<boolean>\n /** Requested close (scrim press or Escape). */\n readonly onRequestClose?: () => void\n readonly children?: MindeesNode\n /** Close when the scrim is pressed (default true). */\n readonly closeOnBackdrop?: boolean\n /** Extra style merged into the scrim. */\n readonly backdrop?: Reactive<StyleInput>\n /** Explicit overlay host target (else the backend's overlay layer). */\n readonly mount?: unknown\n}\n\n/** A portal-backed modal dialog: a dismissable scrim + a focus-scoped dialog above the tree. */\nexport const Modal: Component<ModalProps> = (props) => {\n const isVisible = toAccessor(props.visible, false)\n return () => {\n if (!isVisible()) return null\n\n const scrimStyle = (): StyleInput =>\n flattenStyle([\n {\n position: 'absolute',\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n },\n typeof props.backdrop === 'function' ? props.backdrop() : (props.backdrop ?? {}),\n ])\n const scrim = createElement(Pressable, {\n style: scrimStyle,\n label: 'Close',\n ...(props.closeOnBackdrop !== false && props.onRequestClose\n ? { onPress: props.onRequestClose }\n : {}),\n })\n\n const dialog = createElement(\n FocusScope,\n {\n role: props.role ?? 'dialog',\n ...(props.onRequestClose ? { onEscape: props.onRequestClose } : {}),\n ...(props.label !== undefined ? { label: props.label } : {}),\n },\n props.children,\n )\n\n // A full-screen flex-center container holds the scrim (behind) + the dialog (on top, centered).\n const container = createElement(\n View,\n {\n style: () => ({\n position: 'fixed',\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }),\n },\n scrim,\n dialog,\n )\n return portal(container, props.mount !== undefined ? { mount: props.mount } : undefined)\n }\n}\n\n/** Props for {@link Toast}. */\nexport interface ToastProps {\n /** Whether the toast is shown (static or reactive). */\n readonly visible: Reactive<boolean>\n /** Convenience message (string or node); or pass `children`. */\n readonly message?: MindeesNode\n readonly children?: MindeesNode\n /** Auto-dismiss after this many ms (calls `onDismiss`). `0`/omitted → stays until hidden. */\n readonly duration?: number\n readonly onDismiss?: () => void\n /** Anchor edge (default `bottom`). */\n readonly position?: 'top' | 'bottom'\n /** a11y role (default `status`; use `alert` for errors). */\n readonly role?: Role\n /** Explicit overlay host target (else the backend's overlay layer). */\n readonly mount?: unknown\n}\n\n/**\n * A portal-backed transient notification (Snackbar). Controlled by `visible`; optionally auto-dismisses\n * after `duration` ms. Anchored bottom (or top) via the overlay layer — RN ships none built-in.\n */\nexport const Toast: Component<ToastProps> = (props) => {\n const isVisible = toAccessor(props.visible, false)\n\n // Auto-dismiss: (re)arm a timer whenever the toast is shown; clear it on hide/unmount/re-run.\n effect(() => {\n if (!isVisible()) return\n const ms = props.duration\n if (ms && ms > 0 && typeof setTimeout === 'function' && props.onDismiss) {\n const id = setTimeout(() => props.onDismiss?.(), ms)\n onCleanup(() => clearTimeout(id))\n }\n })\n\n return () => {\n if (!isVisible()) return null\n const atTop = props.position === 'top'\n const bubble = createElement(\n View,\n {\n role: props.role ?? 'status',\n style: () => ({\n backgroundColor: '#1f2430',\n borderRadius: 12,\n paddingTop: 12,\n paddingBottom: 12,\n paddingLeft: 16,\n paddingRight: 16,\n maxWidth: 480,\n }),\n },\n typeof props.message === 'string'\n ? createElement(Text, { style: () => ({ color: '#ffffff' }) }, props.message)\n : (props.children ?? props.message),\n )\n const container = createElement(\n View,\n {\n style: () => ({\n position: 'fixed',\n left: 0,\n right: 0,\n [atTop ? 'top' : 'bottom']: 0,\n display: 'flex',\n flexDirection: 'row',\n justifyContent: 'center',\n padding: 16,\n }),\n },\n bubble,\n )\n return portal(container, props.mount !== undefined ? { mount: props.mount } : undefined)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"overlay.js","names":["doc"],"sources":["../src/overlay.ts"],"sourcesContent":["/**\n * Atlas overlays — `Modal` + `FocusScope`, built on core's `portal`.\n *\n * `Modal` gates a portal by a reactive `visible`: on open it relocates a scrim + a focus-scoped\n * dialog to the renderer's overlay layer (above the tree); on close the gating region re-runs,\n * firing the portal's cleanup (unmount) and the FocusScope's focus-restore. `FocusScope` captures\n * the previously-focused element, auto-focuses its container, and restores focus on unmount —\n * DOM-feature-detected, so it no-ops on native/headless (the dialog markup + a11y still serialize).\n *\n * v1 scope: web is fully interactive (scrim dismiss, Escape, focus capture/restore). Native is\n * declarative — `role=\"dialog\"` + `aria-modal` are carried to the host; a true focus trap +\n * tab-cycling and back-button handling are a host follow-up (see the portal-modal ADR). Tab\n * cycling within the scope is also deferred (needs a descendant query).\n *\n * @module\n */\n\nimport {\n type Component,\n createElement,\n effect,\n type MindeesNode,\n onCleanup,\n portal,\n} from '@mindees/core'\nimport type { A11yProps, Role } from './a11y'\nimport type { Reactive } from './host'\nimport { Pressable, Text, View } from './primitives'\nimport { flattenStyle, type StyleInput } from './style'\n\nfunction toAccessor<T>(value: Reactive<T>, fallback: T): () => T {\n return typeof value === 'function'\n ? (value as () => T)\n : () => (value === undefined ? fallback : value)\n}\n\n/** Props for {@link FocusScope}. */\nexport interface FocusScopeProps {\n readonly children?: MindeesNode\n /** Focus the scope container on mount (default true). */\n readonly autoFocus?: boolean\n /** Restore focus to the previously-focused element on unmount (default true). */\n readonly restoreFocus?: boolean\n /** Called when Escape is pressed inside the scope. */\n readonly onEscape?: () => void\n /** Dialog role (default `'dialog'`). */\n readonly role?: Role\n /** Accessible name (`aria-label`). */\n readonly label?: string\n /** Extra style on the scope container. */\n readonly style?: Reactive<StyleInput>\n}\n\n/** Tabbable descendants of a focus scope (the standard focusable set, minus `tabindex=\"-1\"`). */\nconst FOCUSABLE_SELECTOR =\n 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])'\n\ninterface FocusableNode {\n focus?: () => void\n querySelectorAll?: (selector: string) => ArrayLike<{ focus?: () => void }>\n isConnected?: boolean\n}\n\n/** A focus-scoped, `aria-modal` container. Captures + restores focus AND traps Tab on web; declarative elsewhere. */\nexport const FocusScope: Component<FocusScopeProps> = (props) => {\n let restore: (() => void) | null = null\n let scopeNode: FocusableNode | null = null\n const captureAndFocus = (host: unknown): void => {\n if (typeof document === 'undefined') return // native/headless: declarative only\n const node = host as FocusableNode | null\n scopeNode = node\n const doc = document as unknown as { activeElement?: { focus?: () => void } | null }\n const previous = doc.activeElement ?? null\n if (props.restoreFocus !== false && previous && typeof previous.focus === 'function') {\n restore = () => previous.focus?.()\n }\n if (props.autoFocus !== false && node && typeof node.focus === 'function') {\n // Defer: `ref` fires before the portal subtree is connected to the document, so a synchronous\n // focus() would no-op. By the microtask the subtree is mounted; skip if it closed first.\n queueMicrotask(() => {\n if (node.isConnected) node.focus?.()\n })\n }\n }\n // Trap Tab within the scope (WCAG 2.4.3): wrap focus from the last tabbable to the first (and back on\n // Shift+Tab), so keyboard focus can't escape an open modal. No-op if the scope has no tabbable children.\n const trapTab = (e: { shiftKey?: boolean; preventDefault?: () => void }): void => {\n const node = scopeNode\n if (typeof document === 'undefined' || !node?.querySelectorAll) return\n const all = Array.from(node.querySelectorAll(FOCUSABLE_SELECTOR))\n if (all.length === 0) {\n e.preventDefault?.()\n node.focus?.() // nothing tabbable inside → keep focus on the dialog itself\n return\n }\n const first = all[0]\n const last = all[all.length - 1]\n const active = (document as unknown as { activeElement?: unknown }).activeElement\n if (e.shiftKey && active === first) {\n e.preventDefault?.()\n last?.focus?.()\n } else if (!e.shiftKey && active === last) {\n e.preventDefault?.()\n first?.focus?.()\n }\n }\n onCleanup(() => restore?.())\n\n const callerStyle = props.style\n const style: Reactive<StyleInput> = () =>\n flattenStyle([\n { position: 'relative' },\n typeof callerStyle === 'function' ? callerStyle() : (callerStyle ?? {}),\n ])\n\n const hostProps: Record<string, unknown> = {\n role: props.role ?? 'dialog',\n 'aria-modal': 'true',\n tabindex: -1,\n ref: captureAndFocus,\n style,\n }\n if (props.label !== undefined) hostProps['aria-label'] = props.label\n hostProps.onKeyDown = (e: unknown) => {\n const ev = e as { key?: string; shiftKey?: boolean; preventDefault?: () => void }\n if (ev.key === 'Escape') props.onEscape?.()\n else if (ev.key === 'Tab') trapTab(ev)\n }\n // A RAW host `view` (not the curated View primitive) so ref/tabindex/onKeyDown/aria-modal pass through.\n return createElement('view', hostProps, props.children)\n}\n\n/** Props for {@link Modal}. */\nexport interface ModalProps extends A11yProps {\n /** Whether the modal is open (static or reactive). */\n readonly visible: Reactive<boolean>\n /** Requested close (scrim press or Escape). */\n readonly onRequestClose?: () => void\n readonly children?: MindeesNode\n /** Close when the scrim is pressed (default true). */\n readonly closeOnBackdrop?: boolean\n /** Extra style merged into the scrim. */\n readonly backdrop?: Reactive<StyleInput>\n /** Explicit overlay host target (else the backend's overlay layer). */\n readonly mount?: unknown\n}\n\n/** A portal-backed modal dialog: a dismissable scrim + a focus-scoped dialog above the tree. */\nexport const Modal: Component<ModalProps> = (props) => {\n const isVisible = toAccessor(props.visible, false)\n return () => {\n if (!isVisible()) return null\n\n const scrimStyle = (): StyleInput =>\n flattenStyle([\n {\n position: 'absolute',\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n },\n typeof props.backdrop === 'function' ? props.backdrop() : (props.backdrop ?? {}),\n ])\n const scrim = createElement(Pressable, {\n style: scrimStyle,\n label: 'Close',\n ...(props.closeOnBackdrop !== false && props.onRequestClose\n ? { onPress: props.onRequestClose }\n : {}),\n })\n\n const dialog = createElement(\n FocusScope,\n {\n role: props.role ?? 'dialog',\n ...(props.onRequestClose ? { onEscape: props.onRequestClose } : {}),\n ...(props.label !== undefined ? { label: props.label } : {}),\n },\n props.children,\n )\n\n // A full-screen flex-center container holds the scrim (behind) + the dialog (on top, centered).\n const container = createElement(\n View,\n {\n style: () => ({\n position: 'fixed',\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }),\n },\n scrim,\n dialog,\n )\n return portal(container, props.mount !== undefined ? { mount: props.mount } : undefined)\n }\n}\n\n/** Props for {@link Toast}. */\nexport interface ToastProps {\n /** Whether the toast is shown (static or reactive). */\n readonly visible: Reactive<boolean>\n /** Convenience message (string or node); or pass `children`. */\n readonly message?: MindeesNode\n readonly children?: MindeesNode\n /** Auto-dismiss after this many ms (calls `onDismiss`). `0`/omitted → stays until hidden. */\n readonly duration?: number\n readonly onDismiss?: () => void\n /** Anchor edge (default `bottom`). */\n readonly position?: 'top' | 'bottom'\n /** a11y role (default `status`; use `alert` for errors). */\n readonly role?: Role\n /** Explicit overlay host target (else the backend's overlay layer). */\n readonly mount?: unknown\n}\n\n/**\n * A portal-backed transient notification (Snackbar). Controlled by `visible`; optionally auto-dismisses\n * after `duration` ms. Anchored bottom (or top) via the overlay layer — RN ships none built-in.\n */\nexport const Toast: Component<ToastProps> = (props) => {\n const isVisible = toAccessor(props.visible, false)\n\n // Auto-dismiss: (re)arm a timer whenever the toast is shown; clear it on hide/unmount/re-run.\n effect(() => {\n if (!isVisible()) return\n const ms = props.duration\n if (ms && ms > 0 && typeof setTimeout === 'function' && props.onDismiss) {\n const id = setTimeout(() => props.onDismiss?.(), ms)\n onCleanup(() => clearTimeout(id))\n }\n })\n\n return () => {\n if (!isVisible()) return null\n const atTop = props.position === 'top'\n const bubble = createElement(\n View,\n {\n role: props.role ?? 'status',\n style: () => ({\n backgroundColor: '#1f2430',\n borderRadius: 12,\n paddingTop: 12,\n paddingBottom: 12,\n paddingLeft: 16,\n paddingRight: 16,\n maxWidth: 480,\n }),\n },\n typeof props.message === 'string'\n ? createElement(Text, { style: () => ({ color: '#ffffff' }) }, props.message)\n : (props.children ?? props.message),\n )\n const container = createElement(\n View,\n {\n style: () => ({\n position: 'fixed',\n left: 0,\n right: 0,\n [atTop ? 'top' : 'bottom']: 0,\n display: 'flex',\n flexDirection: 'row',\n justifyContent: 'center',\n padding: 16,\n }),\n },\n bubble,\n )\n return portal(container, props.mount !== undefined ? { mount: props.mount } : undefined)\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.d.ts","names":[],"sources":["../src/primitives.ts"],"mappings":";;;;;;UA+CiB,SAAA,SAAkB,SAAS;EAAA,SACjC,QAAA,GAAW,WAAA;AAAA;AAAA,cAET,IAAA,EAAM,SAAS,CAAC,SAAA;;UAIZ,SAAA,SAAkB,SAAS;EAAA,SACjC,QAAA,GAAW,WAAA;AAAA;AAAA,cAET,IAAA,EAAM,SAAS,CAAC,SAAA;;UAcZ,UAAA,SAAmB,SAAS;EAAA,SAClC,GAAA;EAjBW;EAAA,SAmBX,UAAA;EAnBsB;EAAA,SAqBtB,UAAA;EAlBgD;EAAA,SAoBhD,OAAA;EArBQ;EAAA,SAuBR,QAAA;EATM;EAAA,SAWN,aAAA;;WAEA,KAAA;EAAA,SACA,MAAA;EAbA;EAAA,SAeA,WAAA;EAXA;EAAA,SAaA,MAAA;EATA;EAAA,SAWA,OAAA;AAAA;AAAA,cAEE,KAAA,EAAO,SAAS,CAAC,UAAA;;UA8Cb,cAAA,SAAuB,SAAS;EAAA,SACtC,KAAA,GAAQ,QAAA;EAAA,SACR,WAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;EAlBV;EAAA,SAoBU,SAAA;EApDS;EAAA,SAsDT,IAAA;EARM;EAAA,SAUN,eAAA;;WAEA,YAAA;EAZ6B;EAAA,SAc7B,aAAA;EAbQ;EAAA,SAeR,cAAA;EAbA;EAAA,SAeA,YAAA;EAZA;EAAA,SAcA,SAAA;EAVA;EAAA,SAYA,SAAA;EARA;EAAA,SAUA,OAAA,IAAW,KAAA;EANX;EAAA,SAQA,QAAA,IAAY,KAAA;EAAA,SACZ,OAAA;EAAA,SACA,MAAA;EAJW;EAAA,SAMX,eAAA,IAAmB,KAAA;AAAA;AAAA,cAEjB,SAAA,EAAW,SAAS,CAAC,cAAA;;UA+BjB,gBAAA;EAAA,SACN,OAAA;EAAA,SACA,OAAA;EAAA,SACA,OAAA;AAAA;;UAIM,cAAA,SAAuB,IAAA,CAAK,SAAA;EAAA,SAClC,QAAA,GAAW,WAAA;EAvC0B;EAAA,SAyCrC,OAAA;EAAA,SACA,QAAA;;WAEA,KAAA,GAAQ,QAAA,CAAS,UAAA,MAAgB,KAAA,EAAO,gBAAA,KAAqB,UAAA;AAAA;;;;AAVtD;AAIlB;iBAcgB,YAAA,CAAa,OAAA;EAAW,OAAA;EAAsB,QAAA;AAAA;EAC5D,KAAA,QAAa,gBAAA;EACb,QAAA,EAAU,MAAM,UAAU,KAAA;AAAA;AAAA,cAgDf,SAAA,EAAW,SAAS,CAAC,cAAA;;UA2BjB,WAAA,SAAoB,cAAc;EA3FP;EAAA,SA6FjC,KAAK;AAAA;AAAA,cAEH,MAAA,EAAQ,SAAS,CAAC,WAAA;AAAA,KAe1B,SAAA;AAAA,KACA,WAAA;;UASY,UAAA,SAAmB,SAAA;EAAA,SACzB,SAAA;EAAA,SACA,GAAA;EAAA,SACA,KAAA,GAAQ,SAAA;EAAA,SACR,OAAA,GAAU,WAAA;AAAA;AAAA,cAER,KAAA,EAAO,SAAS,CAAC,UAAA;AAxHoD;AAAA,cAqIrE,GAAA,EAAK,SAAA,CAAU,IAAA,CAAK,UAAA;;cAIpB,MAAA,EAAQ,SAAA,CAAU,IAAA,CAAK,UAAA;;UAInB,WAAA;EArI6C;EAAA,SAuInD,IAAI;AAAA;AAAA,cAEF,MAAA,EAAQ,SAAS,CAAC,WAAA;;UAMd,eAAA,SAAwB,SAAS;EAAA,SACvC,UAAA;EA9I+B;EAAA,SAgJ/B,QAAA,IAAY,KAAA;AAAA;AAAA,cAEV,UAAA,EAAY,SAAS,CAAC,eAAA"}
1
+ {"version":3,"file":"primitives.d.ts","names":[],"sources":["../src/primitives.ts"],"mappings":";;;;;;UA+CiB,SAAA,SAAkB,SAAS;EAAA,SACjC,QAAA,GAAW,WAAA;AAAA;AAAA,cAET,IAAA,EAAM,SAAS,CAAC,SAAA;;UAIZ,SAAA,SAAkB,SAAS;EAAA,SACjC,QAAA,GAAW,WAAA;AAAA;AAAA,cAET,IAAA,EAAM,SAAS,CAAC,SAAA;;UAcZ,UAAA,SAAmB,SAAS;EAAA,SAClC,GAAA;EAjBW;EAAA,SAmBX,UAAA;EAnBsB;EAAA,SAqBtB,UAAA;EAlBgD;EAAA,SAoBhD,OAAA;EArBQ;EAAA,SAuBR,QAAA;EATM;EAAA,SAWN,aAAA;;WAEA,KAAA;EAAA,SACA,MAAA;EAbA;EAAA,SAeA,WAAA;EAXA;EAAA,SAaA,MAAA;EATA;EAAA,SAWA,OAAA;AAAA;AAAA,cAEE,KAAA,EAAO,SAAS,CAAC,UAAA;;UAoDb,cAAA,SAAuB,SAAS;EAAA,SACtC,KAAA,GAAQ,QAAA;EAAA,SACR,WAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;EAlBV;EAAA,SAoBU,SAAA;EA1DS;EAAA,SA4DT,IAAA;EARM;EAAA,SAUN,eAAA;;WAEA,YAAA;EAZ6B;EAAA,SAc7B,aAAA;EAbQ;EAAA,SAeR,cAAA;EAbA;EAAA,SAeA,YAAA;EAZA;EAAA,SAcA,SAAA;EAVA;EAAA,SAYA,SAAA;EARA;EAAA,SAUA,OAAA,IAAW,KAAA;EANX;EAAA,SAQA,QAAA,IAAY,KAAA;EAAA,SACZ,OAAA;EAAA,SACA,MAAA;EAJW;EAAA,SAMX,eAAA,IAAmB,KAAA;AAAA;AAAA,cAEjB,SAAA,EAAW,SAAS,CAAC,cAAA;;UAgCjB,gBAAA;EAAA,SACN,OAAA;EAAA,SACA,OAAA;EAAA,SACA,OAAA;AAAA;;UAIM,cAAA,SAAuB,IAAA,CAAK,SAAA;EAAA,SAClC,QAAA,GAAW,WAAA;EAxC0B;EAAA,SA0CrC,OAAA;EAAA,SACA,QAAA;;WAEA,KAAA,GAAQ,QAAA,CAAS,UAAA,MAAgB,KAAA,EAAO,gBAAA,KAAqB,UAAA;AAAA;;;;AAVtD;AAIlB;iBAcgB,YAAA,CAAa,OAAA;EAAW,OAAA;EAAsB,QAAA;AAAA;EAC5D,KAAA,QAAa,gBAAA;EACb,QAAA,EAAU,MAAM,UAAU,KAAA;AAAA;AAAA,cAgDf,SAAA,EAAW,SAAS,CAAC,cAAA;;UA2BjB,WAAA,SAAoB,cAAc;EA3FP;EAAA,SA6FjC,KAAK;AAAA;AAAA,cAEH,MAAA,EAAQ,SAAS,CAAC,WAAA;AAAA,KAe1B,SAAA;AAAA,KACA,WAAA;;UASY,UAAA,SAAmB,SAAA;EAAA,SACzB,SAAA;EAAA,SACA,GAAA;EAAA,SACA,KAAA,GAAQ,SAAA;EAAA,SACR,OAAA,GAAU,WAAA;AAAA;AAAA,cAER,KAAA,EAAO,SAAS,CAAC,UAAA;AAxHoD;AAAA,cAqIrE,GAAA,EAAK,SAAA,CAAU,IAAA,CAAK,UAAA;;cAIpB,MAAA,EAAQ,SAAA,CAAU,IAAA,CAAK,UAAA;;UAInB,WAAA;EArI6C;EAAA,SAuInD,IAAI;AAAA;AAAA,cAEF,MAAA,EAAQ,SAAS,CAAC,WAAA;;UAMd,eAAA,SAAwB,SAAS;EAAA,SACvC,UAAA;EA9I+B;EAAA,SAgJ/B,QAAA,IAAY,KAAA;AAAA;AAAA,cAEV,UAAA,EAAY,SAAS,CAAC,eAAA"}
@@ -58,7 +58,10 @@ const Image = (props) => {
58
58
  const fallback = props.fallbackSrc;
59
59
  if (fallback !== void 0 || props.onError) host.onError = (e) => {
60
60
  const target = e?.target;
61
- if (fallback !== void 0 && target && target.src !== fallback) target.src = fallback;
61
+ if (fallback !== void 0 && target?.dataset && target.dataset.mindeesFellBack !== "1") {
62
+ target.dataset.mindeesFellBack = "1";
63
+ target.src = fallback;
64
+ }
62
65
  props.onError?.();
63
66
  };
64
67
  if (props.decorative) {
@@ -104,6 +107,7 @@ const TextInput = (props) => {
104
107
  if (e?.key === "Enter") props.onSubmitEditing?.(eventValue(e));
105
108
  };
106
109
  if (props.multiline) {
110
+ delete host.type;
107
111
  if (props.rows !== void 0) host.rows = props.rows;
108
112
  return createElement("textarea", host);
109
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.js","names":[],"sources":["../src/primitives.ts"],"sourcesContent":["/**\n * Atlas primitives — accessible, signals-native UI building blocks. Each is a\n * `Component<P>` over `@mindees/core`'s `createElement`, returning a renderer-agnostic\n * `MindeesNode`. Web rendering is real (via the Helix DOM backend); native is a labeled 🔬\n * research track (the same serializable tree, interpreted by a native host later). See\n * `docs/adr/0022-atlas-primitives.md`.\n *\n * @module\n */\n\nimport {\n type Accessor,\n type Component,\n createElement,\n type MindeesNode,\n signal,\n} from '@mindees/core'\nimport { type BaseProps, type Reactive, resolveStyle, toHostProps } from './host'\nimport { flattenStyle, type StyleInput } from './style'\n\n/** Merge a base layout style with a caller's (possibly reactive) style, staying reactive if it is. */\nfunction withBaseStyle(\n base: StyleInput,\n style: Reactive<StyleInput> | undefined,\n): Reactive<StyleInput> {\n if (typeof style === 'function') {\n const accessor = style as Accessor<StyleInput>\n return () => flattenStyle([base, accessor()])\n }\n return flattenStyle([base, style])\n}\n\n/** Dev-only warning (silent in production). Structural global access — no DOM/Node lib needed. */\nfunction warnDev(message: string): void {\n const g = globalThis as {\n process?: { env?: Record<string, string | undefined> }\n console?: { warn?: (message: string) => void }\n }\n if (g.process?.env?.NODE_ENV === 'production') return\n g.console?.warn?.(`[atlas] ${message}`)\n}\n\nfunction eventValue(event: unknown): string {\n return (event as { target?: { value?: string } })?.target?.value ?? ''\n}\n\n/** A generic container (→ `view`/`div`). */\nexport interface ViewProps extends BaseProps {\n readonly children?: MindeesNode\n}\nexport const View: Component<ViewProps> = (props) =>\n createElement('view', toHostProps(props), props.children)\n\n/** Text content (→ `text`/`span`). No default `role` (a bare span is correct; pass `role` to opt in). */\nexport interface TextProps extends BaseProps {\n readonly children?: MindeesNode\n}\nexport const Text: Component<TextProps> = (props) =>\n createElement('text', toHostProps(props), props.children)\n\n/** Merge extra style keys into a host prop bag, preserving a reactive (accessor) style. */\nfunction mergeHostStyle(host: Record<string, unknown>, extra: Record<string, unknown>): void {\n const cur = host.style\n if (cur === undefined) host.style = extra\n else if (typeof cur === 'function') {\n const acc = cur as () => Record<string, unknown>\n host.style = () => ({ ...acc(), ...extra })\n } else host.style = { ...(cur as Record<string, unknown>), ...extra }\n}\n\n/** An image (→ `image`/`img`). Requires `label` (alt) unless `decorative`. */\nexport interface ImageProps extends BaseProps {\n readonly src: string\n /** Mark purely-decorative images so screen readers skip them (alt=\"\" + aria-hidden). */\n readonly decorative?: boolean\n /** How the image fills its box (→ CSS `object-fit`). */\n readonly resizeMode?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** Native lazy-loading (→ `loading`). */\n readonly loading?: 'lazy' | 'eager'\n /** Decode hint (→ `decoding`). */\n readonly decoding?: 'async' | 'sync' | 'auto'\n /** Fetch-priority hint (→ `fetchpriority`). */\n readonly fetchPriority?: 'high' | 'low' | 'auto'\n /** Intrinsic width/height (px) — reserve layout space to avoid reflow on load. */\n readonly width?: number\n readonly height?: number\n /** Swapped in if `src` fails to load (sets the element's `src` on the `error` event). */\n readonly fallbackSrc?: string\n /** Fires when the image finishes loading. */\n readonly onLoad?: () => void\n /** Fires when the image fails to load (after any `fallbackSrc` swap). */\n readonly onError?: () => void\n}\nexport const Image: Component<ImageProps> = (props) => {\n const host = toHostProps(props)\n host.src = props.src\n if (props.resizeMode) mergeHostStyle(host, { objectFit: props.resizeMode })\n if (props.loading) host.loading = props.loading\n if (props.decoding) host.decoding = props.decoding\n if (props.fetchPriority) host.fetchpriority = props.fetchPriority\n if (props.width !== undefined) host.width = props.width\n if (props.height !== undefined) host.height = props.height\n if (props.onLoad) host.onLoad = () => props.onLoad?.()\n const fallback = props.fallbackSrc\n if (fallback !== undefined || props.onError) {\n host.onError = (e: unknown): void => {\n const target = (e as { target?: { src?: string } } | null)?.target\n if (fallback !== undefined && target && target.src !== fallback) target.src = fallback\n props.onError?.()\n }\n }\n if (props.decorative) {\n host.alt = ''\n host['aria-hidden'] = 'true'\n // A decorative image must expose NO accessible name; drop any label lowered by\n // toHostProps so we don't emit a contradictory aria-label on a hidden element.\n delete host['aria-label']\n delete host['aria-labelledby']\n } else {\n if (props.label === undefined) {\n warnDev('Image without a `label` (alt text); pass `label` or set `decorative`.')\n }\n host.alt = props.label ?? ''\n }\n return createElement('image', host)\n}\n\n/** Map a logical keyboard type to the HTML `inputmode` attribute. */\nconst KEYBOARD_INPUTMODE: Record<string, string> = {\n default: 'text',\n numeric: 'numeric',\n decimal: 'decimal',\n email: 'email',\n tel: 'tel',\n url: 'url',\n search: 'search',\n}\n\n/** A text field (→ `textinput`/`input`, or `textarea` when `multiline`). `value` may be reactive. */\nexport interface TextInputProps extends BaseProps {\n readonly value?: Reactive<string>\n readonly placeholder?: string\n readonly type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url'\n readonly disabled?: boolean\n /** Render a multi-line field (→ `textarea`). */\n readonly multiline?: boolean\n /** Visible rows when `multiline` (→ textarea `rows`). */\n readonly rows?: number\n /** Mask the input (→ `type=\"password\"`); overrides `type`. */\n readonly secureTextEntry?: boolean\n /** On-screen keyboard hint (→ `inputmode`). */\n readonly keyboardType?: 'default' | 'numeric' | 'decimal' | 'email' | 'tel' | 'url' | 'search'\n /** Enter-key label hint (→ `enterkeyhint`). */\n readonly returnKeyType?: 'enter' | 'done' | 'go' | 'next' | 'search' | 'send'\n /** Auto-capitalization (→ `autocapitalize`). */\n readonly autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'\n /** Autofill hint (→ `autocomplete`, e.g. `\"email\"`, `\"current-password\"`). */\n readonly autoComplete?: string\n /** Maximum length (→ `maxlength`). */\n readonly maxLength?: number\n /** Focus on mount (→ `autofocus`). */\n readonly autoFocus?: boolean\n /** Fires on every keystroke with the current value (`input` event). */\n readonly onInput?: (value: string) => void\n /** Fires on commit/blur with the current value (`change` event). */\n readonly onChange?: (value: string) => void\n readonly onFocus?: () => void\n readonly onBlur?: () => void\n /** Fires when the user presses Enter (single-line submit). */\n readonly onSubmitEditing?: (value: string) => void\n}\nexport const TextInput: Component<TextInputProps> = (props) => {\n const host = toHostProps(props)\n if (!host.role) host.role = 'textbox'\n if (props.value !== undefined) host.value = props.value\n if (props.placeholder !== undefined) host.placeholder = props.placeholder\n const type = props.secureTextEntry ? 'password' : props.type\n if (type !== undefined) host.type = type\n if (props.disabled) host.disabled = true\n if (props.keyboardType) host.inputmode = KEYBOARD_INPUTMODE[props.keyboardType]\n if (props.returnKeyType) host.enterkeyhint = props.returnKeyType\n if (props.autoCapitalize) host.autocapitalize = props.autoCapitalize\n if (props.autoComplete !== undefined) host.autocomplete = props.autoComplete\n if (props.maxLength !== undefined) host.maxlength = props.maxLength\n if (props.autoFocus) host.autofocus = true\n if (props.onInput) host.onInput = (e: unknown) => props.onInput?.(eventValue(e))\n if (props.onChange) host.onChange = (e: unknown) => props.onChange?.(eventValue(e))\n if (props.onFocus) host.onFocus = () => props.onFocus?.()\n if (props.onBlur) host.onBlur = () => props.onBlur?.()\n if (props.onSubmitEditing) {\n host.onKeyDown = (e: unknown): void => {\n if ((e as { key?: string } | null)?.key === 'Enter') props.onSubmitEditing?.(eventValue(e))\n }\n }\n if (props.multiline) {\n if (props.rows !== undefined) host.rows = props.rows\n return createElement('textarea', host) // multi-line → real <textarea>\n }\n return createElement('textinput', host)\n}\n\n/** Interaction state exposed to a Pressable style function. */\nexport interface InteractionState {\n readonly hovered: boolean\n readonly pressed: boolean\n readonly focused: boolean\n}\n\n/** A pressable surface with built-in hover/press/focus state. Web-real via DOM events. */\nexport interface PressableProps extends Omit<BaseProps, 'style'> {\n readonly children?: MindeesNode\n /** Called when activated (click / Enter / Space) — skipped while `disabled`. */\n readonly onPress?: () => void\n readonly disabled?: boolean\n /** Static/reactive style, or a function of the live interaction state. */\n readonly style?: Reactive<StyleInput> | ((state: InteractionState) => StyleInput)\n}\n\n/**\n * Create the interaction signals + host handlers a pressable surface needs. Reusable so other\n * primitives can compose interaction state. Web wires REAL DOM events (`click`, `pointer*`,\n * `focus`/`blur`, `keydown`) — never a fake cross-platform `press` event that no-ops on web.\n */\nexport function usePressable(options: { onPress?: () => void; disabled?: boolean } = {}): {\n state: () => InteractionState\n handlers: Record<string, (event: unknown) => void>\n} {\n const hovered = signal(false)\n const pressed = signal(false)\n const focused = signal(false)\n const enabled = (): boolean => !options.disabled\n const fire = (): void => {\n if (enabled()) options.onPress?.()\n }\n // A disabled control is inert: its interaction signals don't update, so a state-driven style\n // shows no hover/press/focus feedback (and `fire` blocks onPress / keyboard activation).\n const handlers: Record<string, (event: unknown) => void> = {\n onClick: () => fire(),\n onPointerEnter: () => {\n if (enabled()) hovered.set(true)\n },\n onPointerLeave: () => {\n if (enabled()) {\n hovered.set(false)\n pressed.set(false)\n }\n },\n onPointerDown: () => {\n if (enabled()) pressed.set(true)\n },\n onPointerUp: () => {\n if (enabled()) pressed.set(false)\n },\n onFocus: () => {\n if (enabled()) focused.set(true)\n },\n onBlur: () => {\n if (enabled()) focused.set(false)\n },\n onKeyDown: (e: unknown) => {\n const ev = e as { key?: string; preventDefault?: () => void }\n if (ev.key === 'Enter' || ev.key === ' ') {\n ev.preventDefault?.() // stop Space from page-scrolling a div[role=button]\n fire()\n }\n },\n }\n return {\n state: () => ({ hovered: hovered(), pressed: pressed(), focused: focused() }),\n handlers,\n }\n}\n\nexport const Pressable: Component<PressableProps> = (props) => {\n const { state, handlers } = usePressable({\n ...(props.onPress ? { onPress: props.onPress } : {}),\n ...(props.disabled ? { disabled: true } : {}),\n })\n // Base host props WITHOUT style (Pressable resolves style itself, supporting a state fn).\n const { style, ...rest } = props\n const host: Record<string, unknown> = { ...toHostProps(rest), ...handlers }\n if (!host.role) host.role = 'button'\n if (props.disabled) host['aria-disabled'] = 'true'\n else host.tabindex = 0\n if (style !== undefined) {\n // Distinguish a state-fn `(state) => StyleInput` from a plain reactive style accessor\n // `() => StyleInput` by ARITY: both are functions, but only the state-fn declares a\n // parameter. Treating every function as a state-fn would subscribe an ordinary\n // reactive style to hover/press/focus, re-running it on every interaction.\n const isStateFn = typeof style === 'function' && style.length >= 1\n host.style = isStateFn\n ? () => flattenStyle((style as (s: InteractionState) => StyleInput)(state()))\n : // Arity ruled the state-fn out, so the remainder is a plain `Reactive<StyleInput>`.\n // TS can't narrow on `.length`, so assert it (mirrors the state-fn cast above).\n resolveStyle(style as Reactive<StyleInput>)\n }\n return createElement('view', host, props.children)\n}\n\n/** A labelled button = {@link Pressable} wrapping a {@link Text}. */\nexport interface ButtonProps extends PressableProps {\n /** Convenience text label (alternative to `children`). */\n readonly title?: string\n}\nexport const Button: Component<ButtonProps> = (props) => {\n const { title, children, ...rest } = props\n // The renderer always passes `children` as an array (`[]` when empty), so `??` wouldn't\n // trigger the title fallback — treat an empty array as \"no children\".\n const hasChildren = Array.isArray(children) ? children.length > 0 : children != null\n const content = hasChildren\n ? children\n : title !== undefined\n ? createElement(Text, null, title)\n : null\n return createElement(Pressable, rest, content)\n}\n\n// --- Layout composition (pure View + style, no new host concepts) ---\n\ntype FlexAlign = 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'\ntype FlexJustify =\n | 'flex-start'\n | 'flex-end'\n | 'center'\n | 'space-between'\n | 'space-around'\n | 'space-evenly'\n\n/** A flex container. `direction` defaults to `column`. */\nexport interface StackProps extends ViewProps {\n readonly direction?: 'row' | 'column'\n readonly gap?: number | string\n readonly align?: FlexAlign\n readonly justify?: FlexJustify\n}\nexport const Stack: Component<StackProps> = (props) => {\n const { direction = 'column', gap, align, justify, style, children, ...rest } = props\n const layout: StyleInput = {\n display: 'flex',\n flexDirection: direction,\n ...(gap !== undefined ? { gap } : {}),\n ...(align !== undefined ? { alignItems: align } : {}),\n ...(justify !== undefined ? { justifyContent: justify } : {}),\n }\n return createElement(View, { ...rest, style: withBaseStyle(layout, style) }, children)\n}\n\n/** A horizontal {@link Stack}. */\nexport const Row: Component<Omit<StackProps, 'direction'>> = (props) =>\n createElement(Stack, { ...props, direction: 'row' }, props.children)\n\n/** A vertical {@link Stack}. */\nexport const Column: Component<Omit<StackProps, 'direction'>> = (props) =>\n createElement(Stack, { ...props, direction: 'column' }, props.children)\n\n/** Flexible (or fixed) empty space. */\nexport interface SpacerProps {\n /** Fixed size (px) instead of flexible fill. */\n readonly size?: number | string\n}\nexport const Spacer: Component<SpacerProps> = (props) =>\n createElement(View, {\n style: props.size !== undefined ? { width: props.size, height: props.size } : { flex: 1 },\n })\n\n/** A scrollable container (→ `scrollview`/`div` with `overflow:auto`). */\nexport interface ScrollViewProps extends ViewProps {\n readonly horizontal?: boolean\n /** Fires on scroll with the host scroll event. */\n readonly onScroll?: (event: unknown) => void\n}\nexport const ScrollView: Component<ScrollViewProps> = (props) => {\n const { horizontal, onScroll, style, children, ...rest } = props\n // A horizontal scroller lays its children out in a row and scrolls along x; a vertical\n // one stacks and scrolls along y. Drive real layout through the curated cross-platform\n // style subset (flexDirection + overflow), not an inert `data-orientation` attribute that\n // no backend reads.\n const host = toHostProps({\n ...rest,\n style: withBaseStyle(\n horizontal\n ? // `display: 'flex'` is required for `flexDirection`/`flexWrap` to take effect —\n // without it the row layout is inert (the element keeps the default block flow).\n { display: 'flex', overflow: 'auto', flexDirection: 'row', flexWrap: 'nowrap' }\n : { overflow: 'auto' },\n style,\n ),\n })\n if (onScroll) host.onScroll = onScroll\n if (horizontal) host['data-orientation'] = 'horizontal' // extra hint for native hosts\n // Orientation is fixed at creation, so emit a distinct tag a native host can branch on in\n // makeElement (a vertical ScrollView and a HorizontalScrollView are different widgets). Web\n // maps both to <div> and derives orientation from the style above.\n return createElement(horizontal ? 'horizontalscrollview' : 'scrollview', host, children)\n}\n"],"mappings":";;;;;;;;;;;;;;AAqBA,SAAS,cACP,MACA,OACsB;CACtB,IAAI,OAAO,UAAU,YAAY;EAC/B,MAAM,WAAW;EACjB,aAAa,aAAa,CAAC,MAAM,SAAS,CAAC,CAAC;CAC9C;CACA,OAAO,aAAa,CAAC,MAAM,KAAK,CAAC;AACnC;;AAGA,SAAS,QAAQ,SAAuB;CACtC,MAAM,IAAI;CAIV,IAAI,EAAE,SAAS,KAAK,aAAa,cAAc;CAC/C,EAAE,SAAS,OAAO,WAAW,SAAS;AACxC;AAEA,SAAS,WAAW,OAAwB;CAC1C,OAAQ,OAA2C,QAAQ,SAAS;AACtE;AAMA,MAAa,QAA8B,UACzC,cAAc,QAAQ,YAAY,KAAK,GAAG,MAAM,QAAQ;AAM1D,MAAa,QAA8B,UACzC,cAAc,QAAQ,YAAY,KAAK,GAAG,MAAM,QAAQ;;AAG1D,SAAS,eAAe,MAA+B,OAAsC;CAC3F,MAAM,MAAM,KAAK;CACjB,IAAI,QAAQ,KAAA,GAAW,KAAK,QAAQ;MAC/B,IAAI,OAAO,QAAQ,YAAY;EAClC,MAAM,MAAM;EACZ,KAAK,eAAe;GAAE,GAAG,IAAI;GAAG,GAAG;EAAM;CAC3C,OAAO,KAAK,QAAQ;EAAE,GAAI;EAAiC,GAAG;CAAM;AACtE;AAyBA,MAAa,SAAgC,UAAU;CACrD,MAAM,OAAO,YAAY,KAAK;CAC9B,KAAK,MAAM,MAAM;CACjB,IAAI,MAAM,YAAY,eAAe,MAAM,EAAE,WAAW,MAAM,WAAW,CAAC;CAC1E,IAAI,MAAM,SAAS,KAAK,UAAU,MAAM;CACxC,IAAI,MAAM,UAAU,KAAK,WAAW,MAAM;CAC1C,IAAI,MAAM,eAAe,KAAK,gBAAgB,MAAM;CACpD,IAAI,MAAM,UAAU,KAAA,GAAW,KAAK,QAAQ,MAAM;CAClD,IAAI,MAAM,WAAW,KAAA,GAAW,KAAK,SAAS,MAAM;CACpD,IAAI,MAAM,QAAQ,KAAK,eAAe,MAAM,SAAS;CACrD,MAAM,WAAW,MAAM;CACvB,IAAI,aAAa,KAAA,KAAa,MAAM,SAClC,KAAK,WAAW,MAAqB;EACnC,MAAM,SAAU,GAA4C;EAC5D,IAAI,aAAa,KAAA,KAAa,UAAU,OAAO,QAAQ,UAAU,OAAO,MAAM;EAC9E,MAAM,UAAU;CAClB;CAEF,IAAI,MAAM,YAAY;EACpB,KAAK,MAAM;EACX,KAAK,iBAAiB;EAGtB,OAAO,KAAK;EACZ,OAAO,KAAK;CACd,OAAO;EACL,IAAI,MAAM,UAAU,KAAA,GAClB,QAAQ,uEAAuE;EAEjF,KAAK,MAAM,MAAM,SAAS;CAC5B;CACA,OAAO,cAAc,SAAS,IAAI;AACpC;;AAGA,MAAM,qBAA6C;CACjD,SAAS;CACT,SAAS;CACT,SAAS;CACT,OAAO;CACP,KAAK;CACL,KAAK;CACL,QAAQ;AACV;AAmCA,MAAa,aAAwC,UAAU;CAC7D,MAAM,OAAO,YAAY,KAAK;CAC9B,IAAI,CAAC,KAAK,MAAM,KAAK,OAAO;CAC5B,IAAI,MAAM,UAAU,KAAA,GAAW,KAAK,QAAQ,MAAM;CAClD,IAAI,MAAM,gBAAgB,KAAA,GAAW,KAAK,cAAc,MAAM;CAC9D,MAAM,OAAO,MAAM,kBAAkB,aAAa,MAAM;CACxD,IAAI,SAAS,KAAA,GAAW,KAAK,OAAO;CACpC,IAAI,MAAM,UAAU,KAAK,WAAW;CACpC,IAAI,MAAM,cAAc,KAAK,YAAY,mBAAmB,MAAM;CAClE,IAAI,MAAM,eAAe,KAAK,eAAe,MAAM;CACnD,IAAI,MAAM,gBAAgB,KAAK,iBAAiB,MAAM;CACtD,IAAI,MAAM,iBAAiB,KAAA,GAAW,KAAK,eAAe,MAAM;CAChE,IAAI,MAAM,cAAc,KAAA,GAAW,KAAK,YAAY,MAAM;CAC1D,IAAI,MAAM,WAAW,KAAK,YAAY;CACtC,IAAI,MAAM,SAAS,KAAK,WAAW,MAAe,MAAM,UAAU,WAAW,CAAC,CAAC;CAC/E,IAAI,MAAM,UAAU,KAAK,YAAY,MAAe,MAAM,WAAW,WAAW,CAAC,CAAC;CAClF,IAAI,MAAM,SAAS,KAAK,gBAAgB,MAAM,UAAU;CACxD,IAAI,MAAM,QAAQ,KAAK,eAAe,MAAM,SAAS;CACrD,IAAI,MAAM,iBACR,KAAK,aAAa,MAAqB;EACrC,IAAK,GAA+B,QAAQ,SAAS,MAAM,kBAAkB,WAAW,CAAC,CAAC;CAC5F;CAEF,IAAI,MAAM,WAAW;EACnB,IAAI,MAAM,SAAS,KAAA,GAAW,KAAK,OAAO,MAAM;EAChD,OAAO,cAAc,YAAY,IAAI;CACvC;CACA,OAAO,cAAc,aAAa,IAAI;AACxC;;;;;;AAwBA,SAAgB,aAAa,UAAwD,CAAC,GAGpF;CACA,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,gBAAyB,CAAC,QAAQ;CACxC,MAAM,aAAmB;EACvB,IAAI,QAAQ,GAAG,QAAQ,UAAU;CACnC;CAkCA,OAAO;EACL,cAAc;GAAE,SAAS,QAAQ;GAAG,SAAS,QAAQ;GAAG,SAAS,QAAQ;EAAE;EAC3E,UAAA;GAhCA,eAAe,KAAK;GACpB,sBAAsB;IACpB,IAAI,QAAQ,GAAG,QAAQ,IAAI,IAAI;GACjC;GACA,sBAAsB;IACpB,IAAI,QAAQ,GAAG;KACb,QAAQ,IAAI,KAAK;KACjB,QAAQ,IAAI,KAAK;IACnB;GACF;GACA,qBAAqB;IACnB,IAAI,QAAQ,GAAG,QAAQ,IAAI,IAAI;GACjC;GACA,mBAAmB;IACjB,IAAI,QAAQ,GAAG,QAAQ,IAAI,KAAK;GAClC;GACA,eAAe;IACb,IAAI,QAAQ,GAAG,QAAQ,IAAI,IAAI;GACjC;GACA,cAAc;IACZ,IAAI,QAAQ,GAAG,QAAQ,IAAI,KAAK;GAClC;GACA,YAAY,MAAe;IACzB,MAAM,KAAK;IACX,IAAI,GAAG,QAAQ,WAAW,GAAG,QAAQ,KAAK;KACxC,GAAG,iBAAiB;KACpB,KAAK;IACP;GACF;EAIO;CACT;AACF;AAEA,MAAa,aAAwC,UAAU;CAC7D,MAAM,EAAE,OAAO,aAAa,aAAa;EACvC,GAAI,MAAM,UAAU,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC;EAClD,GAAI,MAAM,WAAW,EAAE,UAAU,KAAK,IAAI,CAAC;CAC7C,CAAC;CAED,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,MAAM,OAAgC;EAAE,GAAG,YAAY,IAAI;EAAG,GAAG;CAAS;CAC1E,IAAI,CAAC,KAAK,MAAM,KAAK,OAAO;CAC5B,IAAI,MAAM,UAAU,KAAK,mBAAmB;MACvC,KAAK,WAAW;CACrB,IAAI,UAAU,KAAA,GAMZ,KAAK,QADa,OAAO,UAAU,cAAc,MAAM,UAAU,UAEvD,aAAc,MAA8C,MAAM,CAAC,CAAC,IAG1E,aAAa,KAA6B;CAEhD,OAAO,cAAc,QAAQ,MAAM,MAAM,QAAQ;AACnD;AAOA,MAAa,UAAkC,UAAU;CACvD,MAAM,EAAE,OAAO,UAAU,GAAG,SAAS;CASrC,OAAO,cAAc,WAAW,OANZ,MAAM,QAAQ,QAAQ,IAAI,SAAS,SAAS,IAAI,YAAY,QAE5E,WACA,UAAU,KAAA,IACR,cAAc,MAAM,MAAM,KAAK,IAC/B,IACuC;AAC/C;AAoBA,MAAa,SAAgC,UAAU;CACrD,MAAM,EAAE,YAAY,UAAU,KAAK,OAAO,SAAS,OAAO,UAAU,GAAG,SAAS;CAChF,MAAM,SAAqB;EACzB,SAAS;EACT,eAAe;EACf,GAAI,QAAQ,KAAA,IAAY,EAAE,IAAI,IAAI,CAAC;EACnC,GAAI,UAAU,KAAA,IAAY,EAAE,YAAY,MAAM,IAAI,CAAC;EACnD,GAAI,YAAY,KAAA,IAAY,EAAE,gBAAgB,QAAQ,IAAI,CAAC;CAC7D;CACA,OAAO,cAAc,MAAM;EAAE,GAAG;EAAM,OAAO,cAAc,QAAQ,KAAK;CAAE,GAAG,QAAQ;AACvF;;AAGA,MAAa,OAAiD,UAC5D,cAAc,OAAO;CAAE,GAAG;CAAO,WAAW;AAAM,GAAG,MAAM,QAAQ;;AAGrE,MAAa,UAAoD,UAC/D,cAAc,OAAO;CAAE,GAAG;CAAO,WAAW;AAAS,GAAG,MAAM,QAAQ;AAOxE,MAAa,UAAkC,UAC7C,cAAc,MAAM,EAClB,OAAO,MAAM,SAAS,KAAA,IAAY;CAAE,OAAO,MAAM;CAAM,QAAQ,MAAM;AAAK,IAAI,EAAE,MAAM,EAAE,EAC1F,CAAC;AAQH,MAAa,cAA0C,UAAU;CAC/D,MAAM,EAAE,YAAY,UAAU,OAAO,UAAU,GAAG,SAAS;CAK3D,MAAM,OAAO,YAAY;EACvB,GAAG;EACH,OAAO,cACL,aAGI;GAAE,SAAS;GAAQ,UAAU;GAAQ,eAAe;GAAO,UAAU;EAAS,IAC9E,EAAE,UAAU,OAAO,GACvB,KACF;CACF,CAAC;CACD,IAAI,UAAU,KAAK,WAAW;CAC9B,IAAI,YAAY,KAAK,sBAAsB;CAI3C,OAAO,cAAc,aAAa,yBAAyB,cAAc,MAAM,QAAQ;AACzF"}
1
+ {"version":3,"file":"primitives.js","names":[],"sources":["../src/primitives.ts"],"sourcesContent":["/**\n * Atlas primitives — accessible, signals-native UI building blocks. Each is a\n * `Component<P>` over `@mindees/core`'s `createElement`, returning a renderer-agnostic\n * `MindeesNode`. Web rendering is real (via the Helix DOM backend); native is a labeled 🔬\n * research track (the same serializable tree, interpreted by a native host later). See\n * `docs/adr/0022-atlas-primitives.md`.\n *\n * @module\n */\n\nimport {\n type Accessor,\n type Component,\n createElement,\n type MindeesNode,\n signal,\n} from '@mindees/core'\nimport { type BaseProps, type Reactive, resolveStyle, toHostProps } from './host'\nimport { flattenStyle, type StyleInput } from './style'\n\n/** Merge a base layout style with a caller's (possibly reactive) style, staying reactive if it is. */\nfunction withBaseStyle(\n base: StyleInput,\n style: Reactive<StyleInput> | undefined,\n): Reactive<StyleInput> {\n if (typeof style === 'function') {\n const accessor = style as Accessor<StyleInput>\n return () => flattenStyle([base, accessor()])\n }\n return flattenStyle([base, style])\n}\n\n/** Dev-only warning (silent in production). Structural global access — no DOM/Node lib needed. */\nfunction warnDev(message: string): void {\n const g = globalThis as {\n process?: { env?: Record<string, string | undefined> }\n console?: { warn?: (message: string) => void }\n }\n if (g.process?.env?.NODE_ENV === 'production') return\n g.console?.warn?.(`[atlas] ${message}`)\n}\n\nfunction eventValue(event: unknown): string {\n return (event as { target?: { value?: string } })?.target?.value ?? ''\n}\n\n/** A generic container (→ `view`/`div`). */\nexport interface ViewProps extends BaseProps {\n readonly children?: MindeesNode\n}\nexport const View: Component<ViewProps> = (props) =>\n createElement('view', toHostProps(props), props.children)\n\n/** Text content (→ `text`/`span`). No default `role` (a bare span is correct; pass `role` to opt in). */\nexport interface TextProps extends BaseProps {\n readonly children?: MindeesNode\n}\nexport const Text: Component<TextProps> = (props) =>\n createElement('text', toHostProps(props), props.children)\n\n/** Merge extra style keys into a host prop bag, preserving a reactive (accessor) style. */\nfunction mergeHostStyle(host: Record<string, unknown>, extra: Record<string, unknown>): void {\n const cur = host.style\n if (cur === undefined) host.style = extra\n else if (typeof cur === 'function') {\n const acc = cur as () => Record<string, unknown>\n host.style = () => ({ ...acc(), ...extra })\n } else host.style = { ...(cur as Record<string, unknown>), ...extra }\n}\n\n/** An image (→ `image`/`img`). Requires `label` (alt) unless `decorative`. */\nexport interface ImageProps extends BaseProps {\n readonly src: string\n /** Mark purely-decorative images so screen readers skip them (alt=\"\" + aria-hidden). */\n readonly decorative?: boolean\n /** How the image fills its box (→ CSS `object-fit`). */\n readonly resizeMode?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** Native lazy-loading (→ `loading`). */\n readonly loading?: 'lazy' | 'eager'\n /** Decode hint (→ `decoding`). */\n readonly decoding?: 'async' | 'sync' | 'auto'\n /** Fetch-priority hint (→ `fetchpriority`). */\n readonly fetchPriority?: 'high' | 'low' | 'auto'\n /** Intrinsic width/height (px) — reserve layout space to avoid reflow on load. */\n readonly width?: number\n readonly height?: number\n /** Swapped in if `src` fails to load (sets the element's `src` on the `error` event). */\n readonly fallbackSrc?: string\n /** Fires when the image finishes loading. */\n readonly onLoad?: () => void\n /** Fires when the image fails to load (after any `fallbackSrc` swap). */\n readonly onError?: () => void\n}\nexport const Image: Component<ImageProps> = (props) => {\n const host = toHostProps(props)\n host.src = props.src\n if (props.resizeMode) mergeHostStyle(host, { objectFit: props.resizeMode })\n if (props.loading) host.loading = props.loading\n if (props.decoding) host.decoding = props.decoding\n if (props.fetchPriority) host.fetchpriority = props.fetchPriority\n if (props.width !== undefined) host.width = props.width\n if (props.height !== undefined) host.height = props.height\n if (props.onLoad) host.onLoad = () => props.onLoad?.()\n const fallback = props.fallbackSrc\n if (fallback !== undefined || props.onError) {\n host.onError = (e: unknown): void => {\n const target = (e as { target?: { src?: string; dataset?: Record<string, string> } } | null)\n ?.target\n // Swap to the fallback exactly ONCE — guard with a marker, not `src !== fallback` (the live `src`\n // is an absolute URL that never equals the literal fallback, which would re-swap + re-fire forever).\n if (fallback !== undefined && target?.dataset && target.dataset.mindeesFellBack !== '1') {\n target.dataset.mindeesFellBack = '1'\n target.src = fallback\n }\n props.onError?.()\n }\n }\n if (props.decorative) {\n host.alt = ''\n host['aria-hidden'] = 'true'\n // A decorative image must expose NO accessible name; drop any label lowered by\n // toHostProps so we don't emit a contradictory aria-label on a hidden element.\n delete host['aria-label']\n delete host['aria-labelledby']\n } else {\n if (props.label === undefined) {\n warnDev('Image without a `label` (alt text); pass `label` or set `decorative`.')\n }\n host.alt = props.label ?? ''\n }\n return createElement('image', host)\n}\n\n/** Map a logical keyboard type to the HTML `inputmode` attribute. */\nconst KEYBOARD_INPUTMODE: Record<string, string> = {\n default: 'text',\n numeric: 'numeric',\n decimal: 'decimal',\n email: 'email',\n tel: 'tel',\n url: 'url',\n search: 'search',\n}\n\n/** A text field (→ `textinput`/`input`, or `textarea` when `multiline`). `value` may be reactive. */\nexport interface TextInputProps extends BaseProps {\n readonly value?: Reactive<string>\n readonly placeholder?: string\n readonly type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url'\n readonly disabled?: boolean\n /** Render a multi-line field (→ `textarea`). */\n readonly multiline?: boolean\n /** Visible rows when `multiline` (→ textarea `rows`). */\n readonly rows?: number\n /** Mask the input (→ `type=\"password\"`); overrides `type`. */\n readonly secureTextEntry?: boolean\n /** On-screen keyboard hint (→ `inputmode`). */\n readonly keyboardType?: 'default' | 'numeric' | 'decimal' | 'email' | 'tel' | 'url' | 'search'\n /** Enter-key label hint (→ `enterkeyhint`). */\n readonly returnKeyType?: 'enter' | 'done' | 'go' | 'next' | 'search' | 'send'\n /** Auto-capitalization (→ `autocapitalize`). */\n readonly autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'\n /** Autofill hint (→ `autocomplete`, e.g. `\"email\"`, `\"current-password\"`). */\n readonly autoComplete?: string\n /** Maximum length (→ `maxlength`). */\n readonly maxLength?: number\n /** Focus on mount (→ `autofocus`). */\n readonly autoFocus?: boolean\n /** Fires on every keystroke with the current value (`input` event). */\n readonly onInput?: (value: string) => void\n /** Fires on commit/blur with the current value (`change` event). */\n readonly onChange?: (value: string) => void\n readonly onFocus?: () => void\n readonly onBlur?: () => void\n /** Fires when the user presses Enter (single-line submit). */\n readonly onSubmitEditing?: (value: string) => void\n}\nexport const TextInput: Component<TextInputProps> = (props) => {\n const host = toHostProps(props)\n if (!host.role) host.role = 'textbox'\n if (props.value !== undefined) host.value = props.value\n if (props.placeholder !== undefined) host.placeholder = props.placeholder\n const type = props.secureTextEntry ? 'password' : props.type\n if (type !== undefined) host.type = type\n if (props.disabled) host.disabled = true\n if (props.keyboardType) host.inputmode = KEYBOARD_INPUTMODE[props.keyboardType]\n if (props.returnKeyType) host.enterkeyhint = props.returnKeyType\n if (props.autoCapitalize) host.autocapitalize = props.autoCapitalize\n if (props.autoComplete !== undefined) host.autocomplete = props.autoComplete\n if (props.maxLength !== undefined) host.maxlength = props.maxLength\n if (props.autoFocus) host.autofocus = true\n if (props.onInput) host.onInput = (e: unknown) => props.onInput?.(eventValue(e))\n if (props.onChange) host.onChange = (e: unknown) => props.onChange?.(eventValue(e))\n if (props.onFocus) host.onFocus = () => props.onFocus?.()\n if (props.onBlur) host.onBlur = () => props.onBlur?.()\n if (props.onSubmitEditing) {\n host.onKeyDown = (e: unknown): void => {\n if ((e as { key?: string } | null)?.key === 'Enter') props.onSubmitEditing?.(eventValue(e))\n }\n }\n if (props.multiline) {\n delete host.type // a <textarea> has no `type` attribute (a multiline + secureTextEntry combo is moot)\n if (props.rows !== undefined) host.rows = props.rows\n return createElement('textarea', host) // multi-line → real <textarea>\n }\n return createElement('textinput', host)\n}\n\n/** Interaction state exposed to a Pressable style function. */\nexport interface InteractionState {\n readonly hovered: boolean\n readonly pressed: boolean\n readonly focused: boolean\n}\n\n/** A pressable surface with built-in hover/press/focus state. Web-real via DOM events. */\nexport interface PressableProps extends Omit<BaseProps, 'style'> {\n readonly children?: MindeesNode\n /** Called when activated (click / Enter / Space) — skipped while `disabled`. */\n readonly onPress?: () => void\n readonly disabled?: boolean\n /** Static/reactive style, or a function of the live interaction state. */\n readonly style?: Reactive<StyleInput> | ((state: InteractionState) => StyleInput)\n}\n\n/**\n * Create the interaction signals + host handlers a pressable surface needs. Reusable so other\n * primitives can compose interaction state. Web wires REAL DOM events (`click`, `pointer*`,\n * `focus`/`blur`, `keydown`) — never a fake cross-platform `press` event that no-ops on web.\n */\nexport function usePressable(options: { onPress?: () => void; disabled?: boolean } = {}): {\n state: () => InteractionState\n handlers: Record<string, (event: unknown) => void>\n} {\n const hovered = signal(false)\n const pressed = signal(false)\n const focused = signal(false)\n const enabled = (): boolean => !options.disabled\n const fire = (): void => {\n if (enabled()) options.onPress?.()\n }\n // A disabled control is inert: its interaction signals don't update, so a state-driven style\n // shows no hover/press/focus feedback (and `fire` blocks onPress / keyboard activation).\n const handlers: Record<string, (event: unknown) => void> = {\n onClick: () => fire(),\n onPointerEnter: () => {\n if (enabled()) hovered.set(true)\n },\n onPointerLeave: () => {\n if (enabled()) {\n hovered.set(false)\n pressed.set(false)\n }\n },\n onPointerDown: () => {\n if (enabled()) pressed.set(true)\n },\n onPointerUp: () => {\n if (enabled()) pressed.set(false)\n },\n onFocus: () => {\n if (enabled()) focused.set(true)\n },\n onBlur: () => {\n if (enabled()) focused.set(false)\n },\n onKeyDown: (e: unknown) => {\n const ev = e as { key?: string; preventDefault?: () => void }\n if (ev.key === 'Enter' || ev.key === ' ') {\n ev.preventDefault?.() // stop Space from page-scrolling a div[role=button]\n fire()\n }\n },\n }\n return {\n state: () => ({ hovered: hovered(), pressed: pressed(), focused: focused() }),\n handlers,\n }\n}\n\nexport const Pressable: Component<PressableProps> = (props) => {\n const { state, handlers } = usePressable({\n ...(props.onPress ? { onPress: props.onPress } : {}),\n ...(props.disabled ? { disabled: true } : {}),\n })\n // Base host props WITHOUT style (Pressable resolves style itself, supporting a state fn).\n const { style, ...rest } = props\n const host: Record<string, unknown> = { ...toHostProps(rest), ...handlers }\n if (!host.role) host.role = 'button'\n if (props.disabled) host['aria-disabled'] = 'true'\n else host.tabindex = 0\n if (style !== undefined) {\n // Distinguish a state-fn `(state) => StyleInput` from a plain reactive style accessor\n // `() => StyleInput` by ARITY: both are functions, but only the state-fn declares a\n // parameter. Treating every function as a state-fn would subscribe an ordinary\n // reactive style to hover/press/focus, re-running it on every interaction.\n const isStateFn = typeof style === 'function' && style.length >= 1\n host.style = isStateFn\n ? () => flattenStyle((style as (s: InteractionState) => StyleInput)(state()))\n : // Arity ruled the state-fn out, so the remainder is a plain `Reactive<StyleInput>`.\n // TS can't narrow on `.length`, so assert it (mirrors the state-fn cast above).\n resolveStyle(style as Reactive<StyleInput>)\n }\n return createElement('view', host, props.children)\n}\n\n/** A labelled button = {@link Pressable} wrapping a {@link Text}. */\nexport interface ButtonProps extends PressableProps {\n /** Convenience text label (alternative to `children`). */\n readonly title?: string\n}\nexport const Button: Component<ButtonProps> = (props) => {\n const { title, children, ...rest } = props\n // The renderer always passes `children` as an array (`[]` when empty), so `??` wouldn't\n // trigger the title fallback — treat an empty array as \"no children\".\n const hasChildren = Array.isArray(children) ? children.length > 0 : children != null\n const content = hasChildren\n ? children\n : title !== undefined\n ? createElement(Text, null, title)\n : null\n return createElement(Pressable, rest, content)\n}\n\n// --- Layout composition (pure View + style, no new host concepts) ---\n\ntype FlexAlign = 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'\ntype FlexJustify =\n | 'flex-start'\n | 'flex-end'\n | 'center'\n | 'space-between'\n | 'space-around'\n | 'space-evenly'\n\n/** A flex container. `direction` defaults to `column`. */\nexport interface StackProps extends ViewProps {\n readonly direction?: 'row' | 'column'\n readonly gap?: number | string\n readonly align?: FlexAlign\n readonly justify?: FlexJustify\n}\nexport const Stack: Component<StackProps> = (props) => {\n const { direction = 'column', gap, align, justify, style, children, ...rest } = props\n const layout: StyleInput = {\n display: 'flex',\n flexDirection: direction,\n ...(gap !== undefined ? { gap } : {}),\n ...(align !== undefined ? { alignItems: align } : {}),\n ...(justify !== undefined ? { justifyContent: justify } : {}),\n }\n return createElement(View, { ...rest, style: withBaseStyle(layout, style) }, children)\n}\n\n/** A horizontal {@link Stack}. */\nexport const Row: Component<Omit<StackProps, 'direction'>> = (props) =>\n createElement(Stack, { ...props, direction: 'row' }, props.children)\n\n/** A vertical {@link Stack}. */\nexport const Column: Component<Omit<StackProps, 'direction'>> = (props) =>\n createElement(Stack, { ...props, direction: 'column' }, props.children)\n\n/** Flexible (or fixed) empty space. */\nexport interface SpacerProps {\n /** Fixed size (px) instead of flexible fill. */\n readonly size?: number | string\n}\nexport const Spacer: Component<SpacerProps> = (props) =>\n createElement(View, {\n style: props.size !== undefined ? { width: props.size, height: props.size } : { flex: 1 },\n })\n\n/** A scrollable container (→ `scrollview`/`div` with `overflow:auto`). */\nexport interface ScrollViewProps extends ViewProps {\n readonly horizontal?: boolean\n /** Fires on scroll with the host scroll event. */\n readonly onScroll?: (event: unknown) => void\n}\nexport const ScrollView: Component<ScrollViewProps> = (props) => {\n const { horizontal, onScroll, style, children, ...rest } = props\n // A horizontal scroller lays its children out in a row and scrolls along x; a vertical\n // one stacks and scrolls along y. Drive real layout through the curated cross-platform\n // style subset (flexDirection + overflow), not an inert `data-orientation` attribute that\n // no backend reads.\n const host = toHostProps({\n ...rest,\n style: withBaseStyle(\n horizontal\n ? // `display: 'flex'` is required for `flexDirection`/`flexWrap` to take effect —\n // without it the row layout is inert (the element keeps the default block flow).\n { display: 'flex', overflow: 'auto', flexDirection: 'row', flexWrap: 'nowrap' }\n : { overflow: 'auto' },\n style,\n ),\n })\n if (onScroll) host.onScroll = onScroll\n if (horizontal) host['data-orientation'] = 'horizontal' // extra hint for native hosts\n // Orientation is fixed at creation, so emit a distinct tag a native host can branch on in\n // makeElement (a vertical ScrollView and a HorizontalScrollView are different widgets). Web\n // maps both to <div> and derives orientation from the style above.\n return createElement(horizontal ? 'horizontalscrollview' : 'scrollview', host, children)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/atlas",
3
- "version": "0.27.2",
3
+ "version": "0.29.0",
4
4
  "description": "MindeesNative Atlas - accessible, signals-native UI primitives + a virtualized recycling list. Renderer-agnostic (web real, native research track).",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -38,12 +38,12 @@
38
38
  "directory": "packages/atlas"
39
39
  },
40
40
  "dependencies": {
41
- "@mindees/core": "0.27.2",
42
- "@mindees/router": "0.27.2"
41
+ "@mindees/core": "0.29.0",
42
+ "@mindees/router": "0.29.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "happy-dom": "20.9.0",
46
- "@mindees/renderer": "0.27.2"
46
+ "@mindees/renderer": "0.29.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsdown",