pinfold 0.0.1 → 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.
@@ -0,0 +1,22 @@
1
+ /* Styles ship separately so SSR and CJS never choke on a CSS import:
2
+ import '@pinfold/css/core.css'; import '@pinfold/css/__NAME__.css';
3
+ `npx pinfold add __NAME__` wires this up for you. */
4
+
5
+ export { __PASCAL__ } from './__PASCAL__';
6
+ export { use__PASCAL__ } from './use__PASCAL__';
7
+ export type {
8
+ __PASCAL__Config,
9
+ __PASCAL__Api,
10
+ __PASCAL__Dimensions,
11
+ __PASCAL__ItemConfig,
12
+ __PASCAL__ItemState,
13
+ __PASCAL__Slot,
14
+ __PASCAL__Variant,
15
+ __PASCAL__State,
16
+ __PASCAL__Parts,
17
+ __PASCAL__PartProps,
18
+ __PASCAL__RootProps,
19
+ __PASCAL__Node,
20
+ } from './types';
21
+
22
+ export { __PASCAL__ as default } from './__PASCAL__';
@@ -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/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Indranil Bhattacharya
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/README.md DELETED
@@ -1,6 +0,0 @@
1
- # pinfold
2
-
3
- The CLI for [Pinfold](https://github.com/IndranilBhattacharya/pinfold), a hooks-first, config-driven
4
- React UI library by Indranil Bhattacharya.
5
-
6
- Pre-release placeholder. The real CLI (`npx pinfold init`, `add`, `theme`, `doctor`) ships as 0.1.0.
package/bin/pinfold.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- console.log('Pinfold is in pre-release. Follow https://github.com/IndranilBhattacharya/pinfold for the first version.');