Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: input field #24

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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