Skip to content

Commit 847bc91

Browse files
authored
Merge pull request #4 from Blankeos/feat/none-parts
feat: Non parts props work + examples in docs.
2 parents 77fd36c + 0ea1c9c commit 847bc91

File tree

9 files changed

+566
-74
lines changed

9 files changed

+566
-74
lines changed

bun.lockb

9.42 KB
Binary file not shown.

dev/components/demos/base-demo.tsx

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { clsx } from 'clsx';
2+
import {
3+
children,
4+
ComponentProps,
5+
createSignal,
6+
createUniqueId,
7+
FlowProps,
8+
JSX,
9+
mergeProps,
10+
Show,
11+
splitProps,
12+
} from 'solid-js';
13+
14+
// Tabs
15+
import { Tabs } from '@kobalte/core/tabs';
16+
17+
// TODO: Dropdown (use @kobalte)
18+
// Dropdown
19+
// import {
20+
// Menu,
21+
// MenuButton,
22+
// MenuItem,
23+
// MenuItems,
24+
// Switch,
25+
// Field,
26+
// Label,
27+
// type SwitchProps,
28+
// type MenuButtonProps,
29+
// type MenuItemProps,
30+
// type MenuItemsProps,
31+
// type MenuProps
32+
// } from '@headlessui/react'
33+
34+
type TabValue = 'preview' | 'code';
35+
36+
// Switch
37+
import { Switch } from '@kobalte/core/switch';
38+
39+
// ===========================================================================
40+
// UI
41+
// ===========================================================================
42+
43+
export type DemoProps = {
44+
ref?: HTMLDivElement;
45+
children: JSX.Element;
46+
class?: string;
47+
defaultValue?: TabValue;
48+
code?: JSX.Element;
49+
minHeight?: string;
50+
title?: JSX.Element;
51+
};
52+
53+
type Props = DemoProps & { onClick?: () => void };
54+
55+
function Demo(props: FlowProps<Props>) {
56+
const _props = mergeProps(
57+
{
58+
defaultValue: 'preview',
59+
minHeight: 'min-h-[20rem]',
60+
children: undefined,
61+
renderTitle: false,
62+
},
63+
props,
64+
);
65+
66+
const [knowsToClick, setKnowsToClick] = createSignal(false);
67+
const [active, setActive] = createSignal(_props.defaultValue);
68+
69+
const id = createUniqueId();
70+
71+
function handleClick() {
72+
setKnowsToClick(true);
73+
_props?.onClick?.();
74+
}
75+
76+
const handleMouseDown: JSX.EventHandler<HTMLElement, MouseEvent> = (event) => {
77+
// Prevent selection of text:
78+
// https://stackoverflow.com/a/43321596
79+
if (event.detail > 1) {
80+
event.preventDefault();
81+
}
82+
};
83+
84+
/** Prevent doublle-render when using it in <Show /> https://github.com/solidjs/solid/issues/2345#issuecomment-2427189199 */
85+
const renderedTitle = children(() => _props.title);
86+
87+
return (
88+
<Tabs
89+
ref={_props.ref}
90+
class={clsx(active() === 'code' && 'dark', 'Demo text-primary not-prose relative isolate')} // reset text color if inside prose
91+
value={active()}
92+
onChange={(val) => setActive(val as TabValue)}
93+
// onValueChange={(val) => setActive(val as TabValue)}
94+
>
95+
<Show when={_props.code}>
96+
{/* <MotionConfig transition={{ layout: { type: 'spring', duration: 0.25, bounce: 0 } }}> */}
97+
<Tabs.List class="bg-zinc-150/90 absolute right-3 top-3 z-10 flex gap-1 rounded-full p-1 backdrop-blur-lg dark:bg-black/60">
98+
<Tabs.Trigger
99+
value="preview"
100+
class={clsx(
101+
active() !== 'preview' && 'hover:transition-[color]',
102+
'dark:text-muted hover:text-primary aria-selected:text-primary relative px-2 py-1 text-xs/4 font-medium text-zinc-600',
103+
)}
104+
>
105+
<Show when={active() === 'preview'}>
106+
{/* // Motion.div */}
107+
<div
108+
class="prefers-dark:!bg-white/15 absolute inset-0 -z-10 size-full bg-white shadow-sm dark:bg-white/25"
109+
style={{ 'border-radius': '999' }}
110+
// layout
111+
// layoutId={`${id}active`}
112+
></div>
113+
</Show>
114+
Preview
115+
</Tabs.Trigger>
116+
<Tabs.Trigger
117+
value="code"
118+
class={clsx(
119+
active() !== 'code' && 'hover:transition-[color]',
120+
'dark:text-muted hover:text-primary aria-selected:text-primary relative px-2 py-1 text-xs/4 font-medium text-zinc-600',
121+
)}
122+
>
123+
<Show when={active() === _props.code}>
124+
{/* // Motion.div */}
125+
<div
126+
class="prefers-dark:!bg-white/15 absolute inset-0 -z-10 size-full bg-white shadow-sm dark:bg-white/25"
127+
style={{ 'border-radius': '999' }}
128+
// layout
129+
// layoutId={`${id}active`}
130+
></div>
131+
</Show>
132+
Code
133+
</Tabs.Trigger>
134+
</Tabs.List>
135+
{/* </MotionConfig> */}
136+
</Show>
137+
<Tabs.Content
138+
value="preview"
139+
class={clsx(
140+
_props.class,
141+
'border-faint relative rounded-lg border data-[state=inactive]:hidden',
142+
)}
143+
>
144+
<Show when={renderedTitle()}>
145+
<div class="absolute left-3 top-3">{renderedTitle()}</div>
146+
</Show>
147+
148+
<div
149+
class={clsx(_props.minHeight, 'flex flex-col items-center justify-center p-5 pb-6')}
150+
onClick={_props?.onClick && handleClick}
151+
onMouseDown={_props?.onClick && handleMouseDown}
152+
>
153+
{_props.children}
154+
{_props?.onClick && (
155+
<span
156+
class={clsx(
157+
'absolute bottom-5 left-0 w-full text-center text-sm text-zinc-400 transition-opacity duration-200 ease-out',
158+
knowsToClick() && 'opacity-0',
159+
)}
160+
>
161+
Click anywhere to change numbers
162+
</span>
163+
)}
164+
</div>
165+
</Tabs.Content>
166+
<Show when={_props.code}>
167+
<Tabs.Content value="code">{_props?.code}</Tabs.Content>«
168+
</Show>
169+
</Tabs>
170+
);
171+
}
172+
173+
export default Demo;
174+
175+
export function DemoTitle(props: JSX.IntrinsicElements['span'] & { children: string }) {
176+
const [_props, other] = splitProps(props, ['class', 'children']);
177+
178+
return (
179+
<span {...other} class={clsx(props.class, 'px-2 py-1.5 text-sm')}>
180+
{props.children}
181+
</span>
182+
);
183+
}
184+
185+
// export function DemoMenu(props: MenuProps) {
186+
// return <Menu {...props} />
187+
// }
188+
189+
// export function DemoMenuButton({
190+
// children,
191+
// class,
192+
// ...props
193+
// }: MenuButtonProps & { children: JSX.Element }) {
194+
// return (
195+
// <MenuButton
196+
// {...props}
197+
// class={clsx(
198+
// class,
199+
// 'group flex h-8 items-center rounded-full px-3 text-xs shadow-sm ring ring-black/[8%] dark:shadow-none dark:ring-white/10'
200+
// )}
201+
// >
202+
// {children}
203+
// <ChevronDown
204+
// class="spring-bounce-0 spring-duration-150 ml-1 size-4 shrink-0 group-data-[active]:rotate-180"
205+
// strokeWidth={2}
206+
// />
207+
// </MenuButton>
208+
// )
209+
// }
210+
211+
// export function DemoMenuItems({ class, ...props }: MenuItemsProps) {
212+
// return (
213+
// <MenuItems
214+
// {...props}
215+
// class={clsx(
216+
// class,
217+
// 'animate-pop-in dark:ring-white/12.5 absolute left-0 top-full mt-2 min-w-full origin-top-left rounded-xl bg-white/90 p-1.5 shadow-sm ring ring-inset ring-black/[8%] backdrop-blur-xl backdrop-saturate-[140%] dark:bg-zinc-950/90 dark:shadow-none'
218+
// )}
219+
// />
220+
// )
221+
// }
222+
223+
// export function DemoMenuItem({
224+
// class,
225+
// children,
226+
// ...props
227+
// }: MenuItemProps<'button'> & { children: JSX.Element }) {
228+
// return (
229+
// <MenuItem
230+
// {...props}
231+
// as="button"
232+
// class={clsx(
233+
// class,
234+
// props.disabled ? 'pr-2' : 'pr-4',
235+
// 'dark:data-[focus]:bg-white/12.5 flex w-full items-center gap-2 rounded-lg py-2 pl-2 text-xs font-medium data-[disabled]:cursor-default data-[focus]:bg-black/5'
236+
// )}
237+
// >
238+
// {children}
239+
// {props.disabled && <Check class="ml-auto h-4 w-4" />}
240+
// </MenuItem>
241+
// )
242+
// }
243+
244+
export function DemoSwitch(props: ComponentProps<typeof Switch>) {
245+
const [_props, other] = splitProps(props, ['class', 'children']);
246+
247+
return (
248+
<Switch {...other} class="flex items-center gap-x-2">
249+
<Switch.Control
250+
class={clsx(
251+
props.class,
252+
'group relative flex h-6 w-11 cursor-pointer rounded-full bg-zinc-200 p-0.5 transition-colors duration-200 ease-in-out focus:outline-none data-[checked]:bg-zinc-950 data-[focus]:outline-2 data-[focus]:outline-blue-500 dark:bg-zinc-800 dark:data-[checked]:bg-zinc-50',
253+
)}
254+
>
255+
<Switch.Thumb class="spring-bounce-0 spring-duration-200 pointer-events-none inline-block size-5 rounded-full bg-white shadow-lg ring-0 transition-transform group-data-[checked]:translate-x-5 dark:bg-zinc-950" />
256+
</Switch.Control>
257+
<Switch.Label class="text-xs">{props.children as any}</Switch.Label>
258+
</Switch>
259+
);
260+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Demo, { DemoSwitch, type DemoProps } from 'dev/components/demos/base-demo';
2+
3+
import { useCycle } from 'dev/hooks/use-cycle';
4+
import { createSignal } from 'solid-js';
5+
import NumberFlow from 'src';
6+
7+
const NUMBERS = [120, 140];
8+
9+
export default function ContinuousDemo(props: Omit<DemoProps, 'children' | 'code'>) {
10+
const [value, cycleValue] = useCycle(NUMBERS);
11+
const [continuous, setContinuous] = createSignal(false);
12+
13+
return (
14+
<>
15+
<Demo
16+
{...props}
17+
title={
18+
<DemoSwitch checked={continuous()} onChange={setContinuous}>
19+
<code class="font-semibold">continuous</code>
20+
</DemoSwitch>
21+
}
22+
onClick={cycleValue}
23+
>
24+
<div class="~text-xl/4xl flex items-center gap-4">
25+
<NumberFlow
26+
continuous={continuous()}
27+
style={{ '--number-flow-char-height': '0.85em' }}
28+
value={value()}
29+
class="text-4xl font-semibold"
30+
/>
31+
</div>
32+
</Demo>
33+
</>
34+
);
35+
}

dev/components/demos/trend.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Demo, { type DemoProps } from 'dev/components/demos/base-demo';
2+
3+
import { useCycle } from 'dev/hooks/use-cycle';
4+
import { Trend } from 'number-flow';
5+
import NumberFlow from 'src';
6+
7+
const NUMBERS = [19, 20];
8+
9+
export default function TrendDemo(props: Omit<DemoProps, 'children' | 'code'>) {
10+
const [value, cycleValue] = useCycle(NUMBERS);
11+
const [trend, cycleTrend] = useCycle([true, false, 'increasing', 'decreasing'] as Trend[]);
12+
13+
return (
14+
<>
15+
<Demo
16+
{...props}
17+
title={
18+
<button class="transition active:scale-95" onClick={cycleTrend}>
19+
<code class="text-xs font-semibold">trend: {JSON.stringify(trend())}</code>
20+
</button>
21+
}
22+
onClick={cycleValue}
23+
>
24+
<div class="~text-xl/4xl flex items-center gap-4">
25+
<NumberFlow
26+
trend={trend()}
27+
style={{ '--number-flow-char-height': '0.85em' }}
28+
value={value()}
29+
class="text-4xl font-semibold"
30+
/>
31+
</div>
32+
</Demo>
33+
</>
34+
);
35+
}

dev/hooks/use-cycle.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createMemo, createSignal } from 'solid-js';
2+
3+
/**
4+
* A hook that toggles between two or multiple values (by implementing a common state pattern).
5+
*
6+
* Forked from https://github.com/Blankeos/bagon-hooks/blob/main/src/use-toggle/use-toggle.ts
7+
*/
8+
export function useCycle<T = boolean>(options: readonly T[] = [false, true] as any) {
9+
const [_options, _setOptions] = createSignal<typeof options>(options);
10+
11+
function toggle() {
12+
const value = _options()[0]!;
13+
const index = Math.abs(_options()!.indexOf(value));
14+
15+
_setOptions(
16+
_options()!
17+
.slice(index + 1)
18+
.concat(value),
19+
);
20+
}
21+
22+
const currentOption = createMemo(() => _options()[0]!);
23+
24+
return [currentOption, toggle] as const;
25+
}

0 commit comments

Comments
 (0)