diff --git a/.changeset/quick-avocados-hope.md b/.changeset/quick-avocados-hope.md new file mode 100644 index 000000000..3c53722a6 --- /dev/null +++ b/.changeset/quick-avocados-hope.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': minor +--- + +Removes SelectList. Updates SelectField features to support SelectList's use case via property/attribute overrides. Updates QuickSearch to use SelectField. diff --git a/.changeset/sixty-mangos-melt.md b/.changeset/sixty-mangos-melt.md new file mode 100644 index 000000000..e145b8620 --- /dev/null +++ b/.changeset/sixty-mangos-melt.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +[ScrollingValue] Add `single` prop to enable individual number display (proper handling of 9) diff --git a/.changeset/slimy-starfishes-report.md b/.changeset/slimy-starfishes-report.md new file mode 100644 index 000000000..b691f6652 --- /dev/null +++ b/.changeset/slimy-starfishes-report.md @@ -0,0 +1,6 @@ +--- +'svelte-ux': patch +--- + +Added new `IconInput` and `IconData` types to enable inclusive & seamless passing of icon arguments between components. Also provides a `asIconData` utility function for type-safe conversion. +Fixed type errors for Button & TextField's use of Icon data. \ No newline at end of file diff --git a/package.json b/package.json index 9244907f7..d99576d39 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "rimraf packages/*/dist && pnpm -r build", "lint": "pnpm -r lint", "format": "pnpm -r format", + "changeset": "changeset", "changeset:version": "changeset version", "changeset:release": "changeset publish", "up-dep": "pnpm -r up --latest" diff --git a/packages/svelte-ux/src/lib/components/Button.svelte b/packages/svelte-ux/src/lib/components/Button.svelte index 5ec447cfc..5c53299f7 100644 --- a/packages/svelte-ux/src/lib/components/Button.svelte +++ b/packages/svelte-ux/src/lib/components/Button.svelte @@ -10,12 +10,13 @@ import type { TailwindColors } from '$lib/types'; import { getComponentTheme } from './theme'; import { getButtonGroup } from './ButtonGroup.svelte'; + import { asIconData, type IconInput } from '$lib/utils/icons'; export let type: 'button' | 'submit' | 'reset' = 'button'; export let href: string | undefined = undefined; export let target: string | undefined = undefined; export let fullWidth: boolean = false; - export let icon: ComponentProps['data'] | ComponentProps | undefined = undefined; + export let icon: IconInput = undefined; export let iconOnly = icon !== undefined && $$slots.default !== true; export let actions: Actions | undefined = undefined; @@ -261,7 +262,7 @@ {#if typeof icon === 'string' || 'icon' in icon} - + {:else} {/if} diff --git a/packages/svelte-ux/src/lib/components/Icon.svelte b/packages/svelte-ux/src/lib/components/Icon.svelte index 39538dcfc..c29608ee2 100644 --- a/packages/svelte-ux/src/lib/components/Icon.svelte +++ b/packages/svelte-ux/src/lib/components/Icon.svelte @@ -14,7 +14,7 @@ export let height = size; export let viewBox = '0 0 24 24'; export let path: string | string[] = ''; - export let data: IconDefinition | string | undefined = undefined; + export let data: IconDefinition | string | null | undefined = undefined; export let svg: string | undefined = undefined; export let svgUrl: string | undefined = undefined; @@ -31,7 +31,7 @@ } = {}; const theme = getComponentTheme('Icon'); - $: if (typeof data === 'object' && 'icon' in data) { + $: if (typeof data === 'object' && data && 'icon' in data) { // Font Awesome const [_width, _height, _ligatures, _unicode, _path] = data.icon; viewBox = `0 0 ${_width} ${_height}`; @@ -59,15 +59,18 @@ $: if (svgUrl) { let promise; if (cache.has(svgUrl)) { - cache.get(svgUrl).then((text) => (svg = text)); + cache.get(svgUrl)?.then((text) => (svg = text)); } else { promise = fetch(svgUrl) .then((resp) => resp.text()) - .catch((e) => { + .catch(() => { // Failed request, remove promise so fetched again - cache.delete(svgUrl); + if (svgUrl && typeof(svgUrl) === "string") { + cache.delete(svgUrl); + } // TODO: Consider showing error icon // throw e; + return ""; }); cache.set(svgUrl, promise); promise.then((text) => { diff --git a/packages/svelte-ux/src/lib/components/QuickSearch.svelte b/packages/svelte-ux/src/lib/components/QuickSearch.svelte index c947973e7..cdef197e8 100644 --- a/packages/svelte-ux/src/lib/components/QuickSearch.svelte +++ b/packages/svelte-ux/src/lib/components/QuickSearch.svelte @@ -3,10 +3,11 @@ import Button from '$lib/components/Button.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import SelectList from '$lib/components/SelectList.svelte'; + import SelectField from '$lib/components/SelectField.svelte'; import { getComponentTheme } from './theme'; import { cls } from '$lib/utils/styles'; import { smScreen } from '$lib/stores'; + import { autoFocus, selectOnFocus } from '$lib/actions'; export let options: { name: string; value: string; group?: string }[] = []; @@ -18,10 +19,11 @@ let open = false; + let fieldActions = (node: any) => [autoFocus(node), selectOnFocus(node)]; + /* TODO: - [ ] Sticky search - - [ ] Refine SelectList / reuse with SelectField (and maybe MultiSelect) - [ ] Improve size of Dialog (move class to Dialog without breaking overflow) - [ ] Load descriptions/meta from +page.ts - [ ] Improve dialog positioning on small viewports (consistent top/bottom with max height) @@ -62,12 +64,15 @@ }} >
- (open = false)} + on:change={() => (open = false)} classes={{ field: { container: 'border-none hover:shadow-none group-focus-within:shadow-none', diff --git a/packages/svelte-ux/src/lib/components/ScrollingValue.svelte b/packages/svelte-ux/src/lib/components/ScrollingValue.svelte index d9cdda5f0..f537848f0 100644 --- a/packages/svelte-ux/src/lib/components/ScrollingValue.svelte +++ b/packages/svelte-ux/src/lib/components/ScrollingValue.svelte @@ -6,6 +6,7 @@ import { getComponentTheme } from './theme'; export let value = 0; + export let single = false; export let format: (value: number) => string | number = (value) => value; export let classes: { @@ -33,7 +34,7 @@ class={cls('col-span-full row-span-full', theme.value, classes.value)} style:transform="translateY({-100 + 100 * offset}%)" > - {format(Math.floor($displayValue + 1))} + {format(Math.floor(single && $displayValue >= 9 ? 0 : $displayValue + 1))}
['autofocus'] = undefined; + export let fieldActions: ComponentProps['actions'] = autofocus + ? (node) => [autoFocus(node, typeof autofocus === 'object' ? autofocus : undefined), selectOnFocus(node)] + : undefined; + + export let showToggleIcon = true; export let classes: { root?: string; - field?: string; + field?: string | ComponentProps['classes']; options?: string; option?: string; selected?: string; @@ -49,6 +61,9 @@ } = {}; const theme = getComponentTheme('SelectField'); + let fieldClasses: ComponentProps['classes']; + $: fieldClasses = typeof(classes.field) === "string" ? { root: classes.field } : classes.field; + // Menu props export let placement: Placement = 'bottom-start'; export let autoPlacement = true; @@ -56,6 +71,7 @@ export let resize = true; export let disableTransition = false; export let menuProps: ComponentProps | undefined = undefined; + export let optionsMode: "list" | "menu" = "menu"; $: filteredOptions = options ?? []; let searchText = ''; @@ -336,7 +352,14 @@ -
+
+ [selectOnFocus(node)]} - class={cls('h-full', theme.field, classes.field)} + actions={fieldActions} + classes={{ + root: cls('h-full'), + ...theme.field, + ...fieldClasses, + }} role="combobox" aria-expanded={open ? "true" : "false"} + aria-autocomplete={optionsMode === "menu" ? "list" : undefined} {...$$restProps} > @@ -371,16 +399,16 @@ {:else if value && clearable}
-
+ {/if}
diff --git a/packages/svelte-ux/src/lib/components/SelectList.svelte b/packages/svelte-ux/src/lib/components/SelectList.svelte deleted file mode 100644 index fe970e716..000000000 --- a/packages/svelte-ux/src/lib/components/SelectList.svelte +++ /dev/null @@ -1,420 +0,0 @@ - - - -
- [autoFocus(node), selectOnFocus(node)]} - class={cls('h-full')} - classes={{ ...theme.field, ...classes.field }} - {...$$restProps} - > - - - - - - {#if loading} - - - - {:else if readonly} - - {:else if value && clearable} -
diff --git a/packages/svelte-ux/src/lib/components/TextField.svelte b/packages/svelte-ux/src/lib/components/TextField.svelte index 92ed6184d..e4c5cb8ff 100644 --- a/packages/svelte-ux/src/lib/components/TextField.svelte +++ b/packages/svelte-ux/src/lib/components/TextField.svelte @@ -14,12 +14,13 @@ import Button from './Button.svelte'; import Icon from './Icon.svelte'; import Input from './Input.svelte'; + import { type IconInput, asIconData } from '$lib/utils/icons'; type InputValue = string | number; const dispatch = createEventDispatcher<{ clear: null; - change: { value: typeof value; inputValue: InputValue; operator: string }; + change: { value: typeof value; inputValue: InputValue; operator?: string }; }>(); export let name: string | undefined = undefined; @@ -45,8 +46,8 @@ export let base = false; export let rounded = false; export let dense = false; - export let icon: string | null = null; - export let iconRight: string | null = null; + export let icon: IconInput = null; + export let iconRight: IconInput = null; export let align: 'left' | 'center' | 'right' = 'left'; export let autofocus: boolean | Parameters[1] = false; // TODO: Find way to conditionally set type based on `multiline` value @@ -176,7 +177,7 @@ $: hasInputValue = inputValue != null && inputValue !== ''; $: hasInsetLabel = ['inset', 'float'].includes(labelPlacement) && label !== ''; - $: hasPrepend = $$slots.prepend || icon != null; + $: hasPrepend = $$slots.prepend || !!icon; $: hasAppend = $$slots.append || iconRight != null || clearable || error || operators || type === 'password'; $: hasPrefix = $$slots.prefix || type === 'currency'; @@ -247,7 +248,7 @@ {#if icon} - + {/if} @@ -417,7 +418,7 @@ {#if error} {:else if iconRight} - + {/if} {/if} diff --git a/packages/svelte-ux/src/lib/components/index.ts b/packages/svelte-ux/src/lib/components/index.ts index 4f8610c91..47e9b8a1d 100644 --- a/packages/svelte-ux/src/lib/components/index.ts +++ b/packages/svelte-ux/src/lib/components/index.ts @@ -68,7 +68,6 @@ export { default as ScrollingValue } from './ScrollingValue.svelte'; export { default as SectionDivider } from './SectionDivider.svelte'; export { default as Selection } from './Selection.svelte'; export { default as SelectField } from './SelectField.svelte'; -export { default as SelectList } from './SelectList.svelte'; export { default as Shine } from './Shine.svelte'; export { default as SpringValue } from './SpringValue.svelte'; export { default as Stack } from './Stack.svelte'; diff --git a/packages/svelte-ux/src/lib/utils/icons.ts b/packages/svelte-ux/src/lib/utils/icons.ts new file mode 100644 index 000000000..a862ed49b --- /dev/null +++ b/packages/svelte-ux/src/lib/utils/icons.ts @@ -0,0 +1,17 @@ + +import type { ComponentProps } from "svelte"; +import type { default as Icon } from "$lib/components/Icon.svelte"; +import { isLiteralObject } from "./object"; + +export type IconInput = ComponentProps['data'] | ComponentProps; +export type IconData = ComponentProps['data']; + +export function asIconData(v: IconInput): IconData { + return isIconComponentProps(v) ? v.data : v; +} + +function isIconComponentProps(v: any): v is ComponentProps { + // `iconName` is a required property of `IconDefinition`, the only other object that `IconInput` supports. + // If it is undefined, then only ComponentProps is viable. + return isLiteralObject(v) && typeof(v['iconName']) === "undefined"; +} diff --git a/packages/svelte-ux/src/routes/docs/components/ScrollingValue/+page.svelte b/packages/svelte-ux/src/routes/docs/components/ScrollingValue/+page.svelte index 993cc478d..33b7c71bb 100644 --- a/packages/svelte-ux/src/routes/docs/components/ScrollingValue/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/ScrollingValue/+page.svelte @@ -67,6 +67,14 @@ +

Individual numbers

+ + + {#each Math.abs(value).toString().split('') as num} + + {/each} + +

Formatted

@@ -86,12 +94,6 @@
-

Font-size

- - - - -

Field

With label and appended actions