@pinfold/react 0.0.0-stage → 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.
Files changed (41) hide show
  1. package/README.md +58 -2
  2. package/accordion/lazy/package.json +6 -0
  3. package/accordion/package.json +6 -0
  4. package/dist/accordion/client.cjs +491 -0
  5. package/dist/accordion/client.d.cts +34 -0
  6. package/dist/accordion/client.d.ts +34 -0
  7. package/dist/accordion/client.js +490 -0
  8. package/dist/accordion/index.cjs +49 -0
  9. package/dist/accordion/index.d.cts +56 -0
  10. package/dist/accordion/index.d.ts +56 -0
  11. package/dist/accordion/index.js +14 -0
  12. package/dist/accordion/lazy.cjs +17 -0
  13. package/dist/accordion/lazy.d.cts +16 -0
  14. package/dist/accordion/lazy.d.ts +16 -0
  15. package/dist/accordion/lazy.js +16 -0
  16. package/dist/chunk-5ZMK623A.js +61 -0
  17. package/dist/chunk-BPPCPHHU.js +70 -0
  18. package/dist/chunk-DQWSDHRY.cjs +71 -0
  19. package/dist/chunk-Q232PTPW.js +115 -0
  20. package/dist/chunk-RJPT3NTP.cjs +62 -0
  21. package/dist/chunk-VPKVOJHA.cjs +116 -0
  22. package/dist/client.cjs +30 -0
  23. package/dist/client.d.cts +120 -0
  24. package/dist/client.d.ts +120 -0
  25. package/dist/client.js +29 -0
  26. package/dist/index.cjs +52 -0
  27. package/dist/index.d.cts +123 -0
  28. package/dist/index.d.ts +123 -0
  29. package/dist/index.js +10 -0
  30. package/dist/types-CkcgnWwL.d.cts +367 -0
  31. package/dist/types-CkcgnWwL.d.ts +367 -0
  32. package/dist/types-N0Yo4zGa.d.cts +374 -0
  33. package/dist/types-N0Yo4zGa.d.ts +374 -0
  34. package/dist/validate-EQHBH4NH.cjs +42 -0
  35. package/dist/validate-WKHQNNGD.js +41 -0
  36. package/llms-full.txt +140 -0
  37. package/llms.txt +26 -0
  38. package/package.json +92 -4
  39. package/registry/accordion.json +20 -0
  40. package/registry/registry.json +25 -0
  41. package/schema/accordion.json +903 -0
