@mindees/atlas 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a11y.d.ts +1 -1
- package/dist/a11y.d.ts.map +1 -1
- package/dist/a11y.js.map +1 -1
- package/dist/components.d.ts +83 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +283 -0
- package/dist/components.js.map +1 -0
- package/dist/environment.d.ts +66 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +72 -0
- package/dist/environment.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/list.d.ts +46 -1
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +47 -1
- package/dist/list.js.map +1 -1
- package/dist/tokens.d.ts +210 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +185 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { toA11yProps } from "./a11y.js";
|
|
2
|
+
import { getEnvironment, setEnvironment, useColorScheme, useKeyboard, useSafeAreaInsets, useWindowDimensions } from "./environment.js";
|
|
2
3
|
import { flattenStyle } from "./style.js";
|
|
3
4
|
import { resolveStyle, toHostProps } from "./host.js";
|
|
4
5
|
import { Button, Column, Image, Pressable, Row, ScrollView, Spacer, Stack, Text, TextInput, View, usePressable } from "./primitives.js";
|
|
6
|
+
import { duration, easing, fontSize, fontWeight, getTheme, lineHeight, palette, radius, space, tokens, useTheme } from "./tokens.js";
|
|
7
|
+
import { Avatar, Badge, Card, Chip, Divider, KeyboardAvoidingView, ProgressBar, SafeAreaView, Switch } from "./components.js";
|
|
5
8
|
import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
6
9
|
//#region src/index.ts
|
|
7
10
|
/** The npm package name. */
|
|
8
11
|
const name = "@mindees/atlas";
|
|
9
12
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
10
|
-
const VERSION = "0.
|
|
13
|
+
const VERSION = "0.2.0";
|
|
11
14
|
/** Current maturity of this package. See the repository `STATUS.md`. */
|
|
12
15
|
const maturity = "experimental";
|
|
13
16
|
/**
|
|
@@ -21,6 +24,6 @@ const info = Object.freeze({
|
|
|
21
24
|
maturity
|
|
22
25
|
});
|
|
23
26
|
//#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 };
|
|
27
|
+
export { Avatar, Badge, Button, Card, Chip, Column, Divider, Image, KeyboardAvoidingView, NotImplementedError, Pressable, ProgressBar, Row, SafeAreaView, ScrollView, Spacer, Stack, Switch, Text, TextInput, VERSION, View, duration, easing, flattenStyle, fontSize, fontWeight, getEnvironment, getTheme, info, lineHeight, maturity, name, notImplemented, palette, radius, resolveStyle, setEnvironment, space, toA11yProps, toHostProps, tokens, useColorScheme, useKeyboard, usePressable, useSafeAreaInsets, useTheme, useWindowDimensions };
|
|
25
28
|
|
|
26
29
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/atlas` (Atlas) — accessible, signals-native UI primitives. Function components\n * over `@mindees/core`'s `createElement` that return renderer-agnostic `MindeesNode` trees:\n * web rendering is real via the Helix DOM backend; native is a labeled 🔬 research track (the\n * same serializable tree, interpreted by a native host later). A curated cross-platform\n * `StyleObject`, typed accessibility, and 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
|
+
{"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.2.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 {\n Avatar,\n type AvatarProps,\n Badge,\n type BadgeProps,\n Card,\n type CardProps,\n Chip,\n type ChipProps,\n Divider,\n type DividerProps,\n KeyboardAvoidingView,\n type KeyboardAvoidingViewProps,\n ProgressBar,\n type ProgressBarProps,\n SafeAreaView,\n type SafeAreaViewProps,\n Switch,\n type SwitchProps,\n} from './components'\nexport {\n type ColorScheme,\n getEnvironment,\n type KeyboardState,\n type PlatformEnvironment,\n type SafeAreaInsets,\n setEnvironment,\n useColorScheme,\n useKeyboard,\n useSafeAreaInsets,\n useWindowDimensions,\n type WindowDimensions,\n} from './environment'\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 {\n duration,\n easing,\n fontSize,\n fontWeight,\n getTheme,\n lineHeight,\n palette,\n radius,\n space,\n type Theme,\n type ThemeColors,\n tokens,\n useTheme,\n} from './tokens'\nexport 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
CHANGED
|
@@ -52,6 +52,51 @@ interface ListOptions<T> {
|
|
|
52
52
|
declare function createList<T>(options: ListOptions<T>): MindeesNode;
|
|
53
53
|
/** Component-style alias for {@link createList}. */
|
|
54
54
|
declare function List<T>(options: ListOptions<T>): MindeesNode;
|
|
55
|
+
/** A list section: an optional header + its rows. */
|
|
56
|
+
interface Section<T> {
|
|
57
|
+
/** Header title (used by the default header when `renderSectionHeader` is omitted). */
|
|
58
|
+
readonly title?: string;
|
|
59
|
+
readonly data: readonly T[];
|
|
60
|
+
/** Stable key for the section (optional). */
|
|
61
|
+
readonly key?: string;
|
|
62
|
+
}
|
|
63
|
+
/** A flattened section-list entry: a header or a row. */
|
|
64
|
+
type Entry<T> = {
|
|
65
|
+
readonly kind: 'header';
|
|
66
|
+
readonly section: Section<T>;
|
|
67
|
+
readonly sectionIndex: number;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: 'item';
|
|
70
|
+
readonly section: Section<T>;
|
|
71
|
+
readonly item: T;
|
|
72
|
+
readonly sectionIndex: number;
|
|
73
|
+
readonly itemIndex: number;
|
|
74
|
+
};
|
|
75
|
+
/** Flatten sections into a single ordered entry list (header, its rows, next header, …). */
|
|
76
|
+
declare function flattenSections<T>(sections: readonly Section<T>[]): Entry<T>[];
|
|
77
|
+
/** Options for {@link createSectionList}. */
|
|
78
|
+
interface SectionListOptions<T> {
|
|
79
|
+
/** The sections, static or reactive. */
|
|
80
|
+
readonly sections: readonly Section<T>[] | (() => readonly Section<T>[]);
|
|
81
|
+
/** Render one row (same lazy-accessor contract as {@link ListOptions.renderItem}). */
|
|
82
|
+
readonly renderItem: (item: () => T, index: () => number) => MindeesNode;
|
|
83
|
+
/** Render a section header (defaults to a `text` of `section.title`). */
|
|
84
|
+
readonly renderSectionHeader?: (section: () => Section<T>) => MindeesNode;
|
|
85
|
+
/** Fixed row height in px (headers and rows share it in v1). */
|
|
86
|
+
readonly itemHeight: number;
|
|
87
|
+
readonly height: number;
|
|
88
|
+
readonly overscan?: number;
|
|
89
|
+
readonly onEndReached?: () => void;
|
|
90
|
+
readonly style?: Reactive<StyleInput>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A virtualized **section list** built on {@link createList}: sections are flattened to a
|
|
94
|
+
* single entry stream (header, rows, …) and windowed, so only visible headers/rows render.
|
|
95
|
+
* Fixed row height in v1 (headers share it) — variable heights track the List's research item.
|
|
96
|
+
*/
|
|
97
|
+
declare function createSectionList<T>(options: SectionListOptions<T>): MindeesNode;
|
|
98
|
+
/** Component-style alias for {@link createSectionList}. */
|
|
99
|
+
declare function SectionList<T>(options: SectionListOptions<T>): MindeesNode;
|
|
55
100
|
//#endregion
|
|
56
|
-
export { List, ListOptions, ListWindow, computeWindow, createList };
|
|
101
|
+
export { List, ListOptions, ListWindow, Section, SectionList, SectionListOptions, computeWindow, createList, createSectionList, flattenSections };
|
|
57
102
|
//# sourceMappingURL=list.d.ts.map
|
package/dist/list.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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;;UASjC,OAAA;EApIuC;EAAA,SAsI7C,KAAA;EAAA,SACA,IAAA,WAAe,CAAC;EAZX;EAAA,SAcL,GAAA;AAAA;;KAIN,KAAA;EAAA,SACU,IAAA;EAAA,SAAyB,OAAA,EAAS,OAAA,CAAQ,CAAA;EAAA,SAAa,YAAA;AAAA;EAAA,SAEvD,IAAA;EAAA,SACA,OAAA,EAAS,OAAA,CAAQ,CAAA;EAAA,SACjB,IAAA,EAAM,CAAA;EAAA,SACN,YAAA;EAAA,SACA,SAAA;AAAA;AAhBf;AAAA,iBAoBgB,eAAA,IAAmB,QAAA,WAAmB,OAAA,CAAQ,CAAA,MAAO,KAAA,CAAM,CAAA;;UAY1D,kBAAA;EAhCQ;EAAA,SAkCd,QAAA,WAAmB,OAAA,CAAQ,CAAA,sBAAuB,OAAA,CAAQ,CAAA;EA/B1D;EAAA,SAiCA,UAAA,GAAa,IAAA,QAAY,CAAA,EAAG,KAAA,mBAAwB,WAAA;EA/BpD;EAAA,SAiCA,mBAAA,IAAuB,OAAA,QAAe,OAAA,CAAQ,CAAA,MAAO,WAAA;EAjClD;EAAA,SAmCH,UAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA;EAAA,SACA,YAAA;EAAA,SACA,KAAA,GAAQ,QAAA,CAAS,UAAA;AAAA;;;;;;iBAQZ,iBAAA,IAAqB,OAAA,EAAS,kBAAA,CAAmB,CAAA,IAAK,WAAA;;iBA+BtD,WAAA,IAAe,OAAA,EAAS,kBAAA,CAAmB,CAAA,IAAK,WAAA"}
|
package/dist/list.js
CHANGED
|
@@ -118,7 +118,53 @@ function createList(options) {
|
|
|
118
118
|
function List(options) {
|
|
119
119
|
return createList(options);
|
|
120
120
|
}
|
|
121
|
+
/** Flatten sections into a single ordered entry list (header, its rows, next header, …). */
|
|
122
|
+
function flattenSections(sections) {
|
|
123
|
+
const out = [];
|
|
124
|
+
sections.forEach((section, sectionIndex) => {
|
|
125
|
+
out.push({
|
|
126
|
+
kind: "header",
|
|
127
|
+
section,
|
|
128
|
+
sectionIndex
|
|
129
|
+
});
|
|
130
|
+
section.data.forEach((item, itemIndex) => {
|
|
131
|
+
out.push({
|
|
132
|
+
kind: "item",
|
|
133
|
+
section,
|
|
134
|
+
item,
|
|
135
|
+
sectionIndex,
|
|
136
|
+
itemIndex
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A virtualized **section list** built on {@link createList}: sections are flattened to a
|
|
144
|
+
* single entry stream (header, rows, …) and windowed, so only visible headers/rows render.
|
|
145
|
+
* Fixed row height in v1 (headers share it) — variable heights track the List's research item.
|
|
146
|
+
*/
|
|
147
|
+
function createSectionList(options) {
|
|
148
|
+
const sectionsOf = typeof options.sections === "function" ? options.sections : () => options.sections;
|
|
149
|
+
return createList({
|
|
150
|
+
items: () => flattenSections(sectionsOf()),
|
|
151
|
+
itemHeight: options.itemHeight,
|
|
152
|
+
height: options.height,
|
|
153
|
+
...options.overscan !== void 0 ? { overscan: options.overscan } : {},
|
|
154
|
+
...options.onEndReached ? { onEndReached: options.onEndReached } : {},
|
|
155
|
+
...options.style !== void 0 ? { style: options.style } : {},
|
|
156
|
+
renderItem: (entry) => () => {
|
|
157
|
+
const e = entry();
|
|
158
|
+
if (e.kind === "header") return options.renderSectionHeader ? options.renderSectionHeader(() => entry().section) : createElement("text", null, e.section.title ?? "");
|
|
159
|
+
return options.renderItem(() => entry().item, () => entry().itemIndex);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Component-style alias for {@link createSectionList}. */
|
|
164
|
+
function SectionList(options) {
|
|
165
|
+
return createSectionList(options);
|
|
166
|
+
}
|
|
121
167
|
//#endregion
|
|
122
|
-
export { List, computeWindow, createList };
|
|
168
|
+
export { List, SectionList, computeWindow, createList, createSectionList, flattenSections };
|
|
123
169
|
|
|
124
170
|
//# sourceMappingURL=list.js.map
|
package/dist/list.js.map
CHANGED
|
@@ -1 +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":"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"}
|
|
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\n// ---------------------------------------------------------------------------\n// SectionList\n// ---------------------------------------------------------------------------\n\n/** A list section: an optional header + its rows. */\nexport interface Section<T> {\n /** Header title (used by the default header when `renderSectionHeader` is omitted). */\n readonly title?: string\n readonly data: readonly T[]\n /** Stable key for the section (optional). */\n readonly key?: string\n}\n\n/** A flattened section-list entry: a header or a row. */\ntype Entry<T> =\n | { readonly kind: 'header'; readonly section: Section<T>; readonly sectionIndex: number }\n | {\n readonly kind: 'item'\n readonly section: Section<T>\n readonly item: T\n readonly sectionIndex: number\n readonly itemIndex: number\n }\n\n/** Flatten sections into a single ordered entry list (header, its rows, next header, …). */\nexport function flattenSections<T>(sections: readonly Section<T>[]): Entry<T>[] {\n const out: Entry<T>[] = []\n sections.forEach((section, sectionIndex) => {\n out.push({ kind: 'header', section, sectionIndex })\n section.data.forEach((item, itemIndex) => {\n out.push({ kind: 'item', section, item, sectionIndex, itemIndex })\n })\n })\n return out\n}\n\n/** Options for {@link createSectionList}. */\nexport interface SectionListOptions<T> {\n /** The sections, static or reactive. */\n readonly sections: readonly Section<T>[] | (() => readonly Section<T>[])\n /** Render one row (same lazy-accessor contract as {@link ListOptions.renderItem}). */\n readonly renderItem: (item: () => T, index: () => number) => MindeesNode\n /** Render a section header (defaults to a `text` of `section.title`). */\n readonly renderSectionHeader?: (section: () => Section<T>) => MindeesNode\n /** Fixed row height in px (headers and rows share it in v1). */\n readonly itemHeight: number\n readonly height: number\n readonly overscan?: number\n readonly onEndReached?: () => void\n readonly style?: Reactive<StyleInput>\n}\n\n/**\n * A virtualized **section list** built on {@link createList}: sections are flattened to a\n * single entry stream (header, rows, …) and windowed, so only visible headers/rows render.\n * Fixed row height in v1 (headers share it) — variable heights track the List's research item.\n */\nexport function createSectionList<T>(options: SectionListOptions<T>): MindeesNode {\n const sectionsOf: () => readonly Section<T>[] =\n typeof options.sections === 'function'\n ? options.sections\n : () => options.sections as readonly Section<T>[]\n\n return createList<Entry<T>>({\n items: () => flattenSections(sectionsOf()),\n itemHeight: options.itemHeight,\n height: options.height,\n ...(options.overscan !== undefined ? { overscan: options.overscan } : {}),\n ...(options.onEndReached ? { onEndReached: options.onEndReached } : {}),\n ...(options.style !== undefined ? { style: options.style } : {}),\n // The branch reads `entry()`, so a recycled slot re-runs this when its entry changes —\n // correct for mixed header/row content (rows still virtualize: only the visible window renders).\n renderItem: (entry) => () => {\n const e = entry()\n if (e.kind === 'header') {\n return options.renderSectionHeader\n ? options.renderSectionHeader(() => entry().section)\n : createElement('text', null, e.section.title ?? '')\n }\n return options.renderItem(\n () => (entry() as Extract<Entry<T>, { kind: 'item' }>).item,\n () => (entry() as Extract<Entry<T>, { kind: 'item' }>).itemIndex,\n )\n },\n })\n}\n\n/** Component-style alias for {@link createSectionList}. */\nexport function SectionList<T>(options: SectionListOptions<T>): MindeesNode {\n return createSectionList(options)\n}\n"],"mappings":"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"}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ColorScheme } from "./environment.js";
|
|
2
|
+
|
|
3
|
+
//#region src/tokens.d.ts
|
|
4
|
+
/** Spacing scale — 8pt system with 4 as the half-step (handbook §8). */
|
|
5
|
+
declare const space: {
|
|
6
|
+
readonly none: 0;
|
|
7
|
+
readonly '3xs': 2;
|
|
8
|
+
readonly '2xs': 4;
|
|
9
|
+
readonly xs: 8;
|
|
10
|
+
readonly sm: 12;
|
|
11
|
+
readonly md: 16;
|
|
12
|
+
readonly lg: 24;
|
|
13
|
+
readonly xl: 32;
|
|
14
|
+
readonly '2xl': 48;
|
|
15
|
+
readonly '3xl': 64;
|
|
16
|
+
};
|
|
17
|
+
/** Corner-radius scale (handbook §19). */
|
|
18
|
+
declare const radius: {
|
|
19
|
+
readonly none: 0;
|
|
20
|
+
readonly sm: 8;
|
|
21
|
+
readonly md: 12;
|
|
22
|
+
readonly lg: 16;
|
|
23
|
+
readonly xl: 20;
|
|
24
|
+
readonly '2xl': 28;
|
|
25
|
+
readonly full: 9999;
|
|
26
|
+
};
|
|
27
|
+
/** Type scale (≈1.25 ratio, base 16) — handbook §9. */
|
|
28
|
+
declare const fontSize: {
|
|
29
|
+
readonly caption: 12;
|
|
30
|
+
readonly footnote: 13;
|
|
31
|
+
readonly body: 16;
|
|
32
|
+
readonly callout: 17;
|
|
33
|
+
readonly headline: 20;
|
|
34
|
+
readonly title3: 25;
|
|
35
|
+
readonly title2: 31;
|
|
36
|
+
readonly title1: 39;
|
|
37
|
+
readonly display: 49;
|
|
38
|
+
};
|
|
39
|
+
/** Line-height (leading) ratios (handbook §9). */
|
|
40
|
+
declare const lineHeight: {
|
|
41
|
+
readonly tight: 1.2;
|
|
42
|
+
readonly snug: 1.3;
|
|
43
|
+
readonly normal: 1.5;
|
|
44
|
+
readonly relaxed: 1.6;
|
|
45
|
+
};
|
|
46
|
+
/** Font weights (handbook §9 — regular + semibold/bold do most of the work). */
|
|
47
|
+
declare const fontWeight: {
|
|
48
|
+
readonly regular: 400;
|
|
49
|
+
readonly medium: 500;
|
|
50
|
+
readonly semibold: 600;
|
|
51
|
+
readonly bold: 700;
|
|
52
|
+
readonly heavy: 800;
|
|
53
|
+
};
|
|
54
|
+
/** Motion durations in ms (handbook §21: micro/standard/large). */
|
|
55
|
+
declare const duration: {
|
|
56
|
+
readonly micro: 150;
|
|
57
|
+
readonly standard: 250;
|
|
58
|
+
readonly large: 400;
|
|
59
|
+
};
|
|
60
|
+
/** Easing curves (handbook §21: ease-out for enter, ease-in for exit). */
|
|
61
|
+
declare const easing: {
|
|
62
|
+
readonly standard: "cubic-bezier(0.2, 0, 0, 1)";
|
|
63
|
+
readonly decelerate: "cubic-bezier(0, 0, 0, 1)";
|
|
64
|
+
readonly accelerate: "cubic-bezier(0.3, 0, 1, 1)";
|
|
65
|
+
};
|
|
66
|
+
/** Raw color ramps (primitive tier — never apply directly; go through a {@link Theme}). */
|
|
67
|
+
declare const palette: {
|
|
68
|
+
readonly white: "#ffffff";
|
|
69
|
+
readonly black: "#000000";
|
|
70
|
+
readonly neutral: {
|
|
71
|
+
readonly 50: "#f8fafc";
|
|
72
|
+
readonly 100: "#f1f5f9";
|
|
73
|
+
readonly 200: "#e2e8f0";
|
|
74
|
+
readonly 300: "#cbd5e1";
|
|
75
|
+
readonly 400: "#94a3b8";
|
|
76
|
+
readonly 500: "#64748b";
|
|
77
|
+
readonly 600: "#475569";
|
|
78
|
+
readonly 700: "#334155";
|
|
79
|
+
readonly 800: "#1e293b";
|
|
80
|
+
readonly 900: "#0f172a";
|
|
81
|
+
readonly 950: "#020617";
|
|
82
|
+
};
|
|
83
|
+
readonly blue: {
|
|
84
|
+
readonly 400: "#60a5fa";
|
|
85
|
+
readonly 500: "#3b82f6";
|
|
86
|
+
readonly 600: "#2563eb";
|
|
87
|
+
readonly 700: "#1d4ed8";
|
|
88
|
+
};
|
|
89
|
+
readonly green: {
|
|
90
|
+
readonly 400: "#4ade80";
|
|
91
|
+
readonly 500: "#22c55e";
|
|
92
|
+
readonly 600: "#16a34a";
|
|
93
|
+
readonly 700: "#15803d";
|
|
94
|
+
};
|
|
95
|
+
readonly amber: {
|
|
96
|
+
readonly 400: "#fbbf24";
|
|
97
|
+
readonly 500: "#f59e0b";
|
|
98
|
+
readonly 600: "#d97706";
|
|
99
|
+
readonly 700: "#b45309";
|
|
100
|
+
};
|
|
101
|
+
readonly red: {
|
|
102
|
+
readonly 400: "#f87171";
|
|
103
|
+
readonly 500: "#ef4444";
|
|
104
|
+
readonly 600: "#dc2626";
|
|
105
|
+
readonly 700: "#b91c1c";
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
/** Non-color scales, grouped for convenience. */
|
|
109
|
+
declare const tokens: {
|
|
110
|
+
readonly space: {
|
|
111
|
+
readonly none: 0;
|
|
112
|
+
readonly '3xs': 2;
|
|
113
|
+
readonly '2xs': 4;
|
|
114
|
+
readonly xs: 8;
|
|
115
|
+
readonly sm: 12;
|
|
116
|
+
readonly md: 16;
|
|
117
|
+
readonly lg: 24;
|
|
118
|
+
readonly xl: 32;
|
|
119
|
+
readonly '2xl': 48;
|
|
120
|
+
readonly '3xl': 64;
|
|
121
|
+
};
|
|
122
|
+
readonly radius: {
|
|
123
|
+
readonly none: 0;
|
|
124
|
+
readonly sm: 8;
|
|
125
|
+
readonly md: 12;
|
|
126
|
+
readonly lg: 16;
|
|
127
|
+
readonly xl: 20;
|
|
128
|
+
readonly '2xl': 28;
|
|
129
|
+
readonly full: 9999;
|
|
130
|
+
};
|
|
131
|
+
readonly fontSize: {
|
|
132
|
+
readonly caption: 12;
|
|
133
|
+
readonly footnote: 13;
|
|
134
|
+
readonly body: 16;
|
|
135
|
+
readonly callout: 17;
|
|
136
|
+
readonly headline: 20;
|
|
137
|
+
readonly title3: 25;
|
|
138
|
+
readonly title2: 31;
|
|
139
|
+
readonly title1: 39;
|
|
140
|
+
readonly display: 49;
|
|
141
|
+
};
|
|
142
|
+
readonly lineHeight: {
|
|
143
|
+
readonly tight: 1.2;
|
|
144
|
+
readonly snug: 1.3;
|
|
145
|
+
readonly normal: 1.5;
|
|
146
|
+
readonly relaxed: 1.6;
|
|
147
|
+
};
|
|
148
|
+
readonly fontWeight: {
|
|
149
|
+
readonly regular: 400;
|
|
150
|
+
readonly medium: 500;
|
|
151
|
+
readonly semibold: 600;
|
|
152
|
+
readonly bold: 700;
|
|
153
|
+
readonly heavy: 800;
|
|
154
|
+
};
|
|
155
|
+
readonly duration: {
|
|
156
|
+
readonly micro: 150;
|
|
157
|
+
readonly standard: 250;
|
|
158
|
+
readonly large: 400;
|
|
159
|
+
};
|
|
160
|
+
readonly easing: {
|
|
161
|
+
readonly standard: "cubic-bezier(0.2, 0, 0, 1)";
|
|
162
|
+
readonly decelerate: "cubic-bezier(0, 0, 0, 1)";
|
|
163
|
+
readonly accelerate: "cubic-bezier(0.3, 0, 1, 1)";
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
/** Semantic color roles (handbook §10/§31) — the layer components and apps consume. */
|
|
167
|
+
interface ThemeColors {
|
|
168
|
+
/** App background. */
|
|
169
|
+
readonly bg: string;
|
|
170
|
+
/** Default surface (cards, sheets). */
|
|
171
|
+
readonly surface: string;
|
|
172
|
+
/** A raised/recessed surface variant. */
|
|
173
|
+
readonly surfaceVariant: string;
|
|
174
|
+
/** High-emphasis text. */
|
|
175
|
+
readonly text: string;
|
|
176
|
+
/** Secondary/muted text. */
|
|
177
|
+
readonly textMuted: string;
|
|
178
|
+
/** Hairline borders/dividers. */
|
|
179
|
+
readonly border: string;
|
|
180
|
+
/** Brand/primary action. */
|
|
181
|
+
readonly primary: string;
|
|
182
|
+
/** Foreground on `primary`. */
|
|
183
|
+
readonly onPrimary: string;
|
|
184
|
+
readonly success: string;
|
|
185
|
+
readonly warning: string;
|
|
186
|
+
readonly danger: string;
|
|
187
|
+
readonly info: string;
|
|
188
|
+
/** Foreground on the semantic tone colors. */
|
|
189
|
+
readonly onTone: string;
|
|
190
|
+
}
|
|
191
|
+
/** A resolved theme: a color scheme + its semantic colors. */
|
|
192
|
+
interface Theme {
|
|
193
|
+
readonly colorScheme: ColorScheme;
|
|
194
|
+
readonly color: ThemeColors;
|
|
195
|
+
}
|
|
196
|
+
/** The theme for a given color scheme (non-reactive). */
|
|
197
|
+
declare function getTheme(colorScheme: ColorScheme): Theme;
|
|
198
|
+
/**
|
|
199
|
+
* The active theme as a reactive accessor — flips light↔dark with
|
|
200
|
+
* {@link useColorScheme}. Use it in accessor styles so dark mode is an automatic,
|
|
201
|
+
* fine-grained token swap (only color nodes re-run).
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* const theme = useTheme()
|
|
205
|
+
* <View style={() => ({ backgroundColor: theme().color.surface })} />
|
|
206
|
+
*/
|
|
207
|
+
declare function useTheme(): () => Theme;
|
|
208
|
+
//#endregion
|
|
209
|
+
export { Theme, ThemeColors, duration, easing, fontSize, fontWeight, getTheme, lineHeight, palette, radius, space, tokens, useTheme };
|
|
210
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","names":[],"sources":["../src/tokens.ts"],"mappings":";;;;cAkBa,KAAA;EAAA;;;;;;;;;;;;cAcA,MAAA;EAAA;;;;;;;;;cAWA,QAAA;EAAA;;;;;;;;;;;cAaA,UAAA;EAAA;;;;;;cAGA,UAAA;EAAA;;;;;;;cASA,QAAA;EAAA;;;;;cAGA,MAAA;EAAA;;;;;cAOA,OAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuBA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAKI,WAAA;;WAEN,EAAA;;WAEA,OAAA;EAJiB;EAAA,SAMjB,cAAA;EANiB;EAAA,SAQjB,IAAA;EAJA;EAAA,SAMA,SAAA;EAFA;EAAA,SAIA,MAAA;EAAA;EAAA,SAEA,OAAA;EAEA;EAAA,SAAA,SAAA;EAAA,SACA,OAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;EAAA,SACA,IAAA;EAEM;EAAA,SAAN,MAAA;AAAA;;UAIM,KAAA;EAAA,SACN,WAAA,EAAa,WAAA;EAAA,SACb,KAAA,EAAO,WAAW;AAAA;;iBA6Cb,QAAA,CAAS,WAAA,EAAa,WAAA,GAAc,KAAK;;AA7C5B;AA6C7B;;;;;;;iBAagB,QAAA,UAAkB,KAAK"}
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useColorScheme } from "./environment.js";
|
|
2
|
+
//#region src/tokens.ts
|
|
3
|
+
/**
|
|
4
|
+
* Design tokens + theming — the 2026-UI/UX-handbook token layer (§7–24, §31).
|
|
5
|
+
*
|
|
6
|
+
* Two tiers (the recommended default): **primitive** scales (raw `space`/`radius`/type/
|
|
7
|
+
* motion/color values) and **semantic** tokens (a {@link Theme}: `bg`/`surface`/`text`/
|
|
8
|
+
* `primary`/…) that carry intent. Dark mode is a **token-set swap** (§23/§31): the same
|
|
9
|
+
* semantic names resolve to different primitives. {@link useTheme} returns a reactive theme
|
|
10
|
+
* driven by {@link useColorScheme}, so themed UI re-themes light↔dark fine-grained — only
|
|
11
|
+
* the color nodes update.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/** Spacing scale — 8pt system with 4 as the half-step (handbook §8). */
|
|
16
|
+
const space = {
|
|
17
|
+
none: 0,
|
|
18
|
+
"3xs": 2,
|
|
19
|
+
"2xs": 4,
|
|
20
|
+
xs: 8,
|
|
21
|
+
sm: 12,
|
|
22
|
+
md: 16,
|
|
23
|
+
lg: 24,
|
|
24
|
+
xl: 32,
|
|
25
|
+
"2xl": 48,
|
|
26
|
+
"3xl": 64
|
|
27
|
+
};
|
|
28
|
+
/** Corner-radius scale (handbook §19). */
|
|
29
|
+
const radius = {
|
|
30
|
+
none: 0,
|
|
31
|
+
sm: 8,
|
|
32
|
+
md: 12,
|
|
33
|
+
lg: 16,
|
|
34
|
+
xl: 20,
|
|
35
|
+
"2xl": 28,
|
|
36
|
+
full: 9999
|
|
37
|
+
};
|
|
38
|
+
/** Type scale (≈1.25 ratio, base 16) — handbook §9. */
|
|
39
|
+
const fontSize = {
|
|
40
|
+
caption: 12,
|
|
41
|
+
footnote: 13,
|
|
42
|
+
body: 16,
|
|
43
|
+
callout: 17,
|
|
44
|
+
headline: 20,
|
|
45
|
+
title3: 25,
|
|
46
|
+
title2: 31,
|
|
47
|
+
title1: 39,
|
|
48
|
+
display: 49
|
|
49
|
+
};
|
|
50
|
+
/** Line-height (leading) ratios (handbook §9). */
|
|
51
|
+
const lineHeight = {
|
|
52
|
+
tight: 1.2,
|
|
53
|
+
snug: 1.3,
|
|
54
|
+
normal: 1.5,
|
|
55
|
+
relaxed: 1.6
|
|
56
|
+
};
|
|
57
|
+
/** Font weights (handbook §9 — regular + semibold/bold do most of the work). */
|
|
58
|
+
const fontWeight = {
|
|
59
|
+
regular: 400,
|
|
60
|
+
medium: 500,
|
|
61
|
+
semibold: 600,
|
|
62
|
+
bold: 700,
|
|
63
|
+
heavy: 800
|
|
64
|
+
};
|
|
65
|
+
/** Motion durations in ms (handbook §21: micro/standard/large). */
|
|
66
|
+
const duration = {
|
|
67
|
+
micro: 150,
|
|
68
|
+
standard: 250,
|
|
69
|
+
large: 400
|
|
70
|
+
};
|
|
71
|
+
/** Easing curves (handbook §21: ease-out for enter, ease-in for exit). */
|
|
72
|
+
const easing = {
|
|
73
|
+
standard: "cubic-bezier(0.2, 0, 0, 1)",
|
|
74
|
+
decelerate: "cubic-bezier(0, 0, 0, 1)",
|
|
75
|
+
accelerate: "cubic-bezier(0.3, 0, 1, 1)"
|
|
76
|
+
};
|
|
77
|
+
/** Raw color ramps (primitive tier — never apply directly; go through a {@link Theme}). */
|
|
78
|
+
const palette = {
|
|
79
|
+
white: "#ffffff",
|
|
80
|
+
black: "#000000",
|
|
81
|
+
neutral: {
|
|
82
|
+
50: "#f8fafc",
|
|
83
|
+
100: "#f1f5f9",
|
|
84
|
+
200: "#e2e8f0",
|
|
85
|
+
300: "#cbd5e1",
|
|
86
|
+
400: "#94a3b8",
|
|
87
|
+
500: "#64748b",
|
|
88
|
+
600: "#475569",
|
|
89
|
+
700: "#334155",
|
|
90
|
+
800: "#1e293b",
|
|
91
|
+
900: "#0f172a",
|
|
92
|
+
950: "#020617"
|
|
93
|
+
},
|
|
94
|
+
blue: {
|
|
95
|
+
400: "#60a5fa",
|
|
96
|
+
500: "#3b82f6",
|
|
97
|
+
600: "#2563eb",
|
|
98
|
+
700: "#1d4ed8"
|
|
99
|
+
},
|
|
100
|
+
green: {
|
|
101
|
+
400: "#4ade80",
|
|
102
|
+
500: "#22c55e",
|
|
103
|
+
600: "#16a34a",
|
|
104
|
+
700: "#15803d"
|
|
105
|
+
},
|
|
106
|
+
amber: {
|
|
107
|
+
400: "#fbbf24",
|
|
108
|
+
500: "#f59e0b",
|
|
109
|
+
600: "#d97706",
|
|
110
|
+
700: "#b45309"
|
|
111
|
+
},
|
|
112
|
+
red: {
|
|
113
|
+
400: "#f87171",
|
|
114
|
+
500: "#ef4444",
|
|
115
|
+
600: "#dc2626",
|
|
116
|
+
700: "#b91c1c"
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/** Non-color scales, grouped for convenience. */
|
|
120
|
+
const tokens = {
|
|
121
|
+
space,
|
|
122
|
+
radius,
|
|
123
|
+
fontSize,
|
|
124
|
+
lineHeight,
|
|
125
|
+
fontWeight,
|
|
126
|
+
duration,
|
|
127
|
+
easing
|
|
128
|
+
};
|
|
129
|
+
const lightTheme = {
|
|
130
|
+
colorScheme: "light",
|
|
131
|
+
color: {
|
|
132
|
+
bg: palette.neutral[50],
|
|
133
|
+
surface: palette.white,
|
|
134
|
+
surfaceVariant: palette.neutral[100],
|
|
135
|
+
text: palette.neutral[900],
|
|
136
|
+
textMuted: palette.neutral[500],
|
|
137
|
+
border: palette.neutral[200],
|
|
138
|
+
primary: palette.blue[600],
|
|
139
|
+
onPrimary: palette.white,
|
|
140
|
+
success: palette.green[700],
|
|
141
|
+
warning: palette.amber[700],
|
|
142
|
+
danger: palette.red[700],
|
|
143
|
+
info: palette.blue[700],
|
|
144
|
+
onTone: palette.white
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const darkTheme = {
|
|
148
|
+
colorScheme: "dark",
|
|
149
|
+
color: {
|
|
150
|
+
bg: palette.neutral[950],
|
|
151
|
+
surface: palette.neutral[900],
|
|
152
|
+
surfaceVariant: palette.neutral[800],
|
|
153
|
+
text: palette.neutral[50],
|
|
154
|
+
textMuted: palette.neutral[400],
|
|
155
|
+
border: palette.neutral[700],
|
|
156
|
+
primary: palette.blue[500],
|
|
157
|
+
onPrimary: palette.white,
|
|
158
|
+
success: palette.green[400],
|
|
159
|
+
warning: palette.amber[400],
|
|
160
|
+
danger: palette.red[400],
|
|
161
|
+
info: palette.blue[400],
|
|
162
|
+
onTone: palette.neutral[950]
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
/** The theme for a given color scheme (non-reactive). */
|
|
166
|
+
function getTheme(colorScheme) {
|
|
167
|
+
return colorScheme === "dark" ? darkTheme : lightTheme;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The active theme as a reactive accessor — flips light↔dark with
|
|
171
|
+
* {@link useColorScheme}. Use it in accessor styles so dark mode is an automatic,
|
|
172
|
+
* fine-grained token swap (only color nodes re-run).
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const theme = useTheme()
|
|
176
|
+
* <View style={() => ({ backgroundColor: theme().color.surface })} />
|
|
177
|
+
*/
|
|
178
|
+
function useTheme() {
|
|
179
|
+
const colorScheme = useColorScheme();
|
|
180
|
+
return () => getTheme(colorScheme());
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
export { duration, easing, fontSize, fontWeight, getTheme, lineHeight, palette, radius, space, tokens, useTheme };
|
|
184
|
+
|
|
185
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","names":[],"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * Design tokens + theming — the 2026-UI/UX-handbook token layer (§7–24, §31).\n *\n * Two tiers (the recommended default): **primitive** scales (raw `space`/`radius`/type/\n * motion/color values) and **semantic** tokens (a {@link Theme}: `bg`/`surface`/`text`/\n * `primary`/…) that carry intent. Dark mode is a **token-set swap** (§23/§31): the same\n * semantic names resolve to different primitives. {@link useTheme} returns a reactive theme\n * driven by {@link useColorScheme}, so themed UI re-themes light↔dark fine-grained — only\n * the color nodes update.\n *\n * @module\n */\n\nimport { type ColorScheme, useColorScheme } from './environment'\n\n// --- Primitive scales --------------------------------------------------------\n\n/** Spacing scale — 8pt system with 4 as the half-step (handbook §8). */\nexport const space = {\n none: 0,\n '3xs': 2,\n '2xs': 4,\n xs: 8,\n sm: 12,\n md: 16,\n lg: 24,\n xl: 32,\n '2xl': 48,\n '3xl': 64,\n} as const\n\n/** Corner-radius scale (handbook §19). */\nexport const radius = {\n none: 0,\n sm: 8,\n md: 12,\n lg: 16,\n xl: 20,\n '2xl': 28,\n full: 9999,\n} as const\n\n/** Type scale (≈1.25 ratio, base 16) — handbook §9. */\nexport const fontSize = {\n caption: 12,\n footnote: 13,\n body: 16,\n callout: 17,\n headline: 20,\n title3: 25,\n title2: 31,\n title1: 39,\n display: 49,\n} as const\n\n/** Line-height (leading) ratios (handbook §9). */\nexport const lineHeight = { tight: 1.2, snug: 1.3, normal: 1.5, relaxed: 1.6 } as const\n\n/** Font weights (handbook §9 — regular + semibold/bold do most of the work). */\nexport const fontWeight = {\n regular: 400,\n medium: 500,\n semibold: 600,\n bold: 700,\n heavy: 800,\n} as const\n\n/** Motion durations in ms (handbook §21: micro/standard/large). */\nexport const duration = { micro: 150, standard: 250, large: 400 } as const\n\n/** Easing curves (handbook §21: ease-out for enter, ease-in for exit). */\nexport const easing = {\n standard: 'cubic-bezier(0.2, 0, 0, 1)',\n decelerate: 'cubic-bezier(0, 0, 0, 1)',\n accelerate: 'cubic-bezier(0.3, 0, 1, 1)',\n} as const\n\n/** Raw color ramps (primitive tier — never apply directly; go through a {@link Theme}). */\nexport const palette = {\n white: '#ffffff',\n black: '#000000',\n neutral: {\n 50: '#f8fafc',\n 100: '#f1f5f9',\n 200: '#e2e8f0',\n 300: '#cbd5e1',\n 400: '#94a3b8',\n 500: '#64748b',\n 600: '#475569',\n 700: '#334155',\n 800: '#1e293b',\n 900: '#0f172a',\n 950: '#020617',\n },\n blue: { 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8' },\n green: { 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d' },\n amber: { 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309' },\n red: { 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c' },\n} as const\n\n/** Non-color scales, grouped for convenience. */\nexport const tokens = { space, radius, fontSize, lineHeight, fontWeight, duration, easing } as const\n\n// --- Semantic tier (themes) --------------------------------------------------\n\n/** Semantic color roles (handbook §10/§31) — the layer components and apps consume. */\nexport interface ThemeColors {\n /** App background. */\n readonly bg: string\n /** Default surface (cards, sheets). */\n readonly surface: string\n /** A raised/recessed surface variant. */\n readonly surfaceVariant: string\n /** High-emphasis text. */\n readonly text: string\n /** Secondary/muted text. */\n readonly textMuted: string\n /** Hairline borders/dividers. */\n readonly border: string\n /** Brand/primary action. */\n readonly primary: string\n /** Foreground on `primary`. */\n readonly onPrimary: string\n readonly success: string\n readonly warning: string\n readonly danger: string\n readonly info: string\n /** Foreground on the semantic tone colors. */\n readonly onTone: string\n}\n\n/** A resolved theme: a color scheme + its semantic colors. */\nexport interface Theme {\n readonly colorScheme: ColorScheme\n readonly color: ThemeColors\n}\n\nconst lightTheme: Theme = {\n colorScheme: 'light',\n color: {\n bg: palette.neutral[50],\n surface: palette.white,\n surfaceVariant: palette.neutral[100],\n text: palette.neutral[900],\n textMuted: palette.neutral[500],\n border: palette.neutral[200],\n primary: palette.blue[600],\n onPrimary: palette.white,\n // -700 tones carry white at ≥4.5:1 (handbook §11).\n success: palette.green[700],\n warning: palette.amber[700],\n danger: palette.red[700],\n info: palette.blue[700],\n onTone: palette.white,\n },\n}\n\nconst darkTheme: Theme = {\n colorScheme: 'dark',\n color: {\n // Not pure black (handbook §23); surfaces get lighter with elevation.\n bg: palette.neutral[950],\n surface: palette.neutral[900],\n surfaceVariant: palette.neutral[800],\n text: palette.neutral[50],\n textMuted: palette.neutral[400],\n border: palette.neutral[700],\n primary: palette.blue[500],\n onPrimary: palette.white,\n // Brighter tones in dark mode; dark text reads on them (handbook §23 desaturate/contrast).\n success: palette.green[400],\n warning: palette.amber[400],\n danger: palette.red[400],\n info: palette.blue[400],\n onTone: palette.neutral[950],\n },\n}\n\n/** The theme for a given color scheme (non-reactive). */\nexport function getTheme(colorScheme: ColorScheme): Theme {\n return colorScheme === 'dark' ? darkTheme : lightTheme\n}\n\n/**\n * The active theme as a reactive accessor — flips light↔dark with\n * {@link useColorScheme}. Use it in accessor styles so dark mode is an automatic,\n * fine-grained token swap (only color nodes re-run).\n *\n * @example\n * const theme = useTheme()\n * <View style={() => ({ backgroundColor: theme().color.surface })} />\n */\nexport function useTheme(): () => Theme {\n const colorScheme = useColorScheme()\n return () => getTheme(colorScheme())\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAa,QAAQ;CACnB,MAAM;CACN,OAAO;CACP,OAAO;CACP,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACP,OAAO;AACT;;AAGA,MAAa,SAAS;CACpB,MAAM;CACN,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACP,MAAM;AACR;;AAGA,MAAa,WAAW;CACtB,SAAS;CACT,UAAU;CACV,MAAM;CACN,SAAS;CACT,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;AACX;;AAGA,MAAa,aAAa;CAAE,OAAO;CAAK,MAAM;CAAK,QAAQ;CAAK,SAAS;AAAI;;AAG7E,MAAa,aAAa;CACxB,SAAS;CACT,QAAQ;CACR,UAAU;CACV,MAAM;CACN,OAAO;AACT;;AAGA,MAAa,WAAW;CAAE,OAAO;CAAK,UAAU;CAAK,OAAO;AAAI;;AAGhE,MAAa,SAAS;CACpB,UAAU;CACV,YAAY;CACZ,YAAY;AACd;;AAGA,MAAa,UAAU;CACrB,OAAO;CACP,OAAO;CACP,SAAS;EACP,IAAI;EACJ,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;CACP;CACA,MAAM;EAAE,KAAK;EAAW,KAAK;EAAW,KAAK;EAAW,KAAK;CAAU;CACvE,OAAO;EAAE,KAAK;EAAW,KAAK;EAAW,KAAK;EAAW,KAAK;CAAU;CACxE,OAAO;EAAE,KAAK;EAAW,KAAK;EAAW,KAAK;EAAW,KAAK;CAAU;CACxE,KAAK;EAAE,KAAK;EAAW,KAAK;EAAW,KAAK;EAAW,KAAK;CAAU;AACxE;;AAGA,MAAa,SAAS;CAAE;CAAO;CAAQ;CAAU;CAAY;CAAY;CAAU;AAAO;AAoC1F,MAAM,aAAoB;CACxB,aAAa;CACb,OAAO;EACL,IAAI,QAAQ,QAAQ;EACpB,SAAS,QAAQ;EACjB,gBAAgB,QAAQ,QAAQ;EAChC,MAAM,QAAQ,QAAQ;EACtB,WAAW,QAAQ,QAAQ;EAC3B,QAAQ,QAAQ,QAAQ;EACxB,SAAS,QAAQ,KAAK;EACtB,WAAW,QAAQ;EAEnB,SAAS,QAAQ,MAAM;EACvB,SAAS,QAAQ,MAAM;EACvB,QAAQ,QAAQ,IAAI;EACpB,MAAM,QAAQ,KAAK;EACnB,QAAQ,QAAQ;CAClB;AACF;AAEA,MAAM,YAAmB;CACvB,aAAa;CACb,OAAO;EAEL,IAAI,QAAQ,QAAQ;EACpB,SAAS,QAAQ,QAAQ;EACzB,gBAAgB,QAAQ,QAAQ;EAChC,MAAM,QAAQ,QAAQ;EACtB,WAAW,QAAQ,QAAQ;EAC3B,QAAQ,QAAQ,QAAQ;EACxB,SAAS,QAAQ,KAAK;EACtB,WAAW,QAAQ;EAEnB,SAAS,QAAQ,MAAM;EACvB,SAAS,QAAQ,MAAM;EACvB,QAAQ,QAAQ,IAAI;EACpB,MAAM,QAAQ,KAAK;EACnB,QAAQ,QAAQ,QAAQ;CAC1B;AACF;;AAGA,SAAgB,SAAS,aAAiC;CACxD,OAAO,gBAAgB,SAAS,YAAY;AAC9C;;;;;;;;;;AAWA,SAAgB,WAAwB;CACtC,MAAM,cAAc,eAAe;CACnC,aAAa,SAAS,YAAY,CAAC;AACrC"}
|