@pinfold/react 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.
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { Slottable as Slottable$1, PropsBag, AccordionFieldMap, AccordionChangeDetails, PinfoldOrientation, PinfoldDir, AccordionMountPanels, PinfoldLazy, PinfoldSize, PinfoldTone, PinfoldRadius, Length } from '@pinfold/core';
2
+ import { Slottable as Slottable$1, PropsBag, AccordionChangeDetails, PinfoldOrientation, PinfoldDir, AccordionMountPanels, PinfoldLazy, PinfoldSize, PinfoldTone, PinfoldRadius, Length, AccordionFieldMap } from '@pinfold/core';
3
3
 
4
4
  /**
5
5
  * A component that is also its own controller.
@@ -34,6 +34,12 @@ declare function usePinfoldComponent<Api extends object, Props extends object, P
34
34
  [K in keyof Parts]: Part;
35
35
  } = {}>(displayName: string, api: Api, render: (props: Props) => React.ReactElement | null, buildParts?: () => Parts): PinfoldComponent<Api & Parts, Props>;
36
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];
37
43
  /** A config field that is a node, or a function of live state returning one. */
38
44
  type Slottable<State, T = React.ReactNode> = Slottable$1<State, T>;
39
45
  /**
@@ -59,35 +65,41 @@ type SlotRenderer<State = unknown> = (args: {
59
65
  children: React.ReactNode;
60
66
  }) => React.ReactNode;
61
67
 
62
- type AccordionSlot = 'root' | 'item' | 'header' | 'trigger' | 'icon' | 'label' | 'title' | 'description' | 'badge' | 'indicator' | 'panel' | 'content';
68
+ type AccordionSlot = 'root' | 'item' | 'header' | 'trigger' | 'icon' | 'label' | 'title' | 'description' | 'badge' | 'endIcon' | 'indicator' | 'panel' | 'content';
63
69
  type AccordionVariant = 'solid' | 'outline' | 'separated' | 'filled' | 'ghost';
64
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 & {});
65
75
  /** Runtime state of a single row. Passed to every function-valued config field. */
