Skip to content

Commit

Permalink
feat: input field (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 authored Nov 19, 2024
1 parent 556ec1e commit 5ec9485
Show file tree
Hide file tree
Showing 18 changed files with 298 additions and 41 deletions.
2 changes: 2 additions & 0 deletions src/docs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ export const routes: Route[] = [
{ name: 'Card', link: '/examples/card' },
{ name: 'Checkbox', link: '/examples/checkbox' },
{ name: 'CloseButton', link: '/examples/close-button' },
{ name: 'Field', link: '/examples/field' },
{ name: 'Heading', link: '/examples/heading' },
{ name: 'IconButton', link: '/examples/icon-button' },
{ name: 'Input', link: '/examples/input' },
{ name: 'Link', link: '/examples/link' },
{ name: 'Logo', link: '/examples/logo' },
{ name: 'Stack', link: '/examples/stack' },
Expand Down
9 changes: 9 additions & 0 deletions src/lib/common/context.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { FieldContext } from '$lib/types.js';
import { withPrefix } from '$lib/utils.js';
import { getContext, hasContext, setContext } from 'svelte';

const fieldKey = Symbol(withPrefix('field'));

export const setFieldContext = (field: FieldContext) => setContext(fieldKey, field);
export const hasFieldContext = (): boolean => hasContext(fieldKey);
export const getFieldContext = (): FieldContext => (getContext(fieldKey) || {}) as FieldContext;
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ContextKey } from '$lib/constants.js';
import { ChildKey } from '$lib/constants.js';
import { withPrefix } from '$lib/utils.js';
import { setContext, type Snippet } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';

export const withChildrenSnippets = (key: ContextKey) => {
const map = $state(new SvelteMap<ContextKey, Snippet>());
export const withChildrenSnippets = (key: ChildKey) => {
const map = $state(new SvelteMap<ChildKey, Snippet>());

setContext(withPrefix(key), {
register: async (child: ContextKey, snippet: Snippet) => {
register: async (child: ChildKey, snippet: Snippet) => {
if (map.has(child)) {
console.warn(`Snippet with key ${child} already exists in the context`);
return;
Expand All @@ -18,6 +18,6 @@ export const withChildrenSnippets = (key: ContextKey) => {
});

return {
getChildren: (key: ContextKey) => map.get(key),
getChildren: (key: ChildKey) => map.get(key),
};
};
12 changes: 6 additions & 6 deletions src/lib/components/Card/Card.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { withChildrenSnippets } from '$lib/common/use-context.svelte.js';
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import { ContextKey } from '$lib/constants.js';
import { ChildKey } from '$lib/constants.js';
import type { Color, Shape } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { mdiChevronDown } from '@mdi/js';
Expand Down Expand Up @@ -104,10 +104,10 @@
expanded = !expanded;
};
const { getChildren: getChildSnippet } = withChildrenSnippets(ContextKey.Card);
const headerChildren = $derived(getChildSnippet(ContextKey.CardHeader));
const bodyChildren = $derived(getChildSnippet(ContextKey.CardBody));
const footerChildren = $derived(getChildSnippet(ContextKey.CardFooter));
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Card);
const headerChildren = $derived(getChildSnippet(ChildKey.CardHeader));
const bodyChildren = $derived(getChildSnippet(ChildKey.CardBody));
const footerChildren = $derived(getChildSnippet(ChildKey.CardFooter));
const headerClasses = 'flex flex-col space-y-1.5';
const headerContainerClasses = $derived(
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/Card/CardBody.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { ContextKey } from '$lib/constants.js';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
Expand All @@ -13,7 +13,7 @@
let { class: className, children }: Props = $props();
</script>

<Child for={ContextKey.Card} as={ContextKey.CardBody}>
<Child for={ChildKey.Card} as={ChildKey.CardBody}>
<div class={twMerge(cleanClass('w-full p-4', className))}>
{@render children?.()}
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/Card/CardFooter.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { ContextKey } from '$lib/constants.js';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
Expand All @@ -10,6 +10,6 @@
let { children }: Props = $props();
</script>

<Child for={ContextKey.Card} as={ContextKey.CardFooter}>
<Child for={ChildKey.Card} as={ChildKey.CardFooter}>
{@render children?.()}
</Child>
4 changes: 2 additions & 2 deletions src/lib/components/Card/CardHeader.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { ContextKey } from '$lib/constants.js';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
Expand All @@ -10,6 +10,6 @@
let { children }: Props = $props();
</script>

<Child for={ContextKey.Card} as={ContextKey.CardHeader}>
<Child for={ChildKey.Card} as={ChildKey.CardHeader}>
{@render children?.()}
</Child>
29 changes: 29 additions & 0 deletions src/lib/components/Form/Field.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
import { setFieldContext } from '$lib/common/context.svelte.js';
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import { ChildKey } from '$lib/constants.js';
import type { FieldContext } from '$lib/types.js';
import { type Snippet } from 'svelte';
type Props = FieldContext & {
children: Snippet;
};
const { children, ...props }: Props = $props();
const state = $state(props);
setFieldContext(state);
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Field);
const helperTextChildren = $derived(getChildSnippet(ChildKey.HelperText));
</script>

<div>
{@render children()}
{#if helperTextChildren}
<div class="pt-1">
{@render helperTextChildren?.()}
</div>
{/if}
</div>
24 changes: 24 additions & 0 deletions src/lib/components/Form/HelperText.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import Text from '$lib/components/Text/Text.svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Color } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
type Props = {
color?: Color;
class?: string;
children?: Snippet;
};
let { class: className, children, color }: Props = $props();
</script>

<Child for={ChildKey.Field} as={ChildKey.HelperText}>
<div class={cleanClass(className)}>
<Text {color} size="small">
{@render children?.()}
</Text>
</div>
</Child>
115 changes: 101 additions & 14 deletions src/lib/components/Form/Input.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,109 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import type { Shape, Size } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import type { HTMLInputAttributes } from 'svelte/elements';
import type { WithElementRef } from 'bits-ui';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
type Props = {
class?: string;
value?: string;
size?: Size;
shape?: Shape;
inputSize?: HTMLInputAttributes['size'];
} & Omit<HTMLInputAttributes, 'size'>;
let {
ref = $bindable(null),
value = $bindable(),
shape = 'semi-round',
size = 'medium',
class: className,
value = $bindable<string>(),
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
}: Props = $props();
const {
label,
readOnly = false,
required = false,
invalid = false,
disabled = false,
} = $derived(getFieldContext());
const labelStyles = tv({
base: '',
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-md',
large: 'text-lg',
giant: 'text-xl',
},
},
});
const inputStyles = tv({
base: 'outline-none disabled:cursor-not-allowed bg-gray-200 dark:bg-gray-600 disabled:bg-gray-300 disabled:text-gray-200 dark:disabled:bg-gray-800 aria-readonly:text-dark/50 dark:aria-readonly:text-dark/75',
variants: {
shape: {
rectangle: 'rounded-none',
'semi-round': '',
round: 'rounded-full',
},
padding: {
base: 'px-3 py-2',
round: 'px-4 py-2',
},
roundedSize: {
tiny: 'rounded-xl',
small: 'rounded-xl',
medium: 'rounded-2xl',
large: 'rounded-2xl',
giant: 'rounded-2xl',
},
textSize: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-md',
large: 'text-lg',
giant: 'text-xl',
},
invalid: {
true: 'border border-danger/80',
false: '',
},
},
});
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
</script>

<input
bind:this={ref}
class={cleanClass(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
bind:value
{...restProps}
/>
<div class="flex flex-col gap-1">
{#if label}
<label id={labelId} for={inputId} class={labelStyles({ size })}>{label}</label>
{/if}
<input
id={label && inputId}
aria-labelledby={label && labelId}
{required}
aria-required={required}
{disabled}
aria-disabled={disabled}
readonly={readOnly}
aria-readonly={readOnly}
class={cleanClass(
inputStyles({
shape,
textSize: size,
padding: shape === 'round' ? 'round' : 'base',
roundedSize: shape === 'semi-round' ? size : undefined,
invalid,
}),
className,
)}
bind:value
{...restProps}
/>
</div>
2 changes: 1 addition & 1 deletion src/lib/components/Text/Text.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
type Props = {
color?: Color;
class?: string;
size?: Size;
children: Snippet;
variant?: 'italic';
fontWeight?: 'light' | 'normal' | 'semi-bold' | 'bold';
class?: string;
};
const { color, size, fontWeight = 'normal', children, class: className }: Props = $props();
Expand Down
4 changes: 3 additions & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export enum ContextKey {
export enum ChildKey {
Field = 'field',
HelperText = 'helped-text',
Card = 'card',
CardHeader = 'card-header',
CardBody = 'card-body',
Expand Down
4 changes: 3 additions & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ export { default as CardHeader } from '$lib/components/Card/CardHeader.svelte';
export { default as CardTitle } from '$lib/components/Card/CardTitle.svelte';
export { default as CloseButton } from '$lib/components/CloseButton/CloseButton.svelte';
export { default as Checkbox } from '$lib/components/Form/Checkbox.svelte';
export { default as Field } from '$lib/components/Form/Field.svelte';
export { default as HelperText } from '$lib/components/Form/HelperText.svelte';
export { default as Input } from '$lib/components/Form/Input.svelte';
export { default as Label } from '$lib/components/Form/Label.svelte';
export { default as Heading } from '$lib/components/Heading/Heading.svelte';
export { default as Icon } from '$lib/components/Icon/Icon.svelte';
export { default as IconButton } from '$lib/components/IconButton/IconButton.svelte';
export { default as Link } from '$lib/components/Link/Link.svelte';
export { default as SupporterBadge } from '$lib/components/SupporterBadge/SupporterBadge.svelte';
export { default as Logo } from '$lib/components/Logo/Logo.svelte';
export { default as HStack } from '$lib/components/Stack/HStack.svelte';
export { default as Stack } from '$lib/components/Stack/Stack.svelte';
export { default as VStack } from '$lib/components/Stack/VStack.svelte';
export { default as SupporterBadge } from '$lib/components/SupporterBadge/SupporterBadge.svelte';
export { default as Text } from '$lib/components/Text/Text.svelte';
export * from '$lib/types.js';
8 changes: 4 additions & 4 deletions src/lib/internal/Child.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts">
import { ContextKey } from '$lib/constants.js';
import { ChildKey } from '$lib/constants.js';
import { withPrefix } from '$lib/utils.js';
import { getContext, type Snippet } from 'svelte';
type ContextType = { register: (key: ContextKey, snippet: Snippet) => void };
type ContextType = { register: (key: ChildKey, snippet: Snippet) => void };
type Props = {
for: ContextKey;
as: ContextKey;
for: ChildKey;
as: ChildKey;
children: Snippet;
};
Expand Down
8 changes: 8 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,11 @@ export type StackProps = StackBaseProps & {
};
export type HStackProps = StackBaseProps;
export type VStackProps = StackBaseProps;

export type FieldContext = {
label?: string;
invalid?: boolean;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
};
7 changes: 4 additions & 3 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { ContextKey } from '$lib/constants.js';

export const cleanClass = (...classNames: unknown[]) => {
return classNames
.filter((className) => {
Expand All @@ -12,4 +10,7 @@ export const cleanClass = (...classNames: unknown[]) => {
.join(' ');
};

export const withPrefix = (key: ContextKey) => `immich-ui-${key}`;
export const withPrefix = (key: string) => `immich-ui-${key}`;

let _count = 0;
export const generateId = (): string => `id-${_count++}`;
Loading

0 comments on commit 5ec9485

Please sign in to comment.