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.
- package/bin/pinfold.mjs +236 -0
- package/lib/commands.mjs +339 -0
- package/lib/detect.mjs +92 -0
- package/lib/mcp.mjs +124 -0
- package/lib/plan.mjs +170 -0
- package/package.json +38 -6
- package/registry.json +21 -0
- package/templates/component/css/__NAME__.css.tmpl +101 -0
- package/templates/component/react/__NAME__.test.tsx.tmpl +66 -0
- package/templates/component/react/__PASCAL__.tsx.tmpl +13 -0
- package/templates/component/react/__PASCAL__View.tsx.tmpl +114 -0
- package/templates/component/react/index.ts.tmpl +22 -0
- package/templates/component/react/lazy.ts.tmpl +11 -0
- package/templates/component/react/types.ts.tmpl +142 -0
- package/templates/component/react/use__PASCAL__.tsx.tmpl +181 -0
- package/LICENSE +0 -21
- package/README.md +0 -6
- package/bin/pinfold.js +0 -2
|
@@ -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
package/bin/pinfold.js
DELETED