66
- interface AccordionItemState {
67
- id: string;
76
+ interface AccordionItemState<Id extends string = string> {
77
+ id: Id;
68
78
  index: number;
69
79
  open: boolean;
70
80
  disabled: boolean;
71
- config: AccordionItemConfig;
81
+ config: AccordionItemConfig<Id>;
72
82
  triggerId: string;
73
83
  panelId: string;
74
84
  }
75
85
  /** Anything renderable in a row: a node, or a function of the row's live state. */
76
- type AccordionNode = Slottable<AccordionItemState>;
86
+ type AccordionNode<Id extends string = string> = Slottable<AccordionItemState<Id>>;
77
87
  /** One row of the accordion. This is the JSON a consumer — or a model — writes. */
78
- interface AccordionItemConfig {
79
- /** Stable key. Falls back to the array index when omitted. */
80
- id?: string;
81
- title: AccordionNode;
82
- content?: AccordionNode;
88
+ interface AccordionItemConfig<Id extends string = string> {
89
+ /** Stable key. Falls back to the array index when omitted. Give every row one to get typed ids. */
90
+ id?: Id;
91
+ title: AccordionNode<Id>;
92
+ content?: AccordionNode<Id>;
83
93
  /** Secondary line under the title. */
84
- description?: AccordionNode;
85
- /** Leading visual — icon, avatar, anything. */
86
- icon?: AccordionNode;
94
+ description?: AccordionNode<Id>;
95
+ /** Leading visual: a node, or a string (an emoji, or an icon-font name with `classNames.icon`). Overrides the accordion-wide `icon`. */
96
+ icon?: AccordionNode<Id>;
87
97
  /** Trailing chip rendered before the indicator. */
88
- badge?: AccordionNode;
98
+ badge?: AccordionNode<Id>;
99
+ /** Trailing visual just before the indicator. Overrides the accordion-wide `endIcon`. */
100
+ endIcon?: AccordionNode<Id>;
89
101
  /** Overrides the accordion-wide indicator for this row only. */
90
- indicator?: AccordionIndicator | AccordionNode;
102
+ indicator?: AccordionIndicator | AccordionNode<Id>;
91
103
  disabled?: boolean;
92
104
  /** Open on first render (uncontrolled only). */
93
105
  defaultOpen?: boolean;
@@ -99,53 +111,78 @@ interface AccordionItemConfig {
99
111
  type AccordionOverflow = 'wrap' | 'truncate' | {
100
112
  lines: number;
101
113
  };
102
- type AccordionValueDetails = AccordionChangeDetails<Event | React.SyntheticEvent>;
103
- interface AccordionBehaviourConfig {
114
+ type AccordionValueDetails<Id extends string = string> = Omit<AccordionChangeDetails<Event | React.SyntheticEvent>, 'id'> & {
115
+ /** The row that triggered the change. Absent for `openAll` / `closeAll`. */
116
+ id?: Id;
117
+ };
118
+ interface AccordionBehaviourConfig<Id extends string = string> {
104
119
  /**
105
- * The rows. The only required option. With `fields`, any record shape works:
106
- * `items: faqRows, fields: { id: 'slug', title: 'question', content: 'answer' }`.
120
+ * `single` closes siblings on open.
121
+ * @default 'single'
107
122
  */
108
- items: AccordionItemConfig[] | ReadonlyArray<Record<string, any>>;
109
- /** Maps raw records onto rows, so CMS or database results need no `.map()`. */
110
- fields?: AccordionFieldMap;
111
- /** `single` closes siblings on open. Default `single`. */
112
123
  mode?: 'single' | 'multiple';
113
- /** In `single` mode, allow closing the open item. Default true. */
124
+ /**
125
+ * Allow closing the last open row. `false` keeps at least one row open, in either mode.
126
+ * @default true
127
+ */
114
128
  collapsible?: boolean;
115
- /** Controlled open ids. */
116
- value?: string[];
117
- defaultValue?: string[];
129
+ /**
130
+ * Controlled open ids. Any `string[]` state works (`useState<string[]>`); for typed state use
131
+ * `useState<AccordionItemId<typeof items>[]>`.
132
+ */
133
+ value?: ReadonlyArray<string>;
134
+ /** Initially open ids (uncontrolled). */
135
+ defaultValue?: ReadonlyArray<NoInfer<Id>>;
118
136
  /** `details.reason`: 'click' | 'keyboard' | 'api' | 'find-in-page' | 'hash'. */
119
- onValueChange?: (value: string[], details: AccordionValueDetails) => void;
137
+ onValueChange?: (value: Id[], details: AccordionValueDetails<Id>) => void;
120
138
  /** Return `false` to block a change (e.g. unsaved edits in the open panel). */
121
- onBeforeChange?: (details: AccordionValueDetails) => boolean | void;
139
+ onBeforeChange?: (details: AccordionValueDetails<Id>) => boolean | void;
122
140
  /** Fires for one item, after `onValueChange`. */
123
- onItemToggle?: (id: string, open: boolean) => void;
124
- /** Arrow-key axis for moving between headers. Default `vertical`. */
141
+ onItemToggle?: (id: Id, open: boolean) => void;
142
+ /**
143
+ * Arrow-key axis for moving between headers.
144
+ * @default 'vertical'
145
+ */
125
146
  orientation?: PinfoldOrientation;
147
+ /** Text direction. Defaults to the provider. */
126
148
  dir?: PinfoldDir;
127
- /** Wrap focus at the ends. Default true. */
149
+ /**
150
+ * Wrap focus at the ends.
151
+ * @default true
152
+ */
128
153
  loop?: boolean;
129
154
  /** Prefix for generated ids — set it for deterministic SSR markup. */
130
155
  id?: string;
131
- /** Disables every row. Rows stay focusable. Default false. */
156
+ /**
157
+ * Disables every row. Rows stay focusable.
158
+ * @default false
159
+ */
132
160
  disabled?: boolean;
161
+ /** Disables these rows by id, for data you cannot annotate row by row. */
162
+ disabledIds?: ReadonlyArray<NoInfer<Id>>;
133
163
  /**
134
164
  * Closed panels use `hidden="until-found"`, so the browser's find-in-page
135
- * searches them and opens the match. Default true.
165
+ * searches them and opens the match.
166
+ * @default true
136
167
  */
137
168
  hiddenUntilFound?: boolean;
138
169
  /**
139
- * When panel content is in the DOM. `always` (default) keeps it searchable
140
- * and indexable; `open` mounts only open panels; `first-open` mounts on
141
- * first open and keeps it.
170
+ * When panel content is in the DOM. `always` keeps it searchable and indexable;
171
+ * `open` mounts only open panels; `first-open` mounts on first open and keeps it.
172
+ * @default 'always'
142
173
  */
143
174
  mountPanels?: AccordionMountPanels;
144
175
  /** @deprecated Use `mountPanels: 'open'`. */
145
176
  unmountClosed?: boolean;
146
- /** `#id` in the URL opens that row and scrolls to it; opening a row updates the hash. Default false. */
177
+ /**
178
+ * `#id` in the URL opens that row and scrolls to it; opening a row updates the hash.
179
+ * @default false
180
+ */
147
181
  syncWithHash?: boolean;
148
- /** Code loading for the `/lazy` entry. The accordion is light, so `auto` loads eagerly. */
182
+ /**
183
+ * Code loading for the `/lazy` entry. The accordion is light, so `auto` loads eagerly.
184
+ * @default 'auto'
185
+ */
149
186
  lazy?: PinfoldLazy;
150
187
  }
151
188
  /**
@@ -159,7 +196,7 @@ interface AccordionDimensions {
159
196
  maxWidth?: Length;
160
197
  height?: Length;
161
198
  maxHeight?: Length;
162
- /** Space between rows (outline / separated / filled variants). */
199
+ /** Space between rows. Overrides `gap`. */
163
200
  gap?: Length;
164
201
  /** Trigger padding. */
165
202
  paddingX?: Length;
@@ -175,27 +212,58 @@ interface AccordionDimensions {
175
212
  iconSize?: Length;
176
213
  indicatorSize?: Length;
177
214
  borderWidth?: Length;
178
- /** Corner radius as a length — overrides the `radius` token preset. */
215
+ /** Container corner radius as a length. Overrides `radius`. */
179
216
  radius?: Length;
217
+ /** Row corner radius as a length. Overrides `itemRadius`. */
218
+ itemRadius?: Length;
180
219
  /** Open/close transition duration, e.g. `'320ms'`. */
181
220
  duration?: Length;
182
221
  }
183
- interface AccordionAppearanceConfig {
222
+ interface AccordionAppearanceConfig<Id extends string = string> {
223
+ /** @default 'solid' */
184
224
  variant?: AccordionVariant;
225
+ /** @default 'md' */
185
226
  size?: PinfoldSize;
227
+ /** @default 'primary' */
186
228
  tone?: PinfoldTone;
187
- radius?: PinfoldRadius;
188
- /** Built-in glyph, your own node, or a function of the row's state. */
189
- indicator?: AccordionIndicator | AccordionNode;
190
- /** Which edge the indicator sits on. Default `end`. */
229
+ /** Corner radius of the container (the element that holds the rows). Unset: the theme radius. */
230
+ radius?: AccordionRadius;
231
+ /** Corner radius of each row. Unset: follows `radius` (square rows inside a solid or outline card). */
232
+ itemRadius?: AccordionRadius;
233
+ /** Space between rows. Unset: none for solid, outline and ghost; `'sm'` for separated and filled. */
234
+ gap?: AccordionGap;
235
+ /** Leading visual for every row: a node, or `(state) => node` (e.g. per row, or open vs closed). */
236
+ icon?: AccordionNode<Id>;
237
+ /** Trailing visual for every row, just before the indicator. Same shape as `icon`. */
238
+ endIcon?: AccordionNode<Id>;
239
+ /**
240
+ * Built-in glyph, your own node, or a function of the row's state.
241
+ * @default 'chevron'
242
+ */
243
+ indicator?: AccordionIndicator | AccordionNode<Id>;
244
+ /** @default 'end' */
191
245
  indicatorPosition?: 'start' | 'end';
192
- /** Heading level wrapping each trigger. Default 3. */
246
+ /**
247
+ * Rotate (or morph) the indicator when a row opens.
248
+ * @default true
249
+ */
250
+ animateIndicator?: boolean;
251
+ /**
252
+ * Heading level wrapping each trigger.
253
+ * @default 3
254
+ */
193
255
  headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
194
- /** Animate open/close. Default true; honours prefers-reduced-motion regardless. */
256
+ /**
257
+ * Animate open/close; `prefers-reduced-motion` always wins.
258
+ * @default true
259
+ */
195
260
  animate?: boolean;
196
- /** Full-width dividers between rows (solid/ghost variants). Default true. */
261
+ /**
262
+ * Lines between rows that touch.
263
+ * @default true
264
+ */
197
265
  dividers?: boolean;
198
- /** Text overflow per slot. Default: title `wrap`, description `{ lines: 2 }`. */
266
+ /** Text overflow per slot. Unset: title `wrap`, description `{ lines: 2 }`. */
199
267
  overflow?: {
200
268
  title?: AccordionOverflow;
201
269
  description?: AccordionOverflow;
@@ -206,15 +274,17 @@ interface AccordionAppearanceConfig {
206
274
  * Drop every Pinfold class so no bundled CSS applies, keeping the behaviour,
207
275
  * ARIA and `data-*` state hooks. For Tailwind or your own stylesheet:
208
276
  * style entirely through `classNames`.
277
+ * @default false
209
278
  */
210
279
  unstyled?: boolean;
211
280
  className?: string;
212
281
  style?: React.CSSProperties;
213
282
  }
214
- interface AccordionConfig extends AccordionBehaviourConfig, AccordionAppearanceConfig, SlotOverrides<AccordionSlot, AccordionItemState> {
283
+ /** Everything except the rows: shared by `AccordionConfig` and `AccordionMappedConfig`. */
284
+ interface AccordionOptions<Id extends string = string> extends AccordionBehaviourConfig<Id>, AccordionAppearanceConfig<Id>, SlotOverrides<AccordionSlot, AccordionItemState<Id>> {
215
285
  /** Replace an entire row. The last resort before dropping to prop getters. */
216
286
  renderItem?: (args: {
217
- state: AccordionItemState;
287
+ state: AccordionItemState<Id>;
218
288
  /** Root props for the row. Spread them or state styling breaks. */
219
289
  props: PropsBag;
220
290
  /** The default header, if you only want to swap the body around it. */
@@ -223,6 +293,25 @@ interface AccordionConfig extends AccordionBehaviourConfig, AccordionAppearanceC
223
293
  panel: React.ReactNode;
224
294
  }) => React.ReactNode;
225
295
  }
296
+ /** Config with rows written as `AccordionItemConfig`. Ids are inferred from them. */
297
+ interface AccordionConfig<Id extends string = string> extends AccordionOptions<Id> {
298
+ /** The rows. The only required option. */
299
+ items: ReadonlyArray<AccordionItemConfig<Id>>;
300
+ /** Raw records need `AccordionMappedConfig` (a `fields` map). */
301
+ fields?: undefined;
302
+ }
303
+ /**
304
+ * Config for raw records (a CMS or database result): `fields` maps their keys onto rows, and
305
+ * every value must be a real key of the record, so a typo is a compile error.
306
+ *
307
+ * useAccordion({ items: faqRows, fields: { id: 'slug', title: 'question', content: 'answer' } });
308
+ */
309
+ interface AccordionMappedConfig<Row extends object = Record<string, unknown>> extends AccordionOptions<string> {
310
+ items: ReadonlyArray<Row>;
311
+ fields: AccordionFieldMap<Row>;
312
+ }
313
+ /** Either config shape. What the implementation and the generic APIs accept. */
314
+ type AnyAccordionConfig = AccordionConfig | AccordionMappedConfig;
226
315
  /** Props accepted by `<Accordion />` itself, merged onto the root element. */
227
316
  interface AccordionRootProps {
228
317
  className?: string;
@@ -231,20 +320,20 @@ interface AccordionRootProps {
231
320
  [key: `data-${string}`]: unknown;
232
321
  }
233
322
  /** Addressing a single row from a sub-component. */
234
- interface AccordionPartProps {
235
- id?: string;
323
+ interface AccordionPartProps<Id extends string = string> {
324
+ id?: Id;
236
325
  index?: number;
237
326
  className?: string;
238
327
  style?: React.CSSProperties;
239
328
  children?: React.ReactNode;
240
329
  }
241
- interface AccordionState {
242
- items: AccordionItemState[];
243
- value: string[];
244
- isOpen: (id: string) => boolean;
245
- open: (id: string) => void;
246
- close: (id: string) => void;
247
- toggle: (id: string) => void;
330
+ interface AccordionState<Id extends string = string> {
331
+ items: AccordionItemState<Id>[];
332
+ value: Id[];
333
+ isOpen: (id: Id) => boolean;
334
+ open: (id: Id) => void;
335
+ close: (id: Id) => void;
336
+ toggle: (id: Id) => void;
248
337
  openAll: () => void;
249
338
  closeAll: () => void;
250
339
  /** Move DOM focus to a header by index. */
@@ -252,27 +341,27 @@ interface AccordionState {
252
341
  /** Equivalent to `<Accordion />`, for callers who prefer a call. */
253
342
  render: (props?: AccordionRootProps) => React.ReactElement;
254
343
  getRootProps: (props?: PropsBag) => PropsBag;
255
- getItemProps: (item: AccordionItemState, props?: PropsBag) => PropsBag;
256
- getHeaderProps: (item: AccordionItemState, props?: PropsBag) => PropsBag;
257
- getTriggerProps: (item: AccordionItemState, props?: PropsBag) => PropsBag;
258
- getPanelProps: (item: AccordionItemState, props?: PropsBag) => PropsBag;
344
+ getItemProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
345
+ getHeaderProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
346
+ getTriggerProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
347
+ getPanelProps: (item: AccordionItemState<Id>, props?: PropsBag) => PropsBag;
259
348
  }
260
349
  /** The sub-components for hand-laid-out accordions. */
261
- interface AccordionParts {
350
+ interface AccordionParts<Id extends string = string> {
262
351
  /** One complete row: header plus panel. */
263
- Item: (props: AccordionPartProps) => React.ReactElement | null;
352
+ Item: (props: AccordionPartProps<Id>) => React.ReactElement | null;
264
353
  /** Just the heading and its button. `children` replaces the default label. */
265
- Trigger: (props: AccordionPartProps) => React.ReactElement | null;
354
+ Trigger: (props: AccordionPartProps<Id>) => React.ReactElement | null;
266
355
  /** Just the collapsible region. `children` replaces the configured content. */
267
- Panel: (props: AccordionPartProps) => React.ReactElement | null;
356
+ Panel: (props: AccordionPartProps<Id>) => React.ReactElement | null;
268
357
  /** Just the open/closed glyph. */
269
- Indicator: (props: AccordionPartProps) => React.ReactElement | null;
358
+ Indicator: (props: AccordionPartProps<Id>) => React.ReactElement | null;
270
359
  }
271
360
  /**
272
361
  * What `useAccordion()` returns: a component you render, that is also the
273
362
  * controller, that also carries its parts.
274
363
  */
275
- type AccordionApi = PinfoldComponent<AccordionState & AccordionParts, AccordionRootProps>;
364
+ type AccordionApi<Id extends string = string> = PinfoldComponent<AccordionState<Id> & AccordionParts<Id>, AccordionRootProps>;
276
365
 
277
366
  export { usePinfoldComponent as u };
278
- export type { AccordionConfig as A, PinfoldComponent as P, SlotOverrides as S, AccordionApi as a, SlotRenderer as b, Slottable as c };
367
+ export type { AccordionConfig as A, PinfoldComponent as P, SlotOverrides as S, AccordionMappedConfig as a, AccordionApi as b, AnyAccordionConfig as c, AccordionOptions as d, SlotRenderer as e, Slottable as f };