@pinfold/core 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # @pinfold/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The framework-free layer of [Pinfold](../../README.md): each component's behaviour rules, the
4
+ helpers, keyboard rules, component metadata and the OKLCH palette engine that every Pinfold
5
+ adapter shares. No React,
6
+ no dependencies. A test fails the build if a framework import ever appears here.
7
+
8
+ Most apps never install this directly. `@pinfold/react` depends on it.
9
+
10
+ ## Exports
11
+
12
+ | | |
13
+ | --- | --- |
14
+ | `cn`, `mergeProps`, `composeRefs` | class joining and prop merging (handlers chain, styles merge) |
15
+ | `variants` | `cva`-style class resolution |
16
+ | `nextIndexFromKey` | the WAI-ARIA roving-focus key map (orientation, RTL, loop) |
17
+ | `dimensionVars` | `dimensions` config → `--pinfold-<component>-*` custom properties |
18
+ | `resolveNode`, `dataAttr`, `ariaAttr` | slot value and attribute helpers |
19
+ | `warnOnce`, `__DEV__` | dev-only diagnostics, stripped in production |
20
+ | `setAccordionItem`, `openAllAccordion`, `accordionFocusTarget`, … | the accordion's rules as pure functions |
21
+
22
+ ## Metadata
23
+
24
+ `@pinfold/core/meta`: `accordionMeta` (every option, default, slot, keyboard key and ARIA rule),
25
+ `validateConfig`, `toJsonSchema` and `metaToMarkdown`. Docs, the playground, the CLI, the MCP
26
+ server and dev validation are all generated from it. Apps never ship it.
27
+
28
+ ## Palette
29
+
30
+ ```js
31
+ import { createTheme, contrastRatio, toDTCG } from '@pinfold/core/palette';
32
+
33
+ const theme = createTheme({ primary: 'emerald', accent: 'amber', radius: 'lg' });
34
+ theme.vars; // { '--pinfold-primary-500': '#009b60', … }
35
+ theme.css; // the stylesheet `npx pinfold theme` writes
36
+ theme.contrast; // WCAG report per role and step
37
+ toDTCG(theme); // W3C design tokens
38
+ ```
39
+
40
+ Seven roles × eleven OKLCH steps, gamut-mapped, with sRGB hex fallbacks.
41
+
42
+ MIT © Indranil Bhattacharya
package/dist/index.cjs ADDED
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ // src/utils.ts
4
+ function cn(...parts) {
5
+ let out = "";
6
+ for (let i = 0; i < parts.length; i++) {
7
+ const p = parts[i];
8
+ if (p) out = out ? out + " " + p : p;
9
+ }
10
+ return out;
11
+ }
12
+ function resolveNode(value, state) {
13
+ return typeof value === "function" ? value(state) : value;
14
+ }
15
+ function dataAttr(condition) {
16
+ return condition ? "" : void 0;
17
+ }
18
+ function ariaAttr(condition) {
19
+ return condition ? "true" : void 0;
20
+ }
21
+ function composeRefs(...refs) {
22
+ return (node) => {
23
+ for (let i = 0; i < refs.length; i++) {
24
+ const ref = refs[i];
25
+ if (typeof ref === "function") ref(node);
26
+ else if (ref) ref.current = node;
27
+ }
28
+ };
29
+ }
30
+ var isHandler = (key) => key.length > 2 && key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110;
31
+ function mergeProps(base, overrides) {
32
+ if (!overrides) return base;
33
+ const out = { ...base };
34
+ for (const key in overrides) {
35
+ const ours = base[key];
36
+ const theirs = overrides[key];
37
+ if (isHandler(key) && typeof ours === "function" && typeof theirs === "function") {
38
+ out[key] = (...args) => {
39
+ ours(...args);
40
+ const ev = args[0];
41
+ if (ev && ev.defaultPrevented) return;
42
+ theirs(...args);
43
+ };
44
+ } else if (key === "className") {
45
+ out[key] = cn(ours, theirs);
46
+ } else if (key === "style") {
47
+ out[key] = { ...ours, ...theirs };
48
+ } else if (key === "ref") {
49
+ out[key] = composeRefs(ours, theirs);
50
+ } else {
51
+ out[key] = theirs;
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+
57
+ // src/variants.ts
58
+ function variants(config) {
59
+ const { base = "", variants: map, defaultVariants = {} } = config;
60
+ return function resolve(props) {
61
+ var _a;
62
+ let out = base;
63
+ for (const key in map) {
64
+ const chosen = (_a = props == null ? void 0 : props[key]) != null ? _a : defaultVariants[key];
65
+ if (chosen == null) continue;
66
+ const cls = map[key][chosen];
67
+ if (cls) out = cn(out, cls);
68
+ }
69
+ return cn(out, props == null ? void 0 : props.className);
70
+ };
71
+ }
72
+
73
+ // src/navigation.ts
74
+ function nextIndexFromKey(key, current, count, { orientation = "vertical", dir = "ltr", loop = true } = {}) {
75
+ if (count === 0) return null;
76
+ const forward = orientation === "vertical" ? "ArrowDown" : dir === "rtl" ? "ArrowLeft" : "ArrowRight";
77
+ const backward = orientation === "vertical" ? "ArrowUp" : dir === "rtl" ? "ArrowRight" : "ArrowLeft";
78
+ switch (key) {
79
+ case forward: {
80
+ const next = current + 1;
81
+ return next < count ? next : loop ? 0 : current;
82
+ }
83
+ case backward: {
84
+ const prev = current - 1;
85
+ return prev >= 0 ? prev : loop ? count - 1 : current;
86
+ }
87
+ case "Home":
88
+ return 0;
89
+ case "End":
90
+ return count - 1;
91
+ default:
92
+ return null;
93
+ }
94
+ }
95
+
96
+ // src/dimensions.ts
97
+ function dimensionVars(component, dimensions) {
98
+ if (!dimensions) return void 0;
99
+ const out = {};
100
+ for (const [key, raw] of Object.entries(dimensions)) {
101
+ const value = raw;
102
+ if (value === void 0 || value === null || value === "") continue;
103
+ out[`--pinfold-${component}-${kebab(key)}`] = typeof value === "number" ? `${value}px` : value;
104
+ }
105
+ return out;
106
+ }
107
+ var kebab = (s) => s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
108
+
109
+ // src/accordion.logic.ts
110
+ function accordionRowId(item, index) {
111
+ var _a;
112
+ return (_a = item.id) != null ? _a : String(index);
113
+ }
114
+ function accordionPartIds(baseId, rowId) {
115
+ return { triggerId: `${baseId}-trigger-${rowId}`, panelId: `${baseId}-panel-${rowId}` };
116
+ }
117
+ function normalizeAccordionValue(value, mode) {
118
+ return mode === "single" ? value.slice(0, 1) : value.slice();
119
+ }
120
+ function initialAccordionValue(items, defaultValue, mode) {
121
+ if (defaultValue) return normalizeAccordionValue(defaultValue, mode);
122
+ const open = [];
123
+ items.forEach((item, i) => {
124
+ if (item.defaultOpen) open.push(accordionRowId(item, i));
125
+ });
126
+ return normalizeAccordionValue(open, mode);
127
+ }
128
+ function setAccordionItem(value, rows, id, open, rules) {
129
+ const row = rows.find((r) => r.id === id);
130
+ if (!row || row.disabled || rules.disabled) return null;
131
+ const isOpen = value.indexOf(id) !== -1;
132
+ if (isOpen === open) return null;
133
+ if (!open && rules.mode === "single" && !rules.collapsible) return null;
134
+ if (open) return rules.mode === "single" ? [id] : value.concat(id);
135
+ return value.filter((v) => v !== id);
136
+ }
137
+ function openAllAccordion(value, rows, rules) {
138
+ if (rules.mode === "single" || rules.disabled) return null;
139
+ const next = rows.filter((r) => !r.disabled || value.indexOf(r.id) !== -1).map((r) => r.id);
140
+ return sameSet(value, next) ? null : next;
141
+ }
142
+ function closeAllAccordion(value, rows, rules) {
143
+ if (rules.disabled || rules.mode === "single" && !rules.collapsible) return null;
144
+ const locked = new Set(rows.filter((r) => r.disabled).map((r) => r.id));
145
+ const next = value.filter((id) => locked.has(id));
146
+ return sameSet(value, next) ? null : next;
147
+ }
148
+ function accordionFocusTarget(key, index, count, options = {}) {
149
+ return nextIndexFromKey(key, index, count, options);
150
+ }
151
+ function mapAccordionFields(records, fields) {
152
+ if (!fields) return records;
153
+ return records.map((record) => {
154
+ const out = { ...record };
155
+ for (const key in fields) {
156
+ const source = fields[key];
157
+ if (source && record != null && source in record) {
158
+ const v = record[source];
159
+ out[key] = key === "id" && v != null ? String(v) : v;
160
+ }
161
+ }
162
+ return out;
163
+ });
164
+ }
165
+ function shouldMountPanel(policy, open, everOpened) {
166
+ if (policy === "always" || open) return true;
167
+ return policy === "first-open" && everOpened;
168
+ }
169
+ function sameSet(a, b) {
170
+ return a.length === b.length && a.every((v) => b.indexOf(v) !== -1);
171
+ }
172
+
173
+ // src/dev.ts
174
+ var __DEV__ = typeof process !== "undefined" && !!process.env && process.env.NODE_ENV !== "production";
175
+ var seen = /* @__PURE__ */ new Set();
176
+ function warnOnce(condition, message) {
177
+ if (!__DEV__ || !condition || seen.has(message)) return;
178
+ seen.add(message);
179
+ console.warn(`[pinfold] ${message}`);
180
+ }
181
+ function resetWarnings() {
182
+ seen.clear();
183
+ }
184
+
185
+ // src/index.ts
186
+ var version = "0.1.0";
187
+
188
+ exports.__DEV__ = __DEV__;
189
+ exports.accordionFocusTarget = accordionFocusTarget;
190
+ exports.accordionPartIds = accordionPartIds;
191
+ exports.accordionRowId = accordionRowId;
192
+ exports.ariaAttr = ariaAttr;
193
+ exports.closeAllAccordion = closeAllAccordion;
194
+ exports.cn = cn;
195
+ exports.composeRefs = composeRefs;
196
+ exports.dataAttr = dataAttr;
197
+ exports.dimensionVars = dimensionVars;
198
+ exports.initialAccordionValue = initialAccordionValue;
199
+ exports.mapAccordionFields = mapAccordionFields;
200
+ exports.mergeProps = mergeProps;
201
+ exports.nextIndexFromKey = nextIndexFromKey;
202
+ exports.normalizeAccordionValue = normalizeAccordionValue;
203
+ exports.openAllAccordion = openAllAccordion;
204
+ exports.resetWarnings = resetWarnings;
205
+ exports.resolveNode = resolveNode;
206
+ exports.setAccordionItem = setAccordionItem;
207
+ exports.shouldMountPanel = shouldMountPanel;
208
+ exports.variants = variants;
209
+ exports.version = version;
210
+ exports.warnOnce = warnOnce;
@@ -0,0 +1,185 @@
1
+ /** Shared scale used by every Pinfold component. */
2
+ type PinfoldSize = 'sm' | 'md' | 'lg';
3
+ type PinfoldRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';
4
+ type PinfoldTone = 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
5
+ type PinfoldOrientation = 'horizontal' | 'vertical';
6
+ type PinfoldDir = 'ltr' | 'rtl';
7
+ type PinfoldTheme = 'light' | 'dark' | 'system';
8
+ /** When a component's code loads. `auto`: light components eagerly, heavy ones when visible. */
9
+ type PinfoldLazy = 'auto' | 'eager' | 'visible' | 'idle';
10
+ /** Visual style pack, applied through `data-pinfold-style`. */
11
+ type PinfoldStylePack = 'default' | 'glass' | 'bauhaus' | 'brutalist' | 'noise' | 'soft';
12
+ /** Any DOM props bag a component hands back from a `getXProps()` call. */
13
+ type PropsBag = Record<string, any>;
14
+ /** A CSS length. Numbers are px, strings pass through (`'50%'`, `'12rem'`, `calc()`). */
15
+ type Length = number | string;
16
+ /** A callback ref or a `{ current }` object, the two shapes every framework's refs reduce to. */
17
+ type Ref<T> = ((node: T | null) => void) | {
18
+ current: T | null;
19
+ } | null;
20
+
21
+ /** Join truthy class names. Deliberately tiny — no `clsx` dependency. */
22
+ declare function cn(...parts: Array<string | false | null | undefined>): string;
23
+ /**
24
+ * A slot value that may be static or derived from live state.
25
+ *
26
+ * Every renderable field in a Pinfold config accepts both, so a caret can react to
27
+ * `open` without the consumer dropping down to a full slot override.
28
+ */
29
+ type Slottable<State, T = unknown> = T | ((state: State) => T);
30
+ /** Resolve a `Slottable` against the current state. */
31
+ declare function resolveNode<State, T>(value: Slottable<State, T> | undefined, state: State): T | undefined;
32
+ /** `data-*` attributes render when true and vanish when false. */
33
+ declare function dataAttr(condition: unknown): '' | undefined;
34
+ /** `aria-*` attributes want the literal strings. */
35
+ declare function ariaAttr(condition: unknown): 'true' | undefined;
36
+ declare function composeRefs<T>(...refs: Array<Ref<T> | undefined>): (node: T | null) => void;
37
+ /**
38
+ * Merge a user props bag over ours: event handlers chain (ours first, then
39
+ * theirs, unless ours called `preventDefault`), `className` concatenates,
40
+ * `style` shallow-merges, everything else overrides.
41
+ */
42
+ declare function mergeProps(base: PropsBag, overrides?: PropsBag): PropsBag;
43
+
44
+ type VariantShape = Record<string, Record<string, string>>;
45
+ type VariantProps<V extends VariantShape> = {
46
+ [K in keyof V]?: keyof V[K] | null | undefined;
47
+ };
48
+ /**
49
+ * 20-line class-variance resolver. Same ergonomics as `cva` without the
50
+ * dependency or its runtime — components are static strings plus a lookup.
51
+ */
52
+ declare function variants<V extends VariantShape>(config: {
53
+ base?: string;
54
+ variants: V;
55
+ defaultVariants?: VariantProps<V>;
56
+ }): (props?: VariantProps<V> & {
57
+ className?: string;
58
+ }) => string;
59
+
60
+ interface ListNavOptions {
61
+ orientation?: PinfoldOrientation;
62
+ dir?: PinfoldDir;
63
+ /** Wrap from last to first. Default true. */
64
+ loop?: boolean;
65
+ }
66
+ /**
67
+ * WAI-ARIA roving-focus key resolution, shared by every list-like component
68
+ * (accordion, tabs, menu, listbox, toolbar…).
69
+ *
70
+ * Returns the index to move focus to, or `null` when the key is not ours —
71
+ * callers must not `preventDefault()` on `null`.
72
+ */
73
+ declare function nextIndexFromKey(key: string, current: number, count: number, { orientation, dir, loop }?: ListNavOptions): number | null;
74
+
75
+ /**
76
+ * Turn a `dimensions` config object into CSS custom properties on the root.
77
+ *
78
+ * dimensionVars('accordion', { paddingX: 24, panelMaxHeight: '18rem' })
79
+ * // → { '--pinfold-accordion-padding-x': '24px', '--pinfold-accordion-panel-max-height': '18rem' }
80
+ *
81
+ * Numbers become px; strings pass through untouched, so `'50%'`, `'12rem'`,
82
+ * `'clamp(...)'` and `var(...)` all work. Every component's CSS reads these
83
+ * with a fallback, which is why one shared resolver covers all of them.
84
+ */
85
+ declare function dimensionVars<D extends object>(component: string, dimensions: D | undefined): Record<string, string> | undefined;
86
+
87
+ /**
88
+ * Accordion rules as pure functions. No DOM, no framework: every adapter
89
+ * (React today) calls these and only wires the results to its own state and
90
+ * events, so behaviour can never drift between adapters.
91
+ */
92
+ type AccordionMode = 'single' | 'multiple';
93
+ /** Why the open set changed. Passed to `onBeforeChange` and `onValueChange`. */
94
+ type AccordionChangeReason = 'click' | 'keyboard' | 'api' | 'find-in-page' | 'hash';
95
+ interface AccordionChangeDetails<E = unknown> {
96
+ /** The row that triggered the change. Absent for `openAll` / `closeAll`. */
97
+ id?: string;
98
+ open: boolean;
99
+ reason: AccordionChangeReason;
100
+ event?: E;
101
+ }
102
+ /** The minimum a row needs for the rules: its id and whether it is disabled. */
103
+ interface AccordionRowLike {
104
+ id: string;
105
+ disabled?: boolean;
106
+ }
107
+ interface AccordionRules {
108
+ mode: AccordionMode;
109
+ collapsible: boolean;
110
+ /** Disables every row. */
111
+ disabled?: boolean;
112
+ }
113
+ /** Row id: the configured `id`, else the index as a string. */
114
+ declare function accordionRowId(item: {
115
+ id?: string;
116
+ }, index: number): string;
117
+ /** Trigger and panel ids for a row, derived from the instance's base id. */
118
+ declare function accordionPartIds(baseId: string, rowId: string): {
119
+ triggerId: string;
120
+ panelId: string;
121
+ };
122
+ /** Single mode keeps at most one open id. */
123
+ declare function normalizeAccordionValue(value: readonly string[], mode: AccordionMode): string[];
124
+ /** The first open set: `defaultValue` when given, else every row with `defaultOpen`. */
125
+ declare function initialAccordionValue(items: ReadonlyArray<{
126
+ id?: string;
127
+ defaultOpen?: boolean;
128
+ }>, defaultValue: readonly string[] | undefined, mode: AccordionMode): string[];
129
+ /**
130
+ * The open set after asking row `id` to become `open`, or `null` when the
131
+ * rules refuse (unknown or disabled row, no change, or closing the only open
132
+ * row of a non-collapsible single accordion).
133
+ */
134
+ declare function setAccordionItem(value: readonly string[], rows: readonly AccordionRowLike[], id: string, open: boolean, rules: AccordionRules): string[] | null;
135
+ /** `openAll`: every enabled row, multiple mode only. */
136
+ declare function openAllAccordion(value: readonly string[], rows: readonly AccordionRowLike[], rules: AccordionRules): string[] | null;
137
+ /** `closeAll`: refused for a non-collapsible single accordion. Disabled rows keep their state. */
138
+ declare function closeAllAccordion(value: readonly string[], rows: readonly AccordionRowLike[], rules: AccordionRules): string[] | null;
139
+ interface AccordionKeyOptions {
140
+ orientation?: PinfoldOrientation;
141
+ dir?: PinfoldDir;
142
+ loop?: boolean;
143
+ }
144
+ /**
145
+ * The header index to focus for a key, or `null` when the key is not ours.
146
+ * Disabled headers stay focusable (APG), so they are not skipped.
147
+ */
148
+ declare function accordionFocusTarget(key: string, index: number, count: number, options?: AccordionKeyOptions): number | null;
149
+ /**
150
+ * Field mapping: lets raw records (a CMS row, a DB result) feed the accordion
151
+ * without a `.map()`. `{ id: 'slug', title: 'question', content: 'answer' }`.
152
+ */
153
+ type AccordionFieldMap = Partial<Record<'id' | 'title' | 'content' | 'description' | 'icon' | 'badge' | 'disabled' | 'defaultOpen', string>>;
154
+ declare function mapAccordionFields<T>(records: readonly any[], fields: AccordionFieldMap | undefined): T[];
155
+ /** When panel content is mounted. */
156
+ type AccordionMountPanels = 'always' | 'open' | 'first-open';
157
+ /** Whether a panel's content should be in the DOM right now. */
158
+ declare function shouldMountPanel(policy: AccordionMountPanels, open: boolean, everOpened: boolean): boolean;
159
+
160
+ /**
161
+ * Development-only diagnostics.
162
+ *
163
+ * Config-driven APIs fail quietly: a duplicated `id` or a `value` array that
164
+ * silently switches a component from uncontrolled to controlled produces
165
+ * confusing behaviour rather than an error. These warnings turn that class of
166
+ * mistake into a message, and vanish entirely from production builds because
167
+ * bundlers fold the `NODE_ENV` check.
168
+ */
169
+ declare const __DEV__: boolean;
170
+ /** Warn at most once per distinct message, so a render loop cannot spam the console. */
171
+ declare function warnOnce(condition: unknown, message: string): void;
172
+ /** Test hook — lets a suite assert the same warning twice. */
173
+ declare function resetWarnings(): void;
174
+
175
+ /**
176
+ * @pinfold/core: framework-free building blocks. Nothing here imports React or
177
+ * any other UI framework (enforced by `framework-isolation.test.ts`), so a
178
+ * future adapter reuses exactly the same rules.
179
+ */
180
+
181
+ /** Package version, injected at publish time by the release script. */
182
+ declare const version = "0.1.0";
183
+
184
+ export { __DEV__, accordionFocusTarget, accordionPartIds, accordionRowId, ariaAttr, closeAllAccordion, cn, composeRefs, dataAttr, dimensionVars, initialAccordionValue, mapAccordionFields, mergeProps, nextIndexFromKey, normalizeAccordionValue, openAllAccordion, resetWarnings, resolveNode, setAccordionItem, shouldMountPanel, variants, version, warnOnce };
185
+ export type { AccordionChangeDetails, AccordionChangeReason, AccordionFieldMap, AccordionKeyOptions, AccordionMode, AccordionMountPanels, AccordionRowLike, AccordionRules, Length, ListNavOptions, PinfoldDir, PinfoldLazy, PinfoldOrientation, PinfoldRadius, PinfoldSize, PinfoldStylePack, PinfoldTheme, PinfoldTone, PropsBag, Ref, Slottable, VariantProps };
@@ -0,0 +1,185 @@
1
+ /** Shared scale used by every Pinfold component. */
2
+ type PinfoldSize = 'sm' | 'md' | 'lg';
3
+ type PinfoldRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';
4
+ type PinfoldTone = 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
5
+ type PinfoldOrientation = 'horizontal' | 'vertical';
6
+ type PinfoldDir = 'ltr' | 'rtl';
7
+ type PinfoldTheme = 'light' | 'dark' | 'system';
8
+ /** When a component's code loads. `auto`: light components eagerly, heavy ones when visible. */
9
+ type PinfoldLazy = 'auto' | 'eager' | 'visible' | 'idle';
10
+ /** Visual style pack, applied through `data-pinfold-style`. */
11
+ type PinfoldStylePack = 'default' | 'glass' | 'bauhaus' | 'brutalist' | 'noise' | 'soft';
12
+ /** Any DOM props bag a component hands back from a `getXProps()` call. */
13
+ type PropsBag = Record<string, any>;
14
+ /** A CSS length. Numbers are px, strings pass through (`'50%'`, `'12rem'`, `calc()`). */
15
+ type Length = number | string;
16
+ /** A callback ref or a `{ current }` object, the two shapes every framework's refs reduce to. */
17
+ type Ref<T> = ((node: T | null) => void) | {
18
+ current: T | null;
19
+ } | null;
20
+
21
+ /** Join truthy class names. Deliberately tiny — no `clsx` dependency. */
22
+ declare function cn(...parts: Array<string | false | null | undefined>): string;
23
+ /**
24
+ * A slot value that may be static or derived from live state.
25
+ *
26
+ * Every renderable field in a Pinfold config accepts both, so a caret can react to
27
+ * `open` without the consumer dropping down to a full slot override.
28
+ */
29
+ type Slottable<State, T = unknown> = T | ((state: State) => T);
30
+ /** Resolve a `Slottable` against the current state. */
31
+ declare function resolveNode<State, T>(value: Slottable<State, T> | undefined, state: State): T | undefined;
32
+ /** `data-*` attributes render when true and vanish when false. */
33
+ declare function dataAttr(condition: unknown): '' | undefined;
34
+ /** `aria-*` attributes want the literal strings. */
35
+ declare function ariaAttr(condition: unknown): 'true' | undefined;
36
+ declare function composeRefs<T>(...refs: Array<Ref<T> | undefined>): (node: T | null) => void;
37
+ /**
38
+ * Merge a user props bag over ours: event handlers chain (ours first, then
39
+ * theirs, unless ours called `preventDefault`), `className` concatenates,
40
+ * `style` shallow-merges, everything else overrides.
41
+ */
42
+ declare function mergeProps(base: PropsBag, overrides?: PropsBag): PropsBag;
43
+
44
+ type VariantShape = Record<string, Record<string, string>>;
45
+ type VariantProps<V extends VariantShape> = {
46
+ [K in keyof V]?: keyof V[K] | null | undefined;
47
+ };
48
+ /**
49
+ * 20-line class-variance resolver. Same ergonomics as `cva` without the
50
+ * dependency or its runtime — components are static strings plus a lookup.
51
+ */
52
+ declare function variants<V extends VariantShape>(config: {
53
+ base?: string;
54
+ variants: V;
55
+ defaultVariants?: VariantProps<V>;
56
+ }): (props?: VariantProps<V> & {
57
+ className?: string;
58
+ }) => string;
59
+
60
+ interface ListNavOptions {
61
+ orientation?: PinfoldOrientation;
62
+ dir?: PinfoldDir;
63
+ /** Wrap from last to first. Default true. */
64
+ loop?: boolean;
65
+ }
66
+ /**
67
+ * WAI-ARIA roving-focus key resolution, shared by every list-like component
68
+ * (accordion, tabs, menu, listbox, toolbar…).
69
+ *
70
+ * Returns the index to move focus to, or `null` when the key is not ours —
71
+ * callers must not `preventDefault()` on `null`.
72
+ */
73
+ declare function nextIndexFromKey(key: string, current: number, count: number, { orientation, dir, loop }?: ListNavOptions): number | null;
74
+
75
+ /**
76
+ * Turn a `dimensions` config object into CSS custom properties on the root.
77
+ *
78
+ * dimensionVars('accordion', { paddingX: 24, panelMaxHeight: '18rem' })
79
+ * // → { '--pinfold-accordion-padding-x': '24px', '--pinfold-accordion-panel-max-height': '18rem' }
80
+ *
81
+ * Numbers become px; strings pass through untouched, so `'50%'`, `'12rem'`,
82
+ * `'clamp(...)'` and `var(...)` all work. Every component's CSS reads these
83
+ * with a fallback, which is why one shared resolver covers all of them.
84
+ */
85
+ declare function dimensionVars<D extends object>(component: string, dimensions: D | undefined): Record<string, string> | undefined;
86
+
87
+ /**
88
+ * Accordion rules as pure functions. No DOM, no framework: every adapter
89
+ * (React today) calls these and only wires the results to its own state and
90
+ * events, so behaviour can never drift between adapters.
91
+ */
92
+ type AccordionMode = 'single' | 'multiple';
93
+ /** Why the open set changed. Passed to `onBeforeChange` and `onValueChange`. */
94
+ type AccordionChangeReason = 'click' | 'keyboard' | 'api' | 'find-in-page' | 'hash';
95
+ interface AccordionChangeDetails<E = unknown> {
96
+ /** The row that triggered the change. Absent for `openAll` / `closeAll`. */
97
+ id?: string;
98
+ open: boolean;
99
+ reason: AccordionChangeReason;
100
+ event?: E;
101
+ }
102
+ /** The minimum a row needs for the rules: its id and whether it is disabled. */
103
+ interface AccordionRowLike {
104
+ id: string;
105
+ disabled?: boolean;
106
+ }
107
+ interface AccordionRules {
108
+ mode: AccordionMode;
109
+ collapsible: boolean;
110
+ /** Disables every row. */
111
+ disabled?: boolean;
112
+ }
113
+ /** Row id: the configured `id`, else the index as a string. */
114
+ declare function accordionRowId(item: {
115
+ id?: string;
116
+ }, index: number): string;
117
+ /** Trigger and panel ids for a row, derived from the instance's base id. */
118
+ declare function accordionPartIds(baseId: string, rowId: string): {
119
+ triggerId: string;
120
+ panelId: string;
121
+ };
122
+ /** Single mode keeps at most one open id. */
123
+ declare function normalizeAccordionValue(value: readonly string[], mode: AccordionMode): string[];
124
+ /** The first open set: `defaultValue` when given, else every row with `defaultOpen`. */
125
+ declare function initialAccordionValue(items: ReadonlyArray<{
126
+ id?: string;
127
+ defaultOpen?: boolean;
128
+ }>, defaultValue: readonly string[] | undefined, mode: AccordionMode): string[];
129
+ /**
130
+ * The open set after asking row `id` to become `open`, or `null` when the
131
+ * rules refuse (unknown or disabled row, no change, or closing the only open
132
+ * row of a non-collapsible single accordion).
133
+ */
134
+ declare function setAccordionItem(value: readonly string[], rows: readonly AccordionRowLike[], id: string, open: boolean, rules: AccordionRules): string[] | null;
135
+ /** `openAll`: every enabled row, multiple mode only. */
136
+ declare function openAllAccordion(value: readonly string[], rows: readonly AccordionRowLike[], rules: AccordionRules): string[] | null;
137
+ /** `closeAll`: refused for a non-collapsible single accordion. Disabled rows keep their state. */
138
+ declare function closeAllAccordion(value: readonly string[], rows: readonly AccordionRowLike[], rules: AccordionRules): string[] | null;
139
+ interface AccordionKeyOptions {
140
+ orientation?: PinfoldOrientation;
141
+ dir?: PinfoldDir;
142
+ loop?: boolean;
143
+ }
144
+ /**
145
+ * The header index to focus for a key, or `null` when the key is not ours.
146
+ * Disabled headers stay focusable (APG), so they are not skipped.
147
+ */
148
+ declare function accordionFocusTarget(key: string, index: number, count: number, options?: AccordionKeyOptions): number | null;
149
+ /**
150
+ * Field mapping: lets raw records (a CMS row, a DB result) feed the accordion
151
+ * without a `.map()`. `{ id: 'slug', title: 'question', content: 'answer' }`.
152
+ */
153
+ type AccordionFieldMap = Partial<Record<'id' | 'title' | 'content' | 'description' | 'icon' | 'badge' | 'disabled' | 'defaultOpen', string>>;
154
+ declare function mapAccordionFields<T>(records: readonly any[], fields: AccordionFieldMap | undefined): T[];
155
+ /** When panel content is mounted. */
156
+ type AccordionMountPanels = 'always' | 'open' | 'first-open';
157
+ /** Whether a panel's content should be in the DOM right now. */
158
+ declare function shouldMountPanel(policy: AccordionMountPanels, open: boolean, everOpened: boolean): boolean;
159
+
160
+ /**
161
+ * Development-only diagnostics.
162
+ *
163
+ * Config-driven APIs fail quietly: a duplicated `id` or a `value` array that
164
+ * silently switches a component from uncontrolled to controlled produces
165
+ * confusing behaviour rather than an error. These warnings turn that class of
166
+ * mistake into a message, and vanish entirely from production builds because
167
+ * bundlers fold the `NODE_ENV` check.
168
+ */
169
+ declare const __DEV__: boolean;
170
+ /** Warn at most once per distinct message, so a render loop cannot spam the console. */
171
+ declare function warnOnce(condition: unknown, message: string): void;
172
+ /** Test hook — lets a suite assert the same warning twice. */
173
+ declare function resetWarnings(): void;
174
+
175
+ /**
176
+ * @pinfold/core: framework-free building blocks. Nothing here imports React or
177
+ * any other UI framework (enforced by `framework-isolation.test.ts`), so a
178
+ * future adapter reuses exactly the same rules.
179
+ */
180
+
181
+ /** Package version, injected at publish time by the release script. */
182
+ declare const version = "0.1.0";
183
+
184
+ export { __DEV__, accordionFocusTarget, accordionPartIds, accordionRowId, ariaAttr, closeAllAccordion, cn, composeRefs, dataAttr, dimensionVars, initialAccordionValue, mapAccordionFields, mergeProps, nextIndexFromKey, normalizeAccordionValue, openAllAccordion, resetWarnings, resolveNode, setAccordionItem, shouldMountPanel, variants, version, warnOnce };
185
+ export type { AccordionChangeDetails, AccordionChangeReason, AccordionFieldMap, AccordionKeyOptions, AccordionMode, AccordionMountPanels, AccordionRowLike, AccordionRules, Length, ListNavOptions, PinfoldDir, PinfoldLazy, PinfoldOrientation, PinfoldRadius, PinfoldSize, PinfoldStylePack, PinfoldTheme, PinfoldTone, PropsBag, Ref, Slottable, VariantProps };