@mindees/atlas 0.1.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/LICENSE +31 -0
- package/README.md +62 -0
- package/dist/a11y.d.ts +43 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/a11y.js +27 -0
- package/dist/a11y.js.map +1 -0
- package/dist/host.d.ts +23 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +23 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/list.d.ts +57 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +124 -0
- package/dist/list.js.map +1 -0
- package/dist/primitives.d.ts +98 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +176 -0
- package/dist/primitives.js.map +1 -0
- package/dist/style.d.ts +80 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +23 -0
- package/dist/style.js.map +1 -0
- package/dist/theme.d.ts +54 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +92 -0
- package/dist/theme.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
MindeesNative is dual-licensed under either of:
|
|
4
|
+
|
|
5
|
+
- **Apache License, Version 2.0** ([LICENSE-APACHE](./LICENSE-APACHE) or
|
|
6
|
+
<https://www.apache.org/licenses/LICENSE-2.0>)
|
|
7
|
+
- **MIT license** ([LICENSE-MIT](./LICENSE-MIT) or
|
|
8
|
+
<https://opensource.org/licenses/MIT>)
|
|
9
|
+
|
|
10
|
+
at your option.
|
|
11
|
+
|
|
12
|
+
This `MIT OR Apache-2.0` dual-license is the same model used by the Rust
|
|
13
|
+
ecosystem and many modern open-source projects. It gives downstream users
|
|
14
|
+
maximum flexibility: the MIT option is short and permissive, while the Apache
|
|
15
|
+
option adds an explicit patent grant.
|
|
16
|
+
|
|
17
|
+
## SPDX identifier
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
SPDX-License-Identifier: MIT OR Apache-2.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Contribution
|
|
24
|
+
|
|
25
|
+
Unless you explicitly state otherwise, any contribution intentionally
|
|
26
|
+
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
|
27
|
+
license, shall be dual-licensed as above, without any additional terms or
|
|
28
|
+
conditions.
|
|
29
|
+
|
|
30
|
+
Contributions are accepted under the **Developer Certificate of Origin (DCO)**.
|
|
31
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for details on signing off your commits.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @mindees/atlas
|
|
2
|
+
|
|
3
|
+
The Atlas component library โ MindeesNative's batteries-included UI primitives.
|
|
4
|
+
|
|
5
|
+
> Note: named `@mindees/atlas` (not `@mindees/ui`) because `@mindees/ui` is a
|
|
6
|
+
> separate, pre-existing React Native + Expo UI kit on npm. Atlas is this
|
|
7
|
+
> framework's own component library.
|
|
8
|
+
|
|
9
|
+
> Status: ๐งช **Experimental** โ Phase 12A/12B are implemented and tested:
|
|
10
|
+
> accessible, signals-native UI primitives, a structural theme subpath, and a
|
|
11
|
+
> virtualized recycling `List`. Web rendering is real through the Helix DOM backend;
|
|
12
|
+
> native Atlas rendering is still tied to the broader native app bridge research track.
|
|
13
|
+
> See the repository [STATUS.md](../../STATUS.md).
|
|
14
|
+
|
|
15
|
+
## What works today
|
|
16
|
+
|
|
17
|
+
- **Primitives** โ `View`, `Text`, `Image`, `TextInput`, `Pressable`, `Button`,
|
|
18
|
+
`Stack`, `Row`, `Column`, `Spacer`, and `ScrollView` return renderer-agnostic
|
|
19
|
+
`MindeesNode` trees over `@mindees/core` `createElement`.
|
|
20
|
+
- **Styling** โ `StyleObject`, `StyleInput`, and `flattenStyle` normalize a curated
|
|
21
|
+
cross-platform style subset; numeric web styles lower to `px` through the renderer.
|
|
22
|
+
- **Accessibility** โ typed `role` / `aria-*` helpers lower into host props, and
|
|
23
|
+
`Image` requires a `label` unless explicitly marked decorative.
|
|
24
|
+
- **Interaction** โ `Pressable` and `usePressable` use real DOM events on web
|
|
25
|
+
(`click`, pointer, focus, Enter, Space), with reactive hover/press/focus state.
|
|
26
|
+
- **Theme** โ `@mindees/atlas/theme` exports `ThemeTokens`, `defaultTokens`, and
|
|
27
|
+
`createTheme()`; token selection uses `@mindees/core` selector isolation.
|
|
28
|
+
- **List** โ `@mindees/atlas/list` exports `List`, `createList`, and `computeWindow`.
|
|
29
|
+
It renders a fixed pool of recycled row regions for fixed-height virtualization;
|
|
30
|
+
variable-height measurement is a ๐ฌ research track.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Button, Column, Text } from '@mindees/atlas'
|
|
36
|
+
import { List } from '@mindees/atlas/list'
|
|
37
|
+
import { createTheme } from '@mindees/atlas/theme'
|
|
38
|
+
|
|
39
|
+
const theme = createTheme({ colors: { primary: '#2563eb' } })
|
|
40
|
+
const primary = theme.select((tokens) => tokens.colors.primary)
|
|
41
|
+
|
|
42
|
+
export const Screen = Column({
|
|
43
|
+
gap: 12,
|
|
44
|
+
children: [
|
|
45
|
+
Text({ children: 'Atlas works on the Helix DOM backend today.' }),
|
|
46
|
+
Button({ title: 'Save', style: () => ({ backgroundColor: primary(), padding: 12 }) }),
|
|
47
|
+
List({
|
|
48
|
+
items: ['One', 'Two', 'Three'],
|
|
49
|
+
height: 120,
|
|
50
|
+
itemHeight: 40,
|
|
51
|
+
renderItem: (item) => Text({ children: item }),
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Design rationale: [ADR-0022](../../docs/adr/0022-atlas-primitives.md) and
|
|
58
|
+
[ADR-0023](../../docs/adr/0023-atlas-list.md).
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
`MIT OR Apache-2.0`
|
package/dist/a11y.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/a11y.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Atlas accessibility โ a single typed `A11yProps` surface lowered to `role` + `aria-*`
|
|
4
|
+
* attribute props. The DOM backend passes these through verbatim (`setAttribute`), so web a11y
|
|
5
|
+
* is real; native hosts receive them as serialized props (interpretation is a ๐ฌ research-track
|
|
6
|
+
* host concern โ carried, never silently dropped). See `docs/adr/0022-atlas-primitives.md`.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/** A WAI-ARIA-ish role, mapped straight to the host `role` attribute on web. */
|
|
11
|
+
type Role = 'button' | 'link' | 'image' | 'heading' | 'list' | 'listitem' | 'text' | 'textbox' | 'checkbox' | 'switch' | 'radio' | 'tab' | 'tabpanel' | 'dialog' | 'alert' | 'status' | 'none' | 'presentation';
|
|
12
|
+
/** Accessibility state, lowered to the matching `aria-*` attributes. */
|
|
13
|
+
interface A11yState {
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
selected?: boolean;
|
|
16
|
+
checked?: boolean;
|
|
17
|
+
expanded?: boolean;
|
|
18
|
+
busy?: boolean;
|
|
19
|
+
hidden?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** The accessibility surface every Atlas primitive accepts. */
|
|
22
|
+
interface A11yProps {
|
|
23
|
+
/** ARIA role (web `role`). */
|
|
24
|
+
role?: Role;
|
|
25
|
+
/** Accessible name (`aria-label`). */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Id(s) of the element(s) labelling this one (`aria-labelledby`). */
|
|
28
|
+
labelledBy?: string;
|
|
29
|
+
/** Id(s) of the element(s) describing this one (`aria-describedby`). */
|
|
30
|
+
describedBy?: string;
|
|
31
|
+
/** Live-region politeness (`aria-live`). */
|
|
32
|
+
live?: 'off' | 'polite' | 'assertive';
|
|
33
|
+
/** Accessibility state (`aria-disabled`/`-selected`/`-checked`/`-expanded`/`-busy`/`aria-hidden`). */
|
|
34
|
+
state?: A11yState;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Lower {@link A11yProps} to a host prop bag of `role` + `aria-*` (only keys that are defined,
|
|
38
|
+
* so omitted props stay omitted โ exactOptionalPropertyTypes-safe).
|
|
39
|
+
*/
|
|
40
|
+
declare function toA11yProps(a11y: A11yProps): Record<string, string>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { A11yProps, A11yState, Role, toA11yProps };
|
|
43
|
+
//# sourceMappingURL=a11y.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y.d.ts","names":[],"sources":["../src/a11y.ts"],"mappings":";;AAUA;;;;AAAgB;AAqBhB;;;KArBY,IAAA;;UAqBK,SAAA;EACf,QAAA;EACA,QAAA;EACA,OAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;AAAA;;UAIe,SAAA;EAYE;EAVjB,IAAA,GAAO,IAAA;EAAA;EAEP,KAAA;EAEA;EAAA,UAAA;EAIA;EAFA,WAAA;EAIQ;EAFR,IAAA;EAEiB;EAAjB,KAAA,GAAQ,SAAS;AAAA;;;;;iBAOH,WAAA,CAAY,IAAA,EAAM,SAAA,GAAY,MAAM"}
|
package/dist/a11y.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/a11y.ts
|
|
2
|
+
/**
|
|
3
|
+
* Lower {@link A11yProps} to a host prop bag of `role` + `aria-*` (only keys that are defined,
|
|
4
|
+
* so omitted props stay omitted โ exactOptionalPropertyTypes-safe).
|
|
5
|
+
*/
|
|
6
|
+
function toA11yProps(a11y) {
|
|
7
|
+
const out = {};
|
|
8
|
+
if (a11y.role !== void 0) out.role = a11y.role;
|
|
9
|
+
if (a11y.label !== void 0) out["aria-label"] = a11y.label;
|
|
10
|
+
if (a11y.labelledBy !== void 0) out["aria-labelledby"] = a11y.labelledBy;
|
|
11
|
+
if (a11y.describedBy !== void 0) out["aria-describedby"] = a11y.describedBy;
|
|
12
|
+
if (a11y.live !== void 0) out["aria-live"] = a11y.live;
|
|
13
|
+
const s = a11y.state;
|
|
14
|
+
if (s) {
|
|
15
|
+
if (s.disabled !== void 0) out["aria-disabled"] = String(s.disabled);
|
|
16
|
+
if (s.selected !== void 0) out["aria-selected"] = String(s.selected);
|
|
17
|
+
if (s.checked !== void 0) out["aria-checked"] = String(s.checked);
|
|
18
|
+
if (s.expanded !== void 0) out["aria-expanded"] = String(s.expanded);
|
|
19
|
+
if (s.busy !== void 0) out["aria-busy"] = String(s.busy);
|
|
20
|
+
if (s.hidden !== void 0) out["aria-hidden"] = String(s.hidden);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { toA11yProps };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=a11y.js.map
|
package/dist/a11y.js.map
ADDED
|
@@ -0,0 +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 | '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 expanded?: boolean\n busy?: boolean\n hidden?: boolean\n}\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 /** Accessibility state (`aria-disabled`/`-selected`/`-checked`/`-expanded`/`-busy`/`aria-hidden`). */\n state?: A11yState\n}\n\n/**\n * Lower {@link A11yProps} to a host prop bag of `role` + `aria-*` (only keys that are defined,\n * so omitted props stay omitted โ exactOptionalPropertyTypes-safe).\n */\nexport function toA11yProps(a11y: A11yProps): Record<string, string> {\n const out: Record<string, string> = {}\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 const s = a11y.state\n if (s) {\n if (s.disabled !== undefined) out['aria-disabled'] = String(s.disabled)\n if (s.selected !== undefined) out['aria-selected'] = String(s.selected)\n if (s.checked !== undefined) out['aria-checked'] = String(s.checked)\n if (s.expanded !== undefined) out['aria-expanded'] = String(s.expanded)\n if (s.busy !== undefined) out['aria-busy'] = String(s.busy)\n if (s.hidden !== undefined) out['aria-hidden'] = String(s.hidden)\n }\n return out\n}\n"],"mappings":";;;;;AA4DA,SAAgB,YAAY,MAAyC;CACnE,MAAM,MAA8B,CAAC;CACrC,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;CACrD,MAAM,IAAI,KAAK;CACf,IAAI,GAAG;EACL,IAAI,EAAE,aAAa,KAAA,GAAW,IAAI,mBAAmB,OAAO,EAAE,QAAQ;EACtE,IAAI,EAAE,aAAa,KAAA,GAAW,IAAI,mBAAmB,OAAO,EAAE,QAAQ;EACtE,IAAI,EAAE,YAAY,KAAA,GAAW,IAAI,kBAAkB,OAAO,EAAE,OAAO;EACnE,IAAI,EAAE,aAAa,KAAA,GAAW,IAAI,mBAAmB,OAAO,EAAE,QAAQ;EACtE,IAAI,EAAE,SAAS,KAAA,GAAW,IAAI,eAAe,OAAO,EAAE,IAAI;EAC1D,IAAI,EAAE,WAAW,KAAA,GAAW,IAAI,iBAAiB,OAAO,EAAE,MAAM;CAClE;CACA,OAAO;AACT"}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { A11yProps } from "./a11y.js";
|
|
2
|
+
import { StyleInput, StyleObject } from "./style.js";
|
|
3
|
+
import { Accessor } from "@mindees/core";
|
|
4
|
+
|
|
5
|
+
//#region src/host.d.ts
|
|
6
|
+
/** A value that may be static or a reactive accessor โ a function makes it a fine-grained binding. */
|
|
7
|
+
type Reactive<T> = T | Accessor<T>;
|
|
8
|
+
/** Props every Atlas primitive accepts. */
|
|
9
|
+
interface BaseProps extends A11yProps {
|
|
10
|
+
/** Style (or a list of styles), static or reactive. */
|
|
11
|
+
readonly style?: Reactive<StyleInput>;
|
|
12
|
+
/** Host element id. */
|
|
13
|
+
readonly id?: string;
|
|
14
|
+
/** Test identifier (lowered to `data-testid`). */
|
|
15
|
+
readonly testID?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Resolve a `Reactive<StyleInput>` to a flattened style object, or an accessor of one. */
|
|
18
|
+
declare function resolveStyle(style: Reactive<StyleInput>): StyleObject | Accessor<StyleObject>;
|
|
19
|
+
/** Lower the base props (style + a11y + id/testID) to a host prop bag (omitted stays omitted). */
|
|
20
|
+
declare function toHostProps(props: BaseProps): Record<string, unknown>;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { BaseProps, Reactive, resolveStyle, toHostProps };
|
|
23
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;;KAcY,QAAA,MAAc,CAAA,GAAI,QAAA,CAAS,CAAA;;UAGtB,SAAA,SAAkB,SAAA;EAHL;EAAA,SAKnB,KAAA,GAAQ,QAAA,CAAS,UAAA;EALY;EAAA,SAO7B,EAAA;EAJM;EAAA,SAMN,MAAA;AAAA;;iBAIK,YAAA,CAAa,KAAA,EAAO,QAAA,CAAS,UAAA,IAAc,WAAA,GAAc,QAAA,CAAS,WAAA;;iBASlE,WAAA,CAAY,KAAA,EAAO,SAAA,GAAY,MAAM"}
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { toA11yProps } from "./a11y.js";
|
|
2
|
+
import { flattenStyle } from "./style.js";
|
|
3
|
+
//#region src/host.ts
|
|
4
|
+
/** Resolve a `Reactive<StyleInput>` to a flattened style object, or an accessor of one. */
|
|
5
|
+
function resolveStyle(style) {
|
|
6
|
+
if (typeof style === "function") {
|
|
7
|
+
const accessor = style;
|
|
8
|
+
return () => flattenStyle(accessor());
|
|
9
|
+
}
|
|
10
|
+
return flattenStyle(style);
|
|
11
|
+
}
|
|
12
|
+
/** Lower the base props (style + a11y + id/testID) to a host prop bag (omitted stays omitted). */
|
|
13
|
+
function toHostProps(props) {
|
|
14
|
+
const host = { ...toA11yProps(props) };
|
|
15
|
+
if (props.style !== void 0) host.style = resolveStyle(props.style);
|
|
16
|
+
if (props.id !== void 0) host.id = props.id;
|
|
17
|
+
if (props.testID !== void 0) host["data-testid"] = props.testID;
|
|
18
|
+
return host;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { resolveStyle, toHostProps };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=host.js.map
|
package/dist/host.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.js","names":[],"sources":["../src/host.ts"],"sourcesContent":["/**\n * The propโhost adapter. Atlas primitives expose a curated, typed prop surface; `toHostProps`\n * lowers the shared base (style + accessibility + id/testID) to the host prop bag that\n * `createElement` receives โ the renderer then special-cases `style` (object โ inline/native)\n * and passes `role`/`aria-*`/attributes through. Each primitive adds its own events/extra props.\n *\n * @module\n */\n\nimport type { Accessor } from '@mindees/core'\nimport { type A11yProps, toA11yProps } from './a11y'\nimport { flattenStyle, type StyleInput, type StyleObject } from './style'\n\n/** A value that may be static or a reactive accessor โ a function makes it a fine-grained binding. */\nexport type Reactive<T> = T | Accessor<T>\n\n/** Props every Atlas primitive accepts. */\nexport interface BaseProps extends A11yProps {\n /** Style (or a list of styles), static or reactive. */\n readonly style?: Reactive<StyleInput>\n /** Host element id. */\n readonly id?: string\n /** Test identifier (lowered to `data-testid`). */\n readonly testID?: string\n}\n\n/** Resolve a `Reactive<StyleInput>` to a flattened style object, or an accessor of one. */\nexport function resolveStyle(style: Reactive<StyleInput>): StyleObject | Accessor<StyleObject> {\n if (typeof style === 'function') {\n const accessor = style as Accessor<StyleInput>\n return () => flattenStyle(accessor())\n }\n return flattenStyle(style)\n}\n\n/** Lower the base props (style + a11y + id/testID) to a host prop bag (omitted stays omitted). */\nexport function toHostProps(props: BaseProps): Record<string, unknown> {\n const host: Record<string, unknown> = { ...toA11yProps(props) }\n if (props.style !== undefined) host.style = resolveStyle(props.style)\n if (props.id !== undefined) host.id = props.id\n if (props.testID !== undefined) host['data-testid'] = props.testID\n return host\n}\n"],"mappings":";;;;AA2BA,SAAgB,aAAa,OAAkE;CAC7F,IAAI,OAAO,UAAU,YAAY;EAC/B,MAAM,WAAW;EACjB,aAAa,aAAa,SAAS,CAAC;CACtC;CACA,OAAO,aAAa,KAAK;AAC3B;;AAGA,SAAgB,YAAY,OAA2C;CACrE,MAAM,OAAgC,EAAE,GAAG,YAAY,KAAK,EAAE;CAC9D,IAAI,MAAM,UAAU,KAAA,GAAW,KAAK,QAAQ,aAAa,MAAM,KAAK;CACpE,IAAI,MAAM,OAAO,KAAA,GAAW,KAAK,KAAK,MAAM;CAC5C,IAAI,MAAM,WAAW,KAAA,GAAW,KAAK,iBAAiB,MAAM;CAC5D,OAAO;AACT"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { A11yProps, A11yState, Role, toA11yProps } from "./a11y.js";
|
|
2
|
+
import { StyleInput, StyleObject, StyleValue, flattenStyle } from "./style.js";
|
|
3
|
+
import { BaseProps, Reactive, resolveStyle, toHostProps } from "./host.js";
|
|
4
|
+
import { Button, ButtonProps, Column, Image, ImageProps, InteractionState, Pressable, PressableProps, Row, ScrollView, ScrollViewProps, Spacer, SpacerProps, Stack, StackProps, Text, TextInput, TextInputProps, TextProps, View, ViewProps, usePressable } from "./primitives.js";
|
|
5
|
+
import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@mindees/core";
|
|
6
|
+
|
|
7
|
+
//#region src/index.d.ts
|
|
8
|
+
/** The npm package name. */
|
|
9
|
+
declare const name = "@mindees/atlas";
|
|
10
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
11
|
+
declare const VERSION = "0.1.0";
|
|
12
|
+
/** Current maturity of this package. See the repository `STATUS.md`. */
|
|
13
|
+
declare const maturity: Maturity;
|
|
14
|
+
/**
|
|
15
|
+
* Static identity + maturity metadata for this package. Frozen so the
|
|
16
|
+
* self-reported identity tooling introspects cannot be mutated at runtime,
|
|
17
|
+
* matching the `readonly` fields of {@link PackageInfo}.
|
|
18
|
+
*/
|
|
19
|
+
declare const info: PackageInfo;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type A11yProps, type A11yState, type BaseProps, Button, type ButtonProps, Column, Image, type ImageProps, type InteractionState, type Maturity, NotImplementedError, type PackageInfo, Pressable, type PressableProps, type Reactive, type Role, Row, ScrollView, type ScrollViewProps, Spacer, type SpacerProps, Stack, type StackProps, type StyleInput, type StyleObject, type StyleValue, Text, TextInput, type TextInputProps, type TextProps, VERSION, View, type ViewProps, flattenStyle, info, maturity, name, notImplemented, resolveStyle, toA11yProps, toHostProps, usePressable };
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;AAkBA;AAAA,cAHa,IAAA;;cAGA,OAAA;AAAO;AAAA,cAGP,QAAA,EAAU,QAAyB;;;;AAAA;AAOhD;cAAa,IAAA,EAAM,WAAiE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { toA11yProps } from "./a11y.js";
|
|
2
|
+
import { flattenStyle } from "./style.js";
|
|
3
|
+
import { resolveStyle, toHostProps } from "./host.js";
|
|
4
|
+
import { Button, Column, Image, Pressable, Row, ScrollView, Spacer, Stack, Text, TextInput, View, usePressable } from "./primitives.js";
|
|
5
|
+
import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
/** The npm package name. */
|
|
8
|
+
const name = "@mindees/atlas";
|
|
9
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
10
|
+
const VERSION = "0.1.0";
|
|
11
|
+
/** Current maturity of this package. See the repository `STATUS.md`. */
|
|
12
|
+
const maturity = "experimental";
|
|
13
|
+
/**
|
|
14
|
+
* Static identity + maturity metadata for this package. Frozen so the
|
|
15
|
+
* self-reported identity tooling introspects cannot be mutated at runtime,
|
|
16
|
+
* matching the `readonly` fields of {@link PackageInfo}.
|
|
17
|
+
*/
|
|
18
|
+
const info = Object.freeze({
|
|
19
|
+
name,
|
|
20
|
+
version: VERSION,
|
|
21
|
+
maturity
|
|
22
|
+
});
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Button, Column, Image, NotImplementedError, Pressable, Row, ScrollView, Spacer, Stack, Text, TextInput, VERSION, View, flattenStyle, info, maturity, name, notImplemented, resolveStyle, toA11yProps, toHostProps, usePressable };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 a structural theme (on the `@mindees/atlas/theme`\n * subpath). 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.1.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 { type A11yProps, type A11yState, type Role, toA11yProps } from './a11y'\nexport { type BaseProps, type Reactive, resolveStyle, toHostProps } from './host'\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 { flattenStyle, type StyleInput, type StyleObject, type StyleValue } from './style'\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/list.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { StyleInput } from "./style.js";
|
|
2
|
+
import { Reactive } from "./host.js";
|
|
3
|
+
import { MindeesNode } from "@mindees/core";
|
|
4
|
+
|
|
5
|
+
//#region src/list.d.ts
|
|
6
|
+
/** The computed visible window over the item list. */
|
|
7
|
+
interface ListWindow {
|
|
8
|
+
/** First visible index (inclusive), overscan applied. */
|
|
9
|
+
readonly startIndex: number;
|
|
10
|
+
/** Last visible index (exclusive), overscan applied. */
|
|
11
|
+
readonly endIndex: number;
|
|
12
|
+
/** Total scrollable height (px) = itemCount ร itemHeight. */
|
|
13
|
+
readonly totalHeight: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Pure window math: which item indices are visible for a given scroll offset. Exported and
|
|
17
|
+
* exhaustively unit-tested โ the deterministic heart of virtualization (no signals, no DOM).
|
|
18
|
+
*/
|
|
19
|
+
declare function computeWindow(scrollTop: number, viewportHeight: number, itemHeight: number, itemCount: number, overscan: number): ListWindow;
|
|
20
|
+
/** Options for {@link createList}. */
|
|
21
|
+
interface ListOptions<T> {
|
|
22
|
+
/** The items, static or reactive. */
|
|
23
|
+
readonly items: readonly T[] | (() => readonly T[]);
|
|
24
|
+
/**
|
|
25
|
+
* Render one row. Receives reactive `item`/`index` **accessors** โ consume them LAZILY
|
|
26
|
+
* (`Text({ children: () => item().name })`, a `style` fn, or pass them to a child) so a
|
|
27
|
+
* recycled slot patches in place. Reading `item()`/`index()` synchronously in the body bakes
|
|
28
|
+
* the value in and opts the row out of recycling (it re-runs renderItem on reuse instead).
|
|
29
|
+
*/
|
|
30
|
+
readonly renderItem: (item: () => T, index: () => number) => MindeesNode;
|
|
31
|
+
/** Fixed row height in px (variable heights are a research track). */
|
|
32
|
+
readonly itemHeight: number;
|
|
33
|
+
/** Viewport height in px. */
|
|
34
|
+
readonly height: number;
|
|
35
|
+
/** Extra rows rendered above/below the viewport (default 3, clamped 0โ50). */
|
|
36
|
+
readonly overscan?: number;
|
|
37
|
+
/** Read the current scroll offset (test/SSR injection seam; default 0). */
|
|
38
|
+
readonly getScrollOffset?: () => number;
|
|
39
|
+
/**
|
|
40
|
+
* Fires once when the last item is within the rendered window โ including at mount if the list
|
|
41
|
+
* already fits the viewport โ and re-arms when the end scrolls back out (e.g. to load more).
|
|
42
|
+
*/
|
|
43
|
+
readonly onEndReached?: () => void;
|
|
44
|
+
/** Extra style on the scroll container. */
|
|
45
|
+
readonly style?: Reactive<StyleInput>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a virtualized recycling list as a renderer-agnostic `MindeesNode`.
|
|
49
|
+
*
|
|
50
|
+
* @throws RangeError if `itemHeight`/`height` are not positive finite numbers.
|
|
51
|
+
*/
|
|
52
|
+
declare function createList<T>(options: ListOptions<T>): MindeesNode;
|
|
53
|
+
/** Component-style alias for {@link createList}. */
|
|
54
|
+
declare function List<T>(options: ListOptions<T>): MindeesNode;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { List, ListOptions, ListWindow, computeWindow, createList };
|
|
57
|
+
//# sourceMappingURL=list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","names":[],"sources":["../src/list.ts"],"mappings":";;;;;;UA6BiB,UAAA;EAef;EAAA,SAbS,UAAA;EAeT;EAAA,SAbS,QAAA;EAeR;EAAA,SAbQ,WAAA;AAAA;AAyBX;;;;AAAA,iBAlBgB,aAAA,CACd,SAAA,UACA,cAAA,UACA,UAAA,UACA,SAAA,UACA,QAAA,WACC,UAAU;;UAYI,WAAA;EAwBW;EAAA,SAtBjB,KAAA,WAAgB,CAAA,qBAAsB,CAAA;EAsBtB;;;;;;EAAA,SAfhB,UAAA,GAAa,IAAA,QAAY,CAAA,EAAG,KAAA,mBAAwB,WAAA;EAA3B;EAAA,SAEzB,UAAA;EAF4B;EAAA,SAI5B,MAAA;EAFA;EAAA,SAIA,QAAA;EAAA;EAAA,SAEA,eAAA;EAKA;;;;EAAA,SAAA,YAAA;EAE2B;EAAA,SAA3B,KAAA,GAAQ,QAAA,CAAS,UAAA;AAAA;;;;;;iBAoBZ,UAAA,IAAc,OAAA,EAAS,WAAA,CAAY,CAAA,IAAK,WAAA;;iBA2HxC,IAAA,IAAQ,OAAA,EAAS,WAAA,CAAY,CAAA,IAAK,WAAA"}
|
package/dist/list.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { flattenStyle } from "./style.js";
|
|
2
|
+
import { ScrollView } from "./primitives.js";
|
|
3
|
+
import { batch, createElement, effect, memo, signal } from "@mindees/core";
|
|
4
|
+
//#region src/list.ts
|
|
5
|
+
/**
|
|
6
|
+
* Atlas `List` โ a virtualized, **recycling** list. Only the visible window (+ overscan) is
|
|
7
|
+
* rendered; rows are a FIXED POOL of per-slot reactive regions, NOT one region returning
|
|
8
|
+
* `items.map(...)` (the Helix reconciler has no keyed array diff, so a single array region
|
|
9
|
+
* would tear down + remount every visible row on each scroll). Each slot region depends only
|
|
10
|
+
* on its own `active` signal, so a row that stays in view keeps its identity and `renderItem`
|
|
11
|
+
* runs once for it โ only rows scrolling across the window edge are (re)created. A total-height
|
|
12
|
+
* spacer keeps the native scrollbar correct; rows are absolutely positioned by `transform`.
|
|
13
|
+
*
|
|
14
|
+
* Fixed row height in v1 (deterministic windowing, headless-testable with zero real scroll);
|
|
15
|
+
* variable/measured heights are a ๐ฌ research track. See `docs/adr/0023-atlas-list.md`.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Pure window math: which item indices are visible for a given scroll offset. Exported and
|
|
21
|
+
* exhaustively unit-tested โ the deterministic heart of virtualization (no signals, no DOM).
|
|
22
|
+
*/
|
|
23
|
+
function computeWindow(scrollTop, viewportHeight, itemHeight, itemCount, overscan) {
|
|
24
|
+
const totalHeight = itemCount * itemHeight;
|
|
25
|
+
if (itemCount <= 0) return {
|
|
26
|
+
startIndex: 0,
|
|
27
|
+
endIndex: 0,
|
|
28
|
+
totalHeight: 0
|
|
29
|
+
};
|
|
30
|
+
const top = Math.max(0, Math.min(scrollTop, Math.max(0, totalHeight - viewportHeight)));
|
|
31
|
+
const firstVisible = Math.floor(top / itemHeight);
|
|
32
|
+
const lastVisible = Math.ceil((top + viewportHeight) / itemHeight);
|
|
33
|
+
return {
|
|
34
|
+
startIndex: Math.max(0, firstVisible - overscan),
|
|
35
|
+
endIndex: Math.min(itemCount, lastVisible + overscan),
|
|
36
|
+
totalHeight
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function readScrollTop(event) {
|
|
40
|
+
const top = event?.target?.scrollTop;
|
|
41
|
+
return typeof top === "number" && Number.isFinite(top) ? top : 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a virtualized recycling list as a renderer-agnostic `MindeesNode`.
|
|
45
|
+
*
|
|
46
|
+
* @throws RangeError if `itemHeight`/`height` are not positive finite numbers.
|
|
47
|
+
*/
|
|
48
|
+
function createList(options) {
|
|
49
|
+
const { renderItem, itemHeight, height } = options;
|
|
50
|
+
if (!Number.isFinite(itemHeight) || itemHeight <= 0) throw new RangeError("List itemHeight must be a positive number");
|
|
51
|
+
if (!Number.isFinite(height) || height <= 0) throw new RangeError("List height must be a positive number");
|
|
52
|
+
const overscan = Math.max(0, Math.min(50, Math.floor(options.overscan ?? 3)));
|
|
53
|
+
const itemsOf = typeof options.items === "function" ? options.items : () => options.items;
|
|
54
|
+
return () => {
|
|
55
|
+
const scrollTop = signal(options.getScrollOffset ? options.getScrollOffset() : 0);
|
|
56
|
+
const poolSize = Math.ceil(height / itemHeight) + 2 * overscan + 1;
|
|
57
|
+
const windowMemo = memo(() => computeWindow(scrollTop(), height, itemHeight, itemsOf().length, overscan), { equals: (a, b) => a.startIndex === b.startIndex && a.endIndex === b.endIndex });
|
|
58
|
+
const slots = Array.from({ length: poolSize }, () => ({
|
|
59
|
+
item: signal(void 0),
|
|
60
|
+
index: signal(0),
|
|
61
|
+
top: signal(0),
|
|
62
|
+
active: signal(false)
|
|
63
|
+
}));
|
|
64
|
+
let endReachedFired = false;
|
|
65
|
+
effect(() => {
|
|
66
|
+
const { startIndex, endIndex } = windowMemo();
|
|
67
|
+
const items = itemsOf();
|
|
68
|
+
const used = new Array(poolSize).fill(false);
|
|
69
|
+
batch(() => {
|
|
70
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
71
|
+
const s = (i % poolSize + poolSize) % poolSize;
|
|
72
|
+
used[s] = true;
|
|
73
|
+
const slot = slots[s];
|
|
74
|
+
if (!slot) continue;
|
|
75
|
+
slot.item.set(items[i]);
|
|
76
|
+
slot.index.set(i);
|
|
77
|
+
slot.top.set(i * itemHeight);
|
|
78
|
+
slot.active.set(true);
|
|
79
|
+
}
|
|
80
|
+
for (let s = 0; s < poolSize; s++) if (!used[s]) slots[s]?.active.set(false);
|
|
81
|
+
});
|
|
82
|
+
if (options.onEndReached && items.length > 0) if (endIndex >= items.length) {
|
|
83
|
+
if (!endReachedFired) {
|
|
84
|
+
endReachedFired = true;
|
|
85
|
+
options.onEndReached();
|
|
86
|
+
}
|
|
87
|
+
} else endReachedFired = false;
|
|
88
|
+
});
|
|
89
|
+
const spacer = createElement("view", { style: () => ({
|
|
90
|
+
position: "relative",
|
|
91
|
+
width: "100%",
|
|
92
|
+
height: itemsOf().length * itemHeight
|
|
93
|
+
}) }, ...slots.map((slot, s) => () => {
|
|
94
|
+
if (!slot.active()) return null;
|
|
95
|
+
const content = renderItem(() => slot.item(), () => slot.index());
|
|
96
|
+
return createElement("view", {
|
|
97
|
+
key: `atlas-list-row-${s}`,
|
|
98
|
+
style: () => ({
|
|
99
|
+
position: "absolute",
|
|
100
|
+
left: 0,
|
|
101
|
+
right: 0,
|
|
102
|
+
top: 0,
|
|
103
|
+
height: itemHeight,
|
|
104
|
+
transform: `translateY(${slot.top()}px)`
|
|
105
|
+
})
|
|
106
|
+
}, content);
|
|
107
|
+
}));
|
|
108
|
+
return createElement(ScrollView, {
|
|
109
|
+
onScroll: (event) => scrollTop.set(readScrollTop(event)),
|
|
110
|
+
style: flattenStyle([{
|
|
111
|
+
height,
|
|
112
|
+
position: "relative"
|
|
113
|
+
}, options.style])
|
|
114
|
+
}, spacer);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Component-style alias for {@link createList}. */
|
|
118
|
+
function List(options) {
|
|
119
|
+
return createList(options);
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { List, computeWindow, createList };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=list.js.map
|
package/dist/list.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.js","names":[],"sources":["../src/list.ts"],"sourcesContent":["/**\n * Atlas `List` โ a virtualized, **recycling** list. Only the visible window (+ overscan) is\n * rendered; rows are a FIXED POOL of per-slot reactive regions, NOT one region returning\n * `items.map(...)` (the Helix reconciler has no keyed array diff, so a single array region\n * would tear down + remount every visible row on each scroll). Each slot region depends only\n * on its own `active` signal, so a row that stays in view keeps its identity and `renderItem`\n * runs once for it โ only rows scrolling across the window edge are (re)created. A total-height\n * spacer keeps the native scrollbar correct; rows are absolutely positioned by `transform`.\n *\n * Fixed row height in v1 (deterministic windowing, headless-testable with zero real scroll);\n * variable/measured heights are a ๐ฌ research track. See `docs/adr/0023-atlas-list.md`.\n *\n * @module\n */\n\nimport {\n batch,\n createElement,\n effect,\n type MindeesNode,\n memo,\n type Signal,\n signal,\n} from '@mindees/core'\nimport type { Reactive } from './host'\nimport { ScrollView } from './primitives'\nimport { flattenStyle, type StyleInput } from './style'\n\n/** The computed visible window over the item list. */\nexport interface ListWindow {\n /** First visible index (inclusive), overscan applied. */\n readonly startIndex: number\n /** Last visible index (exclusive), overscan applied. */\n readonly endIndex: number\n /** Total scrollable height (px) = itemCount ร itemHeight. */\n readonly totalHeight: number\n}\n\n/**\n * Pure window math: which item indices are visible for a given scroll offset. Exported and\n * exhaustively unit-tested โ the deterministic heart of virtualization (no signals, no DOM).\n */\nexport function computeWindow(\n scrollTop: number,\n viewportHeight: number,\n itemHeight: number,\n itemCount: number,\n overscan: number,\n): ListWindow {\n const totalHeight = itemCount * itemHeight\n if (itemCount <= 0) return { startIndex: 0, endIndex: 0, totalHeight: 0 }\n const top = Math.max(0, Math.min(scrollTop, Math.max(0, totalHeight - viewportHeight)))\n const firstVisible = Math.floor(top / itemHeight)\n const lastVisible = Math.ceil((top + viewportHeight) / itemHeight)\n const startIndex = Math.max(0, firstVisible - overscan)\n const endIndex = Math.min(itemCount, lastVisible + overscan)\n return { startIndex, endIndex, totalHeight }\n}\n\n/** Options for {@link createList}. */\nexport interface ListOptions<T> {\n /** The items, static or reactive. */\n readonly items: readonly T[] | (() => readonly T[])\n /**\n * Render one row. Receives reactive `item`/`index` **accessors** โ consume them LAZILY\n * (`Text({ children: () => item().name })`, a `style` fn, or pass them to a child) so a\n * recycled slot patches in place. Reading `item()`/`index()` synchronously in the body bakes\n * the value in and opts the row out of recycling (it re-runs renderItem on reuse instead).\n */\n readonly renderItem: (item: () => T, index: () => number) => MindeesNode\n /** Fixed row height in px (variable heights are a research track). */\n readonly itemHeight: number\n /** Viewport height in px. */\n readonly height: number\n /** Extra rows rendered above/below the viewport (default 3, clamped 0โ50). */\n readonly overscan?: number\n /** Read the current scroll offset (test/SSR injection seam; default 0). */\n readonly getScrollOffset?: () => number\n /**\n * Fires once when the last item is within the rendered window โ including at mount if the list\n * already fits the viewport โ and re-arms when the end scrolls back out (e.g. to load more).\n */\n readonly onEndReached?: () => void\n /** Extra style on the scroll container. */\n readonly style?: Reactive<StyleInput>\n}\n\ninterface Slot<T> {\n readonly item: Signal<T | undefined>\n readonly index: Signal<number>\n readonly top: Signal<number>\n readonly active: Signal<boolean>\n}\n\nfunction readScrollTop(event: unknown): number {\n const top = (event as { target?: { scrollTop?: number } })?.target?.scrollTop\n return typeof top === 'number' && Number.isFinite(top) ? top : 0\n}\n\n/**\n * Create a virtualized recycling list as a renderer-agnostic `MindeesNode`.\n *\n * @throws RangeError if `itemHeight`/`height` are not positive finite numbers.\n */\nexport function createList<T>(options: ListOptions<T>): MindeesNode {\n const { renderItem, itemHeight, height } = options\n if (!Number.isFinite(itemHeight) || itemHeight <= 0) {\n throw new RangeError('List itemHeight must be a positive number')\n }\n if (!Number.isFinite(height) || height <= 0) {\n throw new RangeError('List height must be a positive number')\n }\n const overscan = Math.max(0, Math.min(50, Math.floor(options.overscan ?? 3)))\n const itemsOf: () => readonly T[] =\n typeof options.items === 'function' ? options.items : () => options.items as readonly T[]\n\n // Return a reactive-region accessor so ALL the signals/memo/effect below are created under the\n // mounting owner (the renderer runs this once via bindReactiveChild) and are disposed on\n // unmount. Creating them eagerly here would leave them un-owned (currentOwner === null at call\n // time) and leak past `dispose()`. Validation above stays synchronous (throws at call time).\n return () => {\n const scrollTop = signal(options.getScrollOffset ? options.getScrollOffset() : 0)\n const poolSize = Math.ceil(height / itemHeight) + 2 * overscan + 1\n\n // Re-run the assignment only when the integer window actually changes (not every pixel).\n const windowMemo = memo(\n () => computeWindow(scrollTop(), height, itemHeight, itemsOf().length, overscan),\n {\n equals: (a, b) => a.startIndex === b.startIndex && a.endIndex === b.endIndex,\n },\n )\n\n const slots: Slot<T>[] = Array.from({ length: poolSize }, () => ({\n item: signal<T | undefined>(undefined),\n index: signal(0),\n top: signal(0),\n active: signal(false),\n }))\n\n let endReachedFired = false\n // Assign each visible index to slot `index % poolSize`; deactivate the rest. Max window size\n // equals poolSize (the `+1` is that exact margin โ do NOT remove it), and any run of โค\n // poolSize consecutive indices has distinct residues mod poolSize, so no two visible indices\n // ever share a slot.\n effect(() => {\n const { startIndex, endIndex } = windowMemo()\n const items = itemsOf()\n const used = new Array<boolean>(poolSize).fill(false)\n batch(() => {\n for (let i = startIndex; i < endIndex; i++) {\n const s = ((i % poolSize) + poolSize) % poolSize\n used[s] = true\n const slot = slots[s]\n if (!slot) continue\n slot.item.set(items[i])\n slot.index.set(i)\n slot.top.set(i * itemHeight)\n slot.active.set(true)\n }\n for (let s = 0; s < poolSize; s++) {\n if (!used[s]) slots[s]?.active.set(false)\n }\n })\n // onEndReached: fire when the last item is within the window (incl. at mount if the list\n // fits the viewport); re-arm when the end leaves the window.\n if (options.onEndReached && items.length > 0) {\n if (endIndex >= items.length) {\n if (!endReachedFired) {\n endReachedFired = true\n options.onEndReached()\n }\n } else {\n endReachedFired = false\n }\n }\n })\n\n // One reactive region per slot. The body depends ONLY on `active()`, so a slot that stays\n // active never re-runs renderItem; item/index/top flow through the inner accessors โ provided\n // renderItem consumes them lazily (see the ListOptions.renderItem contract).\n const rows: MindeesNode[] = slots.map((slot, s) => () => {\n if (!slot.active()) return null\n const content = renderItem(\n () => slot.item() as T,\n () => slot.index(),\n )\n return createElement(\n 'view',\n {\n key: `atlas-list-row-${s}`,\n style: () => ({\n position: 'absolute',\n left: 0,\n right: 0,\n top: 0,\n height: itemHeight,\n transform: `translateY(${slot.top()}px)`,\n }),\n },\n content,\n )\n })\n\n const spacer = createElement(\n 'view',\n {\n style: () => ({\n position: 'relative',\n width: '100%',\n height: itemsOf().length * itemHeight,\n }),\n },\n ...rows,\n )\n\n return createElement(\n ScrollView,\n {\n onScroll: (event: unknown) => scrollTop.set(readScrollTop(event)),\n style: flattenStyle([{ height, position: 'relative' }, options.style as StyleInput]),\n },\n spacer,\n )\n }\n}\n\n/** Component-style alias for {@link createList}. */\nexport function List<T>(options: ListOptions<T>): MindeesNode {\n return createList(options)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,cACd,WACA,gBACA,YACA,WACA,UACY;CACZ,MAAM,cAAc,YAAY;CAChC,IAAI,aAAa,GAAG,OAAO;EAAE,YAAY;EAAG,UAAU;EAAG,aAAa;CAAE;CACxE,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,WAAW,KAAK,IAAI,GAAG,cAAc,cAAc,CAAC,CAAC;CACtF,MAAM,eAAe,KAAK,MAAM,MAAM,UAAU;CAChD,MAAM,cAAc,KAAK,MAAM,MAAM,kBAAkB,UAAU;CAGjE,OAAO;EAAE,YAFU,KAAK,IAAI,GAAG,eAAe,QAE5B;EAAG,UADJ,KAAK,IAAI,WAAW,cAAc,QACvB;EAAG;CAAY;AAC7C;AAqCA,SAAS,cAAc,OAAwB;CAC7C,MAAM,MAAO,OAA+C,QAAQ;CACpE,OAAO,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,IAAI,MAAM;AACjE;;;;;;AAOA,SAAgB,WAAc,SAAsC;CAClE,MAAM,EAAE,YAAY,YAAY,WAAW;CAC3C,IAAI,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,GAChD,MAAM,IAAI,WAAW,2CAA2C;CAElE,IAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACxC,MAAM,IAAI,WAAW,uCAAuC;CAE9D,MAAM,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,MAAM,QAAQ,YAAY,CAAC,CAAC,CAAC;CAC5E,MAAM,UACJ,OAAO,QAAQ,UAAU,aAAa,QAAQ,cAAc,QAAQ;CAMtE,aAAa;EACX,MAAM,YAAY,OAAO,QAAQ,kBAAkB,QAAQ,gBAAgB,IAAI,CAAC;EAChF,MAAM,WAAW,KAAK,KAAK,SAAS,UAAU,IAAI,IAAI,WAAW;EAGjE,MAAM,aAAa,WACX,cAAc,UAAU,GAAG,QAAQ,YAAY,QAAQ,EAAE,QAAQ,QAAQ,GAC/E,EACE,SAAS,GAAG,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,SACtE,CACF;EAEA,MAAM,QAAmB,MAAM,KAAK,EAAE,QAAQ,SAAS,UAAU;GAC/D,MAAM,OAAsB,KAAA,CAAS;GACrC,OAAO,OAAO,CAAC;GACf,KAAK,OAAO,CAAC;GACb,QAAQ,OAAO,KAAK;EACtB,EAAE;EAEF,IAAI,kBAAkB;EAKtB,aAAa;GACX,MAAM,EAAE,YAAY,aAAa,WAAW;GAC5C,MAAM,QAAQ,QAAQ;GACtB,MAAM,OAAO,IAAI,MAAe,QAAQ,EAAE,KAAK,KAAK;GACpD,YAAY;IACV,KAAK,IAAI,IAAI,YAAY,IAAI,UAAU,KAAK;KAC1C,MAAM,KAAM,IAAI,WAAY,YAAY;KACxC,KAAK,KAAK;KACV,MAAM,OAAO,MAAM;KACnB,IAAI,CAAC,MAAM;KACX,KAAK,KAAK,IAAI,MAAM,EAAE;KACtB,KAAK,MAAM,IAAI,CAAC;KAChB,KAAK,IAAI,IAAI,IAAI,UAAU;KAC3B,KAAK,OAAO,IAAI,IAAI;IACtB;IACA,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAC5B,IAAI,CAAC,KAAK,IAAI,MAAM,IAAI,OAAO,IAAI,KAAK;GAE5C,CAAC;GAGD,IAAI,QAAQ,gBAAgB,MAAM,SAAS,GACzC,IAAI,YAAY,MAAM;QAChB,CAAC,iBAAiB;KACpB,kBAAkB;KAClB,QAAQ,aAAa;IACvB;UAEA,kBAAkB;EAGxB,CAAC;EA4BD,MAAM,SAAS,cACb,QACA,EACE,cAAc;GACZ,UAAU;GACV,OAAO;GACP,QAAQ,QAAQ,EAAE,SAAS;EAC7B,GACF,GACA,GAhC0B,MAAM,KAAK,MAAM,YAAY;GACvD,IAAI,CAAC,KAAK,OAAO,GAAG,OAAO;GAC3B,MAAM,UAAU,iBACR,KAAK,KAAK,SACV,KAAK,MAAM,CACnB;GACA,OAAO,cACL,QACA;IACE,KAAK,kBAAkB;IACvB,cAAc;KACZ,UAAU;KACV,MAAM;KACN,OAAO;KACP,KAAK;KACL,QAAQ;KACR,WAAW,cAAc,KAAK,IAAI,EAAE;IACtC;GACF,GACA,OACF;EACF,CAWQ,CACR;EAEA,OAAO,cACL,YACA;GACE,WAAW,UAAmB,UAAU,IAAI,cAAc,KAAK,CAAC;GAChE,OAAO,aAAa,CAAC;IAAE;IAAQ,UAAU;GAAW,GAAG,QAAQ,KAAmB,CAAC;EACrF,GACA,MACF;CACF;AACF;;AAGA,SAAgB,KAAQ,SAAsC;CAC5D,OAAO,WAAW,OAAO;AAC3B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { StyleInput } from "./style.js";
|
|
2
|
+
import { BaseProps, Reactive } from "./host.js";
|
|
3
|
+
import { Component, MindeesNode } from "@mindees/core";
|
|
4
|
+
|
|
5
|
+
//#region src/primitives.d.ts
|
|
6
|
+
/** A generic container (โ `view`/`div`). */
|
|
7
|
+
interface ViewProps extends BaseProps {
|
|
8
|
+
readonly children?: MindeesNode;
|
|
9
|
+
}
|
|
10
|
+
declare const View: Component<ViewProps>;
|
|
11
|
+
/** Text content (โ `text`/`span`). No default `role` (a bare span is correct; pass `role` to opt in). */
|
|
12
|
+
interface TextProps extends BaseProps {
|
|
13
|
+
readonly children?: MindeesNode;
|
|
14
|
+
}
|
|
15
|
+
declare const Text: Component<TextProps>;
|
|
16
|
+
/** An image (โ `image`/`img`). Requires `label` (alt) unless `decorative`. */
|
|
17
|
+
interface ImageProps extends BaseProps {
|
|
18
|
+
readonly src: string;
|
|
19
|
+
/** Mark purely-decorative images so screen readers skip them (alt="" + aria-hidden). */
|
|
20
|
+
readonly decorative?: boolean;
|
|
21
|
+
}
|
|
22
|
+
declare const Image: Component<ImageProps>;
|
|
23
|
+
/** A text field (โ `textinput`/`input`). `value` may be reactive (controlled). */
|
|
24
|
+
interface TextInputProps extends BaseProps {
|
|
25
|
+
readonly value?: Reactive<string>;
|
|
26
|
+
readonly placeholder?: string;
|
|
27
|
+
readonly type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url';
|
|
28
|
+
readonly disabled?: boolean;
|
|
29
|
+
/** Fires on every keystroke with the current value (`input` event). */
|
|
30
|
+
readonly onInput?: (value: string) => void;
|
|
31
|
+
/** Fires on commit/blur with the current value (`change` event). */
|
|
32
|
+
readonly onChange?: (value: string) => void;
|
|
33
|
+
}
|
|
34
|
+
declare const TextInput: Component<TextInputProps>;
|
|
35
|
+
/** Interaction state exposed to a Pressable style function. */
|
|
36
|
+
interface InteractionState {
|
|
37
|
+
readonly hovered: boolean;
|
|
38
|
+
readonly pressed: boolean;
|
|
39
|
+
readonly focused: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** A pressable surface with built-in hover/press/focus state. Web-real via DOM events. */
|
|
42
|
+
interface PressableProps extends Omit<BaseProps, 'style'> {
|
|
43
|
+
readonly children?: MindeesNode;
|
|
44
|
+
/** Called when activated (click / Enter / Space) โ skipped while `disabled`. */
|
|
45
|
+
readonly onPress?: () => void;
|
|
46
|
+
readonly disabled?: boolean;
|
|
47
|
+
/** Static/reactive style, or a function of the live interaction state. */
|
|
48
|
+
readonly style?: Reactive<StyleInput> | ((state: InteractionState) => StyleInput);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create the interaction signals + host handlers a pressable surface needs. Reusable so other
|
|
52
|
+
* primitives can compose interaction state. Web wires REAL DOM events (`click`, `pointer*`,
|
|
53
|
+
* `focus`/`blur`, `keydown`) โ never a fake cross-platform `press` event that no-ops on web.
|
|
54
|
+
*/
|
|
55
|
+
declare function usePressable(options?: {
|
|
56
|
+
onPress?: () => void;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
}): {
|
|
59
|
+
state: () => InteractionState;
|
|
60
|
+
handlers: Record<string, (event: unknown) => void>;
|
|
61
|
+
};
|
|
62
|
+
declare const Pressable: Component<PressableProps>;
|
|
63
|
+
/** A labelled button = {@link Pressable} wrapping a {@link Text}. */
|
|
64
|
+
interface ButtonProps extends PressableProps {
|
|
65
|
+
/** Convenience text label (alternative to `children`). */
|
|
66
|
+
readonly title?: string;
|
|
67
|
+
}
|
|
68
|
+
declare const Button: Component<ButtonProps>;
|
|
69
|
+
type FlexAlign = 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
70
|
+
type FlexJustify = 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
|
|
71
|
+
/** A flex container. `direction` defaults to `column`. */
|
|
72
|
+
interface StackProps extends ViewProps {
|
|
73
|
+
readonly direction?: 'row' | 'column';
|
|
74
|
+
readonly gap?: number | string;
|
|
75
|
+
readonly align?: FlexAlign;
|
|
76
|
+
readonly justify?: FlexJustify;
|
|
77
|
+
}
|
|
78
|
+
declare const Stack: Component<StackProps>;
|
|
79
|
+
/** A horizontal {@link Stack}. */
|
|
80
|
+
declare const Row: Component<Omit<StackProps, 'direction'>>;
|
|
81
|
+
/** A vertical {@link Stack}. */
|
|
82
|
+
declare const Column: Component<Omit<StackProps, 'direction'>>;
|
|
83
|
+
/** Flexible (or fixed) empty space. */
|
|
84
|
+
interface SpacerProps {
|
|
85
|
+
/** Fixed size (px) instead of flexible fill. */
|
|
86
|
+
readonly size?: number | string;
|
|
87
|
+
}
|
|
88
|
+
declare const Spacer: Component<SpacerProps>;
|
|
89
|
+
/** A scrollable container (โ `scrollview`/`div` with `overflow:auto`). */
|
|
90
|
+
interface ScrollViewProps extends ViewProps {
|
|
91
|
+
readonly horizontal?: boolean;
|
|
92
|
+
/** Fires on scroll with the host scroll event. */
|
|
93
|
+
readonly onScroll?: (event: unknown) => void;
|
|
94
|
+
}
|
|
95
|
+
declare const ScrollView: Component<ScrollViewProps>;
|
|
96
|
+
//#endregion
|
|
97
|
+
export { Button, ButtonProps, Column, Image, ImageProps, InteractionState, Pressable, PressableProps, Row, ScrollView, ScrollViewProps, Spacer, SpacerProps, Stack, StackProps, Text, TextInput, TextInputProps, TextProps, View, ViewProps, usePressable };
|
|
98
|
+
//# sourceMappingURL=primitives.d.ts.map
|
|
@@ -0,0 +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;;UAIZ,UAAA,SAAmB,SAAS;EAAA,SAClC,GAAA;EAPW;EAAA,SASX,UAAA;AAAA;AAAA,cAEE,KAAA,EAAO,SAAS,CAAC,UAAA;;UAoBb,cAAA,SAAuB,SAAS;EAAA,SACtC,KAAA,GAAQ,QAAA;EAAA,SACR,WAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;;WAEA,OAAA,IAAW,KAAA;EA/Bc;EAAA,SAiCzB,QAAA,IAAY,KAAA;AAAA;AAAA,cAEV,SAAA,EAAW,SAAS,CAAC,cAAA;AAhCb;AAAA,UA6CJ,gBAAA;EAAA,SACN,OAAA;EAAA,SACA,OAAA;EAAA,SACA,OAAA;AAAA;AA1BX;AAAA,UA8BiB,cAAA,SAAuB,IAAA,CAAK,SAAA;EAAA,SAClC,QAAA,GAAW,WAAA;EA/B2B;EAAA,SAiCtC,OAAA;EAAA,SACA,QAAA;EAjCQ;EAAA,SAmCR,KAAA,GAAQ,QAAA,CAAS,UAAA,MAAgB,KAAA,EAAO,gBAAA,KAAqB,UAAA;AAAA;;;;;;iBAQxD,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;EAlGlB;EAAA,SAoGtB,KAAK;AAAA;AAAA,cAEH,MAAA,EAAQ,SAAS,CAAC,WAAA;AAAA,KAe1B,SAAA;AAAA,KACA,WAAA;AA/GL;AAAA,UAwHiB,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;;cAajB,GAAA,EAAK,SAAA,CAAU,IAAA,CAAK,UAAA;;cAIpB,MAAA,EAAQ,SAAA,CAAU,IAAA,CAAK,UAAA;;UAInB,WAAA;EAlJN;EAAA,SAoJA,IAAI;AAAA;AAAA,cAEF,MAAA,EAAQ,SAAS,CAAC,WAAA;;UAMd,eAAA,SAAwB,SAAS;EAAA,SACvC,UAAA;EAxJwC;EAAA,SA0JxC,QAAA,IAAY,KAAA;AAAA;AAAA,cAEV,UAAA,EAAY,SAAS,CAAC,eAAA"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { flattenStyle } from "./style.js";
|
|
2
|
+
import { resolveStyle, toHostProps } from "./host.js";
|
|
3
|
+
import { createElement, signal } from "@mindees/core";
|
|
4
|
+
//#region src/primitives.ts
|
|
5
|
+
/**
|
|
6
|
+
* Atlas primitives โ accessible, signals-native UI building blocks. Each is a
|
|
7
|
+
* `Component<P>` over `@mindees/core`'s `createElement`, returning a renderer-agnostic
|
|
8
|
+
* `MindeesNode`. Web rendering is real (via the Helix DOM backend); native is a labeled ๐ฌ
|
|
9
|
+
* research track (the same serializable tree, interpreted by a native host later). See
|
|
10
|
+
* `docs/adr/0022-atlas-primitives.md`.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** Merge a base layout style with a caller's (possibly reactive) style, staying reactive if it is. */
|
|
15
|
+
function withBaseStyle(base, style) {
|
|
16
|
+
if (typeof style === "function") {
|
|
17
|
+
const accessor = style;
|
|
18
|
+
return () => flattenStyle([base, accessor()]);
|
|
19
|
+
}
|
|
20
|
+
return flattenStyle([base, style]);
|
|
21
|
+
}
|
|
22
|
+
/** Dev-only warning (silent in production). Structural global access โ no DOM/Node lib needed. */
|
|
23
|
+
function warnDev(message) {
|
|
24
|
+
const g = globalThis;
|
|
25
|
+
if (g.process?.env?.NODE_ENV === "production") return;
|
|
26
|
+
g.console?.warn?.(`[atlas] ${message}`);
|
|
27
|
+
}
|
|
28
|
+
function eventValue(event) {
|
|
29
|
+
return event?.target?.value ?? "";
|
|
30
|
+
}
|
|
31
|
+
const View = (props) => createElement("view", toHostProps(props), props.children);
|
|
32
|
+
const Text = (props) => createElement("text", toHostProps(props), props.children);
|
|
33
|
+
const Image = (props) => {
|
|
34
|
+
const host = toHostProps(props);
|
|
35
|
+
host.src = props.src;
|
|
36
|
+
if (props.decorative) {
|
|
37
|
+
host.alt = "";
|
|
38
|
+
host["aria-hidden"] = "true";
|
|
39
|
+
delete host["aria-label"];
|
|
40
|
+
delete host["aria-labelledby"];
|
|
41
|
+
} else {
|
|
42
|
+
if (props.label === void 0) warnDev("Image without a `label` (alt text); pass `label` or set `decorative`.");
|
|
43
|
+
host.alt = props.label ?? "";
|
|
44
|
+
}
|
|
45
|
+
return createElement("image", host);
|
|
46
|
+
};
|
|
47
|
+
const TextInput = (props) => {
|
|
48
|
+
const host = toHostProps(props);
|
|
49
|
+
if (!host.role) host.role = "textbox";
|
|
50
|
+
if (props.value !== void 0) host.value = props.value;
|
|
51
|
+
if (props.placeholder !== void 0) host.placeholder = props.placeholder;
|
|
52
|
+
if (props.type !== void 0) host.type = props.type;
|
|
53
|
+
if (props.disabled) host.disabled = true;
|
|
54
|
+
if (props.onInput) host.onInput = (e) => props.onInput?.(eventValue(e));
|
|
55
|
+
if (props.onChange) host.onChange = (e) => props.onChange?.(eventValue(e));
|
|
56
|
+
return createElement("textinput", host);
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Create the interaction signals + host handlers a pressable surface needs. Reusable so other
|
|
60
|
+
* primitives can compose interaction state. Web wires REAL DOM events (`click`, `pointer*`,
|
|
61
|
+
* `focus`/`blur`, `keydown`) โ never a fake cross-platform `press` event that no-ops on web.
|
|
62
|
+
*/
|
|
63
|
+
function usePressable(options = {}) {
|
|
64
|
+
const hovered = signal(false);
|
|
65
|
+
const pressed = signal(false);
|
|
66
|
+
const focused = signal(false);
|
|
67
|
+
const enabled = () => !options.disabled;
|
|
68
|
+
const fire = () => {
|
|
69
|
+
if (enabled()) options.onPress?.();
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
state: () => ({
|
|
73
|
+
hovered: hovered(),
|
|
74
|
+
pressed: pressed(),
|
|
75
|
+
focused: focused()
|
|
76
|
+
}),
|
|
77
|
+
handlers: {
|
|
78
|
+
onClick: () => fire(),
|
|
79
|
+
onPointerEnter: () => {
|
|
80
|
+
if (enabled()) hovered.set(true);
|
|
81
|
+
},
|
|
82
|
+
onPointerLeave: () => {
|
|
83
|
+
if (enabled()) {
|
|
84
|
+
hovered.set(false);
|
|
85
|
+
pressed.set(false);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
onPointerDown: () => {
|
|
89
|
+
if (enabled()) pressed.set(true);
|
|
90
|
+
},
|
|
91
|
+
onPointerUp: () => {
|
|
92
|
+
if (enabled()) pressed.set(false);
|
|
93
|
+
},
|
|
94
|
+
onFocus: () => {
|
|
95
|
+
if (enabled()) focused.set(true);
|
|
96
|
+
},
|
|
97
|
+
onBlur: () => {
|
|
98
|
+
if (enabled()) focused.set(false);
|
|
99
|
+
},
|
|
100
|
+
onKeyDown: (e) => {
|
|
101
|
+
const ev = e;
|
|
102
|
+
if (ev.key === "Enter" || ev.key === " ") {
|
|
103
|
+
ev.preventDefault?.();
|
|
104
|
+
fire();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const Pressable = (props) => {
|
|
111
|
+
const { state, handlers } = usePressable({
|
|
112
|
+
...props.onPress ? { onPress: props.onPress } : {},
|
|
113
|
+
...props.disabled ? { disabled: true } : {}
|
|
114
|
+
});
|
|
115
|
+
const { style, ...rest } = props;
|
|
116
|
+
const host = {
|
|
117
|
+
...toHostProps(rest),
|
|
118
|
+
...handlers
|
|
119
|
+
};
|
|
120
|
+
if (!host.role) host.role = "button";
|
|
121
|
+
if (props.disabled) host["aria-disabled"] = "true";
|
|
122
|
+
else host.tabindex = 0;
|
|
123
|
+
if (style !== void 0) host.style = typeof style === "function" && style.length >= 1 ? () => flattenStyle(style(state())) : resolveStyle(style);
|
|
124
|
+
return createElement("view", host, props.children);
|
|
125
|
+
};
|
|
126
|
+
const Button = (props) => {
|
|
127
|
+
const { title, children, ...rest } = props;
|
|
128
|
+
return createElement(Pressable, rest, (Array.isArray(children) ? children.length > 0 : children != null) ? children : title !== void 0 ? createElement(Text, null, title) : null);
|
|
129
|
+
};
|
|
130
|
+
const Stack = (props) => {
|
|
131
|
+
const { direction = "column", gap, align, justify, style, children, ...rest } = props;
|
|
132
|
+
const layout = {
|
|
133
|
+
display: "flex",
|
|
134
|
+
flexDirection: direction,
|
|
135
|
+
...gap !== void 0 ? { gap } : {},
|
|
136
|
+
...align !== void 0 ? { alignItems: align } : {},
|
|
137
|
+
...justify !== void 0 ? { justifyContent: justify } : {}
|
|
138
|
+
};
|
|
139
|
+
return createElement(View, {
|
|
140
|
+
...rest,
|
|
141
|
+
style: withBaseStyle(layout, style)
|
|
142
|
+
}, children);
|
|
143
|
+
};
|
|
144
|
+
/** A horizontal {@link Stack}. */
|
|
145
|
+
const Row = (props) => createElement(Stack, {
|
|
146
|
+
...props,
|
|
147
|
+
direction: "row"
|
|
148
|
+
}, props.children);
|
|
149
|
+
/** A vertical {@link Stack}. */
|
|
150
|
+
const Column = (props) => createElement(Stack, {
|
|
151
|
+
...props,
|
|
152
|
+
direction: "column"
|
|
153
|
+
}, props.children);
|
|
154
|
+
const Spacer = (props) => createElement(View, { style: props.size !== void 0 ? {
|
|
155
|
+
width: props.size,
|
|
156
|
+
height: props.size
|
|
157
|
+
} : { flex: 1 } });
|
|
158
|
+
const ScrollView = (props) => {
|
|
159
|
+
const { horizontal, onScroll, style, children, ...rest } = props;
|
|
160
|
+
const host = toHostProps({
|
|
161
|
+
...rest,
|
|
162
|
+
style: withBaseStyle(horizontal ? {
|
|
163
|
+
display: "flex",
|
|
164
|
+
overflow: "auto",
|
|
165
|
+
flexDirection: "row",
|
|
166
|
+
flexWrap: "nowrap"
|
|
167
|
+
} : { overflow: "auto" }, style)
|
|
168
|
+
});
|
|
169
|
+
if (onScroll) host.onScroll = onScroll;
|
|
170
|
+
if (horizontal) host["data-orientation"] = "horizontal";
|
|
171
|
+
return createElement("scrollview", host, children);
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
export { Button, Column, Image, Pressable, Row, ScrollView, Spacer, Stack, Text, TextInput, View, usePressable };
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=primitives.js.map
|
|
@@ -0,0 +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/** 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}\nexport const Image: Component<ImageProps> = (props) => {\n const host = toHostProps(props)\n host.src = props.src\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/** A text field (โ `textinput`/`input`). `value` may be reactive (controlled). */\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 /** 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}\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 if (props.type !== undefined) host.type = props.type\n if (props.disabled) host.disabled = 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 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 return createElement('scrollview', host, children)\n}\n"],"mappings":"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"}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/style.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Atlas styling โ a curated, typed `StyleObject` that is the single cross-platform style
|
|
4
|
+
* vehicle. The renderer applies one `style` object to web inline styles (numbers โ `px` for
|
|
5
|
+
* dimensional props) and serializes the same object as a native prop, so Atlas never needs a
|
|
6
|
+
* second channel. The subset is hand-picked to be meaningful on **both** web and native;
|
|
7
|
+
* anything outside it goes through the explicit `unsafeStyle` escape hatch. See
|
|
8
|
+
* `docs/adr/0022-atlas-primitives.md`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** A single style value: a string (e.g. `'red'`, `'50%'`) or a number (px on web, dp on native). */
|
|
13
|
+
type StyleValue = string | number;
|
|
14
|
+
/** A curated, cross-platform-meaningful style object. */
|
|
15
|
+
interface StyleObject {
|
|
16
|
+
display?: 'flex' | 'none';
|
|
17
|
+
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
|
|
18
|
+
justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
|
|
19
|
+
alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
20
|
+
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
21
|
+
flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse';
|
|
22
|
+
flex?: number | string;
|
|
23
|
+
flexGrow?: number;
|
|
24
|
+
flexShrink?: number;
|
|
25
|
+
flexBasis?: StyleValue;
|
|
26
|
+
gap?: StyleValue;
|
|
27
|
+
rowGap?: StyleValue;
|
|
28
|
+
columnGap?: StyleValue;
|
|
29
|
+
width?: StyleValue;
|
|
30
|
+
height?: StyleValue;
|
|
31
|
+
minWidth?: StyleValue;
|
|
32
|
+
minHeight?: StyleValue;
|
|
33
|
+
maxWidth?: StyleValue;
|
|
34
|
+
maxHeight?: StyleValue;
|
|
35
|
+
padding?: StyleValue;
|
|
36
|
+
paddingTop?: StyleValue;
|
|
37
|
+
paddingRight?: StyleValue;
|
|
38
|
+
paddingBottom?: StyleValue;
|
|
39
|
+
paddingLeft?: StyleValue;
|
|
40
|
+
margin?: StyleValue;
|
|
41
|
+
marginTop?: StyleValue;
|
|
42
|
+
marginRight?: StyleValue;
|
|
43
|
+
marginBottom?: StyleValue;
|
|
44
|
+
marginLeft?: StyleValue;
|
|
45
|
+
position?: 'relative' | 'absolute' | 'fixed' | 'sticky' | 'static';
|
|
46
|
+
top?: StyleValue;
|
|
47
|
+
right?: StyleValue;
|
|
48
|
+
bottom?: StyleValue;
|
|
49
|
+
left?: StyleValue;
|
|
50
|
+
overflow?: 'visible' | 'hidden' | 'scroll' | 'auto';
|
|
51
|
+
zIndex?: number;
|
|
52
|
+
backgroundColor?: string;
|
|
53
|
+
opacity?: number;
|
|
54
|
+
borderRadius?: StyleValue;
|
|
55
|
+
borderWidth?: StyleValue;
|
|
56
|
+
borderColor?: string;
|
|
57
|
+
borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none';
|
|
58
|
+
boxShadow?: string;
|
|
59
|
+
color?: string;
|
|
60
|
+
fontSize?: StyleValue;
|
|
61
|
+
fontWeight?: number | string;
|
|
62
|
+
fontFamily?: string;
|
|
63
|
+
lineHeight?: StyleValue;
|
|
64
|
+
letterSpacing?: StyleValue;
|
|
65
|
+
textAlign?: 'left' | 'center' | 'right' | 'justify';
|
|
66
|
+
textDecoration?: string;
|
|
67
|
+
transform?: string;
|
|
68
|
+
cursor?: string;
|
|
69
|
+
}
|
|
70
|
+
/** A style or a (possibly nested) list of styles with falsy entries dropped โ `flattenStyle` merges it. */
|
|
71
|
+
type StyleInput = StyleObject | false | null | undefined | StyleInput[];
|
|
72
|
+
/**
|
|
73
|
+
* Merge a {@link StyleInput} (a style, or an array of styles with `false`/`null`/`undefined`
|
|
74
|
+
* entries skipped) into one {@link StyleObject}; later entries win. Lets conditional styles
|
|
75
|
+
* compose: `flattenStyle([base, active && activeStyle, props.style])`.
|
|
76
|
+
*/
|
|
77
|
+
declare function flattenStyle(input: StyleInput): StyleObject;
|
|
78
|
+
//#endregion
|
|
79
|
+
export { StyleInput, StyleObject, StyleValue, flattenStyle };
|
|
80
|
+
//# sourceMappingURL=style.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.d.ts","names":[],"sources":["../src/style.ts"],"mappings":";;AAYA;;;;AAAsB;AAGtB;;;;;KAHY,UAAA;;UAGK,WAAA;EAEf,OAAA;EACA,aAAA;EACA,cAAA;EAOA,UAAA;EACA,SAAA;EACA,QAAA;EACA,IAAA;EACA,QAAA;EACA,UAAA;EACA,SAAA,GAAY,UAAA;EACZ,GAAA,GAAM,UAAA;EACN,MAAA,GAAS,UAAA;EACT,SAAA,GAAY,UAAA;EAGZ,KAAA,GAAQ,UAAA;EACR,MAAA,GAAS,UAAA;EACT,QAAA,GAAW,UAAA;EACX,SAAA,GAAY,UAAA;EACZ,QAAA,GAAW,UAAA;EACX,SAAA,GAAY,UAAA;EACZ,OAAA,GAAU,UAAA;EACV,UAAA,GAAa,UAAA;EACb,YAAA,GAAe,UAAA;EACf,aAAA,GAAgB,UAAA;EAChB,WAAA,GAAc,UAAA;EACd,MAAA,GAAS,UAAA;EACT,SAAA,GAAY,UAAA;EACZ,WAAA,GAAc,UAAA;EACd,YAAA,GAAe,UAAA;EACf,UAAA,GAAa,UAAA;EACb,QAAA;EACA,GAAA,GAAM,UAAA;EACN,KAAA,GAAQ,UAAA;EACR,MAAA,GAAS,UAAA;EACT,IAAA,GAAO,UAAA;EACP,QAAA;EACA,MAAA;EAGA,eAAA;EACA,OAAA;EACA,YAAA,GAAe,UAAA;EACf,WAAA,GAAc,UAAA;EACd,WAAA;EACA,WAAA;EACA,SAAA;EAGA,KAAA;EACA,QAAA,GAAW,UAAA;EACX,UAAA;EACA,UAAA;EACA,UAAA,GAAa,UAAA;EACb,aAAA,GAAgB,UAAA;EAChB,SAAA;EACA,cAAA;EAGA,SAAA;EACA,MAAA;AAAA;;KAIU,UAAA,GAAa,WAAA,8BAAyC,UAAU;;;;;;iBAO5D,YAAA,CAAa,KAAA,EAAO,UAAA,GAAa,WAAW"}
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/style.ts
|
|
2
|
+
/**
|
|
3
|
+
* Merge a {@link StyleInput} (a style, or an array of styles with `false`/`null`/`undefined`
|
|
4
|
+
* entries skipped) into one {@link StyleObject}; later entries win. Lets conditional styles
|
|
5
|
+
* compose: `flattenStyle([base, active && activeStyle, props.style])`.
|
|
6
|
+
*/
|
|
7
|
+
function flattenStyle(input) {
|
|
8
|
+
const out = {};
|
|
9
|
+
const visit = (value) => {
|
|
10
|
+
if (!value) return;
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
for (const v of value) visit(v);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
Object.assign(out, value);
|
|
16
|
+
};
|
|
17
|
+
visit(input);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { flattenStyle };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=style.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../src/style.ts"],"sourcesContent":["/**\n * Atlas styling โ a curated, typed `StyleObject` that is the single cross-platform style\n * vehicle. The renderer applies one `style` object to web inline styles (numbers โ `px` for\n * dimensional props) and serializes the same object as a native prop, so Atlas never needs a\n * second channel. The subset is hand-picked to be meaningful on **both** web and native;\n * anything outside it goes through the explicit `unsafeStyle` escape hatch. See\n * `docs/adr/0022-atlas-primitives.md`.\n *\n * @module\n */\n\n/** A single style value: a string (e.g. `'red'`, `'50%'`) or a number (px on web, dp on native). */\nexport type StyleValue = string | number\n\n/** A curated, cross-platform-meaningful style object. */\nexport interface StyleObject {\n // Flexbox layout\n display?: 'flex' | 'none'\n flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse'\n justifyContent?:\n | 'flex-start'\n | 'flex-end'\n | 'center'\n | 'space-between'\n | 'space-around'\n | 'space-evenly'\n alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'\n alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'\n flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse'\n flex?: number | string\n flexGrow?: number\n flexShrink?: number\n flexBasis?: StyleValue\n gap?: StyleValue\n rowGap?: StyleValue\n columnGap?: StyleValue\n\n // Box model\n width?: StyleValue\n height?: StyleValue\n minWidth?: StyleValue\n minHeight?: StyleValue\n maxWidth?: StyleValue\n maxHeight?: StyleValue\n padding?: StyleValue\n paddingTop?: StyleValue\n paddingRight?: StyleValue\n paddingBottom?: StyleValue\n paddingLeft?: StyleValue\n margin?: StyleValue\n marginTop?: StyleValue\n marginRight?: StyleValue\n marginBottom?: StyleValue\n marginLeft?: StyleValue\n position?: 'relative' | 'absolute' | 'fixed' | 'sticky' | 'static'\n top?: StyleValue\n right?: StyleValue\n bottom?: StyleValue\n left?: StyleValue\n overflow?: 'visible' | 'hidden' | 'scroll' | 'auto'\n zIndex?: number\n\n // Visual\n backgroundColor?: string\n opacity?: number\n borderRadius?: StyleValue\n borderWidth?: StyleValue\n borderColor?: string\n borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none'\n boxShadow?: string\n\n // Text\n color?: string\n fontSize?: StyleValue\n fontWeight?: number | string\n fontFamily?: string\n lineHeight?: StyleValue\n letterSpacing?: StyleValue\n textAlign?: 'left' | 'center' | 'right' | 'justify'\n textDecoration?: string\n\n // Transform / interaction (web-leaning but harmless on native)\n transform?: string\n cursor?: string\n}\n\n/** A style or a (possibly nested) list of styles with falsy entries dropped โ `flattenStyle` merges it. */\nexport type StyleInput = StyleObject | false | null | undefined | StyleInput[]\n\n/**\n * Merge a {@link StyleInput} (a style, or an array of styles with `false`/`null`/`undefined`\n * entries skipped) into one {@link StyleObject}; later entries win. Lets conditional styles\n * compose: `flattenStyle([base, active && activeStyle, props.style])`.\n */\nexport function flattenStyle(input: StyleInput): StyleObject {\n const out: StyleObject = {}\n const visit = (value: StyleInput): void => {\n if (!value) return\n if (Array.isArray(value)) {\n for (const v of value) visit(v)\n return\n }\n Object.assign(out, value)\n }\n visit(input)\n return out\n}\n"],"mappings":";;;;;;AA8FA,SAAgB,aAAa,OAAgC;CAC3D,MAAM,MAAmB,CAAC;CAC1B,MAAM,SAAS,UAA4B;EACzC,IAAI,CAAC,OAAO;EACZ,IAAI,MAAM,QAAQ,KAAK,GAAG;GACxB,KAAK,MAAM,KAAK,OAAO,MAAM,CAAC;GAC9B;EACF;EACA,OAAO,OAAO,KAAK,KAAK;CAC1B;CACA,MAAM,KAAK;CACX,OAAO;AACT"}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Context, ContextProvider } from "@mindees/core";
|
|
2
|
+
|
|
3
|
+
//#region src/theme.d.ts
|
|
4
|
+
/** A design-token theme. External token packages can satisfy this shape. */
|
|
5
|
+
interface ThemeTokens {
|
|
6
|
+
readonly colors: {
|
|
7
|
+
readonly background: string;
|
|
8
|
+
readonly surface: string;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
readonly textMuted: string;
|
|
11
|
+
readonly primary: string;
|
|
12
|
+
readonly onPrimary: string;
|
|
13
|
+
readonly border: string;
|
|
14
|
+
readonly danger: string;
|
|
15
|
+
};
|
|
16
|
+
/** Spacing scale (px), index 0 = none. */
|
|
17
|
+
readonly space: readonly number[];
|
|
18
|
+
readonly radii: {
|
|
19
|
+
readonly sm: number;
|
|
20
|
+
readonly md: number;
|
|
21
|
+
readonly lg: number;
|
|
22
|
+
readonly full: number;
|
|
23
|
+
};
|
|
24
|
+
readonly fontSizes: {
|
|
25
|
+
readonly sm: number;
|
|
26
|
+
readonly md: number;
|
|
27
|
+
readonly lg: number;
|
|
28
|
+
readonly xl: number;
|
|
29
|
+
};
|
|
30
|
+
readonly fontWeights: {
|
|
31
|
+
readonly regular: number;
|
|
32
|
+
readonly medium: number;
|
|
33
|
+
readonly bold: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** A sensible, neutral default theme so Atlas works standalone. */
|
|
37
|
+
declare const defaultTokens: ThemeTokens;
|
|
38
|
+
/** The Atlas theme context (defaults to {@link defaultTokens}). */
|
|
39
|
+
declare const ThemeContext: Context<ThemeTokens>;
|
|
40
|
+
/** One-level-deep partial of the token groups (arrays are replaced wholesale, not deep-partialed). */
|
|
41
|
+
type DeepPartial<T> = { [K in keyof T]?: T[K] extends ReadonlyArray<unknown> ? T[K] : T[K] extends object ? Partial<T[K]> : T[K] };
|
|
42
|
+
/**
|
|
43
|
+
* Create a theme provider. Pass partial overrides (deep-merged one level onto the defaults) or
|
|
44
|
+
* nothing for the defaults. Use `.select(t => โฆ)` for a reactive, isolated token accessor that
|
|
45
|
+
* a primitive consumes as a `Reactive<StyleObject>`.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const theme = createTheme({ colors: { primary: '#7c3aed' } })
|
|
49
|
+
* const accent = theme.select((t) => t.colors.primary)
|
|
50
|
+
*/
|
|
51
|
+
declare function createTheme(overrides?: DeepPartial<ThemeTokens>): ContextProvider<ThemeTokens>;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ThemeContext, ThemeTokens, createTheme, defaultTokens };
|
|
54
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","names":[],"sources":["../src/theme.ts"],"mappings":";;;;UAaiB,WAAA;EAAA,SACN,MAAA;IAAA,SACE,UAAA;IAAA,SACA,OAAA;IAAA,SACA,IAAA;IAAA,SACA,SAAA;IAAA,SACA,OAAA;IAAA,SACA,SAAA;IAAA,SACA,MAAA;IAAA,SACA,MAAA;EAAA;EAUF;EAAA,SAPA,KAAA;EAAA,SACA,KAAA;IAAA,SACE,EAAA;IAAA,SACA,EAAA;IAAA,SACA,EAAA;IAAA,SACA,IAAA;EAAA;EAAA,SAEF,SAAA;IAAA,SACE,EAAA;IAAA,SACA,EAAA;IAAA,SACA,EAAA;IAAA,SACA,EAAA;EAAA;EAAA,SAEF,WAAA;IAAA,SAAwB,OAAA;IAAA,SAA0B,MAAA;IAAA,SAAyB,IAAA;EAAA;AAAA;AAsBxC;AAAA,cAlBjC,aAAA,EAAe,WAe3B;;cAGY,YAAA,EAAc,OAAO,CAAC,WAAA;;KAc9B,WAAA,oBACS,CAAA,IAAK,CAAA,CAAE,CAAA,UAAW,aAAA,YAC1B,CAAA,CAAE,CAAA,IACF,CAAA,CAAE,CAAA,mBACA,OAAA,CAAQ,CAAA,CAAE,CAAA,KACV,CAAA,CAAE,CAAA;;;;;;;;;;iBAYM,WAAA,CACd,SAAA,GAAW,WAAA,CAAY,WAAA,IACtB,eAAA,CAAgB,WAAA"}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createContext, createProvider } from "@mindees/core";
|
|
2
|
+
//#region src/theme.ts
|
|
3
|
+
/**
|
|
4
|
+
* Atlas theming โ a structural `ThemeTokens` interface + a minimal `defaultTokens`, consumed
|
|
5
|
+
* through a `@mindees/core` selector-isolated context. Atlas takes **no dependency** on any
|
|
6
|
+
* tokens package (the user's published `@mindees/tokens` can satisfy `ThemeTokens` and be
|
|
7
|
+
* injected by the app); a component that selects one slice (`t => t.colors.primary`) only
|
|
8
|
+
* re-runs when that slice changes. See `docs/adr/0022-atlas-primitives.md`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** A sensible, neutral default theme so Atlas works standalone. */
|
|
13
|
+
const defaultTokens = {
|
|
14
|
+
colors: {
|
|
15
|
+
background: "#ffffff",
|
|
16
|
+
surface: "#f5f5f5",
|
|
17
|
+
text: "#111827",
|
|
18
|
+
textMuted: "#6b7280",
|
|
19
|
+
primary: "#2563eb",
|
|
20
|
+
onPrimary: "#ffffff",
|
|
21
|
+
border: "#e5e7eb",
|
|
22
|
+
danger: "#dc2626"
|
|
23
|
+
},
|
|
24
|
+
space: [
|
|
25
|
+
0,
|
|
26
|
+
4,
|
|
27
|
+
8,
|
|
28
|
+
12,
|
|
29
|
+
16,
|
|
30
|
+
24,
|
|
31
|
+
32,
|
|
32
|
+
48,
|
|
33
|
+
64
|
|
34
|
+
],
|
|
35
|
+
radii: {
|
|
36
|
+
sm: 4,
|
|
37
|
+
md: 8,
|
|
38
|
+
lg: 16,
|
|
39
|
+
full: 9999
|
|
40
|
+
},
|
|
41
|
+
fontSizes: {
|
|
42
|
+
sm: 12,
|
|
43
|
+
md: 14,
|
|
44
|
+
lg: 18,
|
|
45
|
+
xl: 24
|
|
46
|
+
},
|
|
47
|
+
fontWeights: {
|
|
48
|
+
regular: 400,
|
|
49
|
+
medium: 500,
|
|
50
|
+
bold: 700
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/** The Atlas theme context (defaults to {@link defaultTokens}). */
|
|
54
|
+
const ThemeContext = createContext(defaultTokens);
|
|
55
|
+
/** Deep-merge (one level) token overrides onto the defaults. */
|
|
56
|
+
function mergeTokens(overrides) {
|
|
57
|
+
return {
|
|
58
|
+
colors: {
|
|
59
|
+
...defaultTokens.colors,
|
|
60
|
+
...overrides.colors
|
|
61
|
+
},
|
|
62
|
+
space: overrides.space ?? defaultTokens.space,
|
|
63
|
+
radii: {
|
|
64
|
+
...defaultTokens.radii,
|
|
65
|
+
...overrides.radii
|
|
66
|
+
},
|
|
67
|
+
fontSizes: {
|
|
68
|
+
...defaultTokens.fontSizes,
|
|
69
|
+
...overrides.fontSizes
|
|
70
|
+
},
|
|
71
|
+
fontWeights: {
|
|
72
|
+
...defaultTokens.fontWeights,
|
|
73
|
+
...overrides.fontWeights
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a theme provider. Pass partial overrides (deep-merged one level onto the defaults) or
|
|
79
|
+
* nothing for the defaults. Use `.select(t => โฆ)` for a reactive, isolated token accessor that
|
|
80
|
+
* a primitive consumes as a `Reactive<StyleObject>`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const theme = createTheme({ colors: { primary: '#7c3aed' } })
|
|
84
|
+
* const accent = theme.select((t) => t.colors.primary)
|
|
85
|
+
*/
|
|
86
|
+
function createTheme(overrides = {}) {
|
|
87
|
+
return createProvider(ThemeContext, mergeTokens(overrides));
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { ThemeContext, createTheme, defaultTokens };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["/**\n * Atlas theming โ a structural `ThemeTokens` interface + a minimal `defaultTokens`, consumed\n * through a `@mindees/core` selector-isolated context. Atlas takes **no dependency** on any\n * tokens package (the user's published `@mindees/tokens` can satisfy `ThemeTokens` and be\n * injected by the app); a component that selects one slice (`t => t.colors.primary`) only\n * re-runs when that slice changes. See `docs/adr/0022-atlas-primitives.md`.\n *\n * @module\n */\n\nimport { type Context, type ContextProvider, createContext, createProvider } from '@mindees/core'\n\n/** A design-token theme. External token packages can satisfy this shape. */\nexport interface ThemeTokens {\n readonly colors: {\n readonly background: string\n readonly surface: string\n readonly text: string\n readonly textMuted: string\n readonly primary: string\n readonly onPrimary: string\n readonly border: string\n readonly danger: string\n }\n /** Spacing scale (px), index 0 = none. */\n readonly space: readonly number[]\n readonly radii: {\n readonly sm: number\n readonly md: number\n readonly lg: number\n readonly full: number\n }\n readonly fontSizes: {\n readonly sm: number\n readonly md: number\n readonly lg: number\n readonly xl: number\n }\n readonly fontWeights: { readonly regular: number; readonly medium: number; readonly bold: number }\n}\n\n/** A sensible, neutral default theme so Atlas works standalone. */\nexport const defaultTokens: ThemeTokens = {\n colors: {\n background: '#ffffff',\n surface: '#f5f5f5',\n text: '#111827',\n textMuted: '#6b7280',\n primary: '#2563eb',\n onPrimary: '#ffffff',\n border: '#e5e7eb',\n danger: '#dc2626',\n },\n space: [0, 4, 8, 12, 16, 24, 32, 48, 64],\n radii: { sm: 4, md: 8, lg: 16, full: 9999 },\n fontSizes: { sm: 12, md: 14, lg: 18, xl: 24 },\n fontWeights: { regular: 400, medium: 500, bold: 700 },\n}\n\n/** The Atlas theme context (defaults to {@link defaultTokens}). */\nexport const ThemeContext: Context<ThemeTokens> = createContext(defaultTokens)\n\n/** Deep-merge (one level) token overrides onto the defaults. */\nfunction mergeTokens(overrides: DeepPartial<ThemeTokens>): ThemeTokens {\n return {\n colors: { ...defaultTokens.colors, ...overrides.colors },\n space: overrides.space ?? defaultTokens.space,\n radii: { ...defaultTokens.radii, ...overrides.radii },\n fontSizes: { ...defaultTokens.fontSizes, ...overrides.fontSizes },\n fontWeights: { ...defaultTokens.fontWeights, ...overrides.fontWeights },\n }\n}\n\n/** One-level-deep partial of the token groups (arrays are replaced wholesale, not deep-partialed). */\ntype DeepPartial<T> = {\n [K in keyof T]?: T[K] extends ReadonlyArray<unknown>\n ? T[K]\n : T[K] extends object\n ? Partial<T[K]>\n : T[K]\n}\n\n/**\n * Create a theme provider. Pass partial overrides (deep-merged one level onto the defaults) or\n * nothing for the defaults. Use `.select(t => โฆ)` for a reactive, isolated token accessor that\n * a primitive consumes as a `Reactive<StyleObject>`.\n *\n * @example\n * const theme = createTheme({ colors: { primary: '#7c3aed' } })\n * const accent = theme.select((t) => t.colors.primary)\n */\nexport function createTheme(\n overrides: DeepPartial<ThemeTokens> = {},\n): ContextProvider<ThemeTokens> {\n return createProvider(ThemeContext, mergeTokens(overrides))\n}\n"],"mappings":";;;;;;;;;;;;AA0CA,MAAa,gBAA6B;CACxC,QAAQ;EACN,YAAY;EACZ,SAAS;EACT,MAAM;EACN,WAAW;EACX,SAAS;EACT,WAAW;EACX,QAAQ;EACR,QAAQ;CACV;CACA,OAAO;EAAC;EAAG;EAAG;EAAG;EAAI;EAAI;EAAI;EAAI;EAAI;CAAE;CACvC,OAAO;EAAE,IAAI;EAAG,IAAI;EAAG,IAAI;EAAI,MAAM;CAAK;CAC1C,WAAW;EAAE,IAAI;EAAI,IAAI;EAAI,IAAI;EAAI,IAAI;CAAG;CAC5C,aAAa;EAAE,SAAS;EAAK,QAAQ;EAAK,MAAM;CAAI;AACtD;;AAGA,MAAa,eAAqC,cAAc,aAAa;;AAG7E,SAAS,YAAY,WAAkD;CACrE,OAAO;EACL,QAAQ;GAAE,GAAG,cAAc;GAAQ,GAAG,UAAU;EAAO;EACvD,OAAO,UAAU,SAAS,cAAc;EACxC,OAAO;GAAE,GAAG,cAAc;GAAO,GAAG,UAAU;EAAM;EACpD,WAAW;GAAE,GAAG,cAAc;GAAW,GAAG,UAAU;EAAU;EAChE,aAAa;GAAE,GAAG,cAAc;GAAa,GAAG,UAAU;EAAY;CACxE;AACF;;;;;;;;;;AAoBA,SAAgB,YACd,YAAsC,CAAC,GACT;CAC9B,OAAO,eAAe,cAAc,YAAY,SAAS,CAAC;AAC5D"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindees/atlas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MindeesNative Atlas - accessible, signals-native UI primitives + a virtualized recycling list. Renderer-agnostic (web real, native research track).",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./theme": {
|
|
17
|
+
"types": "./dist/theme.d.ts",
|
|
18
|
+
"import": "./dist/theme.js"
|
|
19
|
+
},
|
|
20
|
+
"./list": {
|
|
21
|
+
"types": "./dist/list.d.ts",
|
|
22
|
+
"import": "./dist/list.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/mindees/mindees.git",
|
|
31
|
+
"directory": "packages/atlas"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@mindees/core": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"happy-dom": "20.9.0",
|
|
38
|
+
"@mindees/renderer": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|