finish custom class and improved options overhaul
This commit is contained in:
@@ -1,21 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { MouseEventHandler } from 'svelte/elements';
|
||||
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
||||
import Spinner from './Spinner.svelte';
|
||||
|
||||
let {
|
||||
icon,
|
||||
animate = true,
|
||||
loading,
|
||||
children,
|
||||
onclick
|
||||
}: {
|
||||
interface Props {
|
||||
icon?: string;
|
||||
animate?: boolean;
|
||||
loading?: boolean;
|
||||
class?: ClassValue | null | undefined;
|
||||
children: Snippet;
|
||||
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||
} = $props();
|
||||
}
|
||||
|
||||
let { icon, animate = true, loading, class: classValue, children, onclick }: Props = $props();
|
||||
|
||||
let iconElement = $state<HTMLSpanElement | null>(null);
|
||||
|
||||
@@ -68,7 +65,8 @@
|
||||
class={[
|
||||
'button group relative flex gap-3 overflow-hidden rounded-sm px-5',
|
||||
'text-background cursor-pointer py-3 font-medium transition-colors',
|
||||
!loading ? ' bg-primary hover:bg-secondary' : 'bg-primary/50 cursor-not-allowed '
|
||||
!loading ? ' bg-primary hover:bg-secondary' : 'bg-primary/50 cursor-not-allowed ',
|
||||
classValue
|
||||
]}
|
||||
onclick={handleButtonClick}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
interface Props {
|
||||
class?: ClassValue | null | undefined;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { class: classValue, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen items-center justify-center sm:flex">
|
||||
<div class={['min-h-screen items-center justify-center sm:flex', classValue]}>
|
||||
<main class="m-4 max-w-(--breakpoint-sm) sm:mb-[5%] sm:w-full lg:max-w-[950px]">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import { validate } from '@svelte-toolkit/validate';
|
||||
import { generateIdentifier } from './util.js';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
value?: CheckboxState;
|
||||
color?: 'default' | 'contrast';
|
||||
class?: ClassValue | null | undefined;
|
||||
children?: Snippet;
|
||||
onchange: (value: CheckboxState) => void;
|
||||
}
|
||||
@@ -21,6 +23,7 @@
|
||||
required = false,
|
||||
value = $bindable(false),
|
||||
color = 'contrast',
|
||||
class: classValue,
|
||||
children,
|
||||
onchange
|
||||
}: Props = $props();
|
||||
@@ -30,7 +33,7 @@
|
||||
let valid = $state(true);
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class={['flex items-center', classValue]}>
|
||||
<input
|
||||
type="hidden"
|
||||
{name}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { scale } from 'svelte/transition';
|
||||
import { generateIdentifier } from './util.js';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
@@ -42,6 +43,7 @@
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
notFoundMessage?: string;
|
||||
class?: ClassValue | null | undefined;
|
||||
onvalidate?: (e: InputValidatorEvent) => void;
|
||||
onchange?: (e: ComboboxChangeEvent) => void;
|
||||
}
|
||||
@@ -54,10 +56,11 @@
|
||||
matchWidth = false,
|
||||
options,
|
||||
required = false,
|
||||
invalidMessage,
|
||||
invalidMessage = 'Please select an option',
|
||||
label,
|
||||
placeholder,
|
||||
notFoundMessage = 'No results found',
|
||||
class: classValue,
|
||||
onvalidate,
|
||||
onchange
|
||||
}: Props = $props();
|
||||
@@ -324,7 +327,7 @@
|
||||
{/if}
|
||||
</Portal>
|
||||
|
||||
<div>
|
||||
<div class={classValue}>
|
||||
<!-- Combobox Label -->
|
||||
{#if label}
|
||||
<Label for={id}>{label}</Label>
|
||||
@@ -335,7 +338,7 @@
|
||||
{name}
|
||||
{id}
|
||||
value={value?.value ?? ''}
|
||||
class="hidden"
|
||||
type="hidden"
|
||||
use:validate={validateOpts}
|
||||
onvalidate={(e) => {
|
||||
valid = e.detail.valid;
|
||||
@@ -349,11 +352,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Error message if invalid -->
|
||||
{#if invalidMessage}
|
||||
<div class={['opacity-0 transition-opacity', !valid && 'opacity-100']}>
|
||||
<Label for={id} error>{invalidMessage}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={['opacity-0 transition-opacity', !valid && 'opacity-100']}>
|
||||
<Label for={id} error>{invalidMessage}</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet searchInputBox(caret: boolean = true)}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
label,
|
||||
required = false,
|
||||
invalidMessage = 'Valid date is required',
|
||||
class: classList,
|
||||
class: classValue,
|
||||
format = ['year', 'month', 'day']
|
||||
}: Props = $props();
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class={classValue}>
|
||||
<!-- Date input Label -->
|
||||
{#if label}
|
||||
<Label for={id}>{label}</Label>
|
||||
@@ -213,8 +213,7 @@
|
||||
'border-accent rounded-sm border bg-white px-[1.125rem] py-3.5 font-normal transition-colors',
|
||||
'text-text dark:border-accent/50 dark:bg-text-800 dark:text-background dark:sm:bg-slate-800',
|
||||
'ring-accent focus-within:ring-1',
|
||||
!valid && 'border-red-500!',
|
||||
classList
|
||||
!valid && 'border-red-500!'
|
||||
]}
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { MouseEventHandler } from 'svelte/elements';
|
||||
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
icon?: string;
|
||||
iconPosition?: 'left' | 'right';
|
||||
disabled?: boolean;
|
||||
class?: ClassValue | null | undefined;
|
||||
children: Snippet;
|
||||
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
let {
|
||||
icon,
|
||||
iconPosition = 'right',
|
||||
disabled = false,
|
||||
class: classValue,
|
||||
children,
|
||||
onclick
|
||||
}: {
|
||||
icon?: string;
|
||||
iconPosition?: 'left' | 'right';
|
||||
disabled?: boolean;
|
||||
children: Snippet;
|
||||
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||
} = $props();
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#snippet iconSnippet()}
|
||||
@@ -24,7 +28,8 @@
|
||||
<button
|
||||
class={[
|
||||
'text-accent hover:text-primary inline-flex cursor-pointer items-center gap-1.5 transition-colors',
|
||||
disabled && 'pointer-events-none cursor-not-allowed opacity-50'
|
||||
disabled && 'pointer-events-none cursor-not-allowed opacity-50',
|
||||
classValue
|
||||
]}
|
||||
{onclick}
|
||||
>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
for: target,
|
||||
error,
|
||||
bigError,
|
||||
children
|
||||
}: { for: string; error?: boolean; bigError?: boolean; children: Snippet } = $props();
|
||||
interface Props {
|
||||
for: string;
|
||||
error?: boolean;
|
||||
bigError?: boolean;
|
||||
class?: ClassValue | null | undefined;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { for: target, error, bigError, class: classValue, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<label
|
||||
@@ -16,7 +20,8 @@
|
||||
error && !bigError
|
||||
? 'mt-1 text-sm font-normal text-red-500'
|
||||
: 'text-text dark:text-background mb-2 text-base font-medium',
|
||||
bigError && 'text-red-500!'
|
||||
bigError && 'text-red-500!',
|
||||
classValue
|
||||
]}
|
||||
>
|
||||
{@render children()}
|
||||
|
||||
@@ -21,21 +21,25 @@
|
||||
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { MouseEventHandler } from 'svelte/elements';
|
||||
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
disabled?: boolean;
|
||||
tab?: 'current' | 'new';
|
||||
class?: ClassValue | null | undefined;
|
||||
children: Snippet;
|
||||
onclick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
let {
|
||||
href,
|
||||
disabled = false,
|
||||
tab = 'current',
|
||||
class: classValue,
|
||||
children,
|
||||
onclick
|
||||
}: {
|
||||
href: string;
|
||||
disabled?: boolean;
|
||||
tab?: 'current' | 'new';
|
||||
children: Snippet;
|
||||
onclick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
} = $props();
|
||||
}: Props = $props();
|
||||
|
||||
if (PUBLIC_BASEPATH && !href.startsWith('http://') && !href.startsWith('https://')) {
|
||||
let prefix = trim(PUBLIC_BASEPATH, '/');
|
||||
@@ -46,7 +50,8 @@
|
||||
<a
|
||||
class={[
|
||||
'text-accent hover:text-primary inline-flex items-center gap-1.5 transition-colors',
|
||||
disabled && 'pointer-events-none cursor-not-allowed opacity-50'
|
||||
disabled && 'pointer-events-none cursor-not-allowed opacity-50',
|
||||
classValue
|
||||
]}
|
||||
{href}
|
||||
target={tab === 'new' ? '_blank' : undefined}
|
||||
|
||||
@@ -1,39 +1,13 @@
|
||||
<script lang="ts" module>
|
||||
export type RadioGroupOption =
|
||||
| {
|
||||
value: string;
|
||||
label?: string;
|
||||
}
|
||||
| string;
|
||||
|
||||
/** getLabel returns the option label if it exists*/
|
||||
const getLabel = (option: RadioGroupOption): string => {
|
||||
if (typeof option === 'string') {
|
||||
return option;
|
||||
} else {
|
||||
return option.label ?? option.value;
|
||||
}
|
||||
};
|
||||
|
||||
/** getValue returns the option value */
|
||||
const getValue = (option: RadioGroupOption): string => {
|
||||
if (typeof option === 'string') {
|
||||
return option;
|
||||
} else {
|
||||
return option.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { RadioGroup, type RadioGroupProps } from 'melt/builders';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import { scale } from 'svelte/transition';
|
||||
import Label from './Label.svelte';
|
||||
import { validate } from '@svelte-toolkit/validate';
|
||||
import { getLabel, getValue, type Option } from './util.js';
|
||||
|
||||
interface Props extends RadioGroupProps {
|
||||
options: RadioGroupOption[];
|
||||
options: Option[];
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
invalidMessage?: string;
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
validate: validateOpts,
|
||||
invalidMessage = 'Field is required',
|
||||
ref = $bindable<HTMLInputElement | null>(null),
|
||||
class: classList
|
||||
class: classValue
|
||||
}: Props = $props();
|
||||
|
||||
let valid: boolean = $state(true);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class={classValue}>
|
||||
{#if label}
|
||||
<Label for={id}>{label}</Label>
|
||||
{/if}
|
||||
@@ -50,7 +50,6 @@
|
||||
onvalidate={(e) => {
|
||||
valid = e.detail.valid;
|
||||
}}
|
||||
class={classList}
|
||||
/>
|
||||
|
||||
{#if validateOpts}
|
||||
|
||||
@@ -18,21 +18,27 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ExpandableCombobox, { type ComboboxItem } from './Combobox.svelte';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
import Combobox, { type ComboboxItem } from './Combobox.svelte';
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
invalidMessage?: string;
|
||||
required?: boolean;
|
||||
class?: ClassValue | null | undefined;
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
name,
|
||||
value = $bindable(''),
|
||||
invalidMessage,
|
||||
required
|
||||
}: {
|
||||
label?: string;
|
||||
name: string;
|
||||
value?: string;
|
||||
invalidMessage?: string;
|
||||
required?: boolean;
|
||||
} = $props();
|
||||
invalidMessage = 'Please select a timezone',
|
||||
required,
|
||||
class: classValue
|
||||
}: Props = $props();
|
||||
|
||||
const sortedTimeZones = Intl.supportedValuesOf('timeZone')
|
||||
.map((timeZone) => {
|
||||
@@ -88,7 +94,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<ExpandableCombobox
|
||||
<Combobox
|
||||
{label}
|
||||
{name}
|
||||
{invalidMessage}
|
||||
@@ -97,6 +103,7 @@
|
||||
{options}
|
||||
matchWidth
|
||||
placeholder="Select a timezone"
|
||||
class={classValue}
|
||||
/>
|
||||
|
||||
{#snippet timezoneLabel(item: ComboboxItem)}
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
import Label from './Label.svelte';
|
||||
import ToggleSelect from './ToggleSelect.svelte';
|
||||
import { validate, validateInput } from '@svelte-toolkit/validate';
|
||||
import { generateIdentifier, getLabel, getValue, type Option } from './util.js';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
options: Option[];
|
||||
selected?: string[];
|
||||
name?: string;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
missingMessage?: string;
|
||||
class?: ClassValue | null | undefined;
|
||||
}
|
||||
|
||||
let {
|
||||
items,
|
||||
options,
|
||||
selected = $bindable([]),
|
||||
name,
|
||||
label,
|
||||
required,
|
||||
missingMessage
|
||||
}: {
|
||||
items: string[];
|
||||
selected?: string[];
|
||||
name?: string;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
missingMessage?: string;
|
||||
} = $props();
|
||||
missingMessage,
|
||||
class: classValue
|
||||
}: Props = $props();
|
||||
|
||||
const id = $derived(generateIdentifier('toggle-group', name));
|
||||
|
||||
let inputElement: HTMLInputElement | undefined = $state<HTMLInputElement>();
|
||||
let valid: boolean = $state(true);
|
||||
@@ -33,15 +41,16 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label && name}
|
||||
<Label for={name}>{label}</Label>
|
||||
<div class={classValue}>
|
||||
{#if label}
|
||||
<Label for={id}>{label}</Label>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{#if name}
|
||||
<input
|
||||
type="hidden"
|
||||
{id}
|
||||
{name}
|
||||
required={required ? true : false}
|
||||
use:validate={{ required, baseval: '[]' }}
|
||||
@@ -52,20 +61,19 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each items as item}
|
||||
<ToggleSelect name={name ? undefined : `toggl_${item}`} ontoggle={makeSelectedHandler(item)}>
|
||||
<span class="capitalize">{item}</span>
|
||||
{#each options as opt}
|
||||
<ToggleSelect
|
||||
name={name ? undefined : `toggle_${getValue(opt)}`}
|
||||
ontoggle={makeSelectedHandler(getValue(opt))}
|
||||
>
|
||||
<span class={[typeof opt === 'string' && 'capitalize']}>{getLabel(opt)}</span>
|
||||
</ToggleSelect>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if name}
|
||||
<div class={['mt-2 opacity-0 transition-opacity', !valid && 'opacity-100']}>
|
||||
<Label for={name} error>
|
||||
{missingMessage !== undefined && missingMessage !== ''
|
||||
? missingMessage
|
||||
: 'Field is required'}
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={['mt-2 opacity-0 transition-opacity', !valid && 'opacity-100']}>
|
||||
<Label for={id} error>
|
||||
{missingMessage !== undefined && missingMessage !== '' ? missingMessage : 'Field is required'}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { MouseEventHandler } from 'svelte/elements';
|
||||
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
name,
|
||||
selected = false,
|
||||
children,
|
||||
onclick,
|
||||
ontoggle
|
||||
}: {
|
||||
interface Props {
|
||||
name?: string;
|
||||
selected?: boolean;
|
||||
class?: ClassValue | null | undefined;
|
||||
children: Snippet;
|
||||
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||
ontoggle?: (selected: boolean) => void;
|
||||
} = $props();
|
||||
}
|
||||
|
||||
let { name, selected = false, class: classValue, children, onclick, ontoggle }: Props = $props();
|
||||
|
||||
const handleToggleSelectClick: MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||
selected = !selected; // update state
|
||||
@@ -28,9 +25,13 @@
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="rounded-3xl border px-6 py-2.5 font-medium transition-colors {selected
|
||||
? 'border-secondary bg-primary text-background hover:bg-primary-600'
|
||||
: 'border-accent text-text dark:border-accent/50 dark:bg-text dark:text-background dark:hover:bg-text-900 bg-white hover:bg-slate-100'}"
|
||||
class={[
|
||||
'rounded-3xl border px-6 py-2.5 font-medium transition-colors',
|
||||
selected
|
||||
? 'border-secondary bg-primary text-background hover:bg-primary-600'
|
||||
: 'border-accent text-text dark:border-accent/50 dark:bg-text dark:text-background dark:hover:bg-text-900 bg-white hover:bg-slate-100',
|
||||
classValue
|
||||
]}
|
||||
onclick={handleToggleSelectClick}
|
||||
>
|
||||
{@render children()}
|
||||
|
||||
@@ -10,7 +10,6 @@ export { default as Link } from './Link.svelte';
|
||||
export { default as PhoneInput } from './PhoneInput.svelte';
|
||||
export { default as PinInput } from './PinInput.svelte';
|
||||
export { default as RadioGroup } from './RadioGroup.svelte';
|
||||
export { default as Select } from './Select.svelte';
|
||||
export { default as Spinner } from './Spinner.svelte';
|
||||
export { default as StateMachine, type StateMachinePage } from './StateMachine.svelte';
|
||||
export { default as StyledRawInput } from './StyledRawInput.svelte';
|
||||
@@ -19,3 +18,4 @@ export { default as TimeInput } from './TimeInput.svelte';
|
||||
export { default as TimezoneInput } from './TimezoneInput.svelte';
|
||||
export { default as ToggleGroup } from './ToggleGroup.svelte';
|
||||
export { default as ToggleSelect } from './ToggleSelect.svelte';
|
||||
export { type Option, getLabel, getValue } from './util.js';
|
||||
|
||||
@@ -15,3 +15,31 @@ export const generateIdentifier = (prefix?: string, identifier?: string): string
|
||||
const timestampPart = Date.now().toString(36);
|
||||
return `${prefix ? prefix + '-' : ''}${randomPart}-${timestampPart}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Option type definition for a select option. Used in various components.
|
||||
*/
|
||||
export type Option =
|
||||
| {
|
||||
value: string;
|
||||
label?: string;
|
||||
}
|
||||
| string;
|
||||
|
||||
/** getLabel returns the option label if it exists*/
|
||||
export const getLabel = (option: Option): string => {
|
||||
if (typeof option === 'string') {
|
||||
return option;
|
||||
} else {
|
||||
return option.label ?? option.value;
|
||||
}
|
||||
};
|
||||
|
||||
/** getValue returns the option value */
|
||||
export const getValue = (option: Option): string => {
|
||||
if (typeof option === 'string') {
|
||||
return option;
|
||||
} else {
|
||||
return option.value;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
<ToggleGroup
|
||||
name="example-toggle-group"
|
||||
label="Toggler"
|
||||
items={['item one', 'item two', 'item three']}
|
||||
options={['item one', 'item two', { value: 'complex', label: 'Complex item' }]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user