Skip to content

Commit 2b6cc01

Browse files
committed
[shadcn] Use DatePicker for date input type, use Slider for range input type
1 parent b5a9529 commit 2b6cc01

File tree

11 files changed

+307
-185
lines changed

11 files changed

+307
-185
lines changed

.changeset/three-monkeys-smoke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sjsf/shadcn-theme": minor
3+
---
4+
5+
Use `DatePicker` for `date` input type, use `Slider` for `range` input type

packages/shadcn-theme/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@
7777
"@types/eslint": "catalog:",
7878
"ajv": "catalog:",
7979
"autoprefixer": "catalog:",
80-
"bits-ui": "1.0.0-next.31",
80+
"bits-ui": "1.0.0-next.35",
8181
"clsx": "^2.1.1",
8282
"eslint": "catalog:",
8383
"eslint-config-prettier": "catalog:",
8484
"eslint-plugin-svelte": "catalog:",
8585
"globals": "catalog:",
86-
"lucide-svelte": "^0.453.0",
86+
"lucide-svelte": "^0.454.0",
8787
"postcss": "catalog:",
8888
"prettier": "catalog:",
8989
"prettier-plugin-svelte": "catalog:",

packages/shadcn-theme/src/lib/theme/context.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {
1919
} from 'bits-ui';
2020
import { Popover } from 'bits-ui';
2121

22-
type CalendarProps = WithoutChildrenOrChild<Calendar.RootProps>;
22+
export type CalendarProps = WithoutChildrenOrChild<Calendar.RootProps>;
2323

