feat: better components
This commit is contained in:
31
client/src/lib/ui/form/context.svelte.ts
Normal file
31
client/src/lib/ui/form/context.svelte.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { getContext, hasContext, setContext } from 'svelte';
|
||||
|
||||
type Item = {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const key = 'form';
|
||||
export function setFormContext(id: string, name: string) {
|
||||
const item = getFormContext();
|
||||
if (!item) {
|
||||
const item: Item = $state({
|
||||
id,
|
||||
name,
|
||||
});
|
||||
setContext(key, item);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
item.id = id;
|
||||
item.name = name;
|
||||
}
|
||||
|
||||
export function getFormContext() {
|
||||
if (!hasContext(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getContext(key) as Item;
|
||||
}
|
30
client/src/lib/ui/form/errors.svelte
Normal file
30
client/src/lib/ui/form/errors.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { getFormContext } from './context.svelte';
|
||||
import type { WithElementRef, WithoutChildren } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { Violation } from '@bufbuild/protovalidate';
|
||||
import type { ConnectError } from '@connectrpc/connect';
|
||||
|
||||
type Props = WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
|
||||
errors?: Violation[] | ConnectError;
|
||||
};
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
errors = $bindable(),
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
const item = getFormContext();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn('text-red text-sm', className)} {...restProps}>
|
||||
{#if errors && Array.isArray(errors)}
|
||||
{#each errors as error}
|
||||
<label for={item?.id}>{error.message}</label>
|
||||
{/each}
|
||||
{:else if errors}
|
||||
<span>{errors.message}</span>
|
||||
{/if}
|
||||
</div>
|
21
client/src/lib/ui/form/field.svelte
Normal file
21
client/src/lib/ui/form/field.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { setFormContext } from './context.svelte';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
name?: string;
|
||||
};
|
||||
let { ref = $bindable(null), class: className, name, children, ...restProps }: Props = $props();
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
if (name) {
|
||||
setFormContext(uid, name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn('flex flex-col gap-1')} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
9
client/src/lib/ui/form/index.ts
Normal file
9
client/src/lib/ui/form/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import Field from './field.svelte';
|
||||
import Errors from './errors.svelte';
|
||||
import Label from './label.svelte';
|
||||
|
||||
export {
|
||||
Field,
|
||||
Errors,
|
||||
Label
|
||||
};
|
29
client/src/lib/ui/form/label.svelte
Normal file
29
client/src/lib/ui/form/label.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { getFormContext } from './context.svelte';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type Props = WithElementRef<HTMLAttributes<HTMLLabelElement>>;
|
||||
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();
|
||||
|
||||
const item = getFormContext();
|
||||
|
||||
function formatName(name: string) {
|
||||
// Replace _ with spaces
|
||||
name = name.replace('_', ' ');
|
||||
|
||||
// Capitalize first letter
|
||||
name = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
return name;
|
||||
}
|
||||
</script>
|
||||
|
||||
<label bind:this={ref} class={cn('text-sm', className)} for={item?.id} {...restProps}>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if item}
|
||||
{formatName(item.name)}
|
||||
{/if}
|
||||
</label>
|
Reference in New Issue
Block a user