pinfold 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.
@@ -0,0 +1,11 @@
1
+ import { createLazyComponent } from '../lazy';
2
+ import type { __PASCAL__Config } from './types';
3
+
4
+ /** Code-split entry point: nothing loads until the component first renders. */
5
+ export const __PASCAL__ = createLazyComponent<__PASCAL__Config>(
6
+ () => import('./index').then((m) => ({ default: m.__PASCAL__ })),
7
+ { displayName: 'Lazy__PASCAL__' },
8
+ );
9
+
10
+ export type { __PASCAL__Config } from './types';
11
+ export default __PASCAL__;
@@ -0,0 +1,142 @@
1
+ import type * as React from 'react';
2
+ import type {
3
+ PinfoldDir,
4
+ PinfoldOrientation,
5
+ PinfoldRadius,
6
+ PinfoldSize,
7
+ PinfoldTone,
8
+ Length,
9
+ PropsBag,
10
+ } from '@pinfold/core';
11
+ import type { PinfoldComponent } from '../component';
12
+ import type { SlotOverrides, Slottable } from '../types';
13
+
14
+ /** Name every visual part. Consumers target these by name; do not rename later. */
15
+ export type __PASCAL__Slot = 'root' | 'item' | 'label';
16
+
17
+ export type __PASCAL__Variant = 'solid' | 'outline' | 'ghost';
18
+
19
+ /** Runtime state of one row. Passed to every function-valued config field. */
20
+ export interface __PASCAL__ItemState {
21
+ id: string;
22
+ index: number;
23
+ selected: boolean;
24
+ disabled: boolean;
25
+ config: __PASCAL__ItemConfig;
26
+ }
27
+
28
+ /** Anything renderable: a node, or a function of the row's live state. */
29
+ export type __PASCAL__Node = Slottable<__PASCAL__ItemState>;
30
+
31
+ /** One row of data. This is the JSON a consumer — or a model — writes. */
32
+ export interface __PASCAL__ItemConfig {
33
+ id?: string;
34
+ label: __PASCAL__Node;
35
+ disabled?: boolean;
36
+ /** Per-row class and style overrides, same slot names as the root config. */
37
+ classNames?: Partial<Record<__PASCAL__Slot, string>>;
38
+ styles?: Partial<Record<__PASCAL__Slot, React.CSSProperties>>;
39
+ }
40
+
41
+ export interface __PASCAL__BehaviourConfig {
42
+ items: __PASCAL__ItemConfig[];
43
+ value?: string | null;
44
+ defaultValue?: string | null;
45
+ onValueChange?: (value: string | null) => void;
46
+ orientation?: PinfoldOrientation;
47
+ dir?: PinfoldDir;
48
+ loop?: boolean;
49
+ /** Id prefix — set it for deterministic SSR markup. */
50
+ id?: string;
51
+ }
52
+
53
+ /** Numbers are px, strings pass through. Overrides the `size` preset per key. */
54
+ export interface __PASCAL__Dimensions {
55
+ width?: Length;
56
+ maxWidth?: Length;
57
+ gap?: Length;
58
+ paddingX?: Length;
59
+ paddingY?: Length;
60
+ fontSize?: Length;
61
+ radius?: Length;
62
+ borderWidth?: Length;
63
+ duration?: Length;
64
+ }
65
+
66
+ export interface __PASCAL__AppearanceConfig {
67
+ variant?: __PASCAL__Variant;
68
+ size?: PinfoldSize;
69
+ tone?: PinfoldTone;
70
+ radius?: PinfoldRadius;
71
+ animate?: boolean;
72
+ dimensions?: __PASCAL__Dimensions;
73
+ /** Drop every Pinfold class so you can style purely with Tailwind or your own CSS. */
74
+ unstyled?: boolean;
75
+ className?: string;
76
+ style?: React.CSSProperties;
77
+ }
78
+
79
+ export interface __PASCAL__Config
80
+ extends __PASCAL__BehaviourConfig,
81
+ __PASCAL__AppearanceConfig,
82
+ SlotOverrides<__PASCAL__Slot, __PASCAL__ItemState> {
83
+ /** Replace an entire row. The last resort before dropping to prop getters. */
84
+ renderItem?: (args: {
85
+ state: __PASCAL__ItemState;
86
+ props: PropsBag;
87
+ children: React.ReactNode;
88
+ }) => React.ReactNode;
89
+ }
90
+
91
+ /** Appearance after provider defaults are folded in. */
92
+ export interface Resolved__PASCAL__Appearance {
93
+ variant: __PASCAL__Variant;
94
+ size: PinfoldSize;
95
+ tone: PinfoldTone;
96
+ radius: PinfoldRadius;
97
+ animate: boolean;
98
+ unstyled: boolean;
99
+ }
100
+
101
+ /** Props accepted by the component itself, merged onto the root element. */
102
+ export interface __PASCAL__RootProps {
103
+ className?: string;
104
+ style?: React.CSSProperties;
105
+ children?: never;
106
+ [key: `data-${string}`]: unknown;
107
+ }
108
+
109
+ /** Addressing a single row from a sub-component. */
110
+ export interface __PASCAL__PartProps {
111
+ id?: string;
112
+ index?: number;
113
+ className?: string;
114
+ style?: React.CSSProperties;
115
+ children?: React.ReactNode;
116
+ }
117
+
118
+ export interface __PASCAL__State {
119
+ items: __PASCAL__ItemState[];
120
+ value: string | null;
121
+ select: (id: string) => void;
122
+ clear: () => void;
123
+ focusItem: (index: number) => void;
124
+ /** Equivalent to rendering the component, for callers who prefer a call. */
125
+ render: (props?: __PASCAL__RootProps) => React.ReactElement;
126
+ getRootProps: (props?: PropsBag) => PropsBag;
127
+ getItemProps: (item: __PASCAL__ItemState, props?: PropsBag) => PropsBag;
128
+ }
129
+
130
+ /** Sub-components for hand-laid-out layouts the config cannot express. */
131
+ export interface __PASCAL__Parts {
132
+ Item: (props: __PASCAL__PartProps) => React.ReactElement | null;
133
+ }
134
+
135
+ /**
136
+ * What `use__PASCAL__()` returns: a component you render, that is also the
137
+ * controller, that also carries its parts.
138
+ */
139
+ export type __PASCAL__Api = PinfoldComponent<
140
+ __PASCAL__State & __PASCAL__Parts,
141
+ __PASCAL__RootProps
142
+ >;
@@ -0,0 +1,181 @@
1
+ import * as React from 'react';
2
+ import { ariaAttr, dataAttr, mergeProps, nextIndexFromKey, warnOnce, __DEV__ } from '@pinfold/core';
3
+ import { useControllableState, useEvent, useId } from '../hooks';
4
+ import { usePinfold } from '../provider';
5
+ import { usePinfoldComponent } from '../component';
6
+ import { renderItem, renderRoot, type ViewContext } from './__PASCAL__View';
7
+ import type {
8
+ Resolved__PASCAL__Appearance,
9
+ __PASCAL__Api,
10
+ __PASCAL__Config,
11
+ __PASCAL__ItemState,
12
+ __PASCAL__Parts,
13
+ __PASCAL__PartProps,
14
+ __PASCAL__RootProps,
15
+ __PASCAL__State,
16
+ } from './types';
17
+
18
+ /**
19
+ * TODO: implement the matching WAI-ARIA Authoring Practices pattern.
20
+ * https://www.w3.org/WAI/ARIA/apg/patterns/
21
+ *
22
+ * What ships here is the Pinfold skeleton: controllable state, roving focus, prop
23
+ * getters, dev diagnostics, and a hook that returns a component:
24
+ *
25
+ * const Thing = use__PASCAL__({ items });
26
+ * return <Thing className="mt-4" />;
27
+ *
28
+ * Replace the roles and keyboard map with the ones your pattern requires, then
29
+ * delete this comment.
30
+ */
31
+ export function use__PASCAL__(config: __PASCAL__Config): __PASCAL__Api {
32
+ const ctx = usePinfold();
33
+ const {
34
+ items: itemConfigs,
35
+ value: controlled,
36
+ defaultValue = null,
37
+ onValueChange,
38
+ orientation = 'horizontal',
39
+ dir = ctx.dir,
40
+ loop = true,
41
+ id: idProp,
42
+ } = config;
43
+
44
+ const appearance: Resolved__PASCAL__Appearance = {
45
+ variant: config.variant ?? 'solid',
46
+ size: config.size ?? ctx.size,
47
+ tone: config.tone ?? ctx.tone,
48
+ radius: config.radius ?? ctx.radius,
49
+ animate: config.animate ?? !ctx.disableAnimation,
50
+ unstyled: config.unstyled ?? false,
51
+ };
52
+
53
+ const baseId = useId(idProp);
54
+ const refs = React.useRef<Array<HTMLElement | null>>([]);
55
+
56
+ if (__DEV__) validate(config);
57
+
58
+ const [value, setValue] = useControllableState<string | null>({
59
+ value: controlled,
60
+ defaultValue,
61
+ onChange: onValueChange,
62
+ });
63
+
64
+ const items = React.useMemo<__PASCAL__ItemState[]>(
65
+ () =>
66
+ itemConfigs.map((item, index) => {
67
+ const id = item.id ?? String(index);
68
+ return { id, index, selected: value === id, disabled: !!item.disabled, config: item };
69
+ }),
70
+ [itemConfigs, value],
71
+ );
72
+
73
+ const select = useEvent((id: string) => {
74
+ const item = items.find((i) => i.id === id);
75
+ if (!item || item.disabled) return;
76
+ setValue(id);
77
+ });
78
+
79
+ const focusItem = useEvent((index: number) => refs.current[index]?.focus());
80
+
81
+ const onKeyDown = useEvent((event: React.KeyboardEvent, index: number) => {
82
+ const next = nextIndexFromKey(event.key, index, items.length, { orientation, dir, loop });
83
+ if (next === null) return;
84
+ event.preventDefault();
85
+ focusItem(next);
86
+ });
87
+
88
+ // Rendering reads through this ref so the returned component keeps a stable
89
+ // identity while always seeing the current config and state.
90
+ const viewRef = React.useRef<ViewContext>(null as unknown as ViewContext);
91
+
92
+ const state: __PASCAL__State = {
93
+ items,
94
+ value,
95
+ select,
96
+ clear: useEvent(() => setValue(null)),
97
+ focusItem,
98
+ render: (props?: __PASCAL__RootProps) => renderRoot(viewRef.current, props),
99
+
100
+ getRootProps: (props) =>
101
+ mergeProps({ 'data-pinfold': '__NAME__', 'data-orientation': orientation, dir }, props),
102
+
103
+ getItemProps: (item, props) =>
104
+ mergeProps(
105
+ {
106
+ type: 'button',
107
+ id: `${baseId}-item-${item.id}`,
108
+ 'aria-disabled': ariaAttr(item.disabled),
109
+ 'data-state': item.selected ? 'selected' : 'idle',
110
+ 'data-disabled': dataAttr(item.disabled),
111
+ 'data-index': item.index,
112
+ ref: (node: HTMLElement | null) => {
113
+ refs.current[item.index] = node;
114
+ },
115
+ onClick: (event: React.MouseEvent) => {
116
+ if (item.disabled) {
117
+ event.preventDefault();
118
+ return;
119
+ }
120
+ api.select(item.id);
121
+ },
122
+ onKeyDown: (event: React.KeyboardEvent) => onKeyDown(event, item.index),
123
+ },
124
+ props,
125
+ ),
126
+ };
127
+
128
+ // Built once (inside usePinfoldComponent): a fresh identity each render would
129
+ // make React tear down and rebuild the subtree on every parent update.
130
+ const buildParts = () => {
131
+ const pick = (props: __PASCAL__PartProps): __PASCAL__ItemState | undefined => {
132
+ const rows = viewRef.current.api.items;
133
+ return props.index != null
134
+ ? rows[props.index]
135
+ : props.id != null
136
+ ? rows.find((row) => row.id === props.id)
137
+ : rows[0];
138
+ };
139
+
140
+ return {
141
+ Item: (props: __PASCAL__PartProps) => {
142
+ const item = pick(props);
143
+ return item
144
+ ? renderItem(viewRef.current, item, { className: props.className, style: props.style })
145
+ : null;
146
+ },
147
+ };
148
+ };
149
+
150
+ const api = usePinfoldComponent<__PASCAL__State, __PASCAL__RootProps, __PASCAL__Parts>(
151
+ '__PASCAL__',
152
+ state,
153
+ (props) => renderRoot(viewRef.current, props),
154
+ buildParts,
155
+ );
156
+
157
+ viewRef.current = { api, config, a: appearance };
158
+
159
+ return api;
160
+ }
161
+
162
+ /** Dev-only checks for mistakes that would otherwise show up as odd behaviour. */
163
+ function validate(config: __PASCAL__Config): void {
164
+ const { items, value, onValueChange } = config;
165
+
166
+ warnOnce(!Array.isArray(items), '__PASCAL__: `items` must be an array.');
167
+ if (!Array.isArray(items)) return;
168
+
169
+ const ids = items.map((item, i) => item.id ?? String(i));
170
+ const duplicate = ids.find((id, i) => ids.indexOf(id) !== i);
171
+ warnOnce(
172
+ duplicate !== undefined,
173
+ `__PASCAL__: duplicate item id ${JSON.stringify(duplicate)}. Ids must be unique.`,
174
+ );
175
+
176
+ warnOnce(
177
+ value !== undefined && onValueChange === undefined,
178
+ '__PASCAL__: `value` was passed without `onValueChange`, so it is frozen. ' +
179
+ 'Use `defaultValue` for uncontrolled behaviour.',
180
+ );
181
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
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.