2424
export interface ThemeComponents {
2525
Button: Component<HTMLButtonAttributes>;
@@ -51,19 +51,32 @@ export interface ThemeComponents {
5151
Textarea: Component<WithElementRef<HTMLTextareaAttributes>, {}, 'value' | 'ref'>;
5252
}
5353

54+
export type DateFormatter = (date: Date) => string;
55+
5456
export interface ThemeContext {
5557
components: ThemeComponents;
58+
formatDate?: DateFormatter;
5659
}
5760

5861
const THEME_CONTEXT = Symbol('theme-context');
5962

60-
export function getThemeContext(): Required<ThemeContext> {
63+
export function getThemeContext(): Required<Omit<ThemeContext, 'components'>> & {
64+
components: Required<ThemeComponents>;
65+
} {
6166
return getContext(THEME_CONTEXT);
6267
}
6368

6469
export function setThemeContext(ctx: ThemeContext) {
6570
// TODO: Remove Proxy in next major
71+
const dateTimeFormat = new Intl.DateTimeFormat(undefined, {
72+
year: 'numeric',
73+
month: '2-digit',
74+
day: 'numeric'
75+
});
6676
setContext(THEME_CONTEXT, {
77+
get formatDate() {
78+
return ctx.formatDate ?? ((date) => dateTimeFormat.format(date));
79+
},
6780
get components() {
6881
return new Proxy(ctx.components, {
6982
get(target, prop, receiver) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<script lang="ts">
2+
import { type WidgetProps } from '@sjsf/form';
3+
import { getLocalTimeZone, parseDate } from '@internationalized/date';
4+
5+
import { cn } from '$lib/utils';
6+
7+
import { getThemeContext } from '../context';
8+
9+
const ctx = getThemeContext();
10+
11+
const { Popover, PopoverTrigger, Button, PopoverContent, Calendar } = $derived(ctx.components);
12+
13+
let { value = $bindable(), attributes }: WidgetProps<'text'> = $props();
14+
const date = {
15+
get value() {
16+
return value ? parseDate(value) : undefined;
17+
},
18+
set value(v) {
19+
if (!v) {
20+
value = undefined;
21+
return;
22+
}
23+
value = v.toDate(getLocalTimeZone()).toLocaleDateString('en-CA');
24+
}
25+
};
26+
const formattedValue = $derived.by(() => {
27+
const v = date.value;
28+
if (v === undefined) {
29+
return attributes.placeholder;
30+
}
31+
return ctx.formatDate(v.toDate(getLocalTimeZone()));
32+
});
33+
</script>
34+
35+
<Popover>
36+
<PopoverTrigger>
37+
{#snippet child({ props })}
38+
<Button {...props} class={cn('w-full', date.value === undefined && 'text-muted-foreground')}>
39+
{formattedValue}
40+
</Button>
41+
{/snippet}
42+
</PopoverTrigger>
43+
<PopoverContent>
44+
<Calendar
45+
type="single"
46+
initialFocus
47+
bind:value={date.value}
48+
id={attributes.id}
49+
disabled={attributes.disabled}
50+
readonly={attributes.readonly ?? undefined}
51+
onchange={attributes.onchange as any}
52+
oninput={attributes.oninput as any}
53+
onblur={attributes.onblur as any}
54+
/>
55+
</PopoverContent>
56+
</Popover>

packages/shadcn-theme/src/lib/theme/widgets/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
import type { Widget, Widgets, WidgetType } from '@sjsf/form';
2+
import type {
3+
Calendar,
4+
Checkbox,
5+
RadioGroup,
6+
Select,
7+
Slider,
8+
Switch,
9+
WithoutChildrenOrChild
10+
} from 'bits-ui';
211

312
import TextWidget from './text-widget.svelte';
413
import TextareaWidget from './textarea-widget.svelte';
@@ -9,6 +18,17 @@ import RadioWidget from './radio-widget.svelte';
918
import CheckboxesWidget from './checkboxes-widget.svelte';
1019
import FileWidget from './file-widget.svelte';
1120

21+
declare module '@sjsf/form' {
22+
interface Inputs {
23+
shadcnCheckbox: WithoutChildrenOrChild<Checkbox.RootProps>;
24+
shadcnCalendar: WithoutChildrenOrChild<Calendar.RootProps>;
25+
shadcnRadio: WithoutChildrenOrChild<RadioGroup.ItemProps>;
26+
shadcnSelect: Select.RootProps;
27+
shadcnSlider: WithoutChildrenOrChild<Slider.RootProps>;
28+
shadcnSwitch: WithoutChildrenOrChild<Switch.RootProps>;
29+
}
30+
}
31+
1232
export const registry: { [T in WidgetType]: Widget<T> } = {
1333
text: TextWidget,
1434
textarea: TextareaWidget,
@@ -17,7 +37,7 @@ export const registry: { [T in WidgetType]: Widget<T> } = {
1737
checkbox: CheckBoxWidget,
1838
radio: RadioWidget,
1939
checkboxes: CheckboxesWidget,
20-
file: FileWidget,
40+
file: FileWidget
2141
};
2242

2343
// @ts-expect-error TODO: improve `widgets` type
Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,19 @@
11
<script lang="ts">
2-
// import type { ComponentProps } from 'svelte';
32
import type { WidgetProps } from '@sjsf/form';
43
5-
import { getThemeContext } from '../context'
4+
import { getThemeContext } from '../context';
65
7-
const ctx = getThemeContext();
6+
import Slider from './slider.svelte';
87
9-
const { Input } = $derived(ctx.components)
8+
const ctx = getThemeContext();
109
11-
let { value = $bindable(), attributes }: WidgetProps<'number'> = $props();
10+
const { Input } = $derived(ctx.components);
1211
13-
// const mapped = {
14-
// get value() {
15-
// return [value ?? 0];
16-
// },
17-
// set value(v) {
18-
// value = v[0];
19-
// }
20-
// }
12+
let { value = $bindable(), attributes, ...rest }: WidgetProps<'number'> = $props();
2113
</script>
2214

23-
<!-- {#if attributes.type === 'range'}
24-
<Slider bind:value={mapped.value} {...attributes as ComponentProps<typeof Slider>} />
25-
{:else} -->
15+
{#if attributes.type === 'range'}
16+
<Slider {...rest} {attributes} bind:value />
17+
{:else}
2618
<Input type="number" bind:value {...attributes} />
27-
<!-- {/if} -->
19+
{/if}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<script lang="ts">
2+
import type { HTMLInputAttributes } from 'svelte/elements';
3+
import type { WidgetProps } from '@sjsf/form';
4+
5+
import { getThemeContext } from '../context';
6+
7+
const ctx = getThemeContext();
8+
9+
const { Slider } = $derived(ctx.components);
10+
11+
let { value = $bindable(), attributes }: WidgetProps<'number'> = $props();
12+
const slider = {
13+
get value() {
14+
return [value ?? 0];
15+
},
16+
set value(v) {
17+
value = v[0];
18+
}
19+
};
20+
21+
function n(value: HTMLInputAttributes['max']) {
22+
if (!value) {
23+
return undefined;
24+
}
25+
if (typeof value === 'number') {
26+
return value;
27+
}
28+
const number = Number(value);
29+
return isNaN(number) ? undefined : number;
30+
}
31+
</script>
32+
33+
<Slider
34+
bind:value={slider.value}
35+
id={attributes.id}
36+
max={n(attributes.max)}
37+
min={n(attributes.min)}
38+
step={n(attributes.step)}
39+
onchange={attributes.onchange as any}
40+
oninput={attributes.oninput as any}
41+
onblur={attributes.onblur as any}
42+
disabled={attributes.disabled}
43+
/>
Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,19 @@
11
<script lang="ts">
22
import { type WidgetProps } from '@sjsf/form';
3-
3+
44
import { getThemeContext } from '../context';
55
6+
import DatePicker from './date-picker.svelte';
7+
68
const ctx = getThemeContext();
79
810
const { Input } = $derived(ctx.components)
911
10-
let { value = $bindable(), attributes }: WidgetProps<'text'> = $props();
11-
12-
// const date = $derived(new Date(value ?? 'invalid'));
13-
// const isValid = $derived(!isNaN(date as unknown as number));
14-
// const dateFormatOptions: Intl.DateTimeFormatOptions = {
15-
// year: 'numeric',
16-
// month: 'long',
17-
// day: 'numeric'
18-
// };
19-
// const formattedValue = $derived(
20-
// isValid
21-
// ? new Intl.DateTimeFormat(undefined, dateFormatOptions).format(date)
22-
// : (attributes.placeholder ?? value)
23-
// );
12+
let { value = $bindable(), attributes, ...rest }: WidgetProps<'text'> = $props();
2413
</script>
2514

26-
<!-- {#if attributes.type === 'date'}
27-
<Popover>
28-
<PopoverTrigger>
29-
{#snippet child({ props })}
30-
<Button {...props} class={cn(!isValid && 'text-muted-foreground')}>
31-
{formattedValue}
32-
</Button>
33-
{/snippet}
34-
</PopoverTrigger>
35-
<PopoverContent>
36-
<Calendar bind:value initialFocus />
37-
</PopoverContent>
38-
</Popover>
39-
{:else} -->
15+
{#if attributes.type === 'date'}
16+
<DatePicker {...rest} {attributes} bind:value />
17+
{:else}
4018
<Input type="text" bind:value {...attributes} />
41-
<!-- {/if} -->
19+
{/if}

packages/testing/src/demo/widgets.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const uiStates = (uiSchema: UiSchema): UiSchema => ({
7171
"ui:options": {
7272
...uiSchema["ui:options"],
7373
input: {
74+
...uiSchema["ui:options"]?.input,
7475
placeholder: "placeholder",
7576
},
7677
}

0 commit comments

Comments
 (0)