Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/ui/src/docs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
mdiCheckboxMultipleMarked,
mdiCheckboxMultipleMarkedOutline,
mdiCheckboxOutline,
mdiClock,
mdiClockOutline,
mdiCloseCircle,
mdiCloseCircleOutline,
mdiCodeBlockBraces,
Expand Down Expand Up @@ -184,6 +186,7 @@ export const componentGroups: ComponentGroup[] = [
},
{ name: 'Select', icon: mdiFormDropdown },
{ name: 'Switch', icon: mdiToggleSwitchOutline, activeIcon: mdiToggleSwitch },
{ name: 'TimeInput', icon: mdiClockOutline, activeIcon: mdiClock },
{ name: 'DatePicker', icon: mdiCalendar },
],
},
Expand Down
79 changes: 6 additions & 73 deletions packages/ui/src/lib/components/Input/Input.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import Icon from '$lib/components/Icon/Icon.svelte';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import { styleVariants } from '$lib/styles.js';
import InputIcon from '$lib/internal/InputIcon.svelte';
import { inputContainerStyles, inputStyles } from '$lib/styles.js';
import type { InputProps } from '$lib/types.js';
import { cleanClass, generateId, isIconLike } from '$lib/utilities/internal.js';
import { cleanClass, generateId } from '$lib/utilities/internal.js';
import { tv } from 'tailwind-variants';