@@ -0,0 +1,374 @@
1
+ import * as React from 'react';
2
+ import { Slottable as Slottable$1, PropsBag, AccordionChangeDetails, PinfoldOrientation, PinfoldDir, AccordionMountPanels, PinfoldLazy, PinfoldSize, PinfoldTone, PinfoldRadius, Length, AccordionFieldMap } from '@pinfold/core';
3
+
4
+ /**
5
+ * A component that is also its own controller.
6
+ *
7
+ * const Accordion = useAccordion(config);
8
+ * return <Accordion className="mt-6" />; // renders
9
+ * Accordion.toggle('faq-1'); // controls
10
+ * <Accordion.Panel id="faq-1" /> // composes
11
+ */
12
+ type PinfoldComponent<Api, Props> = ((props: Props) => React.ReactElement | null) & Api & {
13
+ displayName?: string;
14
+ };
15
+ type Part = (props: any) => React.ReactElement | null;
16
+ /**
17
+ * Turn a render function plus an API object (and optional parts) into a
18
+ * stable component.
19
+ *
20
+ * Identity stability: a component created fresh on every render is a *new
21
+ * element type* to React, which remounts the whole subtree on each parent
22
+ * update, losing focus, selection, scroll position and running transitions.
23
+ * So the component and its parts are created once and read the current
24
+ * render through a ref.
25
+ *
26
+ * Freshness: because the identity never changes, a memoising parent (the
27
+ * React Compiler, `React.memo`, a cached element) may skip re-rendering it.
28
+ * So every hook render bumps a version, and after commit the component and
29
+ * each part re-render through `useSyncExternalStore` if they have not yet
30
+ * seen it. In the ordinary case they already rendered with the parent, the
31
+ * snapshot matches, and React does no extra work.
32
+ */
33
+ declare function usePinfoldComponent<Api extends object, Props extends object, Parts extends {
34
+ [K in keyof Parts]: Part;
35
+ } = {}>(displayName: string, api: Api, render: (props: Props) => React.ReactElement | null, buildParts?: () => Parts): PinfoldComponent<Api & Parts, Props>;
36
+
37
+ /**
38
+ * Blocks type inference from a position, so a value there is checked against what was inferred
39
+ * elsewhere instead of widening it (a wrong id is an error, not a new id). TypeScript 5.4 has
40
+ * this built in; this form works back to 4.7, the published floor.
41
+ */
42
+ type NoInfer<T> = [T][T extends any ? 0 : never];
43
+ /** A config field that is a node, or a function of live state returning one. */
44
+ type Slottable<State, T = React.ReactNode> = Slottable$1<State, T>;
45
+ /**
46
+ * Per-slot escape hatches. Every Pinfold component exposes the same four, so
47
+ * learning one component teaches all of them.
48
+ *
49
+ * - `classNames` — append a class to one slot.
50
+ * - `slotProps` — merge arbitrary DOM props into one slot (event handlers chain).
51
+ * - `styles` — inline style for one slot.
52
+ * - `render` — replace one slot's element entirely, keeping its behaviour props.
53
+ */
54
+ interface SlotOverrides<Slot extends string, State = unknown> {
55
+ classNames?: Partial<Record<Slot, string>>;
56
+ styles?: Partial<Record<Slot, React.CSSProperties>>;
57
+ slotProps?: Partial<Record<Slot, PropsBag>>;
58
+ render?: Partial<Record<Slot, SlotRenderer<State>>>;
59
+ }
60
+ type SlotRenderer<State = unknown> = (args: {
61
+ /** Behaviour props (ARIA, handlers, refs). Spread these or accessibility breaks. */
62
+ props: PropsBag;
63
+ /** Live component state for this slot. */
64
+ state: State;
65
+ children: React.ReactNode;
66
+ }) => React.ReactNode;
67
+
68
+ type AccordionSlot = 'root' | 'item' | 'header' | 'trigger' | 'icon' | 'label' | 'title' | 'description' | 'badge' | 'endIcon' | 'indicator' | 'panel' | 'content';
69
+ type AccordionVariant = 'solid' | 'outline' | 'separated' | 'filled' | 'ghost';
70
+ type AccordionIndicator = 'chevron' | 'plus' | 'caret' | 'arrow' | 'none';
71
+ /** A radius preset (the theme scale) or a length: `20` is px, strings pass through. */
72
+ type AccordionRadius = PinfoldRadius | number | (string & {});
73
+ /** A spacing preset or a length: `12` is px, strings pass through. */
74
+ type AccordionGap = 'none' | 'sm' | 'md' | 'lg' | number | (string & {});
75
+ /**
76
+ * The ids of a list of rows, e.g. for typed controlled state:
77
+ * `useState<AccordionItemId<typeof items>[]>([])`.
78
+ */
79
+ type AccordionItemId<Items extends ReadonlyArray<{
80
+ id?: string;
81
+ }>> = NonNullable<Items[number]['id']>;
82
+ /** Runtime state of a single row. Passed to every function-valued config field. */
83
+ interface AccordionItemState<Id extends string = string> {
84
+ id: Id;
85
+ index: number;
86
+ open: boolean;
87
+ disabled: boolean;
88
+ config: AccordionItemConfig<Id>;
89
+ triggerId: string;
90
+ panelId: string;
91
+ }
92
+ /** Anything renderable in a row: a node, or a function of the row's live state. */
93
+ type AccordionNode<Id extends string = string> = Slottable<AccordionItemState<Id>>;
94
+ /** One row of the accordion. This is the JSON a consumer — or a model — writes. */
95
+ interface AccordionItemConfig<Id extends string = string> {
96
+ /** Stable key. Falls back to the array index when omitted. Give every row one to get typed ids. */
97
+ id?: Id;
98
+ title: AccordionNode<Id>;
99
+ content?: AccordionNode<Id>;
100
+ /** Secondary line under the title. */
101
+ description?: AccordionNode<Id>;
102
+ /** Leading visual: a node, or a string (an emoji, or an icon-font name with `classNames.icon`). Overrides the accordion-wide `icon`. */
103
+ icon?: AccordionNode<Id>;
104
+ /** Trailing chip rendered before the indicator. */
105
+ badge?: AccordionNode<Id>;
106
+ /** Trailing visual just before the indicator. Overrides the accordion-wide `endIcon`. */
107
+ endIcon?: AccordionNode<Id>;
108
+ /** Overrides the accordion-wide indicator for this row only. */
109
+ indicator?: AccordionIndicator | AccordionNode<Id>;
110
+ disabled?: boolean;
111
+ /** Open on first render (uncontrolled only). */
112
+ defaultOpen?: boolean;
113
+ /** Per-row class and style overrides, same slot names as the root config. */
114
+ classNames?: Partial<Record<AccordionSlot, string>>;
115
+ styles?: Partial<Record<AccordionSlot, React.CSSProperties>>;
116
+ }
117
+ /** Text overflow for one slot: wrap, one line with an ellipsis, or clamp to n lines. */
118
+ type AccordionOverflow = 'wrap' | 'truncate' | {
119
+ lines: number;
120
+ };
121
+ type AccordionValueDetails<Id extends string = string> = Omit<AccordionChangeDetails<Event | React.SyntheticEvent>, 'id'> & {
122
+ /** The row that triggered the change. Absent for `openAll` / `closeAll`. */
123
+ id?: Id;
124
+ };
125
+ interface AccordionBehaviourConfig<Id extends string = string> {
126
+ /**
127
+ * `single` closes siblings on open.
128
+ * @default 'single'
129
+ */
130
+ mode?: 'single' | 'multiple';
131
+ /**
132
+ * Allow closing the last open row. `false` keeps at least one row open, in either mode.
133
+ * @default true
134
+ */
135
+ collapsible?: boolean;
136
+ /**
137
+ * Controlled open ids. Any `string[]` state works (`useState<string[]>`); for typed state use
138
+ * `useState<AccordionItemId<typeof items>[]>`.
139
+ */
140
+ value?: ReadonlyArray<string>;
141
+ /** Initially open ids (uncontrolled). */
142
+ defaultValue?: ReadonlyArray<NoInfer<Id>>;
143
+ /** `details.reason`: 'click' | 'keyboard' | 'api' | 'find-in-page' | 'hash'. */
144
+ onValueChange?: (value: Id[], details: AccordionValueDetails<Id>) => void;
145
+ /** Return `false` to block a change (e.g. unsaved edits in the open panel). */
146
+ onBeforeChange?: (details: AccordionValueDetails<Id>) => boolean | void;
147
+ /** Fires for one item, after `onValueChange`. */
148
+ onItemToggle?: (id: Id, open: boolean) => void;
149
+ /**
150
+ * Arrow-key axis for moving between headers.
151
+ * @default 'vertical'
152
+ */
153
+ orientation?: PinfoldOrientation;
154
+ /** Text direction. Defaults to the provider. */
155
+ dir?: PinfoldDir;
156
+ /**
157
+ * Wrap focus at the ends.
158
+ * @default true
159
+ */
160
+ loop?: boolean;
161
+ /** Prefix for generated ids — set it for deterministic SSR markup. */
162
+ id?: string;
163
+ /**
164
+ * Disables every row. Rows stay focusable.
165
+ * @default false
166
+ */
167
+ disabled?: boolean;
168
+ /** Disables these rows by id, for data you cannot annotate row by row. */
169
+ disabledIds?: ReadonlyArray<NoInfer<Id>>;
170
+ /**
171
+ * Closed panels use `hidden="until-found"`, so the browser's find-in-page
172
+ * searches them and opens the match.
173
+ * @default true
174
+ */
175
+ hiddenUntilFound?: boolean;
176
+ /**
177
+ * When panel content is in the DOM. `always` keeps it searchable and indexable;
178
+ * `open` mounts only open panels; `first-open` mounts on first open and keeps it.
179
+ * @default 'always'
180
+ */
181
+ mountPanels?: AccordionMountPanels;
182
+ /** @deprecated Use `mountPanels: 'open'`. */
183
+ unmountClosed?: boolean;
184
+ /**
185
+ * `#id` in the URL opens that row and scrolls to it; opening a row updates the hash.
186
+ * @default false
187
+ */
188
+ syncWithHash?: boolean;
189
+ /**
190
+ * Code loading for the `/lazy` entry. The accordion is light, so `auto` loads eagerly.
191
+ * @default 'auto'
192
+ */
193
+ lazy?: PinfoldLazy;
194
+ }
195
+ /**
196
+ * Explicit sizing. Numbers are px, strings pass through (`'50%'`, `'18rem'`,
197
+ * `'clamp(20rem, 40vw, 32rem)'`). Each key overrides the value the `size`
198
+ * preset would have chosen, so you can take one dimension and leave the rest.
199
+ */
200
+ interface AccordionDimensions {
201
+ width?: Length;
202
+ minWidth?: Length;
203
+ maxWidth?: Length;
204
+ height?: Length;
205
+ maxHeight?: Length;
206
+ /** Space between rows. Overrides `gap`. */
207
+ gap?: Length;
208
+ /** Trigger padding. */
209
+ paddingX?: Length;
210
+ paddingY?: Length;
211
+ /** Panel padding. Defaults to the trigger padding. */
212
+ panelPaddingX?: Length;
213
+ panelPaddingY?: Length;
214
+ /** Cap the open panel and let it scroll past that. */
215
+ panelMaxHeight?: Length;
216
+ fontSize?: Length;
217
+ titleSize?: Length;
218
+ descriptionSize?: Length;
219
+ iconSize?: Length;
220
+ indicatorSize?: Length;
221
+ borderWidth?: Length;
222
+ /** Container corner radius as a length. Overrides `radius`. */
223
+ radius?: Length;
224
+ /** Row corner radius as a length. Overrides `itemRadius`. */
225
+ itemRadius?: Length;
226
+ /** Open/close transition duration, e.g. `'320ms'`. */
227
+ duration?: Length;
228
+ }
229
+ interface AccordionAppearanceConfig<Id extends string = string> {
230
+ /** @default 'solid' */
231
+ variant?: AccordionVariant;
232
+ /** @default 'md' */
233
+ size?: PinfoldSize;
234
+ /** @default 'primary' */
235
+ tone?: PinfoldTone;
236
+ /** Corner radius of the container (the element that holds the rows). Unset: the theme radius. */
237
+ radius?: AccordionRadius;
238
+ /** Corner radius of each row. Unset: follows `radius` (square rows inside a solid or outline card). */
239
+ itemRadius?: AccordionRadius;
240
+ /** Space between rows. Unset: none for solid, outline and ghost; `'sm'` for separated and filled. */
241
+ gap?: AccordionGap;
242
+ /** Leading visual for every row: a node, or `(state) => node` (e.g. per row, or open vs closed). */
243
+ icon?: AccordionNode<Id>;
244
+ /** Trailing visual for every row, just before the indicator. Same shape as `icon`. */
245
+ endIcon?: AccordionNode<Id>;
246
+ /**
247
+ * Built-in glyph, your own node, or a function of the row's state.
248
+ * @default 'chevron'
249
+ */
250
+ indicator?: AccordionIndicator | AccordionNode<Id>;
251
+ /** @default 'end' */
252
+ indicatorPosition?: 'start' | 'end';
253
+ /**
254
+ * Rotate (or morph) the indicator when a row opens.
255
+ * @default true
256
+ */
257
+ animateIndicator?: boolean;
258
+ /**
259
+ * Heading level wrapping each trigger.
260
+ * @default 3
261
+ */
262
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
263
+ /**
264
+ * Animate open/close; `prefers-reduced-motion` always wins.
265
+ * @default true
266
+ */
267
+ animate?: boolean;
268
+ /**
269
+ * Lines between rows that touch.
270
+ * @default true
271
+ */
272
+ dividers?: boolean;
273
+ /** Text overflow per slot. Unset: title `wrap`, description `{ lines: 2 }`. */
274
+ overflow?: {
275
+ title?: AccordionOverflow;
276
+ description?: AccordionOverflow;
277
+ };
278
+ /** Explicit sizing, per dimension. Overrides the `size` preset. */
279
+ dimensions?: AccordionDimensions;
280
+ /**
281
+ * Drop every Pinfold class so no bundled CSS applies, keeping the behaviour,
282
+ * ARIA and `data-*` state hooks. For Tailwind or your own stylesheet:
283
+ * style entirely through `classNames`.
284
+ * @default false
285
+ */
286
+ unstyled?: boolean;
287
+ className?: string;
288
+ style?: React.CSSProperties;
289
+ }
290
+ /** Everything except the rows: shared by `AccordionConfig` and `AccordionMappedConfig`. */
291
+ interface AccordionOptions<Id extends string = string> extends AccordionBehaviourConfig<Id>, AccordionAppearanceConfig<Id>, SlotOverrides<AccordionSlot, AccordionItemState<Id>> {
292
+ /** Replace an entire row. The last resort before dropping to prop getters. */
293
+ renderItem?: (args: {
294
+ state: AccordionItemState<Id>;
295
+ /** Root props for the row. Spread them or state styling breaks. */
296
+ props: PropsBag;
297
+ /** The default header, if you only want to swap the body around it. */
298
+ header: React.ReactNode;
299
+ /** The default panel. */
300
+ panel: React.ReactNode;
301
+ }) => React.ReactNode;
302
+ }
303
+ /** Config with rows written as `AccordionItemConfig`. Ids are inferred from them. */
304
+ interface AccordionConfig<Id extends string = string> extends AccordionOptions<Id> {
305
+ /** The rows. The only required option. */
306
+ items: ReadonlyArray<AccordionItemConfig<Id>>;
307
+ /** Raw records need `AccordionMappedConfig` (a `fields` map). */
308
+ fields?: undefined;
309
+ }
310
+ /**
311
+ * Config for raw records (a CMS or database result): `fields` maps their keys onto rows, and
312
+ * every value must be a real key of the record, so a typo is a compile error.
313
+ *
314
+ * useAccordion({ items: faqRows, fields: { id: 'slug', title: 'question', content: 'answer' } });
315
+ */
316
+ interface AccordionMappedConfig<Row extends object = Record<string, unknown>> extends AccordionOptions<string> {
317
+ items: ReadonlyArray<Row>;
318
+ fields: AccordionFieldMap<Row>;
319
+ }
320
+ /** Either config shape. What the implementation and the generic APIs accept. */
321
+ type AnyAccordionConfig = AccordionConfig | AccordionMappedConfig;
322
+ /** Props accepted by `<Accordion />` itself, merged onto the root element. */
323
+ interface AccordionRootProps {
324
+ className?: string;
325
+ style?: React.CSSProperties;
326
+ children?: never;
327
+ [key: `data-${string}`]: unknown;
328
+ }
329
+ /** Addressing a single row from a sub-component. */
330
+ interface AccordionPartProps<Id extends string = string> {
331
+ id?: Id;
332
+ index?: number;
333
+ className?: string;
334
+ style?: React.CSSProperties;
335
+ children?: React.ReactNode;
336
+ }
337
+ interface AccordionState<Id extends string = string> {
338
+ items: AccordionItemState<Id>[];
339
+ value: Id[];
340
+ isOpen: (id: Id) => boolean;
341
+ open: (id: Id) => void;
342
+ close: (id: Id) => void;
343
+ toggle: (id: Id) => void;
344
+ openAll: () => void;
345
+ closeAll: () => void;
346
+ /** Move DOM focus to a header by index. */
347
+ focusItem: (index: number) => void;
348
+ /** Equivalent to `<Accordion />`, for callers who prefer a call. */
349
+ render: (props?: AccordionRootProps) => React.ReactElement;
350
+ getRootProps: (props?: PropsBag) => PropsBag;
351
+ getItemProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
352
+ getHeaderProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
353
+ getTriggerProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
354
+ getPanelProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
355
+ }
356
+ /** The sub-components for hand-laid-out accordions. */
357
+ interface AccordionParts<Id extends string = string> {
358
+ /** One complete row: header plus panel. */
359
+ Item: (props: AccordionPartProps<Id>) => React.ReactElement | null;
360
+ /** Just the heading and its button. `children` replaces the default label. */
361
+ Trigger: (props: AccordionPartProps<Id>) => React.ReactElement | null;
362
+ /** Just the collapsible region. `children` replaces the configured content. */
363
+ Panel: (props: AccordionPartProps<Id>) => React.ReactElement | null;
364
+ /** Just the open/closed glyph. */
365
+ Indicator: (props: AccordionPartProps<Id>) => React.ReactElement | null;
366
+ }
367
+ /**
368
+ * What `useAccordion()` returns: a component you render, that is also the
369
+ * controller, that also carries its parts.
370
+ */
371
+ type AccordionApi<Id extends string = string> = PinfoldComponent<AccordionState<Id> & AccordionParts<Id>, AccordionRootProps>;
372
+
373
+ export { usePinfoldComponent as y };
374
+ export type { AccordionConfig as A, PinfoldComponent as P, SlotOverrides as S, AccordionMappedConfig as a, AccordionApi as b, AnyAccordionConfig as c, AccordionRootProps as d, AccordionAppearanceConfig as e, AccordionBehaviourConfig as f, AccordionDimensions as g, AccordionGap as h, AccordionIndicator as i, AccordionItemConfig as j, AccordionItemId as k, AccordionItemState as l, AccordionNode as m, AccordionOptions as n, AccordionOverflow as o, AccordionPartProps as p, AccordionParts as q, AccordionRadius as r, AccordionSlot as s, AccordionState as t, AccordionValueDetails as u, AccordionVariant as v, SlotRenderer as w, Slottable as x };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});'use client';
3
+
4
+ // src/accordion/validate.ts
5
+ var _core = require('@pinfold/core');
6
+ var _meta = require('@pinfold/core/meta');
7
+ function validateAccordion(input, config) {
8
+ var _a, _b, _c, _d;
9
+ const shape = input.fields ? { ...input, items: [] } : input;
10
+ for (const problem of _meta.validateConfig.call(void 0, _meta.accordionMeta, shape)) _core.warnOnce.call(void 0, true, problem.message);
11
+ const { items, mode = "single", value, defaultValue, collapsible = true } = config;
12
+ if (!Array.isArray(items)) return;
13
+ const ids = _core.mapAccordionFields.call(void 0, items, config.fields).map((item, i) => _core.accordionRowId.call(void 0, item, i));
14
+ const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
15
+ _core.warnOnce.call(void 0,
16
+ duplicates.length > 0,
17
+ `Accordion: duplicate item id ${JSON.stringify(duplicates[0])}. Ids must be unique \u2014 panels and triggers are wired together by id.`
18
+ );
19
+ _core.warnOnce.call(void 0,
20
+ mode === "single" && ((_a = value == null ? void 0 : value.length) != null ? _a : 0) > 1,
21
+ 'Accordion: `mode: "single"` was given more than one open id in `value`. Only the first will be honoured.'
22
+ );
23
+ _core.warnOnce.call(void 0,
24
+ mode === "single" && !collapsible && ((_b = value != null ? value : defaultValue) != null ? _b : []).length === 0,
25
+ "Accordion: `collapsible: false` with nothing open means the user can never open a row by keyboard from a closed state. Provide a `defaultValue`."
26
+ );
27
+ _core.warnOnce.call(void 0,
28
+ value !== void 0 && config.onValueChange === void 0,
29
+ "Accordion: `value` was passed without `onValueChange`, so the accordion is frozen. Use `defaultValue` for uncontrolled behaviour."
30
+ );
31
+ const inactive = _core.inactiveAccordionOptions.call(void 0, { ...config, gap: (_d = (_c = config.dimensions) == null ? void 0 : _c.gap) != null ? _d : config.gap });
32
+ for (const key in inactive) {
33
+ _core.warnOnce.call(void 0, key in input && input[key] !== void 0, `Accordion: \`${key}\` has no visible effect here. ${inactive[key]}`);
34
+ }
35
+ _core.warnOnce.call(void 0,
36
+ config.unmountClosed !== void 0,
37
+ 'Accordion: `unmountClosed` is deprecated. Use `mountPanels: "open"`.'
38
+ );
39
+ }
40
+
41
+
42
+ exports.validateAccordion = validateAccordion;
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ // src/accordion/validate.ts
4
+ import { accordionRowId, inactiveAccordionOptions, mapAccordionFields, warnOnce } from "@pinfold/core";
5
+ import { accordionMeta, validateConfig } from "@pinfold/core/meta";
6
+ function validateAccordion(input, config) {
7
+ var _a, _b, _c, _d;
8
+ const shape = input.fields ? { ...input, items: [] } : input;
9
+ for (const problem of validateConfig(accordionMeta, shape)) warnOnce(true, problem.message);
10
+ const { items, mode = "single", value, defaultValue, collapsible = true } = config;
11
+ if (!Array.isArray(items)) return;
12
+ const ids = mapAccordionFields(items, config.fields).map((item, i) => accordionRowId(item, i));
13
+ const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
14
+ warnOnce(
15
+ duplicates.length > 0,
16
+ `Accordion: duplicate item id ${JSON.stringify(duplicates[0])}. Ids must be unique \u2014 panels and triggers are wired together by id.`
17
+ );
18
+ warnOnce(
19
+ mode === "single" && ((_a = value == null ? void 0 : value.length) != null ? _a : 0) > 1,
20
+ 'Accordion: `mode: "single"` was given more than one open id in `value`. Only the first will be honoured.'
21
+ );
22
+ warnOnce(
23
+ mode === "single" && !collapsible && ((_b = value != null ? value : defaultValue) != null ? _b : []).length === 0,
24
+ "Accordion: `collapsible: false` with nothing open means the user can never open a row by keyboard from a closed state. Provide a `defaultValue`."
25
+ );
26
+ warnOnce(
27
+ value !== void 0 && config.onValueChange === void 0,
28
+ "Accordion: `value` was passed without `onValueChange`, so the accordion is frozen. Use `defaultValue` for uncontrolled behaviour."
29
+ );
30
+ const inactive = inactiveAccordionOptions({ ...config, gap: (_d = (_c = config.dimensions) == null ? void 0 : _c.gap) != null ? _d : config.gap });
31
+ for (const key in inactive) {
32
+ warnOnce(key in input && input[key] !== void 0, `Accordion: \`${key}\` has no visible effect here. ${inactive[key]}`);
33
+ }
34
+ warnOnce(
35
+ config.unmountClosed !== void 0,
36
+ 'Accordion: `unmountClosed` is deprecated. Use `mountPanels: "open"`.'
37
+ );
38
+ }
39
+ export {
40
+ validateAccordion
41
+ };
package/llms-full.txt ADDED
@@ -0,0 +1,140 @@
1
+ # Pinfold
2
+
3
+ > Hooks-first, config-driven, accessible React UI (v0.2.0). One config object in, an accessible, themed, responsive component out. React 18+, Server Components, plain JS or TypeScript.
4
+
5
+ Rules for generating Pinfold code:
6
+ - Data goes in `items` (or raw records plus `fields`); everything else is optional with good defaults.
7
+ - Client: `const Faq = useAccordion({ items }); return <Faq />`. Server Component: `const Faq = defineAccordion({ items }); return <Faq />`. Same import path.
8
+ - Styles: `import '@pinfold/css/core.css'` once, then `@pinfold/css/<name>.css` per component, in the app entry. Never import CSS from library code.
9
+ - Global defaults: `defineConfig({ theme, radius, stylePack, defaults: { <name>: {...} } })` passed to `<PinfoldProvider config>`.
10
+ - Customise in this order: config, then `classNames`/`styles`/`slotProps`, then `render` per slot, then parts (`<Faq.Panel id>`), then prop getters.
11
+ - Never pass functions from a Server Component; plain data and server-rendered JSX only.
12
+
13
+ ## Install
14
+
15
+ - `npx pinfold init` (or `npm i @pinfold/react @pinfold/css`)
16
+ - shadcn: `npx shadcn add @pinfold/<name>` with `"registries": { "@pinfold": "https://pinfold.indranildev.in/r/{name}.json" }` in components.json
17
+
18
+ # Accordion
19
+
20
+ A vertically stacked set of headings that each reveal a section of content. Config in, accessible accordion out.
21
+
22
+ Import: `@pinfold/react/accordion` · Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
23
+
24
+ ## Client hook
25
+
26
+ ```tsx
27
+ import { useAccordion } from '@pinfold/react/accordion';
28
+
29
+ export function Faq() {
30
+ const Faq = useAccordion({ items: [{ title: 'What is Pinfold?', content: 'Config in, accessible UI out.' }] });
31
+ return <Faq />;
32
+ }
33
+ ```
34
+
35
+ ## Server Component
36
+
37
+ ```tsx
38
+ import { defineAccordion } from '@pinfold/react/accordion';
39
+
40
+ export default async function Page() {
41
+ const Faq = defineAccordion({ items: await getFaq(), fields: { id: 'slug', title: 'question', content: 'answer' } });
42
+ return <Faq />;
43
+ }
44
+ ```
45
+
46
+ ## Options
47
+
48
+ Only `items` is required. Every other option has a default; global defaults come from `defineConfig({ defaults: { accordion: {...} } })`.
49
+
50
+ | Option | Type | Default | Description |
51
+ |---|---|---|---|
52
+ | `items` (required) | array | | The rows. The only required option. |
53
+ | `fields` | object | | Map raw records onto rows: `{ id: 'slug', title: 'question', content: 'answer' }`. |
54
+ | `mode` | "single" \| "multiple" | `"single"` | `single` closes the other rows when one opens. |
55
+ | `collapsible` | boolean | `true` | Lets the user close the last open row. `false` keeps at least one row open, in either mode. |
56
+ | `value` | string[] | | Controlled open ids. Pair with `onValueChange`. |
57
+ | `defaultValue` | string[] | | Initially open ids (uncontrolled). |
58
+ | `disabled` | boolean | `false` | Disables every row. |
59
+ | `disabledIds` | string[] | | Disables these rows by id. |
60
+ | `orientation` | "vertical" \| "horizontal" | `"vertical"` | Arrow-key axis between headers. |
61
+ | `dir` | "ltr" \| "rtl" | | Text direction. Defaults to the provider. |
62
+ | `loop` | boolean | `true` | Focus wraps around at the ends. |
63
+ | `id` | string | | Id prefix. Defaults to `useId()`, which is SSR-safe. |
64
+ | `hiddenUntilFound` | boolean | `true` | Closed panels use `hidden="until-found"`, so the browser’s find-in-page opens them. |
65
+ | `mountPanels` | "always" \| "open" \| "first-open" | `"always"` | When panel content is in the DOM. `always` keeps it searchable and indexable. |
66
+ | `unmountClosed` (deprecated) | boolean | | Deprecated alias of `mountPanels: "open"`. |
67
+ | `syncWithHash` | boolean | `false` | A `#id` in the URL opens that row and scrolls to it; opening a row updates the hash. |
68
+ | `lazy` | "auto" \| "eager" \| "visible" \| "idle" | `"auto"` | When the code loads, for the `/lazy` entry. The accordion is light, so `auto` is eager. |
69
+ | `variant` | "solid" \| "outline" \| "separated" \| "filled" \| "ghost" | `"solid"` | Visual style. |
70
+ | `size` | "sm" \| "md" \| "lg" | `"md"` | Size preset. |
71
+ | `tone` | "primary" \| "neutral" \| "success" \| "warning" \| "danger" \| "info" | `"primary"` | Accent colour role. |
72
+ | `radius` | "none" \| "sm" \| "md" \| "lg" \| "full" \| length | auto: the theme radius (provider `radius`, default md) | Corner radius of the container that holds the rows: a preset, or a length (`20` is px, `'1.25rem'` passes through). Default: the theme radius. |
73
+ | `itemRadius` | "none" \| "sm" \| "md" \| "lg" \| "full" \| length | auto: follows `radius` | Corner radius of each row, same values as `radius`. Default: `radius` for separated, filled and ghost rows; square rows inside a solid or outline card. |
74
+ | `gap` | "none" \| "sm" \| "md" \| "lg" \| length | auto: none; sm for separated and filled | Space between rows: a preset, or a length (`12` is px). Default: none for solid, outline and ghost; `'sm'` for separated and filled. |
75
+ | `icon` | node | | Leading visual for every row: a node, or `(state) => node`. A row’s own `icon` wins. |
76
+ | `endIcon` | node | | Trailing visual for every row, just before the indicator. A row’s own `endIcon` wins. |
77
+ | `indicator` | "chevron" \| "plus" \| "caret" \| "arrow" \| "none" \| node | `"chevron"` | A built-in glyph, your own node, or `(state) => node`. |
78
+ | `indicatorPosition` | "start" \| "end" | `"end"` | Which edge the indicator sits on. |
79
+ | `animateIndicator` | boolean | `true` | Rotate (or morph) the indicator when a row opens. |
80
+ | `headingLevel` | 1 \| 2 \| 3 \| 4 \| 5 \| 6 | `3` | Heading level around each trigger. |
81
+ | `animate` | boolean | `true` | Animate open and close. `prefers-reduced-motion` always wins. |
82
+ | `dividers` | boolean | `true` | Lines between rows (solid, outline and ghost). |
83
+ | `overflow` | object | | Text overflow per slot. Default: title wraps, description clamps to 2 lines. |
84
+ | `dimensions` | object | | Explicit sizes. Numbers are px. Each key overrides the size preset. |
85
+ | `unstyled` | boolean | `false` | Drops every Pinfold class; ARIA and `data-*` stay. For Tailwind or your own CSS. |
86
+ | `className` | string | | Class on the root. |
87
+ | `style` | object | | Inline style on the root. |
88
+ | `classNames` | object | | Classes per slot. |
89
+ | `styles` | object | | Inline styles per slot. |
90
+ | `slotProps` | object | | Any DOM props per slot. Handlers chain. |
91
+ | `render` | object | | Replace a slot element: `({ props, state, children }) => node`. |
92
+ | `renderItem` | function | | Replace a whole row: `({ state, props, header, panel }) => node`. |
93
+ | `onValueChange` | function | | `(ids, details)`. `details: { id, open, reason: 'click' \| 'keyboard' \| 'api' \| 'find-in-page' \| 'hash', event }`. |
94
+ | `onBeforeChange` | function | | `(details) => boolean \| void`. Return `false` to block the change. |
95
+ | `onItemToggle` | function | | `(id, open)`, after `onValueChange`. |
96
+
97
+ ## Row options (`items[]`)
98
+
99
+ | Option | Type | Default | Description |
100
+ |---|---|---|---|
101
+ | `id` | string | | Stable key. Falls back to the array index. |
102
+ | `title` (required) | node | | The row heading. A node or `(state) => node`. |
103
+ | `content` | node | | The panel body. |
104
+ | `description` | node | | A secondary line under the title. |
105
+ | `icon` | node | | A leading visual: a node, or a string (an emoji, or an icon-font ligature with `classNames.icon`). Overrides the accordion-wide `icon`. |
106
+ | `badge` | node | | A trailing chip before the indicator. |
107
+ | `endIcon` | node | | A trailing visual just before the indicator. Overrides the accordion-wide `endIcon`. |
108
+ | `indicator` | "chevron" \| "plus" \| "caret" \| "arrow" \| "none" \| node | | Overrides the accordion-wide indicator for this row. |
109
+ | `disabled` | boolean | | Blocks toggling. The row stays focusable. |
110
+ | `defaultOpen` | boolean | | Open on first render (uncontrolled only). |
111
+ | `classNames` | object | | Per-row classes, by slot. |
112
+ | `styles` | object | | Per-row inline styles, by slot. |
113
+
114
+ ## Slots
115
+
116
+ `root`, `item`, `header`, `trigger`, `icon`, `label`, `title`, `description`, `badge`, `endIcon`, `indicator`, `panel`, `content`
117
+
118
+ ## Keyboard
119
+
120
+ - **Enter / Space**: Toggle the focused row.
121
+ - **ArrowDown / ArrowUp**: Next / previous header (ArrowRight / ArrowLeft when horizontal; flipped in RTL). Wraps when `loop`.
122
+ - **Home / End**: First / last header.
123
+ - **Tab**: Leaves the header list. Closed panels are not in the tab order.
124
+
125
+ ## Accessibility
126
+
127
+ - Each trigger is a `<button>` inside a heading with `aria-level`.
128
+ - Triggers carry `aria-expanded` and `aria-controls` pointing at their panel.
129
+ - Each panel is `role="region"` with `aria-labelledby` pointing at its trigger.
130
+ - Disabled rows use `aria-disabled` and stay focusable.
131
+ - Closed panels leave the accessibility tree and the tab order.
132
+
133
+ ## Data attributes
134
+
135
+ - `data-state` on item, header, trigger, panel: "open" | "closed"
136
+ - `data-disabled` on item, trigger: present when disabled
137
+ - `data-index` on item: row index
138
+ - `data-orientation` on root: "vertical" | "horizontal"
139
+ - `data-mode` on root: "single" | "multiple"
140
+ - `data-transition` on panel: "opening" | "closing", while animating
package/llms.txt ADDED
@@ -0,0 +1,26 @@
1
+ # Pinfold
2
+
3
+ > Hooks-first, config-driven, accessible React UI (v0.2.0). One config object in, an accessible, themed, responsive component out. React 18+, Server Components, plain JS or TypeScript.
4
+
5
+ Rules for generating Pinfold code:
6
+ - Data goes in `items` (or raw records plus `fields`); everything else is optional with good defaults.
7
+ - Client: `const Faq = useAccordion({ items }); return <Faq />`. Server Component: `const Faq = defineAccordion({ items }); return <Faq />`. Same import path.
8
+ - Styles: `import '@pinfold/css/core.css'` once, then `@pinfold/css/<name>.css` per component, in the app entry. Never import CSS from library code.
9
+ - Global defaults: `defineConfig({ theme, radius, stylePack, defaults: { <name>: {...} } })` passed to `<PinfoldProvider config>`.
10
+ - Customise in this order: config, then `classNames`/`styles`/`slotProps`, then `render` per slot, then parts (`<Faq.Panel id>`), then prop getters.
11
+ - Never pass functions from a Server Component; plain data and server-rendered JSX only.
12
+
13
+ ## Install
14
+
15
+ - `npx pinfold init` (or `npm i @pinfold/react @pinfold/css`)
16
+ - shadcn: `npx shadcn add @pinfold/<name>` with `"registries": { "@pinfold": "https://pinfold.indranildev.in/r/{name}.json" }` in components.json
17
+
18
+ ## Components
19
+
20
+ - [Accordion](https://pinfold.indranildev.in/docs/accordion): A vertically stacked set of headings that each reveal a section of content. Config in, accessible accordion out. Import `@pinfold/react/accordion`. Schema: `schema/accordion.json`.
21
+
22
+ ## Reference
23
+
24
+ - [Full reference](https://pinfold.indranildev.in/llms-full.txt): every option of every component (also `llms-full.txt` in this package)
25
+ - [Theming](https://pinfold.indranildev.in/docs/theming): `npx pinfold theme`, style packs (glass, material, noise, brutalist), DTCG tokens
26
+ - Local MCP server: `npx pinfold mcp` (tools: list_components, get_component, get_schema, validate_config, get_install_steps)