let {
Expand All @@ -27,57 +27,6 @@
const { label, description, readOnly, required, invalid, disabled, ...labelProps } = $derived(context());
const size = $derived(initialSize ?? labelProps.size ?? 'small');

const iconStyles = tv({
base: 'flex shrink-0 items-center justify-center',
variants: {
size: {
tiny: 'w-6',
small: 'w-8',
medium: 'w-10',
large: 'w-12',
giant: 'w-14',
},
},
});

const containerStyles = tv({
base: cleanClass(styleVariants.inputContainerCommon, 'flex w-full items-center'),
variants: {
shape: styleVariants.shape,
roundedSize: styleVariants.inputRoundedSize,
invalid: {
true: 'focus-within:ring-danger dark:focus-within:ring-danger dark:ring-danger-300 ring-danger-300 ring-1',
false: '',
},
disabled: {
true: 'bg-light-300 dark:bg-gray-900',
false: '',
},
},
});

const inputStyles = tv({
base: cleanClass(styleVariants.inputCommon, 'w-full flex-1 py-2.5'),
variants: {
textSize: styleVariants.textSize,
leadingPadding: {
base: 'pl-4',
icon: 'pl-0',
},
trailingPadding: {
base: 'pr-4',
icon: 'pr-0',
},
roundedSize: {
tiny: 'rounded-lg',
small: 'rounded-lg',
medium: 'rounded-lg',
large: 'rounded-lg',
giant: 'rounded-lg',
},
},
});

const trailingTextStyles = tv({
variants: {
padding: {
Expand Down Expand Up @@ -105,7 +54,7 @@
<div
bind:this={containerRef}
class={cleanClass(
containerStyles({
inputContainerStyles({
disabled,
shape,
roundedSize: shape === 'semi-round' ? size : undefined,
Expand All @@ -114,15 +63,7 @@
className,
)}
>
{#if leadingIcon}
<div tabindex="-1" class={iconStyles({ size })}>
{#if isIconLike(leadingIcon)}
<Icon size="60%" icon={leadingIcon} />
{:else}
{@render leadingIcon()}
{/if}
</div>
{/if}
<InputIcon icon={leadingIcon} {size} />

<input
id={inputId}
Expand Down Expand Up @@ -151,15 +92,7 @@
>
{/if}

{#if trailingIcon}
<div tabindex="-1" class={iconStyles({ size })}>
{#if isIconLike(trailingIcon)}
<Icon size="60%" icon={trailingIcon} />
{:else}
{@render trailingIcon()}
{/if}
</div>
{/if}
<InputIcon icon={trailingIcon} {size} />
</div>
</div>

Expand Down
127 changes: 127 additions & 0 deletions packages/ui/src/lib/components/TimeInput/TimeInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import Label from '$lib/components/Label/Label.svelte';
import InputIcon from '$lib/internal/InputIcon.svelte';
import { getLocale } from '$lib/state/locale-state.svelte.js';
import { inputContainerStyles, inputStyles, styleVariants } from '$lib/styles.js';
import type { TimeInputProps } from '$lib/types.js';
import { cleanClass } from '$lib/utilities/internal.js';
import { TimeField, type TimeValue } from 'bits-ui';
import { tv } from 'tailwind-variants';

let {
ref = $bindable(null),
class: className,
leadingIcon,
trailingIcon,
containerRef = $bindable(null),
onChange,
minValue,
maxValue,
value = $bindable<TimeValue | undefined>(),
shape = 'semi-round',
size: initialSize,
granularity = 'second',
}: TimeInputProps = $props();

const context = getFieldContext();
const { readOnly, required, invalid, disabled, label, ...labelProps } = $derived(context());
const size = $derived(initialSize ?? labelProps.size ?? 'small');

const styles = tv({
base: cleanClass(styleVariants.inputContainerCommon, 'flex w-full items-center'),
variants: {
shape: styleVariants.shape,
roundedSize: styleVariants.inputRoundedSize,
invalid: {
true: 'border-danger/80 border',
false: '',
},
},
});

const segmentStyles = tv({
base: 'focus:bg-light-300 focus:text-light-900 data-focused:bg-light-300 data-focused:text-light-900 data-placeholder:text-light-400 dark:focus:bg-light-700 dark:focus:text-light-100 dark:data-focused:bg-light-300 dark:data-focused:text-light-900 rounded px-0.5 py-0.5 tabular-nums outline-none data-disabled:cursor-not-allowed',
variants: {
textSize: styleVariants.textSize,
},
});
</script>

<div bind:this={containerRef} class={cleanClass('flex w-full flex-col gap-1', className)}>
<TimeField.Root
onValueChange={onChange}
{minValue}
{maxValue}
{granularity}
bind:value
readonly={readOnly}
locale={getLocale()}
{disabled}
>
{#if label}
<TimeField.Label>
{#snippet child({ props })}
<Label
{...labelProps}
{...props}
class={cleanClass(labelProps.class, props.class)}
requiredIndicator={required === 'indicator'}
{label}
{size}
/>
{/snippet}
</TimeField.Label>
{/if}

<TimeField.Input
bind:ref
class={styles({
shape,
roundedSize: shape === 'semi-round' ? size : undefined,
invalid,
})}
>
{#snippet children({ segments })}
<div
class={cleanClass(
inputContainerStyles({
disabled,
shape,
roundedSize: shape === 'semi-round' ? size : undefined,
invalid,
}),
className,
)}
>
<InputIcon icon={leadingIcon} {size} />

<div
class={cleanClass(
inputStyles({
textSize: size,
leadingPadding: leadingIcon ? 'icon' : 'base',
trailingPadding: trailingIcon ? 'icon' : 'base',
roundedSize: shape === 'semi-round' ? size : undefined,
}),
)}
>
{#each segments as { part, value }, i (`segment-${i}`)}
{#if part === 'literal'}
<TimeField.Segment {part} class="text-muted p-px">
{value}
</TimeField.Segment>
{:else}
<TimeField.Segment {part} class={segmentStyles({ textSize: size })}>
{value}
</TimeField.Segment>
{/if}
{/each}
</div>

<InputIcon icon={trailingIcon} {size} />
</div>
{/snippet}
</TimeField.Input>
</TimeField.Root>
</div>
37 changes: 37 additions & 0 deletions packages/ui/src/lib/internal/InputIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import Icon from '$lib/components/Icon/Icon.svelte';
import type { IconLike, Size } from '$lib/types.js';
import { isIconLike } from '$lib/utilities/internal.js';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';

type Props = {
icon?: IconLike | Snippet;
size?: Size;
};

const { icon, size }: Props = $props();

const iconStyles = tv({
base: 'flex shrink-0 items-center justify-center',
variants: {
size: {
tiny: 'w-6',
small: 'w-8',
medium: 'w-10',
large: 'w-12',
giant: 'w-14',
},
},
});
</script>

{#if icon}
<div tabindex="-1" class={iconStyles({ size })}>
{#if isIconLike(icon)}
<Icon size="60%" {icon} />
{:else}
{@render icon()}
{/if}
</div>
{/if}
41 changes: 41 additions & 0 deletions packages/ui/src/lib/styles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { cleanClass } from '$lib/utilities/internal.js';
import { tv } from 'tailwind-variants';

const color = {
primary: 'text-primary',
secondary: 'text-dark',
Expand Down Expand Up @@ -102,3 +105,41 @@ export const styleVariants = {
giant: 'py-4',
},
};

export const inputStyles = tv({
base: cleanClass(styleVariants.inputCommon, 'w-full flex-1 py-2.5'),
variants: {
textSize: styleVariants.textSize,
leadingPadding: {
base: 'pl-4',
icon: 'pl-0',
},
trailingPadding: {
base: 'pr-4',
icon: 'pr-0',
},
roundedSize: {
tiny: 'rounded-lg',
small: 'rounded-lg',
medium: 'rounded-lg',
large: 'rounded-lg',
giant: 'rounded-lg',
},
},
});

export const inputContainerStyles = tv({
base: cleanClass(styleVariants.inputContainerCommon, 'flex w-full items-center'),
variants: {
shape: styleVariants.shape,
roundedSize: styleVariants.inputRoundedSize,
invalid: {
true: 'focus-within:ring-danger dark:focus-within:ring-danger dark:ring-danger-300 ring-danger-300 ring-1',
false: '',
},
disabled: {
true: 'bg-light-300 dark:bg-gray-900',
false: '',
},
},
});
16 changes: 16 additions & 0 deletions packages/ui/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Shortcut } from '$lib/actions/shortcut.js';
import type { ChildKey } from '$lib/constants.js';
import type { Translations } from '$lib/services/translation.svelte.js';
import type { TimeValue } from 'bits-ui';
import type { DateTime } from 'luxon';
import type { Component, Snippet } from 'svelte';
import type {
Expand Down Expand Up @@ -185,6 +186,21 @@ export type InputProps = BaseInputProps<string> & {
type?: HTMLInputAttributes['type'];
};

export type TimeInputProps = {
ref?: HTMLInputElement | null;
class?: string;
size?: Size;
value?: TimeValue;
shape?: Shape;
granularity?: 'hour' | 'minute' | 'second';
leadingIcon?: IconLike | Snippet;
trailingIcon?: IconLike | Snippet;
containerRef?: HTMLElement | null;
onChange?: (value?: TimeValue) => void;
minValue?: TimeValue;
maxValue?: TimeValue;
};

export type NumberInputProps = BaseInputProps<number | undefined>;

export type PasswordInputProps = BaseInputProps<string> & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import basicExample from './BasicExample.svelte?raw';
</script>

<ComponentPage name="Input" description="Accept number input from the user">
<ComponentPage name="NumberInput" description="Accept number input from the user">
<ComponentTipCard>
<Text>
See <ComponentLink name="Input" /> for more information about states, shapes, sizes, and more.
Expand Down
Loading
Loading