diff --git a/.changeset/mighty-hornets-fix.md b/.changeset/mighty-hornets-fix.md new file mode 100644 index 000000000..3c8de80c2 --- /dev/null +++ b/.changeset/mighty-hornets-fix.md @@ -0,0 +1,6 @@ +--- +'create-svelte-ux': minor +'svelte-ux': minor +--- + +Migrate to @layerstack/\* packages diff --git a/.changeset/perfect-cats-doubt.md b/.changeset/perfect-cats-doubt.md new file mode 100644 index 000000000..1b0a94507 --- /dev/null +++ b/.changeset/perfect-cats-doubt.md @@ -0,0 +1,6 @@ +--- +'create-svelte-ux': minor +'svelte-ux': minor +--- + +breaking: Replace tailwind plugin `svelte-ux/plugins/tailwind.cjs` with `@layerstack/tailwind/plugin` diff --git a/packages/svelte-ux/package.json b/packages/svelte-ux/package.json index 705d8e89e..4959fa884 100644 --- a/packages/svelte-ux/package.json +++ b/packages/svelte-ux/package.json @@ -55,6 +55,11 @@ "dependencies": { "@floating-ui/dom": "^1.6.13", "@fortawesome/fontawesome-common-types": "^6.7.2", + "@layerstack/svelte-actions": "^0.0.11", + "@layerstack/svelte-stores": "^0.0.9", + "@layerstack/svelte-table": "^0.0.12", + "@layerstack/tailwind": "^0.0.11", + "@layerstack/utils": "^0.0.7", "@mdi/js": "^7.4.47", "clsx": "^2.1.1", "culori": "^4.0.1", @@ -84,8 +89,7 @@ "types": "./dist/utils/*.d.ts", "svelte": "./dist/utils/*.js" }, - "./plugins/*": "./dist/plugins/*", - "./styles/*": "./dist/styles/*" + "./plugins/*": "./dist/plugins/*" }, "files": [ "dist" diff --git a/packages/svelte-ux/src/app.d.ts b/packages/svelte-ux/src/app.d.ts index 911f8b6a0..fbfb7a9c4 100644 --- a/packages/svelte-ux/src/app.d.ts +++ b/packages/svelte-ux/src/app.d.ts @@ -23,3 +23,29 @@ declare namespace App { // interface PageState {} // interface Platform {} } + +// TODO: Can this be referenced from `@layerstack/svelte-actions` types.d.ts without breaking other things? +// https://github.com/sveltejs/language-tools/blob/master/docs/preprocessors/typescript.md +declare namespace svelteHTML { + interface HTMLAttributes { + // use:intersection + 'on:intersecting'?: (event: CustomEvent) => void; + + // use:mutate + 'on:mutate'?: (event: CustomEvent) => void; + + // use:movable + 'on:movestart'?: (event: CustomEvent<{ x: number; y: number }>) => void; + 'on:move'?: (event: CustomEvent<{ x: number; y: number; dx: number; dy: number }>) => void; + 'on:moveend'?: (event: CustomEvent<{ x: number; y: number }>) => void; + + // use:popover + 'on:clickOutside'?: (event: CustomEvent) => void; + + // use:overflow + 'on:overflow'?: (event: CustomEvent<{ overflowX: number; overflowY: number }>) => void; + + // use:longpress + 'on:longpress'?: (event: CustomEvent) => void; + } +} diff --git a/packages/svelte-ux/src/docs/Blockquote.svelte b/packages/svelte-ux/src/docs/Blockquote.svelte index 68b959c6b..79a285d23 100644 --- a/packages/svelte-ux/src/docs/Blockquote.svelte +++ b/packages/svelte-ux/src/docs/Blockquote.svelte @@ -1,7 +1,7 @@
any }[]; - actions: Map; - }; - - constructor(node: HTMLElement) { - this.node = node; - - this.changes = { - classes: [], - styles: [], - attributes: [], - eventListeners: [], - actions: new Map(), - }; - } - - addClass(className: string) { - this.node.classList.add(className); - this.changes.classes.push(className); - } - - addStyle(property: string, value: string) { - this.node.style.setProperty(property, value); - this.changes.styles.push({ property, value }); - } - - addAttribute(qualifiedName: string, value: string) { - this.node.setAttribute(qualifiedName, value); - this.changes.attributes.push({ qualifiedName, value }); - } - - addEventListener(type: string, listener: () => any) { - this.node.addEventListener(type, listener); - this.changes.eventListeners.push({ type, listener }); - } - - addAction(action: Action, options: TOptions) { - const existingAction = this.changes.actions.get(action.name); - if (existingAction) { - // Action already created, call action's update() (if available) - existingAction.update?.(options as any); - } else { - // Add new action - this.changes.actions.set(action.name, action(this.node, options as any)); - } - } - - reset() { - this.changes.classes.forEach((className) => { - this.node.classList.remove(className); - }); - - this.changes.styles.forEach(({ property, value }) => { - this.node.style.removeProperty(property); - }); - - this.changes.attributes.forEach(({ qualifiedName, value }) => { - this.node.removeAttribute(qualifiedName); - }); - - this.changes.eventListeners.forEach(({ type, listener }) => { - this.node.removeEventListener(type, listener); - }); - - // Do not destroy actions so internal state is kept - - this.changes = { - ...this.changes, - classes: [], - styles: [], - attributes: [], - eventListeners: [], - }; - } - - destroy() { - this.reset(); - - // Destroy actions (cleanup any global state like actions on `window`, etc) - for (var action of this.changes.actions.values()) { - if (action) { - action.destroy?.(); - } - } - } -} diff --git a/packages/svelte-ux/src/lib/actions/dataBackground.ts b/packages/svelte-ux/src/lib/actions/dataBackground.ts deleted file mode 100644 index 5bac6feef..000000000 --- a/packages/svelte-ux/src/lib/actions/dataBackground.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { Action } from 'svelte/action'; -import { tweened } from 'svelte/motion'; -import { scaleLinear } from 'd3-scale'; - -export type DataBackgroundOptions = { - value: number | null | undefined; - domain?: [number, number]; - - /** - * Set color explicitly. Can also use the following: - * - tailwind gradient classes (`from-blue-200 to-blue-400`) - * - Set CSS variables `--color-from` and `--color-to` - */ - color?: string | ((value: number) => string); - - /** - * Render as bar or fill (heatmap) - */ - mode?: 'bar' | 'fill'; - - /** Inset bar. Pass as [x,y] to specify per axis */ - inset?: number | [number, number]; - - enabled?: boolean; - - /** - * Show baseline - */ - baseline?: boolean; - - tweened?: Parameters>[1]; -}; - -export const dataBackground: Action = (node, options) => { - // Set duration to 0 by default to be instantaneous - const baseline = tweened(0, { duration: 0, ...options?.tweened }); - const barStart = tweened(0, { duration: 0, ...options?.tweened }); - const barEnd = tweened(0, { duration: 0, ...options?.tweened }); - - function update(options: DataBackgroundOptions) { - const { domain, color, mode = 'bar', inset, enabled } = options; - - const value = options.value ?? 0; - - if (enabled === false) { - // remove styles - node.style.backgroundImage = ''; - node.style.backgroundRepeat = ''; - node.style.backgroundSize = ''; - } else { - // Map values from 0% to 100% - const scale = scaleLinear() - .domain(domain ?? [-100, 100]) - .range([0, 100]); - - baseline.set(scale(0)); - baseline.subscribe((value) => { - node.style.setProperty('--baseline', `${value}%`); - }); - - barStart.set(scale(Math.min(0, value))); - barStart.subscribe((value) => { - node.style.setProperty('--barStart', `${value}%`); - }); - - barEnd.set(scale(Math.max(0, value))); - barEnd.subscribe((value) => { - node.style.setProperty('--barEnd', `${value}%`); - }); - - node.style.setProperty( - '--color-from', - (typeof color === 'function' ? color(value) : color) ?? 'var(--tw-gradient-from)' - ); - node.style.setProperty( - '--color-to', - (typeof color === 'function' ? color(value) : color) ?? 'var(--tw-gradient-to)' - ); - - const insetX = Array.isArray(inset) ? inset[0] : inset; - const insetY = Array.isArray(inset) ? inset[1] : inset; - - node.style.backgroundSize = ` - calc(100% - (${insetX}px * 2)) - calc(100% - (${insetY}px * 2))`; - node.style.backgroundPosition = `${insetX}px ${insetY}px`; - - // Show black baseline at `0` first, then value bar - // TODO: Handle baseline at `100%` (only negative numbers) - node.style.backgroundImage = - mode === 'bar' - ? `${ - options.baseline - ? ` - linear-gradient( - to right, - transparent var(--baseline), - currentColor var(--baseline), - currentColor calc(var(--baseline) + 1px), - transparent 0%, - transparent 100% - ), - ` - : '' - } - linear-gradient( - to right, - transparent var(--barStart), - var(--color-from) var(--barStart), - var(--color-to) var(--barEnd), - transparent 0%, - transparent 100% - ) - ` - : `linear-gradient( - to right, - var(--color-from), - var(--color-to) - )`; - - // Add `no-repeat` to fix small gap on 100% and also support `background-origin` setting (inset) - node.style.backgroundRepeat = 'no-repeat'; - } - } - - if (options) { - update(options); - } - - return { update }; -}; diff --git a/packages/svelte-ux/src/lib/actions/focus.ts b/packages/svelte-ux/src/lib/actions/focus.ts deleted file mode 100644 index c9176ca7f..000000000 --- a/packages/svelte-ux/src/lib/actions/focus.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ActionReturn } from 'svelte/action'; -import { delay } from '$lib/utils/promise.js'; - -export function focusMove( - node: HTMLElement | SVGElement, - options: { restoreFocus?: boolean; delay?: number; disabled?: boolean } = { - restoreFocus: false, - delay: 0, - disabled: false, - } -): ActionReturn { - if (!options.disabled) { - let previousActiveElement = document.activeElement; - - // Set `tabIndex` to `-1` which makes any element (ex. div) focusable programmaitcally (and mouse), but not via keyboard navigation - https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex - node.tabIndex = -1; - - // Appear to need to wait for tabIndex to update before applying focus - delay(options.delay ?? 0).then(() => { - node.focus(); - }); - - return { - destroy() { - // Restore previous active element - if ( - !options.disabled && - options.restoreFocus && - previousActiveElement instanceof HTMLElement - ) { - previousActiveElement.focus(); - } - }, - }; - } - - return {}; -} - -// TODO: Add `focusTrap` -// https://css-tricks.com/a-css-approach-to-trap-focus-inside-of-an-element/ -// export function focusTrap(node: HTMLElement): ActionReturn { -// // -// } diff --git a/packages/svelte-ux/src/lib/actions/index.ts b/packages/svelte-ux/src/lib/actions/index.ts deleted file mode 100644 index c8b262daa..000000000 --- a/packages/svelte-ux/src/lib/actions/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './dataBackground.js'; -export * from './input.js'; -export * from './layout.js'; -export * from './mouse.js'; -export * from './multi.js'; -export * from './observer.js'; -export * from './popover.js'; -export * from './portal.js'; -export * from './scroll.js'; -export * from './spotlight.js'; -export * from './sticky.js'; -export * from './styleProps.js'; -export * from './table.js'; diff --git a/packages/svelte-ux/src/lib/actions/input.ts b/packages/svelte-ux/src/lib/actions/input.ts deleted file mode 100644 index a2491f0ed..000000000 --- a/packages/svelte-ux/src/lib/actions/input.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { Action } from 'svelte/action'; -import { focusMove } from './focus.js'; - -/** - * Auto focus node when rendered. Useful for inputs - */ -export function autoFocus( - node: HTMLElement | SVGElement, - options?: Parameters['1'] -) { - // Delay by 1ms by default since Dialog/Drawer/Menu also call `focusMove` but with default `0ms` delay, and we want to focus last - return focusMove(node, { delay: 1, ...options }); -} - -/** - * Selects the text inside a text node when the node is focused - */ -export const selectOnFocus: Action = (node) => { - const handleFocus = (event: Event) => { - node.select(); - }; - - node.addEventListener('focus', handleFocus); - - return { - destroy() { - node.removeEventListener('focus', handleFocus); - }, - }; -}; - -/** - * Blurs the node when Escape is pressed - */ -export const blurOnEscape: Action = (node) => { - const handleKey = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - node.blur(); - } - }; - - (node as HTMLInputElement).addEventListener('keydown', handleKey); - - return { - destroy() { - (node as HTMLInputElement).removeEventListener('keydown', handleKey); - }, - }; -}; - -/** - * Automatically resize textarea based on content - * See: - * - https://svelte.dev/repl/ead0f1fcd2d4402bbbd64eca1d665341?version=3.14.1 - * - https://svelte.dev/repl/f1a7e24a08a54947bb4447f295c741fb?version=3.14.1 - */ -export const autoHeight: Action = (node) => { - function resize({ target }: { target: EventTarget | null }) { - if (target instanceof HTMLElement) { - target.style.height = '1px'; - target.style.height = +target.scrollHeight + 'px'; - } - } - - node.style.overflow = 'hidden'; - node.addEventListener('input', resize); - - // Resize initially - resize({ target: node }); - - return { - destroy() { - node.removeEventListener('input', resize); - }, - }; -}; - -/** - * Debounce event handler (change, input, etc) - */ -export const debounceEvent: Action< - HTMLInputElement | HTMLTextAreaElement, - { type: string; listener: (e: Event) => any; timeout?: number } | undefined | null -> = (node, options) => { - if (options) { - const { type, listener, timeout } = options; - - let lastTimeoutId: ReturnType; - - function onEvent(e: Event) { - clearTimeout(lastTimeoutId); - lastTimeoutId = setTimeout(() => { - listener(e); - }, timeout ?? 300); - } - - node.addEventListener(type, onEvent); - return { - destroy() { - node.removeEventListener(type, onEvent); - }, - }; - } -}; diff --git a/packages/svelte-ux/src/lib/actions/layout.ts b/packages/svelte-ux/src/lib/actions/layout.ts deleted file mode 100644 index 88f649c66..000000000 --- a/packages/svelte-ux/src/lib/actions/layout.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { Action } from 'svelte/action'; - -/** - * Set `height` or `max-height` to viewport height excluding node's current viewport top - */ -export const remainingViewportHeight: Action< - HTMLElement, - { max?: boolean; offset?: number; enabled?: boolean } -> = (node, options) => { - type Options = typeof options; - function update(options: Options) { - const viewportClientTop = node.getBoundingClientRect().top; - const property = options?.max ? 'max-height' : 'height'; - - if (options?.enabled === false) { - node.style.removeProperty(property); - } else { - node.style.setProperty( - property, - `calc(100vh - ${viewportClientTop}px - ${options?.offset ?? 0}px)` - ); - } - } - - update(options); - return { update }; -}; - -/** - * Set `width` or `max-width` to viewport width excluding node's current viewport left - */ -export const remainingViewportWidth: Action< - HTMLElement, - { max?: boolean; offset?: number; enabled?: boolean } -> = (node, options) => { - type Options = typeof options; - function update(options: Options) { - // TODO: Find way to watch/update when viewport location changes (ex. closing side drawer). Resizer observer does not work for these cases. Using the absolute positioned sentinel element sounds promising: https://stackoverflow.com/questions/40251082/an-event-or-observer-for-changes-to-getboundingclientrect - const viewportClientLeft = node.getBoundingClientRect().left; - - const property = options?.max ? 'max-width' : 'width'; - - if (options?.enabled === false) { - node.style.removeProperty(property); - } else { - node.style.setProperty( - property, - `calc(100vw - ${viewportClientLeft}px - ${options?.offset ?? 0}px)` - ); - } - } - - update(options); - return { update }; -}; - -/** - * Watch for overflow changes (x or y) and dispatch `overflowX` / `overflowY` events with amount - */ -export const overflow: Action = (node) => { - let overflowX = 0; - let overflowY = 0; - - function update() { - const prevOverflowX = overflowX; - overflowX = node.scrollWidth - node.clientWidth; - - const prevOverflowY = overflowY; - overflowY = node.scrollHeight - node.clientHeight; - - if (overflowX !== prevOverflowX || overflowY !== prevOverflowY) { - node.dispatchEvent( - new CustomEvent('overflow', { - detail: { - overflowX, - overflowY, - }, - }) - ); - } - } - - // Update when node resized (and on initial mount) - const resizeObserver = new ResizeObserver((entries, observer) => { - update(); - }); - resizeObserver.observe(node); - - // Update when new children (or grandchildren) are added/removed - const mutationObserver = new MutationObserver((entries, observer) => { - update(); - }); - mutationObserver.observe(node, { childList: true, subtree: true /*attributes: true, */ }); // TODO: Attributes without filter cause browser to lock up - - return { - update, - destroy() { - resizeObserver.disconnect(); - mutationObserver.disconnect(); - }, - }; -}; diff --git a/packages/svelte-ux/src/lib/actions/mouse.ts b/packages/svelte-ux/src/lib/actions/mouse.ts deleted file mode 100644 index 72a224904..000000000 --- a/packages/svelte-ux/src/lib/actions/mouse.ts +++ /dev/null @@ -1,220 +0,0 @@ -import type { Action } from 'svelte/action'; - -/** - * Dispatch event after element has been pressed for a duration of time - */ -export const longpress: Action = (node, duration) => { - let timeoutID: number; - - function onMouseDown() { - timeoutID = window.setTimeout(() => { - node.dispatchEvent(new CustomEvent('longpress')); - }, duration); - } - - function onMouseUp() { - clearTimeout(timeoutID); - } - - node.addEventListener('mousedown', onMouseDown); - node.addEventListener('mouseup', onMouseUp); - - return { - update(newDuration: number) { - duration = newDuration; - }, - destroy() { - node.removeEventListener('mousedown', onMouseDown); - node.removeEventListener('mouseup', onMouseUp); - }, - }; -}; - -// /** -// * Dispatch event similar to `click` but only if target is same between `mousedown` and `mouseup` (ie. ignore if drag from within input to body) -// */ -// export const clickWithin: Action = (node, handle) => { -// let clickTarget: EventTarget | null = null; - -// function onMouseDown(e: MouseEvent) { -// clickTarget = e.target; -// } - -// function onMouseUp(e: MouseEvent) { -// if (e.target instanceof HTMLElement && clickTarget === e.target) { -// handle?.(e); -// node.dispatchEvent(new CustomEvent('clickWithin')); -// } -// clickTarget = null; -// } - -// node.addEventListener('mousedown', onMouseDown); -// node.addEventListener('mouseup', onMouseUp); - -// return { -// destroy() { -// node.removeEventListener('mousedown', onMouseDown); -// node.removeEventListener('mouseup', onMouseUp); -// }, -// }; -// }; - -type MovableOptions = { - /** - * Number of pixels to step - */ - step?: number; - - /** - * Percentage of parent element's pixels to step - */ - stepPercent?: number; - - axis?: 'x' | 'y' | 'xy'; -}; - -/** - * Track mouse position changes from mouse down on node to mouse up - */ -export const movable: Action = (node, options = {}) => { - let lastX = 0; - let lastY = 0; - - function onMouseDown(event: MouseEvent) { - lastX = event.clientX; - lastY = event.clientY; - - node.dispatchEvent( - new CustomEvent('movestart', { - detail: { x: lastX, y: lastY }, - }) - ); - - window.addEventListener('mousemove', onMouseMove); - window.addEventListener('mouseup', onMouseUp); - } - - function onMouseMove(event: MouseEvent) { - // TODO: Handle page scroll? clientX/Y is based on viewport (apply to parent?) - let dx = event.clientX - lastX; - let dy = event.clientY - lastY; - - const xEnabled = options?.axis?.includes('x') ?? true; - const yEnabled = options?.axis?.includes('y') ?? true; - - if (options.step) { - if (Math.abs(dx) >= options.step) { - const overStep = dx % options.step; - dx = dx - overStep; - lastX = event.clientX - overStep; - } else { - dx = 0; - } - - if (Math.abs(dy) >= options.step) { - const overStep = dy % options.step; - dy = dy - overStep; - lastY = event.clientY - overStep; - } else { - dy = 0; - } - } else if (options.stepPercent) { - const parentWidth = node.parentElement?.offsetWidth ?? 0; - const parentHeight = node.parentElement?.offsetHeight ?? 0; - - if (Math.abs(dx / parentWidth) >= options.stepPercent) { - const overStep = dx % (parentWidth * options.stepPercent); - dx = dx - overStep; - lastX = event.clientX - overStep; - } else { - dx = 0; - } - - if (Math.abs(dy / parentHeight) >= options.stepPercent) { - const overStep = dy % (parentHeight * options.stepPercent); - dy = dy - overStep; - lastY = event.clientY - overStep; - } else { - dy = 0; - } - } else { - lastX = event.clientX; - lastY = event.clientY; - } - - if ((xEnabled && dx) || (yEnabled && dy)) { - node.dispatchEvent( - new CustomEvent('move', { - detail: { x: lastX, y: lastY, dx: xEnabled ? dx : 0, dy: yEnabled ? dy : 0 }, - }) - ); - } else { - // Not enough change - } - } - - function onMouseUp(event: MouseEvent) { - lastX = event.clientX; - lastY = event.clientY; - - node.dispatchEvent( - new CustomEvent('moveend', { - detail: { x: lastX, y: lastY }, - }) - ); - - window.removeEventListener('mousemove', onMouseMove); - window.removeEventListener('mouseup', onMouseUp); - } - - node.addEventListener('mousedown', onMouseDown); - - return { - destroy() { - node.removeEventListener('mousedown', onMouseDown); - }, - }; -}; - -// TODO: Use options -type MouseCoordsOptions = { - target: HTMLElement; - cssVars: boolean; - context: 'viewport' | 'relative'; -}; - -/** Set relative mouse coordinates as --x/--y CSS variables */ -export const mouseCoords: Action = (node, target = node) => { - function onMouseMove(e: MouseEvent) { - // Mouse coordinates relative to node instead of viewport (`e.offsetX/Y` changes based on target/children) - const rect = node.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - node.style.setProperty('--x', `${x}px`); - node.style.setProperty('--y', `${y}px`); - } - - function onMouseLeave() { - // node.style.removeProperty('--x'); - // node.style.removeProperty('--y'); - node.style.setProperty('--x', `-9999px`); - node.style.setProperty('--y', `-9999px`); - } - - // Init x/y values, clear on exit - onMouseLeave(); - - target.addEventListener('mousemove', onMouseMove); - node.addEventListener('mouseleave', onMouseLeave); - - return { - update(target: HTMLElement | undefined) { - target = target; - }, - destroy() { - target.removeEventListener('mousedown', onMouseMove); - node.removeEventListener('mouseleave', onMouseLeave); - }, - }; -}; diff --git a/packages/svelte-ux/src/lib/actions/multi.ts b/packages/svelte-ux/src/lib/actions/multi.ts deleted file mode 100644 index 45d6d6698..000000000 --- a/packages/svelte-ux/src/lib/actions/multi.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { ActionReturn } from 'svelte/action'; - -export type Actions = ( - node: TNode -) => (ActionReturn | undefined | null | void)[]; - -/** - * Helper action to handle multiple actions as a single action. Useful for adding actions for custom components - */ -export function multi( - node: TNode, - actions?: Actions -): ActionReturn | undefined> | undefined { - let destroyFuncs: ActionReturn['destroy'][] = []; - - function update() { - destroy(); - if (actions) { - destroyFuncs = actions(node) - .filter((x) => x) - .map((x) => (x ? x.destroy : () => {})); - } - } - - function destroy() { - destroyFuncs.forEach((fn) => fn?.()); - } - - if (actions?.length) { - update(); - return { update, destroy }; - } -} diff --git a/packages/svelte-ux/src/lib/actions/observer.ts b/packages/svelte-ux/src/lib/actions/observer.ts deleted file mode 100644 index 8721817cb..000000000 --- a/packages/svelte-ux/src/lib/actions/observer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { getScrollParent } from '$lib/utils/dom.js'; -import type { Action } from 'svelte/action'; - -export const resize: Action = (node, options) => { - let observer = new ResizeObserver((entries, observer) => { - entries.forEach((entry) => { - node.dispatchEvent(new CustomEvent('resize', { detail: entry })); - }); - }); - observer.observe(node, options); - - return { - destroy() { - observer.disconnect(); - }, - }; -}; - -export const intersection: Action = ( - node, - options = {} -) => { - const scrollParent = getScrollParent(node); - // Use viewport (null) if scrollParent = `` - const root = scrollParent === document.body ? null : scrollParent; - - let observer = new IntersectionObserver( - (entries, observer) => { - const entry = entries[0]; - node.dispatchEvent(new CustomEvent('intersecting', { detail: entry })); - }, - { root, ...options } - ); - observer.observe(node); - - return { - destroy() { - observer.disconnect(); - }, - }; -}; - -export const mutate: Action = (node, options) => { - let observer: MutationObserver | null = null; - - function update(options: MutationObserverInit | undefined) { - destroy(); - observer = new MutationObserver((mutations) => { - node.dispatchEvent(new CustomEvent('mutate', { detail: mutations })); - }); - observer.observe(node, options); - } - - function destroy() { - observer?.disconnect(); - observer = null; - } - - update(options); - - return { update, destroy }; -}; diff --git a/packages/svelte-ux/src/lib/actions/popover.ts b/packages/svelte-ux/src/lib/actions/popover.ts deleted file mode 100644 index 6efeb5f74..000000000 --- a/packages/svelte-ux/src/lib/actions/popover.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { Action } from 'svelte/action'; -import { - computePosition, - autoUpdate, - flip, - offset, - shift, - autoPlacement, - size, - type Alignment, - type ComputePositionConfig, - type OffsetOptions, - type Placement, -} from '@floating-ui/dom'; - -import { portal } from './portal.js'; - -export type PopoverOptions = { - anchorEl?: Element | HTMLElement; - placement?: Placement; - offset?: OffsetOptions; - padding?: number; - autoPlacement?: boolean; - matchWidth?: boolean; - resize?: boolean | 'width' | 'height'; -}; - -export const popover: Action = (node, options) => { - const popoverEl = node; - const anchorEl = options?.anchorEl ?? node.parentElement; - - if (!anchorEl) { - return; - } - - const cleanup = autoUpdate(anchorEl, popoverEl as HTMLElement, () => { - // Only allow autoPlacement to swap sides (ex. top/bottom) and not also axises (ex. left/right). Matches flip behavor - const alignment = - options?.autoPlacement && options?.placement - ? (options?.placement.split('-')[1] as Alignment) - : undefined; - const allowedPlacements = - options?.autoPlacement && options?.placement - ? [options?.placement, getOppositePlacement(options?.placement)] - : undefined; - - const positionOptions: ComputePositionConfig = { - placement: options?.placement, - middleware: [ - offset(options?.offset), - options?.autoPlacement ? autoPlacement({ alignment, allowedPlacements }) : flip(), - options?.resize && - size({ - padding: options?.padding, - apply({ availableWidth, availableHeight, elements }) { - Object.assign(elements.floating.style, { - ...((options?.resize === true || options?.resize === 'width') && { - maxWidth: `${availableWidth}px`, - }), - ...((options?.resize === true || options?.resize === 'height') && { - maxHeight: `${availableHeight}px`, - }), - }); - }, - }), - shift({ padding: options?.padding }), - ], - }; - computePosition(anchorEl, popoverEl as HTMLElement, positionOptions).then(({ x, y }) => { - Object.assign((popoverEl as HTMLElement).style, { - left: `${x}px`, - top: `${y}px`, - ...(options?.matchWidth && { - width: `${(anchorEl as HTMLElement).offsetWidth}px`, - }), - }); - }); - }); - - // Used to track if the mouse changed targets between `mousedown` and `mouseup` (ex. drag from within input to body). Better control than `click` - let clickTarget: EventTarget | null = null; - function onMouseDown(e: MouseEvent) { - clickTarget = e.target; - } - document.addEventListener('mousedown', onMouseDown); - - function onMouseUp(e: MouseEvent) { - if ( - e.target instanceof HTMLElement && - clickTarget === e.target && - !anchorEl?.contains(e.target) && - !popoverEl.contains(e.target) - ) { - node.dispatchEvent(new CustomEvent('clickOutside')); - } - } - document.addEventListener('mouseup', onMouseUp); - - const portalResult = portal(node as HTMLElement, {}); - - return { - destroy() { - cleanup(); - portalResult?.destroy?.(); - document.removeEventListener('mousedown', onMouseDown); - document.removeEventListener('mouseup', onMouseUp); - }, - }; -}; - -// See: https://github.com/floating-ui/floating-ui/blob/master/packages/core/src/utils/getOppositePlacement.ts (not exported) -const hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; - -export function getOppositePlacement(placement: T): T { - return placement.replace(/left|right|bottom|top/g, (matched) => (hash as any)[matched]) as T; -} diff --git a/packages/svelte-ux/src/lib/actions/portal.ts b/packages/svelte-ux/src/lib/actions/portal.ts deleted file mode 100644 index d699a1324..000000000 --- a/packages/svelte-ux/src/lib/actions/portal.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Action } from 'svelte/action'; - -export type PortalOptions = - | { - enabled?: boolean; - target?: HTMLElement | string; - } - | boolean; - -type PortalTargets = { - fallbackTarget: Element | null; - originalParent: HTMLElement | null; -}; - -/** - * Render component outside current DOM hierarchy - */ -export const portal: Action = (node, options) => { - const targets = { - // prefer ancestors, but it doesn't have to be an ancestor - fallbackTarget: node.closest('.PortalTarget') ?? document.querySelector('.PortalTarget'), - originalParent: node.parentElement, - }; - let currentTarget = moveNode(node, options, targets); - - return { - update(options) { - currentTarget = moveNode(node, options, targets); - }, - destroy() { - // If target still contains node that was moved, remove it. Not sure if required - if (currentTarget && node.parentElement === currentTarget) { - currentTarget.removeChild(node); - } - }, - }; -}; - -function moveNode( - node: HTMLElement, - options: PortalOptions = {}, - targets: PortalTargets -): Element | null { - const enabled = typeof options === 'boolean' ? options : options.enabled; - if (enabled === false) { - // Put it back where it came from - if (targets.originalParent !== node.parentElement) { - targets.originalParent?.appendChild(node); - } - return targets.originalParent; - } - - const target = getTarget(options, targets.fallbackTarget); - target?.appendChild(node); - return target; -} - -function getTarget(options: PortalOptions = {}, fallbackTarget: Element | null): Element | null { - const target = typeof options === 'object' ? options.target : undefined; - if (target instanceof HTMLElement) { - return target; - } else if (typeof target === 'string') { - return document.querySelector(target); - } else { - return fallbackTarget ?? document.body; - } -} diff --git a/packages/svelte-ux/src/lib/actions/scroll.ts b/packages/svelte-ux/src/lib/actions/scroll.ts deleted file mode 100644 index e5c6f4687..000000000 --- a/packages/svelte-ux/src/lib/actions/scroll.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { Action, ActionReturn } from 'svelte/action'; -import { isVisibleInScrollParent, scrollIntoView as scrollIntoViewUtil } from '../utils/dom.js'; -import type { EventWithTarget } from '../types/typeHelpers.js'; - -export type ScrollIntoViewOptions = { - condition: boolean | ((node: HTMLElement) => boolean); - /** Only scroll if needed (not visible in scroll parent). Similar to non-standard `scrollIntoViewIfNeeded()` */ - onlyIfNeeded?: boolean; - initial?: boolean; - delay?: number; -}; - -export const scrollIntoView: Action = ( - node, - options -) => { - function update(options?: ScrollIntoViewOptions): void { - const condition = - typeof options?.condition === 'boolean' ? options.condition : options?.condition(node); - - const needed = options?.onlyIfNeeded ? !isVisibleInScrollParent(node) : true; - - if (condition && needed) { - setTimeout(() => { - scrollIntoViewUtil(node); - }, options?.delay ?? 0); - } - } - - if (options?.initial !== false) { - update(options); - } - return { update }; -}; - -type ScrollShadowOptions = Partial< - Record< - 'top' | 'bottom' | 'left' | 'right', - { color?: string; offset?: number; blur?: number; spread?: number; scrollRatio?: number } - > ->; - -export const scrollShadow: Action = ( - node, - options -) => { - const defaultOptions = { - offset: 10, - blur: 6, - spread: -7, - color: 'rgba(0,0,0,0.2)', - scrollRatio: 5, - }; - - const resolvedOptions = { - top: { - ...defaultOptions, - ...options?.top, - }, - bottom: { - ...defaultOptions, - ...options?.bottom, - }, - left: { - ...defaultOptions, - ...options?.left, - }, - right: { - ...defaultOptions, - ...options?.right, - }, - }; - - function onScroll(e: EventWithTarget) { - const target = (e.currentTarget ?? e.target) as HTMLElement | null; - - if (!target) { - return; - } - - const { clientWidth, clientHeight, scrollWidth, scrollHeight, scrollTop, scrollLeft } = target; - - const verticalScrollPercent = scrollTop / (scrollHeight - clientHeight); - const horizontalScrollPercent = scrollLeft / (scrollWidth - clientWidth); - - const shadows = []; - - // Top shadow - if (verticalScrollPercent > 0) { - let { offset, blur, spread, color, scrollRatio } = resolvedOptions.top; - offset = Math.min(scrollTop / scrollRatio, offset); - shadows.push(`inset 0px ${offset}px ${blur}px ${spread}px ${color}`); - } - - // Bottom shadow - if (verticalScrollPercent < 1) { - let { offset, blur, spread, color, scrollRatio } = resolvedOptions.bottom; - offset = Math.min((scrollHeight - clientHeight - scrollTop) / scrollRatio, offset); - shadows.push(`inset 0px -${offset}px ${blur}px ${spread}px ${color}`); - } - - // Left shadow - if (horizontalScrollPercent > 0) { - let { offset, blur, spread, color, scrollRatio } = resolvedOptions.left; - offset = Math.min(scrollLeft / scrollRatio, offset); - shadows.push(`inset ${offset}px 0px ${blur}px ${spread}px ${color}`); - } - - // Right shadow - if (horizontalScrollPercent < 1) { - let { offset, blur, spread, color, scrollRatio } = resolvedOptions.right; - offset = Math.min((scrollWidth - clientWidth - scrollLeft) / scrollRatio, offset); - shadows.push(`inset -${offset}px 0px ${blur}px ${spread}px ${color}`); - } - - node.style.setProperty('--shadow', shadows.join(', ')); - - // Apply box-shadow to :after pseudo element so it's rendered on top of content - node.classList.add( - 'relative', - 'overflow-auto', - - 'after:block', - 'after:h-full', - 'after:w-full', - 'after:sticky', - 'after:top-0', - 'after:left-0', - 'after:mt-[-9999px]', - 'after:pointer-events-none', - 'after:[box-shadow:var(--shadow)]' - ); - } - node.addEventListener('scroll', onScroll, { passive: true }); - - // Update if transitions are used (ex. children with `animate:flip`) - node.addEventListener('transitionend', onScroll); - node.addEventListener('animationend', onScroll); - - // Update when node resized (and on initial mount) - let resizeObserver = new ResizeObserver((entries, observer) => { - onScroll({ target: node }); - }); - resizeObserver.observe(node); - - let mutationObserver = new MutationObserver((entries, observer) => { - onScroll({ target: node }); - }); - mutationObserver.observe(node, { childList: true, subtree: true /*attributes: true, */ }); // TODO: Attributes without filter cause browser to lock up - - return { - destroy() { - node.removeEventListener('scroll', onScroll); - node.removeEventListener('transitionend', onScroll); - node.removeEventListener('animationend', onScroll); - resizeObserver.disconnect(); - mutationObserver.disconnect(); - }, - }; -}; - -type ScrollFadeOptions = { - length?: number; - scrollRatio?: number; -}; - -export const scrollFade: Action = (node, options) => { - const length = options?.length ?? 50; - const scrollRatio = options?.scrollRatio ?? 5; - - function onScroll(e: EventWithTarget) { - const target = (e.currentTarget ?? e.target) as HTMLElement | null; - - if (!target) { - return; - } - - const { clientWidth, clientHeight, scrollWidth, scrollHeight, scrollTop, scrollLeft } = target; - - const verticalScrollPercent = scrollTop / (scrollHeight - clientHeight); - const horizontalScrollPercent = scrollLeft / (scrollWidth - clientWidth); - - let gradient: string | null = null; - - if (scrollHeight != clientHeight) { - // Vertically scrollable - const gradients = []; - - if (verticalScrollPercent > 0) { - // Top fade - const topLength = Math.min(scrollTop / scrollRatio, length); - gradients.push(`rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) ${topLength}px`); - } - - // Bottom fade - if (verticalScrollPercent < 1) { - const bottomLength = Math.min( - (scrollHeight - clientHeight - scrollTop) / scrollRatio, - length - ); - gradients.push(`rgba(0, 0, 0, 1) calc(100% - ${bottomLength}px), rgba(0, 0, 0, 0)`); - } - - gradient = `linear-gradient(to bottom, ${gradients.join(',')})`; - } else if (scrollWidth !== clientWidth) { - // Horizontally scrollable - const gradients = []; - - if (horizontalScrollPercent > 0) { - // Left fade - const leftLength = Math.min(scrollLeft / scrollRatio, length); - gradients.push(`rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) ${leftLength}px`); - } - - // Right fade - if (horizontalScrollPercent < 1) { - const rightLength = Math.min( - (scrollWidth - clientWidth - scrollLeft) / scrollRatio, - length - ); - gradients.push(`rgba(0, 0, 0, 1) calc(100% - ${rightLength}px), rgba(0, 0, 0, 0)`); - } - - gradient = `linear-gradient(to right, ${gradients.join(',')})`; - } - node.style.webkitMaskImage = gradient ?? ''; - node.style.maskImage = gradient ?? ''; - } - node.classList.add('overflow-auto'); - node.addEventListener('scroll', onScroll, { passive: true }); - - // Update if transitions are used (ex. children with `animate:flip`) - node.addEventListener('transitionend', onScroll); - node.addEventListener('animationend', onScroll); - - // Update when node resized (and on initial mount) - let resizeObserver = new ResizeObserver((entries, observer) => { - onScroll({ target: node }); - }); - resizeObserver.observe(node); - - let mutationObserver = new MutationObserver((entries, observer) => { - onScroll({ target: node }); - }); - mutationObserver.observe(node, { childList: true, subtree: true /*attributes: true, */ }); // TODO: Attributes without filter cause browser to lock up - - return { - destroy() { - node.removeEventListener('scroll', onScroll); - node.removeEventListener('transitionend', onScroll); - node.removeEventListener('animationend', onScroll); - resizeObserver.disconnect(); - mutationObserver.disconnect(); - }, - }; -}; diff --git a/packages/svelte-ux/src/lib/actions/spotlight.ts b/packages/svelte-ux/src/lib/actions/spotlight.ts deleted file mode 100644 index 9038e71e2..000000000 --- a/packages/svelte-ux/src/lib/actions/spotlight.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { Action } from 'svelte/action'; - -type SpotlightOptions = - | { - radius?: string; - borderWidth?: string; - borderColorStops?: string; - surfaceColorStops?: string; - hover?: { - radius?: string; - borderWidth?: string; - borderColorStops?: string; - surfaceColorStops?: string; - }; - } - | undefined; - -export const spotlight: Action = (node, options) => { - if (options?.radius) { - node.style.setProperty('--default-spotlight-radius', options.radius); - } - if (options?.borderWidth) { - node.style.setProperty('--default-spotlight-border-width', options.borderWidth); - } - if (options?.borderColorStops) { - node.style.setProperty('--default-spotlight-border-color-stops', options.borderColorStops); - } - if (options?.surfaceColorStops) { - node.style.setProperty('--default-spotlight-surface-color-stops', options.surfaceColorStops); - } - - if (options?.hover?.radius) { - node.style.setProperty('--hover-spotlight-radius', options.hover.radius); - } - if (options?.hover?.borderWidth) { - node.style.setProperty('--hover-spotlight-border-width', options.hover.borderWidth); - } - if (options?.hover?.borderColorStops) { - node.style.setProperty('--hover-spotlight-border-color-stops', options.hover.borderColorStops); - } - if (options?.hover?.surfaceColorStops) { - node.style.setProperty( - '--hover-spotlight-surface-color-stops', - options.hover.surfaceColorStops - ); - } - - node.classList.add( - 'relative', - 'isolate', - - options?.radius ? '[--spotlight-radius:var(--default-spotlight-radius)]' : '', - options?.borderWidth ? '[--spotlight-border-width:var(--default-spotlight-border-width)]' : '', - options?.borderColorStops - ? '[--spotlight-border-color-stops:var(--default-spotlight-border-color-stops)]' - : '', - options?.surfaceColorStops - ? '[--spotlight-surface-color-stops:var(--default-spotlight-surface-color-stops)]' - : '', - - options?.hover?.radius ? 'hover:[--spotlight-radius:var(--hover-spotlight-radius)]' : '', - options?.hover?.borderWidth - ? 'hover:[--spotlight-border-width:var(--hover-spotlight-border-width)]' - : '', - options?.hover?.borderColorStops - ? 'hover:[--spotlight-border-color-stops:var(--hover-spotlight-border-color-stops)]' - : '', - options?.hover?.surfaceColorStops - ? 'hover:[--spotlight-surface-color-stops:var(--hover-spotlight-surface-color-stops)]' - : '', - - // Spotlight applied as :after element with 2 background gradients. padding-box for surface, and border-box for border - 'before:absolute', - 'before:inset-0', - 'before:z-[-1]', - 'before:[border:var(--spotlight-border-width)_solid_transparent]', - 'before:[background:fixed_padding-box_radial-gradient(var(--spotlight-radius)_at_var(--x,0px)_var(--y,0px),var(--spotlight-surface-color-stops)),fixed_border-box_radial-gradient(var(--spotlight-radius)_at_var(--x,0px)_var(--y,0px),var(--spotlight-border-color-stops))]' - ); - - return { - destroy() { - // - }, - }; -}; diff --git a/packages/svelte-ux/src/lib/actions/sticky.ts b/packages/svelte-ux/src/lib/actions/sticky.ts deleted file mode 100644 index 1dd4a209b..000000000 --- a/packages/svelte-ux/src/lib/actions/sticky.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { Action } from 'svelte/action'; - -import { keys } from '../types/typeHelpers.js'; -import DomTracker from './_domTracker.js'; - -export type Edge = 'top' | 'bottom' | 'left' | 'right'; - -type StickyOptions = { - [edge in Edge]?: boolean; -}; - -/* - TODO - - [ ] Consider raising a `stuck` event for styling (example: https://svelte.dev/repl/4ad71e00c86c47d29806e17f09ff0869?version=3.35.0) -*/ -export const sticky: Action = (node, options) => { - // Track changes so they can be reversed on an update - const tracker = new DomTracker(node); - - function update(options?: StickyOptions) { - if (options === undefined) { - // Default to top sticky if no options passed - options = { top: true }; - } - - // Reset state from last update - tracker.reset(); - - keys(options).forEach((edge) => { - const enabled = options![edge] ?? false; - - if (enabled) { - // TODO: Could smartly only enable once - tracker.addStyle('position', 'sticky'); - } - - if (enabled) { - switch (edge) { - case 'top': - tracker.addStyle( - 'top', - `calc(var(--sticky-top, 0px) + ${node.offsetTop}px)` // Add offsetTop to parent (for nested table headers) - ); - break; - case 'bottom': - tracker.addStyle('bottom', `calc(var(--sticky-bottom, 0px))`); - break; - case 'left': - // TODO: Determine workaround for reading `node.offsetLeft` having big performance implicaitons - tracker.addStyle( - 'left', - // `calc(var(--sticky-left, 0px) + ${node.offsetLeft}px)` // Add offsetLeft to parent (for columns after the first) - `calc(var(--sticky-left, 0px))` - ); - break; - case 'right': - tracker.addStyle('right', `calc(var(--sticky-right, 0px))`); - break; - } - } - }); - } - update(options); - - function destroy() { - // Do we always need to reset if being unmounted? - tracker.reset(); - } - - return { - update, - destroy, - }; -}; - -type StickyContextOptions = { type: 'page' | 'container' }; - -export const stickyContext: Action = (node, options) => { - const type = options?.type ?? 'page'; - - function setSticky() { - let stickyTop = 0; - let stickyBottom = 0; - - switch (type) { - case 'page': - const marginTop = getComputedStyle(node).marginTop; // Remove marginTop (offsetTop does not include margins) - stickyTop = node.offsetTop - parseInt(marginTop); - - // If any parent is overflow: 'auto', etc, remove their offset top (as they are the scroll container) - let parent = node.parentElement; - while (parent) { - const overflow = getComputedStyle(parent).overflow; - - if (overflow !== 'visible') { - stickyTop -= parent.offsetTop; - } - parent = parent.parentElement; - } - break; - - case 'container': - stickyTop = 0; - node.style.setProperty('overflow', 'scroll'); - break; - - default: - console.error(`Unexpected type: ${type}`); - } - - node.style.setProperty('--sticky-top', `${stickyTop}px`); - - // TODO: Calculate stickyBottom offset instead of always 0 (only useful for last row). Tricky as rows can be rendered one at a time (ex. HierarchyTable) - node.style.setProperty('--sticky-bottom', `${stickyBottom}px`); - } - - setSticky(); -}; diff --git a/packages/svelte-ux/src/lib/actions/styleProps.ts b/packages/svelte-ux/src/lib/actions/styleProps.ts deleted file mode 100644 index 1bd92a781..000000000 --- a/packages/svelte-ux/src/lib/actions/styleProps.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { entries, keys } from '../types/typeHelpers.js'; -import type { Action } from 'svelte/action'; - -type CSSProps = { [key: string]: string | number | boolean | null | undefined }; - -export const styleProps: Action = (node, props) => { - entries(props ?? {}).forEach(([key, value]) => { - // Ignore if null or undefined - if (value != null) { - value = typeof value === 'boolean' ? (value ? 1 : 0) : value; - node.style.setProperty(String(key), String(value)); - } - }); - - let lastProps = {}; - - return { - update(newProps: CSSProps) { - const newKeys = keys(newProps); - keys(lastProps) - .filter((key) => !newKeys.includes(key)) - .forEach((key) => node.style.removeProperty(key)); - - entries(newProps).forEach(([key, value]) => { - // Ignore if null or undefined - if (value != null) { - node.style.setProperty(String(key), String(value)); - } - if (props) { - delete props[key]; - } - }); - - lastProps = newProps; - }, - }; -}; diff --git a/packages/svelte-ux/src/lib/actions/table.ts b/packages/svelte-ux/src/lib/actions/table.ts deleted file mode 100644 index 3d401ddd0..000000000 --- a/packages/svelte-ux/src/lib/actions/table.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { Action } from 'svelte/action'; -import { merge } from 'lodash-es'; -import { extent, max, min } from 'd3-array'; - -import type { ColumnDef, ResolveContext } from '../types/table.js'; -import type tableOrderStore from '../stores/tableOrderStore.js'; - -import { dataBackground } from './dataBackground.js'; -import { sticky } from './sticky.js'; -import { getCellValue } from '../utils/table.js'; -import DomTracker from './_domTracker.js'; -import { entries } from '../types/typeHelpers.js'; - -type TableCellOptions = { - column?: ColumnDef; - rowData?: any; - rowIndex?: number; - tableData?: any[] | null; - order?: ReturnType; - overrides?: Partial; -}; - -export const tableCell: Action = (node, options) => { - // Track changes so they can be reversed on an update - const tracker = new DomTracker(node); - - function update(options: TableCellOptions) { - const { order, rowData, rowIndex, tableData } = options; - const column = merge({}, options.column, options.overrides); - - // TODO: Should we keep a stingified copy of the resolved `column` config in `lastChanges` and exit early if no changes. Maybe just add it if performance dictates - - // Reset state from last update - tracker.reset(); - - if (node.nodeName === 'TH') { - // Order by - if (order && column.orderBy !== false) { - tracker.addClass('cursor-pointer'); - - if (order) { - tracker.addEventListener('click', () => - order.onHeaderClick(new CustomEvent('headerClick', { detail: { column } })) - ); - } - } - } - - if (column.colSpan) { - tracker.addAttribute('colspan', column.colSpan.toString()); - } - if (column.rowSpan) { - tracker.addAttribute('rowspan', column.rowSpan.toString()); - } - - if (column.align) { - // Explicit column alignment - switch (column.align) { - case 'left': - tracker.addClass('text-left'); - break; - case 'center': - tracker.addClass('text-center'); - break; - case 'right': - tracker.addClass('text-right'); - break; - case 'justify': - tracker.addClass('text-justify'); - break; - } - } else if (typeof column.format === 'string') { - // Implicit column alignment based on format - switch (column.format) { - case 'currency': - case 'decimal': - case 'integer': - case 'percent': - tracker.addClass('text-right'); - break; - } - } else { - // Default column alignment - tracker.addClass('text-left'); - } - - const context: ResolveContext = { - column, - rowData, - cellValue: rowData && getCellValue(column, rowData, rowIndex ?? -1), - }; - - if (column.classes) { - if (node.nodeName === 'TH' && column.classes.th) { - const classes = getClasses(column.classes.th, context); - classes?.forEach((className) => tracker.addClass(className)); - } else if (node.nodeName === 'TD' && column.classes.td) { - const classes = getClasses(column.classes.td, context); - classes?.forEach((className) => tracker.addClass(className)); - } - } - - if (column.style) { - if (node.nodeName === 'TH' && column.style.th) { - const styleProperties = getStyleProperties(column.style.th, context); - styleProperties?.forEach(([property, value]) => { - tracker.addStyle(property, value); - }); - } else if (node.nodeName === 'TD' && column.style.td) { - const styleProperties = getStyleProperties(column.style.td, context); - styleProperties?.forEach(([property, value]) => { - tracker.addStyle(property, value); - }); - } - } - - if (column.sticky) { - if (node.nodeName === 'TH') { - // Ignore sticky bottom for header cell - tracker.addAction(sticky, { ...column.sticky, bottom: false }); - - // Increase z-index for other sticky headers (scrolled left) as well as sticky cells below (scrolled up) - // Only need to increase z-index for first and last headers (and higher than sticky data cells below them) - if (column.sticky.left || column.sticky.right) { - tracker.addClass('z-20'); - } - } - if (node.nodeName === 'TD') { - // Ignore sticky top for data cell, and only apply sticky bottom if last row - // Note: Rows are sometimes rendered one by one by Svelte (HierarchyTable) so best to set this explicitly at call site - // TODO: Once sticky/stickyContext actions supported offsetting bottom, this should be removed - const isLastRow = node.closest('table tr:last-child') === node.closest('tr'); - - tracker.addAction(sticky, { - ...column.sticky, - top: false, - bottom: column.sticky.bottom && isLastRow, - }); - - // Increase column z-index for sticky columns - if (column.sticky.left) { - tracker.addClass('z-10'); - } - if (column.sticky.right) { - tracker.addClass('z-10'); - } - } - } - - if (column.dataBackground) { - const extents = extent(tableData ?? [], (d) => getCellValue(column, d)); - - // @ts-expect-error - tracker.addAction(dataBackground, { - value: context.cellValue, - domain: tableData ? [min([0, extents[0]]), max([0, extents[1]])] : undefined, - ...(typeof column.dataBackground === 'function' - ? column.dataBackground?.({ column, cellValue: context.cellValue, rowData }) - : column.dataBackground), - }); - } - } - - function destroy() { - tracker.destroy(); - } - - if (options) { - update(options); - } - - return { - update, - destroy, - }; -}; - -function getClasses( - // @ts-expect-error - classProp: ColumnDef['classes']['td'], - context: ResolveContext -): string[] { - const resolvedClassProp = typeof classProp === 'function' ? classProp(context) : classProp; - - if (typeof resolvedClassProp === 'string') { - return resolvedClassProp - .split(' ') - .map((x) => x.trim()) - .filter((x) => x !== ''); - } else { - return resolvedClassProp; - } -} - -function getStyleProperties( - // @ts-expect-error - styleProp: ColumnDef['style']['td'], - context: ResolveContext -): string[][] { - const resolvedStyleProp = typeof styleProp === 'function' ? styleProp(context) : styleProp; - - if (typeof resolvedStyleProp === 'string') { - const styles = resolvedStyleProp - .split(';') - .map((x) => x.trim()) - .filter((x) => x !== ''); - - return styles.map((style) => { - return style.split(':').map((x) => x.trim()); - }); - } else { - return entries(resolvedStyleProp); - } -} diff --git a/packages/svelte-ux/src/lib/actions/types.d.ts b/packages/svelte-ux/src/lib/actions/types.d.ts deleted file mode 100644 index 5c7a1ff77..000000000 --- a/packages/svelte-ux/src/lib/actions/types.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// https://github.com/sveltejs/language-tools/blob/master/docs/preprocessors/typescript.md -declare namespace svelteHTML { - interface HTMLAttributes { - // use:intersection - 'on:intersecting'?: (event: CustomEvent) => void; - - // use:mutate - 'on:mutate'?: (event: CustomEvent) => void; - - // use:movable - 'on:movestart'?: (event: CustomEvent<{ x: number; y: number }>) => void; - 'on:move'?: (event: CustomEvent<{ x: number; y: number; dx: number; dy: number }>) => void; - 'on:moveend'?: (event: CustomEvent<{ x: number; y: number }>) => void; - - // use:popover - 'on:clickOutside'?: (event: CustomEvent) => void; - - // use:overflow - 'on:overflow'?: (event: CustomEvent<{ overflowX: number; overflowY: number }>) => void; - - // use:longpress - 'on:longpress'?: (event: CustomEvent) => void; - } -} diff --git a/packages/svelte-ux/src/lib/components/AppBar.svelte b/packages/svelte-ux/src/lib/components/AppBar.svelte index 73272ce92..914e78672 100644 --- a/packages/svelte-ux/src/lib/components/AppBar.svelte +++ b/packages/svelte-ux/src/lib/components/AppBar.svelte @@ -1,10 +1,10 @@ import { createEventDispatcher } from 'svelte'; import { mdiMinus, mdiPlus } from '@mdi/js'; + import { cls } from '@layerstack/tailwind'; + import { step as stepUtil } from '@layerstack/utils/number'; + import { selectOnFocus } from '@layerstack/svelte-actions'; import Button from './Button.svelte'; import TextField from './TextField.svelte'; - import { selectOnFocus } from '../actions/input.js'; import { getComponentClasses } from './theme.js'; - import { cls } from '../utils/styles.js'; - import { step as stepUtil } from '../utils/number.js'; export let value: number = 0; export let min: number | undefined = undefined; diff --git a/packages/svelte-ux/src/lib/components/Overflow.svelte b/packages/svelte-ux/src/lib/components/Overflow.svelte index 39df2c4b0..a7f9b6dbb 100644 --- a/packages/svelte-ux/src/lib/components/Overflow.svelte +++ b/packages/svelte-ux/src/lib/components/Overflow.svelte @@ -1,6 +1,7 @@ `; -} diff --git a/packages/svelte-ux/src/lib/types/index.ts b/packages/svelte-ux/src/lib/types/index.ts index 8e06588f8..a85dfd0e8 100644 --- a/packages/svelte-ux/src/lib/types/index.ts +++ b/packages/svelte-ux/src/lib/types/index.ts @@ -1,8 +1,11 @@ -import type { ThemeColors } from './typeHelpers.js'; - -export * from './table.js'; -export * from './typeHelpers.js'; -export * from './typeGuards.js'; +import type { + FlyParams, + SlideParams, + BlurParams, + FadeParams, + ScaleParams, +} from 'svelte/transition'; +import type { ThemeColors } from '@layerstack/tailwind'; export type MenuOption = { label: string; @@ -27,3 +30,5 @@ export type ButtonVariant = export type ButtonColor = ThemeColors | 'default'; export type ButtonSize = 'sm' | 'md' | 'lg'; export type ButtonRounded = boolean | 'full'; + +export type TransitionParams = BlurParams | FadeParams | FlyParams | SlideParams | ScaleParams; diff --git a/packages/svelte-ux/src/lib/types/table.ts b/packages/svelte-ux/src/lib/types/table.ts deleted file mode 100644 index edbe6336d..000000000 --- a/packages/svelte-ux/src/lib/types/table.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { DataBackgroundOptions } from '../actions/dataBackground.js'; -import type { Edge } from '../actions/sticky.js'; -import type { FormatType } from '../utils/format.js'; - -export type ResolveContext = { - column: ColumnDef; - rowData?: TData; - cellValue?: any; -}; - -export type ResolvePropType = - | T - | null - | undefined - | ((context: ResolveContext) => T | null | undefined); - -export type ColumnDef = { - name: string; - header?: string; - value?: string | ((rowData: TData, rowIndex?: number) => any); - format?: FormatType | ((value: any, rowData: TData, rowIndex: number) => string); - /** Render as HTML. Only enable if value from trusted source (else exposing to XSS vulnerability) */ - html?: boolean; - orderBy?: string | boolean | ((a: any, b: any) => number); - columns?: ColumnDef[]; - align?: 'left' | 'right' | 'center' | 'justify'; - - /** - * Apply position sticky to cell. Note: `top` only applies to header (including nested), `bottom` only applies to last data row. - * Requires `use:stickyContext` parent - */ - sticky?: { [edge in Edge]?: boolean }; - - style?: { - th?: ResolvePropType; - td?: ResolvePropType; - }; - classes?: { - th?: ResolvePropType; - td?: ResolvePropType; - }; - - dataBackground?: - | Partial - | ((context: ResolveContext) => Partial); - - /** Set by getHeaders() util */ - colSpan?: number; - /** Set by getHeaders() util */ - rowSpan?: number; - - hidden?: boolean; -}; diff --git a/packages/svelte-ux/src/lib/types/typeGuards.ts b/packages/svelte-ux/src/lib/types/typeGuards.ts deleted file mode 100644 index 17e527109..000000000 --- a/packages/svelte-ux/src/lib/types/typeGuards.ts +++ /dev/null @@ -1,68 +0,0 @@ -type EventType = MouseEvent | TouchEvent; - -// Generic type guard - https://stackoverflow.com/a/43423642/191902 -export function hasKeyOf(object: any, key: string): object is T { - if (object) { - return key in object; - } else { - return false; - } -} - -// Similar to Object.hasOwnProperty -// http://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards -export function hasProperty( - o: T, - name: string | number | symbol -): name is keyof T { - return name in o; -} - -// Typesafe way to get property names -// https://www.meziantou.net/typescript-nameof-operator-equivalent.htm -// https://schneidenbach.gitbooks.io/typescript-cookbook/nameof-operator.html -export function nameof(key: keyof T, instance?: T): keyof T { - return key; -} - -export function isNumber(val: unknown): val is number { - return typeof val === 'number'; -} - -/** - * Check if value is present (not `null`/`undefined`). Useful with `arr.filter(notNull)` - */ -export function notNull(value: T | null | undefined): value is T { - return value != null; -} - -export function isElement(elem?: Element | EventType): elem is Element { - return !!elem && elem instanceof Element; -} - -// functional definition of isSVGElement. Note that SVGSVGElements are HTMLElements -export function isSVGElement(elem?: Element): elem is SVGElement { - return !!elem && (elem instanceof SVGElement || 'ownerSVGElement' in elem); -} - -// functional definition of SVGGElement -export function isSVGSVGElement(elem?: Element | null): elem is SVGSVGElement { - return !!elem && 'createSVGPoint' in elem; -} - -export function isSVGGraphicsElement(elem?: Element | null): elem is SVGGraphicsElement { - return !!elem && 'getScreenCTM' in elem; -} - -// functional definition of TouchEvent -export function isTouchEvent(event?: EventType): event is TouchEvent { - return !!event && 'changedTouches' in event; -} - -// functional definition of event -export function isEvent(event?: EventType | Element): event is EventType { - return ( - !!event && - (event instanceof Event || ('nativeEvent' in event && event.nativeEvent instanceof Event)) - ); -} diff --git a/packages/svelte-ux/src/lib/types/typeHelpers.ts b/packages/svelte-ux/src/lib/types/typeHelpers.ts deleted file mode 100644 index 87967651a..000000000 --- a/packages/svelte-ux/src/lib/types/typeHelpers.ts +++ /dev/null @@ -1,166 +0,0 @@ -// https://basarat.gitbooks.io/typescript/docs/types/never.html#use-case-exhaustive-checks - -import type { colors } from '../styles/theme.js'; -import type { ComponentProps as SvelteComponentProps, SvelteComponent } from 'svelte'; -import type { derived, Readable } from 'svelte/store'; -import type { - FlyParams, - SlideParams, - BlurParams, - FadeParams, - ScaleParams, -} from 'svelte/transition'; - -// https://www.typescriptlang.org/docs/handbook/basic-types.html#never -export function fail(message: string): never { - throw new Error(message); -} - -/** - * Omit properties in `T` defined in `K` - * included with Typescript 3.5 - https://devblogs.microsoft.com/typescript/announcing-typescript-3-5-rc/#the-omit-helper-type - */ -export type Omit = Pick>; - -/** - * - * see: https://stackoverflow.com/a/53936938/191902 - */ -export type Merge = Omit> & N; - -// Get values of object (similar to Object.values()) -export type ValueOf = T[keyof T]; - -// Get keys of object (strongly-typed) -// Reason Object.keys() isn't like this by default due to runtime properties: https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208 -export function keys(o: T) { - return Object.keys(o) as (keyof T)[]; -} -// export const keys = Object.keys as (obj: T) => (Extract)[]; - -export type ObjectKey = string | number | symbol; - -// Get entries (array of [key, value] arrays) of object (strongly-typed) -export function entries( - o: Record -): [`${Extract}`, V][]; -export function entries(o: Map): [K, V][]; -// @ts-expect-error -export function entries(o: Record | Map) { - if (o instanceof Map) return Array.from(o.entries()) as unknown as [K, V][]; - return Object.entries(o) as unknown as [K, V][]; // TODO: Improve based on key/value pair - https://stackoverflow.com/questions/60141960/typescript-key-value-relation-preserving-object-entries-type -} - -// Get object from entries (array of [key, value] arrays) (strongly-typed) -export function fromEntries(entries: [K, V][] | Map): Record { - return Object.fromEntries(entries) as Record; -} - -// https://github.com/Microsoft/TypeScript/issues/17198#issuecomment-315400819 -export function enumKeys(E: any) { - return keys(E).filter((k) => typeof E[k as any] === 'number'); // ["A", "B"] -} -export function enumValues(E: any) { - const keys = enumKeys(E); - return keys.map((k) => E[k as any]); // [0, 1] -} - -// Recursive Map -// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 -// https://stackoverflow.com/a/49021046/191902 -export interface TreeMap extends Map {} - -/** - * Constructs a type consisting of all properties of T set to required and non-nullable (combination of Required and NonNullable) - * https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredt - */ -export type RequiredNonNullable = { [P in keyof T]-?: NonNullable }; - -/** - * Make all properties partial (full tree unlike Partial) - * see: https://stackoverflow.com/questions/47914536/use-partial-in-nested-property-with-typescript - */ -export type RecursivePartial = { - [P in keyof T]?: RecursivePartial; -}; - -// Filter properties of T that value matches type of Match -// example: export type SomeType = FilterPropKeys -// export type StringProps = ({ [P in keyof T]: T[P] extends string ? P : never })[keyof T]; -// https://github.com/microsoft/TypeScript/issues/18211#issuecomment-380862426 -export type FilterPropKeys = { - [P in keyof T]: T[P] extends Match ? P : never; -}[keyof T]; - -/** - * @deprecated ComponentProps should be imported from 'svelte' instead of 'svelte-ux', as it is now included in the main 'svelte' package. This export may be removed in a future release. - * @see https://svelte.dev/docs/svelte#types-componentprops - * @example - * ```ts - * import { ComponentProps } from 'svelte'; - * import MyComponent from './MyComponent.svelte'; - * - * type MyComponentProps = ComponentProps; - * ``` - */ -export type ComponentProps = SvelteComponentProps; -export type ComponentEvents = T extends SvelteComponent ? E : never; -export type ComponentSlots = T extends SvelteComponent ? S : never; - -// Export until `Stores` and `StoresValues` are exported from svelte - https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts#L111-L112 -export type Stores = Parameters[0]; -export type StoresValues = - T extends Readable - ? U - : { - [K in keyof T]: T[K] extends Readable ? U : never; - }; - -export type TransitionParams = BlurParams | FadeParams | FlyParams | SlideParams | ScaleParams; - -export type TailwindColors = - | 'red' - | 'orange' - | 'amber' - | 'yellow' - | 'lime' - | 'green' - | 'emerald' - | 'teal' - | 'cyan' - | 'sky' - | 'blue' - | 'indigo' - | 'violet' - | 'purple' - | 'fuchsia' - | 'pink' - | 'rose' - | 'gray'; - -export type ThemeColors = (typeof colors)[number]; - -export type EventWithTarget = Partial>; - -// Matt Pocock tips //https://www.youtube.com/watch?v=2lCCKiWGlC0 -export type Prettify = { - [K in keyof T]: T[K]; -} & {}; - -/** - * util to make sure we have handled all enum cases in a switch statement - * Just add at the end of the switch statement a `default` like this: - * - * ```ts - * switch (periodType) { - * case xxx: - * ... - * - * default: - * assertNever(periodType); // This will now report unhandled cases - * } - * ``` - */ -export function assertNever(x: never): never { - throw new Error(`Unhandled enum case: ${x}`); -} diff --git a/packages/svelte-ux/src/lib/utils/array.test.ts b/packages/svelte-ux/src/lib/utils/array.test.ts deleted file mode 100644 index 7b0df66a5..000000000 --- a/packages/svelte-ux/src/lib/utils/array.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { sumObjects } from './array.js'; -import { testDate } from './date.test.js'; - -describe('sumObjects', () => { - it('Sum array of objects ', () => { - const values = [ - { one: 1, two: 2, three: '3', extra: 'Hello' }, - { one: 2, two: '4', three: 6 }, - { one: null, two: null, three: null, four: null }, - { one: NaN, two: NaN, three: NaN, four: NaN }, - { one: 'NaN', two: 'NaN', three: 'NaN', four: 'NaN' }, - { one: '3', two: 6, four: '4', startDate: new Date(testDate) }, - ]; - - const actual = sumObjects(values); - const expected = { - one: 6, - two: 12, - three: 9, - four: 4, - extra: 0, - startDate: +new Date(testDate), - }; - - expect(actual).toEqual(expected); - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/array.ts b/packages/svelte-ux/src/lib/utils/array.ts deleted file mode 100644 index d8337c9f8..000000000 --- a/packages/svelte-ux/src/lib/utils/array.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { greatest, rollup } from 'd3-array'; - -import { propAccessor } from './object.js'; -import type { PropAccessorArg } from './object.js'; -import { entries, fromEntries } from '../types/typeHelpers.js'; - -// Helper until Array.flat is more mainstream - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat -// See also: https://lodash.com/docs/4.17.11#flatten -// https://stackoverflow.com/a/55345130/191902 -export function flatten(items: T[][]): T[] { - return items.reduce((prev, next) => prev.concat(next), []); -} - -/** - * Combine values using reducer. Returns null if all values null (unlike d3.sum) - */ -export function combine( - values: (number | null)[], - func: (total: number | null, operand: number | null) => number -) { - if (values.every((x) => x == null)) { - return null; - } - - return values.reduce(func); -} - -/** - * Sum values but maintain null if all values null (unlike d3.sum) - */ -export function sum(items: (object | null)[], prop?: PropAccessorArg) { - const getProp = propAccessor(prop); - const values = items.map((x) => getProp(x as any)); - - return combine(values, (total, operand) => (total || 0) + (operand || 0)); -} - -/** - * Sum array of objects by property - */ -export function sumObjects(items: (object | null)[], prop?: PropAccessorArg) { - const getProp = propAccessor(prop); - - const result = rollup( - items.flatMap((x) => entries(x ?? {})), - (values) => - sum(values, (d) => { - const value = Number(getProp(d[1])); - return Number.isFinite(value) ? value : 0; - }), - (d) => d[0] - ); - - return items.every(Array.isArray) ? Array.from(result.values()) : fromEntries(result); -} - -/** - * Subtract each value from previous but maintain null if all values null (unlike d3.sum) - */ -export function subtract(items: (object | null)[], prop?: PropAccessorArg) { - const getProp = propAccessor(prop); - const values = items.map((x) => getProp(x as any)); - - return combine(values, (total, operand) => (total || 0) - (operand || 0)); -} - -/** - * Average values but maintain null if all values null (unlike d3.mean) - */ -export function average(items: (object | null)[], prop?: PropAccessorArg) { - const total = sum(items, prop); - return total !== null ? total / items.length : null; -} - -/** - * Moving average. - * @see https://observablehq.com/@d3/moving-average - * @see https://mathworld.wolfram.com/MovingAverage.html - */ -export function movingAverage( - items: (object | null)[], - windowSize: number, - prop?: PropAccessorArg -) { - const getProp = propAccessor(prop); - let sum = 0; - - const means = items.map((item, i) => { - const value = getProp(item as any); - sum += value ?? 0; - - if (i >= windowSize - 1) { - const mean = sum / windowSize; - - // Remove oldest item in window for next iteration - const oldestValue = getProp(items[i - windowSize + 1] as any); - sum -= oldestValue ?? 0; - - return mean; - } else { - // Not enough values available in window yet - return null; - } - }); - - return means; -} - -/** - * Return the unique set of values (remove duplicates) - */ -export function unique(values: any[]) { - return Array.from(new Set(values)); -} - -/** - * Join values up to a maximum with `separator`, then truncate with total - */ -export function joinValues(values: string[] = [], max: number = 3, separator = ', ') { - const total = values.length; - - if (total <= max) { - return values.join(separator); - } else { - if (max === 0) { - if (values.length === 1) { - return values[0]; - } else { - return `(${total} total)`; - } - } else { - return `${values.slice(0, max).join(separator)}, ... (${total} total)`; - } - } -} - -/** - * Recursively transverse nested arrays by path - */ -export function nestedFindByPath( - arr: any[], - path: string[], - props?: { - key?: PropAccessorArg; - values?: PropAccessorArg; - }, - depth = 0 -): any { - const getKeyProp = propAccessor(props?.key ?? 'key'); - const getValuesProp = propAccessor(props?.values ?? 'values'); - - const item = arr.find((x) => getKeyProp(x) === path[depth]); - if (depth === path.length - 1) { - return item; - } else { - const children = getValuesProp(item); - if (children) { - return nestedFindByPath(getValuesProp(item), path, props, depth + 1); - } - } -} - -/** - * Recursively transverse nested arrays looking for item - */ -export function nestedFindByPredicate( - arr: any[], - predicate: (item: any, index: number) => boolean, - childrenProp?: PropAccessorArg -): any | undefined { - const getChildrenProp = propAccessor(childrenProp ?? 'children'); - - let match = arr.find(predicate); - if (match) { - return match; - } else { - for (var item of arr) { - const children = getChildrenProp(item); - if (children) { - match = nestedFindByPredicate(getChildrenProp(item), predicate, childrenProp); - if (match) { - return match; - } - } - } - } - - return undefined; -} - -export type TreeNode = { id: string; name: string; level: number; children: TreeNode[] }; - -/** - * Given a flat array of objects with a `level` property, build a nested object with `children` - */ -export function buildTree>(arr: T[]): TreeNode[] { - var levels = [{}] as Array; - arr.forEach((o) => { - levels.length = o.level; - levels[o.level - 1].children = levels[o.level - 1].children || []; - levels[o.level - 1].children?.push(o); - levels[o.level] = o; - }); - return levels[0].children ?? []; -} - -/** - * Transverse array tree in depth-first order and execute callback for each item - */ -export function walk(arr: T[], children: Function, callback: Function) { - arr.forEach((item) => { - callback(item); - - if (children(item)) { - walk(children(item), children, callback); - } - }); -} - -/** - * Build flatten array in depth-first order (using `walk`) - */ -export function flattenTree(arr: T[], children: Function) { - const flatArray: T[] = []; - walk(arr, children, (item: any) => flatArray.push(item)); - - return flatArray; -} - -export function chunk(array: any[], size: number) { - return array.reduce((acc, item, index) => { - const bucket = Math.floor(index / size); - - if (!acc[bucket]) { - acc[bucket] = []; - } - acc[bucket].push(item); - - return acc; - }, []); -} - -/** - * Get evenly spaced samples from array - * see: https://observablehq.com/@mbostock/evenly-spaced-sampling - * see also: https://observablehq.com/@jonhelfman/uniform-sampling-variants - */ -export function samples(array: any[], size: number) { - if (!((size = Math.floor(size)) > 0)) return []; // return nothing - const n = array.length; - if (!(n > size)) return [...array]; // return everything - if (size === 1) return [array[n >> 1]]; // return the midpoint - return Array.from({ length: size }, (_, i) => array[Math.round((i / (size - 1)) * (n - 1))]); -} - -/** - * Adds item at `index` and returns array - * Note: mutates, wrap with immer `produce(array, draft => addItem(draft))` for immutable - */ -export function addItem(array: any[], item: any, index: number) { - array.splice(index, 0, item); - return array; -} - -/** - * Move item `from` index `to` index and returns array - * Note: mutates, wrap with immer `produce(array, draft => moveItem(draft))` for immutable - */ -export function moveItem(array: any[], from: number, to: number) { - var item = array[from]; - array.splice(from, 1); - array.splice(to, 0, item); - return array; -} - -/** - * Remove item at `index` returns array (not removed item) - * Note: mutates, wrap with immer `produce(array, draft => removeItem(draft))` for immutable - */ -export function removeItem(array: any[], index: number) { - array.splice(index, 1); - return array; -} - -/** - * Get the greatest absolute value in an array of numbers - */ -export function greatestAbs(array: number[]) { - return greatest(array, (a, b) => Math.abs(a) - Math.abs(b)); -} diff --git a/packages/svelte-ux/src/lib/utils/date.test.ts b/packages/svelte-ux/src/lib/utils/date.test.ts deleted file mode 100644 index 51081a810..000000000 --- a/packages/svelte-ux/src/lib/utils/date.test.ts +++ /dev/null @@ -1,695 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - formatDate, - getMonthDaysByWeek, - localToUtcDate, - utcToLocalDate, - formatIntl, - formatDateWithLocale, - getPeriodTypeByCode, - getPeriodTypeCode, - getDayOfWeek, - hasDayOfWeek, - replaceDayOfWeek, - isStringDate, -} from './date.js'; -import { formatWithLocale } from './format.js'; -import { createLocaleSettings, defaultLocale } from './locale.js'; -import { - PeriodType, - type FormatDateOptions, - DayOfWeek, - type CustomIntlDateTimeFormatOptions, - DateToken, -} from './date_types.js'; -import { getWeekStartsOnFromIntl } from './dateInternal.js'; - -export const testDate = '2023-11-21'; // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) -const dt_2M_2d = new Date(2023, 10, 21); -const dt_2M_1d = new Date(2023, 10, 7); -const dt_1M_1d = new Date(2023, 2, 7); -const dt_first = new Date(2024, 1, 1); - -const dt_1M_1d_time_pm = new Date(2023, 2, 7, 14, 2, 3, 4); -const dt_1M_1d_time_am = new Date(2023, 2, 7, 1, 2, 3, 4); - -const fr = createLocaleSettings({ - locale: 'fr', - formats: { - dates: { - ordinalSuffixes: { - one: 'er', - }, - }, - }, -}); - -describe('formatDate()', () => { - it('should return empty string for null or undefined date', () => { - // @ts-expect-error - expect(formatDate(null)).equal(''); - // @ts-expect-error - expect(formatDate(undefined)).equal(''); - }); - - it('should return empty string for invalid date', () => { - // @ts-expect-error - expect(formatDate('invalid date')).equal(''); - }); - - describe('should format date for PeriodType.Day', () => { - const localDate = new Date(2023, 10, 21); - const cases = [ - ['short', defaultLocale, '11/21'], - ['short', fr, '21/11'], - ['long', defaultLocale, 'Nov 21, 2023'], - ['long', fr, '21 nov. 2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, localDate, PeriodType.Day, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date string for PeriodType.Day', () => { - const cases = [ - ['short', defaultLocale, '11/21'], - ['short', fr, '21/11'], - ['long', defaultLocale, 'Nov 21, 2023'], - ['long', fr, '21 nov. 2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.Day, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date string for DayTime, TimeOnly', () => { - const cases: [Date, PeriodType, FormatDateOptions, string[]][] = [ - [ - dt_1M_1d_time_pm, - PeriodType.DayTime, - { variant: 'short' }, - ['3/7/2023, 2:02 PM', '07/03/2023 14:02'], - ], - [ - dt_1M_1d_time_pm, - PeriodType.DayTime, - { variant: 'default' }, - ['3/7/2023, 02:02 PM', '07/03/2023 14:02'], - ], - [ - dt_1M_1d_time_pm, - PeriodType.DayTime, - { variant: 'long' }, - ['3/7/2023, 02:02:03 PM', '07/03/2023 14:02:03'], - ], - [dt_1M_1d_time_pm, PeriodType.TimeOnly, { variant: 'short' }, ['2:02 PM', '14:02']], - [dt_1M_1d_time_pm, PeriodType.TimeOnly, { variant: 'default' }, ['02:02:03 PM', '14:02:03']], - [ - dt_1M_1d_time_pm, - PeriodType.TimeOnly, - { variant: 'long' }, - ['02:02:03.004 PM', '14:02:03,004'], - ], - ]; - - for (const c of cases) { - const [date, periodType, options, [expected_default, expected_fr]] = c; - it(c.toString(), () => { - expect(formatWithLocale(defaultLocale, date, periodType, options)).equal(expected_default); - }); - - it(c.toString() + 'fr', () => { - expect(formatWithLocale(fr, date, periodType, options)).equal(expected_fr); - }); - } - }); - - describe('should format date for PeriodType.WeekSun / Mon no mather the locale', () => { - const cases = [ - [PeriodType.WeekSun, 'short', defaultLocale, '11/19 - 11/25'], - [PeriodType.WeekSun, 'short', fr, '19/11 - 25/11'], - [PeriodType.WeekSun, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.WeekSun, 'long', fr, '19/11/2023 - 25/11/2023'], - [PeriodType.WeekMon, 'long', defaultLocale, '11/20/2023 - 11/26/2023'], - [PeriodType.WeekMon, 'long', fr, '20/11/2023 - 26/11/2023'], - ] as const; - - for (const c of cases) { - const [periodType, variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, periodType, { variant })).equal(expected); - }); - } - }); - - describe('should format date for PeriodType.Week with the good weekstarton locale', () => { - const cases = [ - [PeriodType.Week, 'short', defaultLocale, '11/19 - 11/25'], - [PeriodType.Week, 'short', fr, '20/11 - 26/11'], - [PeriodType.Week, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.Week, 'long', fr, '20/11/2023 - 26/11/2023'], - - [PeriodType.Week, 'short', defaultLocale, '11/19 - 11/25'], - [PeriodType.Week, 'short', fr, '20/11 - 26/11'], - [PeriodType.Week, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.Week, 'long', fr, '20/11/2023 - 26/11/2023'], - ] as const; - - for (const c of cases) { - const [periodType, variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, periodType, { variant })).equal(expected); - }); - } - }); - - describe('should format date for PeriodType.Month', () => { - const cases = [ - ['short', defaultLocale, 'Nov'], - ['short', fr, 'nov.'], - ['long', defaultLocale, 'November'], - ['long', fr, 'novembre'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.Month, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date for PeriodType.MonthYear', () => { - const cases = [ - ['short', defaultLocale, 'Nov 23'], - ['short', fr, 'nov. 23'], - ['long', defaultLocale, 'November 2023'], - ['long', fr, 'novembre 2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.MonthYear, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date for PeriodType.Quarter', () => { - const cases = [ - ['short', defaultLocale, 'Oct - Dec 23'], - ['short', fr, 'oct. - déc. 23'], - ['long', defaultLocale, 'October - December 2023'], - ['long', fr, 'octobre - décembre 2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.Quarter, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date for PeriodType.CalendarYear', () => { - const cases = [ - ['short', defaultLocale, '23'], - ['short', fr, '23'], - ['long', defaultLocale, '2023'], - ['long', fr, '2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.CalendarYear, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date for PeriodType.FiscalYearOctober', () => { - const cases = [ - ['short', defaultLocale, '24'], - ['short', fr, '24'], - ['long', defaultLocale, '2024'], - ['long', fr, '2024'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect( - formatDateWithLocale(locales, testDate, PeriodType.FiscalYearOctober, { variant }) - ).equal(expected); - }); - } - }); - - describe('should format date for PeriodType.BiWeek1Sun', () => { - const cases = [ - ['short', defaultLocale, '11/12 - 11/25'], - ['short', fr, '12/11 - 25/11'], - ['long', defaultLocale, '11/12/2023 - 11/25/2023'], - ['long', fr, '12/11/2023 - 25/11/2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.BiWeek1Sun, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date for PeriodType.undefined', () => { - const expected = '2023-11-21T00:00:00-04:00'; - const cases = [ - ['short', defaultLocale], - ['short', fr], - ['long', defaultLocale], - ['long', fr], - ] as const; - - for (const c of cases) { - const [variant, locales] = c; - it(c.toString(), () => { - // @ts-expect-error - expect(formatDateWithLocale(locales, testDate, undefined, { variant })).equal(expected); - }); - } - }); -}); - -describe('formatIntl() tokens', () => { - const cases: [Date, CustomIntlDateTimeFormatOptions, string[]][] = [ - [dt_1M_1d, 'MM/dd/yyyy', ['03/07/2023', '07/03/2023']], - [dt_2M_2d, 'M/d/yyyy', ['11/21/2023', '21/11/2023']], - [dt_2M_1d, 'M/d/yyyy', ['11/7/2023', '07/11/2023']], - [dt_2M_1d, 'M/dd/yyyy', ['11/07/2023', '07/11/2023']], - [dt_1M_1d, 'M/d/yyyy', ['3/7/2023', '07/03/2023']], - [dt_1M_1d, 'MM/d/yyyy', ['03/7/2023', '7/03/2023']], - [dt_2M_2d, 'M/d', ['11/21', '21/11']], - [dt_2M_2d, 'MMM d, yyyy', ['Nov 21, 2023', '21 nov. 2023']], - [dt_2M_1d, 'MMM d, yyyy', ['Nov 7, 2023', '7 nov. 2023']], - [dt_2M_1d, 'MMM do, yyyy', ['Nov 7th, 2023', '7 nov. 2023']], - [dt_2M_2d, 'MMM', ['Nov', 'nov.']], - [dt_2M_2d, 'MMMM', ['November', 'novembre']], - [dt_2M_2d, 'MMM yy', ['Nov 23', 'nov. 23']], - [dt_2M_2d, 'MMMM yyyy', ['November 2023', 'novembre 2023']], - [dt_2M_2d, 'yy', ['23', '23']], - [dt_2M_2d, 'yyyy', ['2023', '2023']], - [dt_2M_2d, { dateStyle: 'full' }, ['Tuesday, November 21, 2023', 'mardi 21 novembre 2023']], - [dt_2M_2d, { dateStyle: 'long' }, ['November 21, 2023', '21 novembre 2023']], - [dt_2M_2d, { dateStyle: 'medium' }, ['Nov 21, 2023', '21 nov. 2023']], - [dt_2M_2d, { dateStyle: 'medium', withOrdinal: true }, ['Nov 21st, 2023', '21 nov. 2023']], - [dt_2M_2d, { dateStyle: 'short' }, ['11/21/23', '21/11/2023']], - [dt_1M_1d, { dateStyle: 'short' }, ['3/7/23', '07/03/2023']], - [dt_first, DateToken.DayOfMonth_withOrdinal, ['1st', '1er']], - - // time - [dt_1M_1d_time_pm, [DateToken.Hour_numeric, DateToken.Minute_numeric], ['2:02 PM', '14:02']], - [dt_1M_1d_time_am, [DateToken.Hour_numeric, DateToken.Minute_numeric], ['1:02 AM', '01:02']], - [ - dt_1M_1d_time_am, - [DateToken.Hour_numeric, DateToken.Minute_numeric, DateToken.Hour_wAMPM], - ['1:02 AM', '1:02 AM'], - ], - [ - dt_1M_1d_time_am, - [DateToken.Hour_2Digit, DateToken.Minute_2Digit, DateToken.Hour_woAMPM], - ['01:02', '01:02'], - ], - [ - dt_1M_1d_time_am, - [DateToken.Hour_numeric, DateToken.Minute_numeric, DateToken.Second_numeric], - ['1:02:03 AM', '01:02:03'], - ], - [ - dt_1M_1d_time_am, - [ - DateToken.Hour_numeric, - DateToken.Minute_numeric, - DateToken.Second_numeric, - DateToken.MiliSecond_3, - ], - ['1:02:03.004 AM', '01:02:03,004'], - ], - ]; - - for (const c of cases) { - const [date, tokens, [expected_default, expected_fr]] = c; - it(c.toString(), () => { - expect(formatIntl(defaultLocale, date, tokens)).equal(expected_default); - }); - - it(c.toString() + 'fr', () => { - expect(formatIntl(fr, date, tokens)).equal(expected_fr); - }); - } -}); - -describe('utcToLocalDate()', () => { - it('in with offset -00 => local', () => { - const utcDate = '2023-11-21T00:00:00-00:00'; - const localDate = utcToLocalDate(utcDate); - expect(localDate.toISOString()).equal('2023-11-21T04:00:00.000Z'); - }); - - it('in without offset, the utc is already +4, to local: another +4', () => { - const utcDate = '2023-11-21T00:00:00'; - const localDate = utcToLocalDate(utcDate); - expect(localDate.toISOString()).equal('2023-11-21T08:00:00.000Z'); - }); -}); - -describe('localToUtcDate()', () => { - it('in with offset -04 => UTC', () => { - const localDate = '2023-11-21T00:00:00-04:00'; - const utcDate = localToUtcDate(localDate); - expect(utcDate.toISOString()).equal('2023-11-21T00:00:00.000Z'); - }); - - it('in with offset -00 => UTC', () => { - const localDate = '2023-11-21T04:00:00-00:00'; - const utcDate = localToUtcDate(localDate); - expect(utcDate.toISOString()).equal('2023-11-21T00:00:00.000Z'); - }); - - it('in without offset == UTC', () => { - const localDate = '2023-11-21T04:00:00'; - const utcDate = localToUtcDate(localDate); - expect(utcDate.toISOString()).equal('2023-11-21T04:00:00.000Z'); - }); -}); - -describe('getMonthDaysByWeek()', () => { - it('default starting Week: Sunday', () => { - const dates = getMonthDaysByWeek(new Date(testDate)); - expect(dates).toMatchInlineSnapshot(` - [ - [ - 2023-10-29T04:00:00.000Z, - 2023-10-30T04:00:00.000Z, - 2023-10-31T04:00:00.000Z, - 2023-11-01T04:00:00.000Z, - 2023-11-02T04:00:00.000Z, - 2023-11-03T04:00:00.000Z, - 2023-11-04T04:00:00.000Z, - ], - [ - 2023-11-05T04:00:00.000Z, - 2023-11-06T04:00:00.000Z, - 2023-11-07T04:00:00.000Z, - 2023-11-08T04:00:00.000Z, - 2023-11-09T04:00:00.000Z, - 2023-11-10T04:00:00.000Z, - 2023-11-11T04:00:00.000Z, - ], - [ - 2023-11-12T04:00:00.000Z, - 2023-11-13T04:00:00.000Z, - 2023-11-14T04:00:00.000Z, - 2023-11-15T04:00:00.000Z, - 2023-11-16T04:00:00.000Z, - 2023-11-17T04:00:00.000Z, - 2023-11-18T04:00:00.000Z, - ], - [ - 2023-11-19T04:00:00.000Z, - 2023-11-20T04:00:00.000Z, - 2023-11-21T04:00:00.000Z, - 2023-11-22T04:00:00.000Z, - 2023-11-23T04:00:00.000Z, - 2023-11-24T04:00:00.000Z, - 2023-11-25T04:00:00.000Z, - ], - [ - 2023-11-26T04:00:00.000Z, - 2023-11-27T04:00:00.000Z, - 2023-11-28T04:00:00.000Z, - 2023-11-29T04:00:00.000Z, - 2023-11-30T04:00:00.000Z, - 2023-12-01T04:00:00.000Z, - 2023-12-02T04:00:00.000Z, - ], - ] - `); - }); - - it('Starting Week: Monday', () => { - const dates = getMonthDaysByWeek(new Date(testDate), 1); - expect(dates).toMatchInlineSnapshot(` - [ - [ - 2023-10-30T04:00:00.000Z, - 2023-10-31T04:00:00.000Z, - 2023-11-01T04:00:00.000Z, - 2023-11-02T04:00:00.000Z, - 2023-11-03T04:00:00.000Z, - 2023-11-04T04:00:00.000Z, - 2023-11-05T04:00:00.000Z, - ], - [ - 2023-11-06T04:00:00.000Z, - 2023-11-07T04:00:00.000Z, - 2023-11-08T04:00:00.000Z, - 2023-11-09T04:00:00.000Z, - 2023-11-10T04:00:00.000Z, - 2023-11-11T04:00:00.000Z, - 2023-11-12T04:00:00.000Z, - ], - [ - 2023-11-13T04:00:00.000Z, - 2023-11-14T04:00:00.000Z, - 2023-11-15T04:00:00.000Z, - 2023-11-16T04:00:00.000Z, - 2023-11-17T04:00:00.000Z, - 2023-11-18T04:00:00.000Z, - 2023-11-19T04:00:00.000Z, - ], - [ - 2023-11-20T04:00:00.000Z, - 2023-11-21T04:00:00.000Z, - 2023-11-22T04:00:00.000Z, - 2023-11-23T04:00:00.000Z, - 2023-11-24T04:00:00.000Z, - 2023-11-25T04:00:00.000Z, - 2023-11-26T04:00:00.000Z, - ], - [ - 2023-11-27T04:00:00.000Z, - 2023-11-28T04:00:00.000Z, - 2023-11-29T04:00:00.000Z, - 2023-11-30T04:00:00.000Z, - 2023-12-01T04:00:00.000Z, - 2023-12-02T04:00:00.000Z, - 2023-12-03T04:00:00.000Z, - ], - ] - `); - }); -}); - -describe('getWeekStartsOnFromIntl() tokens', () => { - it('by default, sunday', () => { - const val = getWeekStartsOnFromIntl(); - expect(val).toBe(DayOfWeek.Sunday); - }); - - it('For en it should be synday', () => { - const val = getWeekStartsOnFromIntl('en'); - expect(val).toBe(DayOfWeek.Sunday); - }); - - it('For fr it should be monday', () => { - const val = getWeekStartsOnFromIntl('fr'); - expect(val).toBe(DayOfWeek.Monday); - }); -}); - -describe('getPeriodTypeByCode()', () => { - it('week', () => { - const val = getPeriodTypeByCode('WEEK'); - expect(val).toBe(PeriodType.Week); - }); -}); - -describe('getPeriodTypeCode()', () => { - it('BiWeek1Sat', () => { - const val = getPeriodTypeCode(PeriodType.BiWeek1Sat); - expect(val).toBe('BIWEEK1-SAT'); - }); -}); - -describe('hasDayOfWeek()', () => { - const data = [ - // Week - [PeriodType.Week, false], - [PeriodType.WeekSun, true], - [PeriodType.WeekMon, true], - [PeriodType.WeekTue, true], - [PeriodType.WeekWed, true], - [PeriodType.WeekThu, true], - [PeriodType.WeekFri, true], - [PeriodType.WeekSat, true], - // BiWeek1 - [PeriodType.BiWeek1, false], - [PeriodType.BiWeek1Sun, true], - [PeriodType.BiWeek1Mon, true], - [PeriodType.BiWeek1Tue, true], - [PeriodType.BiWeek1Wed, true], - [PeriodType.BiWeek1Thu, true], - [PeriodType.BiWeek1Fri, true], - [PeriodType.BiWeek1Sat, true], - // BiWeek2 - [PeriodType.BiWeek2, false], - [PeriodType.BiWeek2Sun, true], - [PeriodType.BiWeek2Mon, true], - [PeriodType.BiWeek2Tue, true], - [PeriodType.BiWeek2Wed, true], - [PeriodType.BiWeek2Thu, true], - [PeriodType.BiWeek2Fri, true], - [PeriodType.BiWeek2Sat, true], - // Other - [PeriodType.Day, false], - [PeriodType.Month, false], - [PeriodType.CalendarYear, false], - ] as const; - - data.forEach(([periodType, dayOfWeek]) => { - it(PeriodType[periodType], () => { - const val = hasDayOfWeek(periodType); - expect(val).toBe(dayOfWeek); - }); - }); -}); - -describe('getDayOfWeek()', () => { - const data = [ - // Week - [PeriodType.Week, null], - [PeriodType.WeekSun, DayOfWeek.Sunday], - [PeriodType.WeekMon, DayOfWeek.Monday], - [PeriodType.WeekTue, DayOfWeek.Tuesday], - [PeriodType.WeekWed, DayOfWeek.Wednesday], - [PeriodType.WeekThu, DayOfWeek.Thursday], - [PeriodType.WeekFri, DayOfWeek.Friday], - [PeriodType.WeekSat, DayOfWeek.Saturday], - // BiWeek1 - [PeriodType.BiWeek1, null], - [PeriodType.BiWeek1Sun, DayOfWeek.Sunday], - [PeriodType.BiWeek1Mon, DayOfWeek.Monday], - [PeriodType.BiWeek1Tue, DayOfWeek.Tuesday], - [PeriodType.BiWeek1Wed, DayOfWeek.Wednesday], - [PeriodType.BiWeek1Thu, DayOfWeek.Thursday], - [PeriodType.BiWeek1Fri, DayOfWeek.Friday], - [PeriodType.BiWeek1Sat, DayOfWeek.Saturday], - // BiWeek2 - [PeriodType.BiWeek2, null], - [PeriodType.BiWeek2Sun, DayOfWeek.Sunday], - [PeriodType.BiWeek2Mon, DayOfWeek.Monday], - [PeriodType.BiWeek2Tue, DayOfWeek.Tuesday], - [PeriodType.BiWeek2Wed, DayOfWeek.Wednesday], - [PeriodType.BiWeek2Thu, DayOfWeek.Thursday], - [PeriodType.BiWeek2Fri, DayOfWeek.Friday], - [PeriodType.BiWeek2Sat, DayOfWeek.Saturday], - // Other - [PeriodType.Day, null], - [PeriodType.Month, null], - [PeriodType.CalendarYear, null], - ] as const; - - data.forEach(([periodType, dayOfWeek]) => { - it(PeriodType[periodType], () => { - const val = getDayOfWeek(periodType); - expect(val).toBe(dayOfWeek); - }); - }); -}); - -describe('replaceDayOfWeek()', () => { - const data = [ - // Week - [PeriodType.Week, DayOfWeek.Sunday, PeriodType.WeekSun], - [PeriodType.WeekSun, DayOfWeek.Sunday, PeriodType.WeekSun], - [PeriodType.WeekSun, DayOfWeek.Monday, PeriodType.WeekMon], - [PeriodType.WeekSun, DayOfWeek.Tuesday, PeriodType.WeekTue], - [PeriodType.WeekSun, DayOfWeek.Wednesday, PeriodType.WeekWed], - [PeriodType.WeekWed, DayOfWeek.Thursday, PeriodType.WeekThu], - [PeriodType.WeekWed, DayOfWeek.Friday, PeriodType.WeekFri], - [PeriodType.WeekSat, DayOfWeek.Saturday, PeriodType.WeekSat], - // BiWeek1 - [PeriodType.BiWeek1, DayOfWeek.Sunday, PeriodType.BiWeek1Sun], - [PeriodType.BiWeek1Sun, DayOfWeek.Sunday, PeriodType.BiWeek1Sun], - [PeriodType.BiWeek1Sun, DayOfWeek.Monday, PeriodType.BiWeek1Mon], - [PeriodType.BiWeek1Sun, DayOfWeek.Tuesday, PeriodType.BiWeek1Tue], - [PeriodType.BiWeek1Sun, DayOfWeek.Wednesday, PeriodType.BiWeek1Wed], - [PeriodType.BiWeek1Wed, DayOfWeek.Thursday, PeriodType.BiWeek1Thu], - [PeriodType.BiWeek1Wed, DayOfWeek.Friday, PeriodType.BiWeek1Fri], - [PeriodType.BiWeek1Sat, DayOfWeek.Saturday, PeriodType.BiWeek1Sat], - // BiWeek2 - [PeriodType.BiWeek2, DayOfWeek.Sunday, PeriodType.BiWeek2Sun], - [PeriodType.BiWeek2Sun, DayOfWeek.Sunday, PeriodType.BiWeek2Sun], - [PeriodType.BiWeek2Sun, DayOfWeek.Monday, PeriodType.BiWeek2Mon], - [PeriodType.BiWeek2Sun, DayOfWeek.Tuesday, PeriodType.BiWeek2Tue], - [PeriodType.BiWeek2Sun, DayOfWeek.Wednesday, PeriodType.BiWeek2Wed], - [PeriodType.BiWeek2Wed, DayOfWeek.Thursday, PeriodType.BiWeek2Thu], - [PeriodType.BiWeek2Wed, DayOfWeek.Friday, PeriodType.BiWeek2Fri], - [PeriodType.BiWeek2Sat, DayOfWeek.Saturday, PeriodType.BiWeek2Sat], - // Other - [PeriodType.Day, DayOfWeek.Sunday, PeriodType.Day], - [PeriodType.Month, DayOfWeek.Sunday, PeriodType.Month], - [PeriodType.CalendarYear, DayOfWeek.Sunday, PeriodType.CalendarYear], - ] as const; - - data.forEach(([periodType, dayOfWeek, expected]) => { - it(`${PeriodType[periodType]} / ${DayOfWeek[dayOfWeek]}`, () => { - const val = replaceDayOfWeek(periodType, dayOfWeek); - expect(val).toBe(expected); - }); - }); -}); - -describe('isStringDate()', () => { - it('date only', () => { - expect(isStringDate('1982-03-30')).true; - }); - - it('date with time (UTC)', () => { - expect(isStringDate('1982-03-30T11:25:59Z')).true; - }); - - it('date with time (offset)', () => { - expect(isStringDate('1982-03-30T11:25:59-04:00')).true; - }); - - it('date with time and 3 digit milliseconds (UTC)', () => { - expect(isStringDate('1982-03-30T11:25:59.123Z')).true; - }); - - it('date with time with 7 digit milliseconds (UTC)', () => { - expect(isStringDate('1982-03-30T11:25:59.1234567Z')).true; - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/date.ts b/packages/svelte-ux/src/lib/utils/date.ts deleted file mode 100644 index 518813be8..000000000 --- a/packages/svelte-ux/src/lib/utils/date.ts +++ /dev/null @@ -1,881 +0,0 @@ -import { - startOfDay, - endOfDay, - startOfWeek, - endOfWeek, - startOfMonth, - endOfMonth, - startOfQuarter, - endOfQuarter, - startOfYear, - endOfYear, - min, - max, - addMonths, - addDays, - differenceInDays, - differenceInWeeks, - differenceInMonths, - differenceInQuarters, - differenceInYears, - addWeeks, - addQuarters, - addYears, - isSameDay, - isSameWeek, - isSameMonth, - isSameQuarter, - isSameYear, - parseISO, - formatISO, -} from 'date-fns'; - -import { hasKeyOf } from '../types/typeGuards.js'; -import { chunk } from './array.js'; -import { - PeriodType, - DayOfWeek, - DateToken, - type SelectedDate, - type CustomIntlDateTimeFormatOptions, - type FormatDateOptions, - type DateFormatVariantPreset, -} from './date_types.js'; -import { defaultLocale, type LocaleSettings } from './locale.js'; -import { assertNever, entries } from '../types/typeHelpers.js'; - -export * from './date_types.js'; - -export function getDayOfWeekName(weekStartsOn: DayOfWeek, locales: string) { - // Create a date object for a specific day (0 = Sunday, 1 = Monday, etc.) - // And "7 of Jan 2024" is a Sunday - const date = new Date(2024, 0, 7 + weekStartsOn); - const formatter = new Intl.DateTimeFormat(locales, { weekday: 'short' }); - return formatter.format(date); -} - -export function getPeriodTypeName(periodType: PeriodType) { - return getPeriodTypeNameWithLocale(defaultLocale, periodType); -} - -export function getPeriodTypeNameWithLocale(settings: LocaleSettings, periodType: PeriodType) { - const { - locale: locale, - dictionary: { Date: dico }, - } = settings; - - switch (periodType) { - case PeriodType.Custom: - return 'Custom'; - - case PeriodType.Day: - return dico.Day; - case PeriodType.DayTime: - return dico.DayTime; - case PeriodType.TimeOnly: - return dico.Time; - - case PeriodType.WeekSun: - return `${dico.Week} (${getDayOfWeekName(DayOfWeek.Sunday, locale)})`; - case PeriodType.WeekMon: - return `${dico.Week} (${getDayOfWeekName(1, locale)})`; - case PeriodType.WeekTue: - return `${dico.Week} (${getDayOfWeekName(2, locale)})`; - case PeriodType.WeekWed: - return `${dico.Week} (${getDayOfWeekName(3, locale)})`; - case PeriodType.WeekThu: - return `${dico.Week} (${getDayOfWeekName(4, locale)})`; - case PeriodType.WeekFri: - return `${dico.Week} (${getDayOfWeekName(5, locale)})`; - case PeriodType.WeekSat: - return `${dico.Week} (${getDayOfWeekName(6, locale)})`; - case PeriodType.Week: - return dico.Week; - - case PeriodType.Month: - return dico.Month; - case PeriodType.MonthYear: - return dico.Month; - case PeriodType.Quarter: - return dico.Quarter; - case PeriodType.CalendarYear: - return dico.CalendarYear; - case PeriodType.FiscalYearOctober: - return dico.FiscalYearOct; - - case PeriodType.BiWeek1Sun: - return `${dico.BiWeek} (${getDayOfWeekName(0, locale)})`; - case PeriodType.BiWeek1Mon: - return `${dico.BiWeek} (${getDayOfWeekName(1, locale)})`; - case PeriodType.BiWeek1Tue: - return `${dico.BiWeek} (${getDayOfWeekName(2, locale)})`; - case PeriodType.BiWeek1Wed: - return `${dico.BiWeek} (${getDayOfWeekName(3, locale)})`; - case PeriodType.BiWeek1Thu: - return `${dico.BiWeek} (${getDayOfWeekName(4, locale)})`; - case PeriodType.BiWeek1Fri: - return `${dico.BiWeek} (${getDayOfWeekName(5, locale)})`; - case PeriodType.BiWeek1Sat: - return `${dico.BiWeek} (${getDayOfWeekName(6, locale)})`; - case PeriodType.BiWeek1: - return dico.BiWeek; - - case PeriodType.BiWeek2Sun: - return `${dico.BiWeek} 2 (${getDayOfWeekName(0, locale)})`; - case PeriodType.BiWeek2Mon: - return `${dico.BiWeek} 2 (${getDayOfWeekName(1, locale)})`; - case PeriodType.BiWeek2Tue: - return `${dico.BiWeek} 2 (${getDayOfWeekName(2, locale)})`; - case PeriodType.BiWeek2Wed: - return `${dico.BiWeek} 2 (${getDayOfWeekName(3, locale)})`; - case PeriodType.BiWeek2Thu: - return `${dico.BiWeek} 2 (${getDayOfWeekName(4, locale)})`; - case PeriodType.BiWeek2Fri: - return `${dico.BiWeek} 2 (${getDayOfWeekName(5, locale)})`; - case PeriodType.BiWeek2Sat: - return `${dico.BiWeek} 2 (${getDayOfWeekName(6, locale)})`; - case PeriodType.BiWeek2: - return `${dico.BiWeek} 2`; - - default: - assertNever(periodType); // This will now report unhandled cases - } -} - -const periodTypeMappings: Record = { - [PeriodType.Custom]: 'CUSTOM', - - [PeriodType.Day]: 'DAY', - [PeriodType.DayTime]: 'DAY-TIME', - [PeriodType.TimeOnly]: 'TIME', - - [PeriodType.WeekSun]: 'WEEK-SUN', - [PeriodType.WeekMon]: 'WEEK-MON', - [PeriodType.WeekTue]: 'WEEK-TUE', - [PeriodType.WeekWed]: 'WEEK-WED', - [PeriodType.WeekThu]: 'WEEK-THU', - [PeriodType.WeekFri]: 'WEEK-FRI', - [PeriodType.WeekSat]: 'WEEK-SAT', - [PeriodType.Week]: 'WEEK', - - [PeriodType.Month]: 'MTH', - [PeriodType.MonthYear]: 'MTH-CY', - [PeriodType.Quarter]: 'QTR', - [PeriodType.CalendarYear]: 'CY', - [PeriodType.FiscalYearOctober]: 'FY-OCT', - - [PeriodType.BiWeek1Sun]: 'BIWEEK1-SUN', - [PeriodType.BiWeek1Mon]: 'BIWEEK1-MON', - [PeriodType.BiWeek1Tue]: 'BIWEEK1-TUE', - [PeriodType.BiWeek1Wed]: 'BIWEEK1-WED', - [PeriodType.BiWeek1Thu]: 'BIWEEK1-THU', - [PeriodType.BiWeek1Fri]: 'BIWEEK1-FRI', - [PeriodType.BiWeek1Sat]: 'BIWEEK1-SAT', - [PeriodType.BiWeek1]: 'BIWEEK1', - - [PeriodType.BiWeek2Sun]: 'BIWEEK2-SUN', - [PeriodType.BiWeek2Mon]: 'BIWEEK2-MON', - [PeriodType.BiWeek2Tue]: 'BIWEEK2-TUE', - [PeriodType.BiWeek2Wed]: 'BIWEEK2-WED', - [PeriodType.BiWeek2Thu]: 'BIWEEK2-THU', - [PeriodType.BiWeek2Fri]: 'BIWEEK2-FRI', - [PeriodType.BiWeek2Sat]: 'BIWEEK2-SAT', - [PeriodType.BiWeek2]: 'BIWEEK2', -}; - -export function getPeriodTypeCode(periodType: PeriodType): string { - return periodTypeMappings[periodType]; -} - -export function getPeriodTypeByCode(code: string): PeriodType { - const element = entries(periodTypeMappings).find((c) => c[1] === code); - return parseInt(String(element?.[0] ?? '1')); -} - -export function getDayOfWeek(periodType: PeriodType): DayOfWeek | null { - if ( - (periodType >= PeriodType.WeekSun && periodType <= PeriodType.WeekSat) || - (periodType >= PeriodType.BiWeek1Sun && periodType <= PeriodType.BiWeek1Sat) || - (periodType >= PeriodType.BiWeek2Sun && periodType <= PeriodType.BiWeek2Sat) - ) { - return (periodType % 10) - 1; - } else { - return null; - } -} - -/** Replace day of week for `periodType`, if applicable */ -export function replaceDayOfWeek(periodType: PeriodType, dayOfWeek: DayOfWeek): PeriodType { - if (hasDayOfWeek(periodType)) { - return periodType - (getDayOfWeek(periodType) ?? 0) + dayOfWeek; - } else if (missingDayOfWeek(periodType)) { - return periodType + dayOfWeek + 1; - } else { - return periodType; - } -} - -/** Check if `periodType` has day of week (Sun-Sat) */ -export function hasDayOfWeek(periodType: PeriodType) { - if (periodType >= PeriodType.WeekSun && periodType <= PeriodType.WeekSat) { - return true; - } - if (periodType >= PeriodType.BiWeek1Sun && periodType <= PeriodType.BiWeek1Sat) { - return true; - } - if (periodType >= PeriodType.BiWeek2Sun && periodType <= PeriodType.BiWeek2Sat) { - return true; - } - - return false; -} - -/** Is `periodType` missing day of week (Sun-Sat) */ -export function missingDayOfWeek(periodType: PeriodType) { - return [PeriodType.Week, PeriodType.BiWeek1, PeriodType.BiWeek2].includes(periodType); -} - -export function getMonths(year = new Date().getFullYear()) { - return Array.from({ length: 12 }, (_, i) => new Date(year, i, 1)); -} - -export function getMonthDaysByWeek( - dateInTheMonth: Date, - weekStartsOn: DayOfWeek = DayOfWeek.Sunday -): Date[][] { - const startOfFirstWeek = startOfWeek(startOfMonth(dateInTheMonth), { weekStartsOn }); - const endOfLastWeek = endOfWeek(endOfMonth(dateInTheMonth), { weekStartsOn }); - - const list = []; - - let valueToAdd = startOfFirstWeek; - while (valueToAdd <= endOfLastWeek) { - list.push(valueToAdd); - valueToAdd = addDays(valueToAdd, 1); - } - - return chunk(list, 7) as Date[][]; -} - -export function getMinSelectedDate(date: SelectedDate | null | undefined) { - if (date instanceof Date) { - return date; - } else if (date instanceof Array) { - return min(date); - } else if (hasKeyOf<{ from: Date }>(date, 'from')) { - return date.from; - } else { - return null; - } -} - -export function getMaxSelectedDate(date: SelectedDate | null | undefined) { - if (date instanceof Date) { - return date; - } else if (date instanceof Array) { - return max(date); - } else if (hasKeyOf<{ from: Date }>(date, 'to')) { - return date.to; - } else { - return null; - } -} - -/* - * Fiscal Year - */ - -export function getFiscalYear(date: Date | null = new Date(), options?: { startMonth?: number }) { - if (date === null) { - // null explicitly passed in (default value overridden) - return NaN; - } - - const startMonth = (options && options.startMonth) || 10; - return date.getMonth() >= startMonth - 1 ? date.getFullYear() + 1 : date.getFullYear(); -} - -export function getFiscalYearRange( - date = new Date(), - options?: { startMonth?: number; numberOfMonths?: number } -) { - const fiscalYear = getFiscalYear(date, options); - const startMonth = (options && options.startMonth) || 10; - const numberOfMonths = (options && options.numberOfMonths) || 12; - - const startDate = new Date((fiscalYear || 0) - 1, startMonth - 1, 1); - const endDate = endOfMonth(addMonths(startDate, numberOfMonths - 1)); - - return { startDate, endDate }; -} - -export function startOfFiscalYear(date: Date, options?: Parameters[1]) { - return getFiscalYearRange(date, options).startDate; -} - -export function endOfFiscalYear(date: Date, options?: Parameters[1]) { - return getFiscalYearRange(date, options).endDate; -} - -export function isSameFiscalYear(dateLeft: Date, dateRight: Date) { - return getFiscalYear(dateLeft) === getFiscalYear(dateRight); -} - -/* - * Bi-Weekly - */ - -const biweekBaseDates = [new Date('1799-12-22T00:00'), new Date('1799-12-15T00:00')]; - -export function startOfBiWeek(date: Date, week: number, startOfWeek: DayOfWeek) { - var weekBaseDate = biweekBaseDates[week - 1]; - var baseDate = addDays(weekBaseDate, startOfWeek); - var periodsSince = Math.floor(differenceInDays(date, baseDate) / 14); - return addDays(baseDate, periodsSince * 14); -} - -export function endOfBiWeek(date: Date, week: number, startOfWeek: DayOfWeek) { - return addDays(startOfBiWeek(date, week, startOfWeek), 13); -} - -export function getDateFuncsByPeriodType( - settings: LocaleSettings, - periodType: PeriodType | null | undefined -) { - if (settings) { - periodType = updatePeriodTypeWithWeekStartsOn(settings.formats.dates.weekStartsOn, periodType); - } - - switch (periodType) { - case PeriodType.Day: - return { - start: startOfDay, - end: endOfDay, - add: addDays, - difference: differenceInDays, - isSame: isSameDay, - }; - - case PeriodType.Week: - case PeriodType.WeekSun: - return { - start: startOfWeek, - end: endOfWeek, - add: addWeeks, - difference: differenceInWeeks, - isSame: isSameWeek, - }; - case PeriodType.WeekMon: - return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 1 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 1 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 1 }), - }; - case PeriodType.WeekTue: - return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 2 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 2 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 2 }), - }; - case PeriodType.WeekWed: - return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 3 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 3 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 3 }), - }; - case PeriodType.WeekThu: - return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 4 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 4 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 4 }), - }; - case PeriodType.WeekFri: - return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 5 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 5 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 5 }), - }; - case PeriodType.WeekSat: - return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 6 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 6 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 6 }), - }; - - case PeriodType.Month: - return { - start: startOfMonth, - end: endOfMonth, - add: addMonths, - difference: differenceInMonths, - isSame: isSameMonth, - }; - case PeriodType.Quarter: - return { - start: startOfQuarter, - end: endOfQuarter, - add: addQuarters, - difference: differenceInQuarters, - isSame: isSameQuarter, - }; - case PeriodType.CalendarYear: - return { - start: startOfYear, - end: endOfYear, - add: addYears, - difference: differenceInYears, - isSame: isSameYear, - }; - case PeriodType.FiscalYearOctober: - return { - start: startOfFiscalYear, - end: endOfFiscalYear, - add: addYears, - difference: differenceInYears, - isSame: isSameFiscalYear, - }; - - // BiWeek 1 - case PeriodType.BiWeek1: - case PeriodType.BiWeek1Sun: - case PeriodType.BiWeek1Mon: - case PeriodType.BiWeek1Tue: - case PeriodType.BiWeek1Wed: - case PeriodType.BiWeek1Thu: - case PeriodType.BiWeek1Fri: - case PeriodType.BiWeek1Sat: - // BiWeek 2 - case PeriodType.BiWeek2: - case PeriodType.BiWeek2Sun: - case PeriodType.BiWeek2Mon: - case PeriodType.BiWeek2Tue: - case PeriodType.BiWeek2Wed: - case PeriodType.BiWeek2Thu: - case PeriodType.BiWeek2Fri: - case PeriodType.BiWeek2Sat: { - const week = getPeriodTypeCode(periodType).startsWith('BIWEEK1') ? 1 : 2; - const dayOfWeek = getDayOfWeek(periodType)!; - return { - start: (date: Date) => startOfBiWeek(date, week, dayOfWeek), - end: (date: Date) => endOfBiWeek(date, week, dayOfWeek), - add: (date: Date, amount: number) => addWeeks(date, amount * 2), - difference: (dateLeft: Date, dateRight: Date) => { - return differenceInWeeks(dateLeft, dateRight) / 2; - }, - isSame: (dateLeft: Date, dateRight: Date) => { - return isSameDay( - startOfBiWeek(dateLeft, week, dayOfWeek), - startOfBiWeek(dateRight, week, dayOfWeek) - ); - }, - }; - } - - // All cases not handled above - case PeriodType.Custom: - case PeriodType.DayTime: - case PeriodType.TimeOnly: - - case PeriodType.MonthYear: - case null: - case undefined: - // Default to end of day if periodType == null, etc - return { - start: startOfDay, - end: endOfDay, - add: addDays, - difference: differenceInDays, - isSame: isSameDay, - }; - - default: - assertNever(periodType); // This will now report unhandled cases - } -} - -export function formatISODate( - date: Date | string | null | undefined, - representation: 'complete' | 'date' | 'time' = 'complete' -) { - if (date == null) { - return ''; - } - - if (typeof date === 'string') { - date = parseISO(date); - } - - return formatISO(date, { representation }); -} - -export function formatIntl( - settings: LocaleSettings, - dt: Date, - tokens_or_intlOptions: CustomIntlDateTimeFormatOptions -) { - const { - locale, - formats: { - dates: { ordinalSuffixes: suffixes }, - }, - } = settings; - - function formatIntlOrdinal(formatter: Intl.DateTimeFormat, with_ordinal = false) { - if (with_ordinal) { - const rules = new Intl.PluralRules(locale, { type: 'ordinal' }); - - const splited = formatter.formatToParts(dt); - return splited - .map((c) => { - if (c.type === 'day') { - const ordinal = rules.select(parseInt(c.value, 10)); - const suffix = suffixes[ordinal]; - return `${c.value}${suffix}`; - } - return c.value; - }) - .join(''); - } - - return formatter.format(dt); - } - - if (typeof tokens_or_intlOptions !== 'string' && !Array.isArray(tokens_or_intlOptions)) { - return formatIntlOrdinal( - new Intl.DateTimeFormat(locale, tokens_or_intlOptions), - tokens_or_intlOptions.withOrdinal - ); - } - - const tokens = Array.isArray(tokens_or_intlOptions) - ? tokens_or_intlOptions.join('') - : tokens_or_intlOptions; - - // Order of includes check is important! (longest first) - const formatter = new Intl.DateTimeFormat(locale, { - year: tokens.includes(DateToken.Year_numeric) - ? 'numeric' - : tokens.includes(DateToken.Year_2Digit) - ? '2-digit' - : undefined, - - month: tokens.includes(DateToken.Month_long) - ? 'long' - : tokens.includes(DateToken.Month_short) - ? 'short' - : tokens.includes(DateToken.Month_2Digit) - ? '2-digit' - : tokens.includes(DateToken.Month_numeric) - ? 'numeric' - : undefined, - - day: tokens.includes(DateToken.DayOfMonth_2Digit) - ? '2-digit' - : tokens.includes(DateToken.DayOfMonth_numeric) - ? 'numeric' - : undefined, - - hour: tokens.includes(DateToken.Hour_2Digit) - ? '2-digit' - : tokens.includes(DateToken.Hour_numeric) - ? 'numeric' - : undefined, - hour12: tokens.includes(DateToken.Hour_woAMPM) - ? false - : tokens.includes(DateToken.Hour_wAMPM) - ? true - : undefined, - - minute: tokens.includes(DateToken.Minute_2Digit) - ? '2-digit' - : tokens.includes(DateToken.Minute_numeric) - ? 'numeric' - : undefined, - - second: tokens.includes(DateToken.Second_2Digit) - ? '2-digit' - : tokens.includes(DateToken.Second_numeric) - ? 'numeric' - : undefined, - - fractionalSecondDigits: tokens.includes(DateToken.MiliSecond_3) ? 3 : undefined, - - weekday: tokens.includes(DateToken.DayOfWeek_narrow) - ? 'narrow' - : tokens.includes(DateToken.DayOfWeek_long) - ? 'long' - : tokens.includes(DateToken.DayOfWeek_short) - ? 'short' - : undefined, - }); - - return formatIntlOrdinal(formatter, tokens.includes(DateToken.DayOfMonth_withOrdinal)); -} - -function range( - settings: LocaleSettings, - date: Date, - weekStartsOn: DayOfWeek, - formatToUse: CustomIntlDateTimeFormatOptions, - biWeek: undefined | 1 | 2 = undefined // undefined means that it's not a bi-week -) { - const start = - biWeek === undefined - ? startOfWeek(date, { weekStartsOn }) - : startOfBiWeek(date, biWeek, weekStartsOn); - const end = - biWeek === undefined - ? endOfWeek(date, { weekStartsOn }) - : endOfBiWeek(date, biWeek, weekStartsOn); - - return formatIntl(settings, start, formatToUse) + ' - ' + formatIntl(settings, end, formatToUse); -} - -export function formatDate( - date: Date | string | null | undefined, - periodType: PeriodType, - options: FormatDateOptions = {} -): string { - return formatDateWithLocale(defaultLocale, date, periodType, options); -} - -export function updatePeriodTypeWithWeekStartsOn( - weekStartsOn: DayOfWeek, - periodType: PeriodType | null | undefined -) { - if (periodType === PeriodType.Week) { - periodType = [ - PeriodType.WeekSun, - PeriodType.WeekMon, - PeriodType.WeekTue, - PeriodType.WeekWed, - PeriodType.WeekThu, - PeriodType.WeekFri, - PeriodType.WeekSat, - ][weekStartsOn]; - } else if (periodType === PeriodType.BiWeek1) { - periodType = [ - PeriodType.BiWeek1Sun, - PeriodType.BiWeek1Mon, - PeriodType.BiWeek1Tue, - PeriodType.BiWeek1Wed, - PeriodType.BiWeek1Thu, - PeriodType.BiWeek1Fri, - PeriodType.BiWeek1Sat, - ][weekStartsOn]; - } else if (periodType === PeriodType.BiWeek2) { - periodType = [ - PeriodType.BiWeek2Sun, - PeriodType.BiWeek2Mon, - PeriodType.BiWeek2Tue, - PeriodType.BiWeek2Wed, - PeriodType.BiWeek2Thu, - PeriodType.BiWeek2Fri, - PeriodType.BiWeek2Sat, - ][weekStartsOn]; - } - - return periodType; -} - -export function formatDateWithLocale( - settings: LocaleSettings, - date: Date | string | null | undefined, - periodType: PeriodType, - options: FormatDateOptions = {} -): string { - if (typeof date === 'string') { - date = parseISO(date); - } - - // Handle 'Invalid Date' - // @ts-expect-error - Date is a number (see: https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript) - if (date == null || isNaN(date)) { - return ''; - } - - const weekStartsOn = options.weekStartsOn ?? settings.formats.dates.weekStartsOn; - - const { day, dayTime, timeOnly, week, month, monthsYear, year } = settings.formats.dates.presets; - - periodType = updatePeriodTypeWithWeekStartsOn(weekStartsOn, periodType) ?? periodType; - - /** Resolve a preset given the chosen variant */ - function rv(preset: DateFormatVariantPreset) { - if (options.variant === 'custom') { - return options.custom ?? preset.default; - } else if (options.custom && !options.variant) { - return options.custom; - } - - return preset[options.variant ?? 'default']; - } - - switch (periodType) { - case PeriodType.Custom: - return formatIntl(settings, date, options.custom!); - - case PeriodType.Day: - return formatIntl(settings, date, rv(day!)!); - - case PeriodType.DayTime: - return formatIntl(settings, date, rv(dayTime!)!); - - case PeriodType.TimeOnly: - return formatIntl(settings, date, rv(timeOnly!)!); - - case PeriodType.Week: //Should never happen, but to make types happy - case PeriodType.WeekSun: - return range(settings, date, 0, rv(week!)!); - case PeriodType.WeekMon: - return range(settings, date, 1, rv(week!)!); - case PeriodType.WeekTue: - return range(settings, date, 2, rv(week!)!); - case PeriodType.WeekWed: - return range(settings, date, 3, rv(week!)!); - case PeriodType.WeekThu: - return range(settings, date, 4, rv(week!)!); - case PeriodType.WeekFri: - return range(settings, date, 5, rv(week!)!); - case PeriodType.WeekSat: - return range(settings, date, 6, rv(week!)!); - - case PeriodType.Month: - return formatIntl(settings, date, rv(month!)!); - - case PeriodType.MonthYear: - return formatIntl(settings, date, rv(monthsYear!)!); - - case PeriodType.Quarter: - return [ - formatIntl(settings, startOfQuarter(date), rv(month!)!), - formatIntl(settings, endOfQuarter(date), rv(monthsYear!)!), - ].join(' - '); - - case PeriodType.CalendarYear: - return formatIntl(settings, date, rv(year!)!); - - case PeriodType.FiscalYearOctober: - const fDate = new Date(getFiscalYear(date), 0, 1); - return formatIntl(settings, fDate, rv(year!)!); - - case PeriodType.BiWeek1: //Should never happen, but to make types happy - case PeriodType.BiWeek1Sun: - return range(settings, date, 0, rv(week!)!, 1); - case PeriodType.BiWeek1Mon: - return range(settings, date, 1, rv(week!)!, 1); - case PeriodType.BiWeek1Tue: - return range(settings, date, 2, rv(week!)!, 1); - case PeriodType.BiWeek1Wed: - return range(settings, date, 3, rv(week!)!, 1); - case PeriodType.BiWeek1Thu: - return range(settings, date, 4, rv(week!)!, 1); - case PeriodType.BiWeek1Fri: - return range(settings, date, 5, rv(week!)!, 1); - case PeriodType.BiWeek1Sat: - return range(settings, date, 6, rv(week!)!, 1); - - case PeriodType.BiWeek2: //Should never happen, but to make types happy - case PeriodType.BiWeek2Sun: - return range(settings, date, 0, rv(week!)!, 2); - case PeriodType.BiWeek2Mon: - return range(settings, date, 1, rv(week!)!, 2); - case PeriodType.BiWeek2Tue: - return range(settings, date, 2, rv(week!)!, 2); - case PeriodType.BiWeek2Wed: - return range(settings, date, 3, rv(week!)!, 2); - case PeriodType.BiWeek2Thu: - return range(settings, date, 4, rv(week!)!, 2); - case PeriodType.BiWeek2Fri: - return range(settings, date, 5, rv(week!)!, 2); - case PeriodType.BiWeek2Sat: - return range(settings, date, 6, rv(week!)!, 2); - - default: - return formatISO(date); - // default: - // assertNever(periodType); // This will now report unhandled cases - } -} - -/** - * Return new Date using UTC date/time as local date/time - */ -export function utcToLocalDate(date: Date | string | null | undefined) { - date = date instanceof Date ? date : typeof date === 'string' ? new Date(date) : new Date(); - - // https://github.com/date-fns/date-fns/issues/376#issuecomment-454163253 - // return new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000); - const d = new Date( - date.getUTCFullYear(), - date.getUTCMonth(), - date.getUTCDate(), - date.getUTCHours(), - date.getUTCMinutes(), - date.getUTCSeconds() - ); - d.setUTCFullYear(date.getUTCFullYear()); - return d; -} - -/** - * Return new Date using local date/time as UTC date/time - */ -export function localToUtcDate(date: Date | string | null | undefined) { - date = date instanceof Date ? date : typeof date === 'string' ? new Date(date) : new Date(); - - // return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000); - - const d = new Date( - Date.UTC( - date.getFullYear(), - date.getMonth(), - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds() - ) - ); - return d; -} - -/** - * Generate a random Date between `from` and `to` (exclusive) - */ -export function randomDate(from: Date, to: Date) { - const fromTime = from.getTime(); - const toTime = to.getTime(); - return new Date(fromTime + Math.random() * (toTime - fromTime)); -} - -// '1982-03-30' -// '1982-03-30T11:25:59Z' -// '1982-03-30T11:25:59-04:00' -// '1982-03-30T11:25:59.123Z' -// '1982-03-30T11:25:59.1234567Z' -const DATE_FORMAT = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(.\d+|)(Z|(-|\+)\d{2}:\d{2}))?$/; - -/** - * Determine if string is UTC (yyyy-mm-ddThh:mm:ssZ) or Offset (yyyy-mm-ddThh:mm:ss-ZZ:ZZ) or Date-only (yyyy-mm-dd) date string - */ -export function isStringDate(value: string) { - return DATE_FORMAT.test(value); -} diff --git a/packages/svelte-ux/src/lib/utils/dateInternal.ts b/packages/svelte-ux/src/lib/utils/dateInternal.ts deleted file mode 100644 index cbeaeadd0..000000000 --- a/packages/svelte-ux/src/lib/utils/dateInternal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DayOfWeek } from './date_types.js'; - -export function getWeekStartsOnFromIntl(locales?: string): DayOfWeek { - if (!locales) { - return DayOfWeek.Sunday; - } - - const locale = new Intl.Locale(locales); - // @ts-expect-error - const weekInfo = locale.weekInfo ?? locale.getWeekInfo?.(); - return (weekInfo?.firstDay ?? 0) % 7; // (in Intl, sunday is 7 not 0, so we need to mod 7) -} diff --git a/packages/svelte-ux/src/lib/utils/dateRange.ts b/packages/svelte-ux/src/lib/utils/dateRange.ts deleted file mode 100644 index 8ef3fb6da..000000000 --- a/packages/svelte-ux/src/lib/utils/dateRange.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { startOfDay, isLeapYear, isAfter, isBefore, subYears } from 'date-fns'; - -import { getDateFuncsByPeriodType, updatePeriodTypeWithWeekStartsOn } from './date.js'; -import { PeriodType } from './date_types.js'; -import type { LocaleSettings } from './locale.js'; - -export type DateRange = { - from: Date | null; - to: Date | null; - periodType?: PeriodType | null; -}; - -function formatMsg( - settings: LocaleSettings, - type: - | 'PeriodDay' - | 'PeriodWeek' - | 'PeriodBiWeek' - | 'PeriodMonth' - | 'PeriodQuarter' - | 'PeriodYear' - | 'PeriodFiscalYear', - lastX: number -) { - return lastX === 0 - ? settings.dictionary.Date[type].Current - : lastX === 1 - ? settings.dictionary.Date[type].Last - : settings.dictionary.Date[type].LastX.replace('{0}', lastX.toString()); -} - -export function getDateRangePresets( - settings: LocaleSettings, - periodType: PeriodType -): { label: string; value: DateRange }[] { - let now = new Date(); - const today = startOfDay(now); - - if (settings) { - periodType = - updatePeriodTypeWithWeekStartsOn(settings.formats.dates.weekStartsOn, periodType) ?? - periodType; - } - - const { start, end, add } = getDateFuncsByPeriodType(settings, periodType); - - switch (periodType) { - case PeriodType.Day: { - const last = start(add(today, -1)); - - return [0, 1, 3, 7, 14, 30].map((lastX) => { - return { - label: formatMsg(settings, 'PeriodDay', lastX), - value: { - from: add(last, -lastX + 1), - to: lastX === 0 ? end(today) : end(last), - periodType, - }, - }; - }); - } - - case PeriodType.WeekSun: - case PeriodType.WeekMon: - case PeriodType.WeekTue: - case PeriodType.WeekWed: - case PeriodType.WeekThu: - case PeriodType.WeekFri: - case PeriodType.WeekSat: { - const last = start(add(today, -1)); - - return [0, 1, 2, 4, 6].map((lastX) => { - return { - label: formatMsg(settings, 'PeriodWeek', lastX), - value: { - from: add(last, -lastX + 1), - to: lastX === 0 ? end(today) : end(last), - periodType, - }, - }; - }); - } - - case PeriodType.BiWeek1Sun: - case PeriodType.BiWeek1Mon: - case PeriodType.BiWeek1Tue: - case PeriodType.BiWeek1Wed: - case PeriodType.BiWeek1Thu: - case PeriodType.BiWeek1Fri: - case PeriodType.BiWeek1Sat: - case PeriodType.BiWeek2Sun: - case PeriodType.BiWeek2Mon: - case PeriodType.BiWeek2Tue: - case PeriodType.BiWeek2Wed: - case PeriodType.BiWeek2Thu: - case PeriodType.BiWeek2Fri: - case PeriodType.BiWeek2Sat: { - const last = start(add(today, -1)); - - return [0, 1, 2, 4, 6].map((lastX) => { - return { - label: formatMsg(settings, 'PeriodBiWeek', lastX), - value: { - from: add(last, -lastX + 1), - to: lastX === 0 ? end(today) : end(last), - periodType, - }, - }; - }); - } - - case PeriodType.Month: { - const last = start(add(today, -1)); - - return [0, 1, 2, 3, 6, 12].map((lastX) => { - return { - label: formatMsg(settings, 'PeriodMonth', lastX), - value: { - from: add(last, -lastX + 1), - to: lastX === 0 ? end(today) : end(last), - periodType, - }, - }; - }); - } - - case PeriodType.Quarter: { - const last = start(add(today, -1)); - - return [0, 1, -1, 4, 12].map((lastX) => { - // Special thing - if (lastX === -1) { - return { - label: settings.dictionary.Date.PeriodQuarterSameLastyear, - value: { - from: start(add(today, -4)), - to: end(add(today, -4)), - periodType, - }, - }; - } - - return { - label: formatMsg(settings, 'PeriodQuarter', lastX), - value: { - from: add(last, -lastX + 1), - to: lastX === 0 ? end(today) : end(last), - periodType, - }, - }; - }); - } - - case PeriodType.CalendarYear: { - const last = start(add(today, -1)); - - return [0, 1, 3, 5].map((lastX) => { - return { - label: formatMsg(settings, 'PeriodYear', lastX), - value: { - from: add(last, -lastX + 1), - to: lastX === 0 ? end(today) : end(last), - periodType, - }, - }; - }); - } - - case PeriodType.FiscalYearOctober: { - const last = start(add(today, -1)); - - return [0, 1, 3, 5].map((lastX) => { - return { - label: formatMsg(settings, 'PeriodFiscalYear', lastX), - value: { - from: add(last, -lastX + 1), - to: lastX === 0 ? end(today) : end(last), - periodType, - }, - }; - }); - } - - default: { - return []; - } - } -} - -export function getPreviousYearPeriodOffset( - periodType: PeriodType, - options?: { - referenceDate?: Date; - alignDayOfWeek?: boolean; - } -) { - switch (periodType) { - case PeriodType.Day: - // If year of reference date is a leap year and is on/after 2/29 - // or - // if year before reference date is a leap year and is before 2/29 - const adjustForLeapYear = options?.referenceDate - ? (isLeapYear(options?.referenceDate) && - isAfter( - options?.referenceDate, - new Date(options?.referenceDate.getFullYear(), /*Feb*/ 1, 28) - )) || - (isLeapYear(subYears(options?.referenceDate, 1)) && - isBefore( - options?.referenceDate, - new Date(options?.referenceDate.getFullYear(), /*Feb*/ 1, 29) - )) - : false; - - return options?.alignDayOfWeek - ? -364 // Align day of week is always 364 days (52 *7). - : adjustForLeapYear - ? -366 - : -365; - - case PeriodType.WeekSun: - case PeriodType.WeekMon: - case PeriodType.WeekTue: - case PeriodType.WeekWed: - case PeriodType.WeekThu: - case PeriodType.WeekFri: - case PeriodType.WeekSat: - return -52; - - case PeriodType.BiWeek1Sun: - case PeriodType.BiWeek1Mon: - case PeriodType.BiWeek1Tue: - case PeriodType.BiWeek1Wed: - case PeriodType.BiWeek1Thu: - case PeriodType.BiWeek1Fri: - case PeriodType.BiWeek1Sat: - case PeriodType.BiWeek2Sun: - case PeriodType.BiWeek2Mon: - case PeriodType.BiWeek2Tue: - case PeriodType.BiWeek2Wed: - case PeriodType.BiWeek2Thu: - case PeriodType.BiWeek2Fri: - case PeriodType.BiWeek2Sat: - return -26; - - case PeriodType.Month: - return -12; - - case PeriodType.Quarter: - return -4; - - case PeriodType.CalendarYear: - case PeriodType.FiscalYearOctober: - return -1; - } -} - -export type PeriodComparison = 'prevPeriod' | 'prevYear' | 'fiftyTwoWeeksAgo'; - -export function getPeriodComparisonOffset( - settings: LocaleSettings, - view: PeriodComparison, - period: DateRange | undefined -) { - if (period == null || period.from == null || period.to == null || period.periodType == null) { - throw new Error('Period must be defined to calculate offset'); - } - - switch (view) { - case 'prevPeriod': - const dateFuncs = getDateFuncsByPeriodType(settings, period.periodType); - return dateFuncs.difference(period.from, period.to) - 1; // Difference counts full days, need additoinal offset - - case 'prevYear': - return getPreviousYearPeriodOffset(period.periodType, { - referenceDate: period.from, - }); - - case 'fiftyTwoWeeksAgo': - return getPreviousYearPeriodOffset(period.periodType, { - alignDayOfWeek: true, - }); - - default: - throw new Error('Unhandled period offset: ' + view); - } -} diff --git a/packages/svelte-ux/src/lib/utils/date_types.ts b/packages/svelte-ux/src/lib/utils/date_types.ts deleted file mode 100644 index f10cfce06..000000000 --- a/packages/svelte-ux/src/lib/utils/date_types.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { DateRange } from './dateRange.js'; - -export type SelectedDate = Date | Date[] | DateRange | null | undefined; - -export type DisabledDate = ((date: Date) => boolean) | Date | Date[] | { from: Date; to: Date }; - -export type Period = { - start: Date; - end: Date; - periodTypeId: PeriodType; -}; - -export enum PeriodType { - Custom = 1, - - Day = 10, - DayTime = 11, - TimeOnly = 15, - - Week = 20, // will be replaced by WeekSun, WeekMon, etc depending on locale `weekStartsOn` - WeekSun = 21, - WeekMon = 22, - WeekTue = 23, - WeekWed = 24, - WeekThu = 25, - WeekFri = 26, - WeekSat = 27, - - Month = 30, - MonthYear = 31, - Quarter = 40, - CalendarYear = 50, - FiscalYearOctober = 60, - - BiWeek1 = 70, // will be replaced by BiWeek1Sun, BiWeek1Mon, etc depending on locale `weekStartsOn` - BiWeek1Sun = 71, - BiWeek1Mon = 72, - BiWeek1Tue = 73, - BiWeek1Wed = 74, - BiWeek1Thu = 75, - BiWeek1Fri = 76, - BiWeek1Sat = 77, - - BiWeek2 = 80, // will be replaced by BiWeek2Sun, BiWeek2Mon, etc depending on locale `weekStartsOn` - BiWeek2Sun = 81, - BiWeek2Mon = 82, - BiWeek2Tue = 83, - BiWeek2Wed = 84, - BiWeek2Thu = 85, - BiWeek2Fri = 86, - BiWeek2Sat = 87, -} - -export enum DayOfWeek { - Sunday = 0, - Monday = 1, - Tuesday = 2, - Wednesday = 3, - Thursday = 4, - Friday = 5, - Saturday = 6, -} - -export enum DateToken { - /** `1982, 1986, 2024` */ - Year_numeric = 'yyy', - /** `82, 86, 24` */ - Year_2Digit = 'yy', - - /** `January, February, ..., December` */ - Month_long = 'MMMM', - /** `Jan, Feb, ..., Dec` */ - Month_short = 'MMM', - /** `01, 02, ..., 12` */ - Month_2Digit = 'MM', - /** `1, 2, ..., 12` */ - Month_numeric = 'M', - - /** `1, 2, ..., 11, 12` */ - Hour_numeric = 'h', - /** `01, 02, ..., 11, 12` */ - Hour_2Digit = 'hh', - /** You should probably not use this. Force with AM/PM (and the good locale), not specifying this will automatically take the good local */ - Hour_wAMPM = 'a', - /** You should probably not use this. Force without AM/PM (and the good locale), not specifying this will automatically take the good local */ - Hour_woAMPM = 'aaaaaa', - - /** `0, 1, ..., 59` */ - Minute_numeric = 'm', - /** `00, 01, ..., 59` */ - Minute_2Digit = 'mm', - - /** `0, 1, ..., 59` */ - Second_numeric = 's', - /** `00, 01, ..., 59` */ - Second_2Digit = 'ss', - - /** `000, 001, ..., 999` */ - MiliSecond_3 = 'SSS', - - /** Minimize digit: `1, 2, 11, ...` */ - DayOfMonth_numeric = 'd', - /** `01, 02, 11, ...` */ - DayOfMonth_2Digit = 'dd', - /** `1st, 2nd, 11th, ...` You can have your local ordinal by passing `ordinalSuffixes` in options / settings */ - DayOfMonth_withOrdinal = 'do', - - /** `M, T, W, T, F, S, S` */ - DayOfWeek_narrow = 'eeeee', - /** `Monday, Tuesday, ..., Sunday` */ - DayOfWeek_long = 'eeee', - /** `Mon, Tue, Wed, ..., Sun` */ - DayOfWeek_short = 'eee', -} - -export type OrdinalSuffixes = { - one?: string; - two?: string; - few?: string; - other?: string; - zero?: string; - many?: string; -}; -export type DateFormatVariant = 'short' | 'default' | 'long'; -export type DateFormatVariantPreset = { - short?: CustomIntlDateTimeFormatOptions; - default?: CustomIntlDateTimeFormatOptions; - long?: CustomIntlDateTimeFormatOptions; -}; -export type CustomIntlDateTimeFormatOptions = - | string - | string[] - | (Intl.DateTimeFormatOptions & { withOrdinal?: boolean }); - -export type FormatDateOptions = { - weekStartsOn?: DayOfWeek; - variant?: DateFormatVariant | 'custom'; - custom?: CustomIntlDateTimeFormatOptions; -}; - -export interface FormatDateLocaleOptions { - weekStartsOn?: DayOfWeek; - baseParsing?: string; - presets?: { - day?: DateFormatVariantPreset; - dayTime?: DateFormatVariantPreset; - timeOnly?: DateFormatVariantPreset; - week?: DateFormatVariantPreset; - month?: DateFormatVariantPreset; - monthsYear?: DateFormatVariantPreset; - year?: DateFormatVariantPreset; - }; - ordinalSuffixes?: OrdinalSuffixes; -} - -export type FormatDateLocalePresets = Required; diff --git a/packages/svelte-ux/src/lib/utils/dictionary.ts b/packages/svelte-ux/src/lib/utils/dictionary.ts deleted file mode 100644 index 614d024f4..000000000 --- a/packages/svelte-ux/src/lib/utils/dictionary.ts +++ /dev/null @@ -1,41 +0,0 @@ -export type DictionaryMessagesOptions = { - Ok?: string; - Cancel?: string; - - Date?: { - Start?: string; - End?: string; - Empty?: string; - - Day?: string; - DayTime?: string; - Time?: string; - Week?: string; - BiWeek?: string; - Month?: string; - Quarter?: string; - CalendarYear?: string; - FiscalYearOct?: string; - - PeriodDay?: PeriodDayMsg; - PeriodWeek?: PeriodDayMsg; - PeriodBiWeek?: PeriodDayMsg; - PeriodMonth?: PeriodDayMsg; - PeriodQuarter?: PeriodDayMsg; - PeriodQuarterSameLastyear?: string; - PeriodYear?: PeriodDayMsg; - PeriodFiscalYear?: PeriodDayMsg; - }; -}; - -export type PeriodDayMsg = { - Current?: string; - Last?: string; - LastX?: string; -}; - -type DeepRequired = Required<{ - [K in keyof T]: T[K] extends Required ? Required : DeepRequired; -}>; - -export type DictionaryMessages = DeepRequired; diff --git a/packages/svelte-ux/src/lib/utils/dom.ts b/packages/svelte-ux/src/lib/utils/dom.ts deleted file mode 100644 index ea4e025c9..000000000 --- a/packages/svelte-ux/src/lib/utils/dom.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Find the closest scrollable parent - * - see: https://stackoverflow.com/questions/35939886/find-first-scrollable-parent - * - see: https://gist.github.com/twxia/bb20843c495a49644be6ea3804c0d775 - */ -export function getScrollParent(node: HTMLElement): HTMLElement { - const isElement = node instanceof HTMLElement; - const overflowX = isElement ? (window?.getComputedStyle(node).overflowX ?? 'visible') : 'unknown'; - const overflowY = isElement ? (window?.getComputedStyle(node).overflowY ?? 'visible') : 'unknown'; - const isHorizontalScrollable = - !['visible', 'hidden'].includes(overflowX) && node.scrollWidth > node.clientWidth; - const isVerticalScrollable = - !['visible', 'hidden'].includes(overflowY) && node.scrollHeight > node.clientHeight; - - if (isHorizontalScrollable || isVerticalScrollable) { - return node; - } else if (node.parentElement) { - return getScrollParent(node.parentElement); - } else { - return document.body; - } -} - -/** - * Scroll node into view of closest scrollable (i.e. overflown) parent. Like `node.scrollIntoView()` but will only scroll immediate container (not viewport) - */ -export function scrollIntoView(node: HTMLElement) { - // TODO: Consider only scrolling if needed - const scrollParent = getScrollParent(node); - const removeScrollParentOffset = scrollParent != node.offsetParent; // ignore `position: absolute` parent, for example - - const nodeOffset = { - top: node.offsetTop - (removeScrollParentOffset ? (scrollParent?.offsetTop ?? 0) : 0), - left: node.offsetLeft - (removeScrollParentOffset ? (scrollParent?.offsetLeft ?? 0) : 0), - }; - - const optionCenter = { - left: node.clientWidth / 2, - top: node.clientHeight / 2, - }; - - const containerCenter = { - left: scrollParent.clientWidth / 2, - top: scrollParent.clientHeight / 2, - }; - - scrollParent.scroll({ - top: nodeOffset.top + optionCenter.top - containerCenter.top, - left: nodeOffset.left + optionCenter.left - containerCenter.left, - behavior: 'smooth', - }); -} - -/** - * Determine if node is currently visible in scroll container - */ -export function isVisibleInScrollParent(node: HTMLElement) { - const nodeRect = node.getBoundingClientRect(); - const scrollParent = getScrollParent(node); - const parentRect = scrollParent.getBoundingClientRect(); - const isVisible = nodeRect.top > parentRect.top && nodeRect.bottom < parentRect.bottom; - return isVisible; -} diff --git a/packages/svelte-ux/src/lib/utils/duration.ts b/packages/svelte-ux/src/lib/utils/duration.ts deleted file mode 100644 index cf0696990..000000000 --- a/packages/svelte-ux/src/lib/utils/duration.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { parseISO } from 'date-fns'; - -export type Duration = { - milliseconds: number; - seconds: number; - minutes: number; - hours: number; - days: number; - years: number; -}; - -export enum DurationUnits { - Year, - Day, - Hour, - Minute, - Second, - Millisecond, -} -// export enum DurationUnits { -// Millisecond = 1, -// Second = 1000 * Millisecond, -// Minute = 60 * Second, -// Hour = 60 * Minute, -// Day = 24 * Hour, -// Year = 365 * Day, -// } - -export function getDuration( - start?: Date | string, - end?: Date | string | null, - duration?: Partial -): Duration | null { - const startDate = typeof start === 'string' ? parseISO(start) : start; - const endDate = typeof end === 'string' ? parseISO(end) : end; - - const differenceInMs = startDate - ? Math.abs(Number(endDate || new Date()) - Number(startDate)) - : undefined; - - if (!Number.isFinite(differenceInMs) && duration == null) { - return null; - } - - var milliseconds = duration?.milliseconds ?? differenceInMs ?? 0; - var seconds = duration?.seconds ?? 0; - var minutes = duration?.minutes ?? 0; - var hours = duration?.hours ?? 0; - var days = duration?.days ?? 0; - var years = duration?.years ?? 0; - - if (milliseconds >= 1000) { - const carrySeconds = (milliseconds - (milliseconds % 1000)) / 1000; - seconds += carrySeconds; - milliseconds = milliseconds - carrySeconds * 1000; - } - - if (seconds >= 60) { - const carryMinutes = (seconds - (seconds % 60)) / 60; - minutes += carryMinutes; - seconds = seconds - carryMinutes * 60; - } - - if (minutes >= 60) { - const carryHours = (minutes - (minutes % 60)) / 60; - hours += carryHours; - minutes = minutes - carryHours * 60; - } - - if (hours >= 24) { - const carryDays = (hours - (hours % 24)) / 24; - days += carryDays; - hours = hours - carryDays * 24; - } - - if (days >= 365) { - const carryYears = (days - (days % 365)) / 365; - years += carryYears; - days = days - carryYears * 365; - } - - return { - milliseconds, - seconds, - minutes, - hours, - days, - years, - }; -} - -// See also: https://stackoverflow.com/questions/19700283/how-to-convert-time-milliseconds-to-hours-min-sec-format-in-javascript/33909506 -export function humanizeDuration(config: { - start?: Date | string; - end?: Date | string | null; - duration?: Partial; - minUnits?: DurationUnits; - totalUnits?: number; - variant?: 'short' | 'long'; -}) { - const { start, end, minUnits, totalUnits = 99, variant = 'short' } = config; - - const duration = getDuration(start, end, config.duration); - - if (duration === null) { - return 'unknown'; - } - - var sentenceArr = []; - var unitNames = - variant === 'short' - ? ['y', 'd', 'h', 'm', 's', 'ms'] - : ['years', 'days', 'hours', 'minutes', 'seconds', 'milliseconds']; - - var unitNums = [ - duration.years, - duration.days, - duration.hours, - duration.minutes, - duration.seconds, - duration.milliseconds, - ].filter((x, i) => i <= (minUnits ?? 99)); - - // Combine unit numbers and names - for (var i in unitNums) { - if (sentenceArr.length >= totalUnits) { - break; - } - - const unitNum = unitNums[i]; - let unitName = unitNames[i]; - - // Hide `0` values unless last unit (and none shown before) - if (unitNum !== 0 || (sentenceArr.length === 0 && Number(i) === unitNums.length - 1)) { - switch (variant) { - case 'short': - sentenceArr.push(unitNum + unitName); - break; - - case 'long': - if (unitNum === 1) { - // Trim off plural `s` - unitName = unitName.slice(0, -1); - } - sentenceArr.push(unitNum + ' ' + unitName); - break; - } - } - } - - const sentence = sentenceArr.join(variant === 'long' ? ' and ' : ' '); - return sentence; -} diff --git a/packages/svelte-ux/src/lib/utils/env.ts b/packages/svelte-ux/src/lib/utils/env.ts deleted file mode 100644 index e28458f3b..000000000 --- a/packages/svelte-ux/src/lib/utils/env.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Since it's not recommended to use `$app/environment` or `import.meta.‍env.SSR`, expose these instead -// See: https://kit.svelte.dev/docs/packaging -export const browser = typeof window !== 'undefined'; -export const ssr = typeof window === 'undefined'; diff --git a/packages/svelte-ux/src/lib/utils/excel.ts b/packages/svelte-ux/src/lib/utils/excel.ts deleted file mode 100644 index 1fec699d4..000000000 --- a/packages/svelte-ux/src/lib/utils/excel.ts +++ /dev/null @@ -1,167 +0,0 @@ -// import { getCellContent, getCellValue, getHeaders, getRowColumns } from './table.js'; -// import type { ColumnDef } from '../types/table.js'; -// import { PeriodType } from './date.js'; -// import { saveAs } from './file'; -// import { isLiteralObject } from './object.js'; - -// const fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8'; -// const fileExtension = '.xlsx'; - -// export async function exportToExcel( -// data: any[], -// columns: ColumnDef[], -// fileName: string, -// limit: number | null = null -// ) { -// // console.log({ -// // data, -// // columns, -// // tableHeaders: getHeaders(columns), -// // tableColumns: getColumns(columns), -// // }) - -// const { default: ExcelJS } = await import('exceljs'); - -// var workbook = new ExcelJS.Workbook(); -// workbook.creator = 'svelte-ux'; -// // workbook.lastModifiedBy = 'Her'; -// // workbook.created = new Date(1985, 8, 30); -// // workbook.modified = new Date(); -// // workbook.lastPrinted = new Date(2016, 9, 27); -// // workbook.calcProperties.fullCalcOnLoad = true; - -// var worksheet = workbook.addWorksheet('Export'); -// // var sheet = workbook.addWorksheet('My Sheet', {views:[{state: 'frozen', xSplit: 1, ySplit:1}]}); - -// // Row => Column => Number of columns spanned -// const columnPlaceholdersByRow = new Map>(); - -// const headers = getHeaders(columns); - -// // Add header rows -// headers.forEach((headerRow, headerRowIndex) => { -// const rowNumber = headerRowIndex + 1; // rows are 1-based -// const row = worksheet.getRow(rowNumber); -// let columnNumber = 1; // columns are 1-based -// const columnPlaceholders = columnPlaceholdersByRow.get(rowNumber); -// const merges: { -// startRow: number; -// startColumn: number; -// endRow: number; -// endColumn: number; -// }[] = []; - -// headerRow.forEach((column, columnIndex) => { -// // If current column was spanned by previous row, shift right number of spanned columns. Loop for consecutive skipped columns -// let findMoreSkipColumns = true; -// do { -// const skipColumns = columnPlaceholders?.get(columnNumber); -// if (skipColumns != null) { -// columnNumber += skipColumns; -// } else { -// findMoreSkipColumns = false; -// } -// } while (findMoreSkipColumns); - -// const cell = row.getCell(columnNumber); -// cell.value = column.header ?? column.name ?? null; - -// if (column.rowSpan) { -// // Add column placeholders for subsequent rows that have been spanned by this column -// for (var x = 1; x <= column.rowSpan; x++) { -// const spannedRowIndex = rowNumber + x; -// const columnPlaceholders = columnPlaceholdersByRow.get(spannedRowIndex) ?? new Map(); -// columnPlaceholders.set(columnNumber, column.colSpan ?? 1); // TODO: -1? -// columnPlaceholdersByRow.set(spannedRowIndex, columnPlaceholders); -// } - -// merges.push({ -// startRow: rowNumber, -// startColumn: columnNumber, -// endRow: rowNumber + column.rowSpan - 1, -// endColumn: columnNumber + (column.colSpan ?? 1) - 1, -// }); -// } else if (column.colSpan) { -// merges.push({ -// startRow: rowNumber, -// startColumn: columnNumber, -// endRow: rowNumber, -// endColumn: columnNumber + column.colSpan - 1, -// }); -// } - -// columnNumber += column.colSpan ?? 1; -// }); - -// // Apply cell merges (rowSpan / colSpan) -// merges.forEach((merge) => { -// worksheet.mergeCells(merge.startRow, merge.startColumn, merge.endRow, merge.endColumn); -// }); -// }); - -// if (limit !== null) { -// data = data.slice(0, limit); -// } - -// // Add data rows -// data.map((rowData, rowIndex) => { -// const rowNumber = headers.length + rowIndex + 1; -// const row = worksheet.getRow(rowNumber); - -// getRowColumns(columns).forEach((column, columnIndex) => { -// const columnNumber = columnIndex + 1; - -// let cellValue = getCellValue(column, rowData, rowIndex); - -// if (isLiteralObject(cellValue) || Array.isArray(cellValue)) { -// // If value is Object or Array, attempt to format -// cellValue = getCellContent(column, rowData, rowIndex); -// } - -// const cell = row.getCell(columnNumber); - -// cell.value = cellValue ?? null; -// cell.numFmt = getFormat(cellValue, column.format) ?? ''; -// }); -// }); - -// // worksheet.autoFilter = 'A1:C1'; // https://github.com/exceljs/exceljs#auto-filters -// worksheet.autoFilter = { -// from: { -// row: headers.length, -// column: 1, -// }, -// to: { -// row: headers.length, -// column: worksheet.columnCount, -// }, -// }; - -// workbook.xlsx.writeBuffer().then((data) => { -// var blob = new Blob([data], { type: fileType }); -// saveAs(blob, fileName + fileExtension); -// }); -// } - -// /** -// * Convert column format to Excel cell format -// */ -// function getFormat(value: any, format: ColumnDef['format']) { -// if (typeof format !== 'function') { -// switch (format) { -// case 'currency': -// return '$#,##0.00'; -// case 'percent': -// return '0.00%'; -// case 'decimal': -// return '#,##0.00'; -// case 'integer': -// return '#,##0'; -// default: -// // TODO: Treat each PeriodType's differently? -// if (format in PeriodType) { -// return 'm/d/yyyy\\ h:mm\\ AM/PM'; -// } -// } -// } -// } diff --git a/packages/svelte-ux/src/lib/utils/file.ts b/packages/svelte-ux/src/lib/utils/file.ts deleted file mode 100644 index ce3ed8e3e..000000000 --- a/packages/svelte-ux/src/lib/utils/file.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Export Blob to file - */ -export function saveAs(blob: Blob, fileName: string) { - var a = document.createElement('a'); - document.body.appendChild(a); - a.style.display = 'none'; - - var url = window.URL.createObjectURL(blob); - a.href = url; - a.download = fileName; - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); -} diff --git a/packages/svelte-ux/src/lib/utils/format.test.ts b/packages/svelte-ux/src/lib/utils/format.test.ts deleted file mode 100644 index 7017ac23e..000000000 --- a/packages/svelte-ux/src/lib/utils/format.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { format } from './format.js'; -import { PeriodType } from './date_types.js'; -import { testDate } from './date.test.js'; -import { parseISO } from 'date-fns'; - -describe('format()', () => { - it('returns empty string for null', () => { - const actual = format(null); - expect(actual).equal(''); - }); - - it('returns empty string for undefined', () => { - const actual = format(undefined); - expect(actual).equal(''); - }); - - it('returns value as string for style "none"', () => { - const actual = format(1234.5678, 'none'); - expect(actual).equal('1234.5678'); - }); - - // See `number.test.ts` for more number tests - it('formats number with number format (integer)', () => { - const actual = format(1234.5678, 'integer'); - expect(actual).equal('1,235'); - }); - - // See `date.test.ts` for more date tests - it('formats date with PeriodType format (date)', () => { - const actual = format(testDate, PeriodType.Day); - expect(actual).equal('11/21/2023'); - }); - - it('formats number with custom function', () => { - const actual = format(1234.5678, (value) => Math.round(value).toString()); - expect(actual).equal('1235'); - }); - - // Default format based on value type - it('format based on value type (integer)', () => { - const actual = format(1234); - expect(actual).equal('1,234'); - }); - it('format based on value type (decimal)', () => { - const actual = format(1234.5678); - expect(actual).equal('1,234.57'); - }); - it('format based on value type (date string)', () => { - const actual = format(testDate); - expect(actual).equal('11/21/2023'); - }); - it('format based on value type (date)', () => { - const actual = format(parseISO(testDate)); - expect(actual).equal('11/21/2023'); - }); - it('format based on value type (string)', () => { - const actual = format('hello'); - expect(actual).equal('hello'); - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/format.ts b/packages/svelte-ux/src/lib/utils/format.ts deleted file mode 100644 index e3869cda0..000000000 --- a/packages/svelte-ux/src/lib/utils/format.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - formatDateWithLocale, - getPeriodTypeNameWithLocale, - getDayOfWeekName, - isStringDate, -} from './date.js'; -import { formatNumberWithLocale } from './number.js'; -import type { FormatNumberOptions, FormatNumberStyle } from './number.js'; -import { defaultLocale, type LocaleSettings } from './locale.js'; -import { PeriodType, type FormatDateOptions, DayOfWeek } from './date_types.js'; - -export type FormatType = FormatNumberStyle | PeriodType | CustomFormatter; -export type CustomFormatter = (value: any) => string; -// re-export for convenience -export type { FormatNumberStyle, PeriodType }; - -/** - * Generic format which can handle Dates, Numbers, or custom format function - */ -export function format(value: null | undefined, format?: FormatType): string; -export function format( - value: number, - format?: FormatNumberStyle | CustomFormatter, - options?: FormatNumberOptions -): string; -export function format( - value: string | Date, - format?: PeriodType | CustomFormatter, - options?: FormatDateOptions -): string; -export function format( - value: any, - format?: FormatType, - options?: FormatNumberOptions | FormatDateOptions -): any { - return formatWithLocale(defaultLocale, value, format, options); -} - -export function formatWithLocale( - settings: LocaleSettings, - value: any, - format?: FormatType, - options?: FormatNumberOptions | FormatDateOptions -) { - if (typeof format === 'function') { - return format(value); - } else if (value instanceof Date || isStringDate(value) || (format && format in PeriodType)) { - return formatDateWithLocale( - settings, - value, - (format ?? PeriodType.Day) as PeriodType, - options as FormatDateOptions - ); - } else if (typeof value === 'number') { - return formatNumberWithLocale( - settings, - value, - format as FormatNumberStyle, - options as FormatNumberOptions - ); - } else if (typeof value === 'string') { - // Keep original value if already string - return value; - } else if (value == null) { - return ''; - } else { - // Provide some reasonable fallback for objects/etc (maybe use stringify() instead) - return `${value}`; - } -} - -export type FormatFunction = (( - value: number | null | undefined, - style: FormatNumberStyle, - options?: FormatNumberOptions -) => string) & - (( - value: Date | string | null | undefined, - period: PeriodType, - options?: FormatDateOptions - ) => string); - -export interface FormatFunctionProperties { - getPeriodTypeName: (period: PeriodType) => string; - getDayOfWeekName: (day: DayOfWeek) => string; - settings: LocaleSettings; -} - -export type FormatFunctions = FormatFunction & FormatFunctionProperties; - -export function buildFormatters(settings: LocaleSettings): FormatFunctions { - const mainFormat = ( - value: any, - style: FormatNumberStyle | PeriodType, - options?: FormatNumberOptions | FormatDateOptions - ) => formatWithLocale(settings, value, style, options); - - mainFormat.settings = settings; - - mainFormat.getDayOfWeekName = (day: DayOfWeek) => getDayOfWeekName(day, settings.locale); - mainFormat.getPeriodTypeName = (period: PeriodType) => - getPeriodTypeNameWithLocale(settings, period); - - return mainFormat; -} diff --git a/packages/svelte-ux/src/lib/utils/icons.ts b/packages/svelte-ux/src/lib/utils/icons.ts index a6aa1e899..db351b8c3 100644 --- a/packages/svelte-ux/src/lib/utils/icons.ts +++ b/packages/svelte-ux/src/lib/utils/icons.ts @@ -1,6 +1,7 @@ import type { ComponentProps } from 'svelte'; +import { isLiteralObject } from '@layerstack/utils/object'; + import type { default as Icon } from '../components/Icon.svelte'; -import { isLiteralObject } from './object.js'; export type IconInput = ComponentProps['data'] | ComponentProps; export type IconData = ComponentProps['data']; diff --git a/packages/svelte-ux/src/lib/utils/index.ts b/packages/svelte-ux/src/lib/utils/index.ts deleted file mode 100644 index ae8398741..000000000 --- a/packages/svelte-ux/src/lib/utils/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -// top-level exports -export { formatDate, getDateFuncsByPeriodType } from './date.js'; -export { PeriodType, DayOfWeek, DateToken } from './date_types.js'; -export * from './date_types.js'; -export { getDuration, humanizeDuration, DurationUnits } from './duration.js'; -export * from './file.js'; -export { - format, - formatWithLocale, - type FormatType, - type FormatFunction, - type FormatFunctionProperties, - type FormatFunctions, - type FormatNumberStyle, -} from './format.js'; -export * from './json.js'; -export * from './logger.js'; -export { round, clamp } from './number.js'; -export * from './promise.js'; -export * from './sort.js'; -export { cls } from './styles.js'; -export * from './string.js'; - -// aliased exports to remove conflicts (and make imports less noisy from top-level) -export * as array from './array.js'; -export * as date from './date.js'; -export * as dateRange from './dateRange.js'; -export * as dom from './dom.js'; -export * as duration from './duration.js'; -export * as env from './env.js'; -export { - defaultLocale, - createLocaleSettings, - type LocaleStore, - type LocaleSettings, - type LocaleSettingsInput, - type NumberPresets, - type NumberPresetsOptions, -} from './locale.js'; -// export * as excel from './excel'; // Remove until `await import('exceljs')` works externally -export * as map from './map.js'; -export * as number from './number.js'; -export * as object from './object.js'; -export * as rollup from './rollup.js'; -export * as routing from './routing.js'; -export * as serialize from './serialize.js'; -export * as styles from './styles.js'; -export * as table from './table.js'; diff --git a/packages/svelte-ux/src/lib/utils/json.test.ts b/packages/svelte-ux/src/lib/utils/json.test.ts deleted file mode 100644 index e2aa0410a..000000000 --- a/packages/svelte-ux/src/lib/utils/json.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { parse, stringify, reviver } from './json.js'; - -describe('json', () => { - it('parse dates', () => { - expect(reviver('dateOnly', '1982-03-30')).instanceOf(Date); - expect(reviver('utcDate', '1982-03-30T11:25:59Z')).instanceOf(Date); - expect(reviver('dateWithOffset', '1982-03-30T11:25:59-04:00')).instanceOf(Date); - expect(reviver('utcDateWith3Ms', '1982-03-30T11:25:59.123Z')).instanceOf(Date); - expect(reviver('utcDateWith6Ms', '1982-03-30T11:25:59.1234567Z')).instanceOf(Date); - }); - - it('round trip', () => { - const original = { - date: new Date(), - map: new Map([ - [1, 'one'], - [2, 'two'], - ]), - set: new Set(['one', 'two', 'three']), - object: { - one: 1, - two: 2, - }, - number: 1234, - string: 'Hello', - }; - const actual = parse(stringify(original)); - - expect(actual.date).instanceOf(Date); - expect(actual.date).eql(original.date); - - expect(actual.map).instanceOf(Map); - expect(actual.map.size).equal(2); - expect(actual.map).eql(original.map); - - expect(actual.set).instanceOf(Set); - expect(actual.set.size).equal(3); - expect(actual.set).eql(original.set); - - expect(actual.object).instanceOf(Object); - expect(Object.keys(actual.object).length).equal(2); - expect(actual.object).eql(original.object); - - expect(actual.number).toBeTypeOf('number'); - expect(actual.number).equal(original.number); - - expect(actual.string).toBeTypeOf('string'); - expect(actual.string).equal(original.string); - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/json.ts b/packages/svelte-ux/src/lib/utils/json.ts deleted file mode 100644 index e65abd150..000000000 --- a/packages/svelte-ux/src/lib/utils/json.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { parseISO } from 'date-fns'; -import { isStringDate } from './date.js'; - -/** - * JSON.stringify() with custom handling for `Map` and `Set`. To be used with `parse()` - */ -export function stringify(value: any): string { - return JSON.stringify(value, replacer); -} - -export function replacer(key: string, value: any) { - if (value instanceof Map) { - return { - _type: 'Map', - value: Array.from(value.entries()), - }; - } else if (value instanceof Set) { - return { - _type: 'Set', - value: Array.from(value.values()), - }; - } else { - return value; - } -} - -/** - * JSON.parse() with support for restoring `Date`, `Map`, and `Set` instances. `Map` and `Set` require using accompanying `stringify()` - */ -export function parse(value: string): T { - let result; - try { - result = JSON.parse(value, reviver); - } catch (e) { - result = value; - } - - return result; -} - -/** - * Convert date strings to Date instances - */ -export function reviver(key: string, value: any) { - if (typeof value === 'string' && isStringDate(value)) { - return parseISO(value); - } else if (typeof value === 'object' && value !== null) { - if (value._type === 'Map') { - return new Map(value.value); - } else if (value._type === 'Set') { - return new Set(value.value); - } - } - - return value; -} diff --git a/packages/svelte-ux/src/lib/utils/locale.ts b/packages/svelte-ux/src/lib/utils/locale.ts deleted file mode 100644 index 776771248..000000000 --- a/packages/svelte-ux/src/lib/utils/locale.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { entries, fromEntries, type Prettify } from '../types/typeHelpers.js'; -import { defaultsDeep } from 'lodash-es'; -import { derived, writable, type Readable, type Writable } from 'svelte/store'; -import { - DateToken, - DayOfWeek, - type FormatDateLocaleOptions, - type FormatDateLocalePresets, -} from './date_types.js'; -import type { DictionaryMessages, DictionaryMessagesOptions } from './dictionary.js'; -import type { FormatNumberOptions, FormatNumberStyle } from './number.js'; -import { getWeekStartsOnFromIntl } from './dateInternal.js'; - -function resolvedLocaleStore( - forceLocales: Writable, - fallbackLocale?: string -) { - return derived(forceLocales, ($forceLocales) => { - let result: string | undefined; - if ($forceLocales?.length) { - if (Array.isArray($forceLocales)) { - result = $forceLocales[0]; - } else { - result = $forceLocales; - } - } - - return result ?? fallbackLocale ?? 'en'; - }); -} - -export interface LocaleStore extends Readable { - set(value: string | null): void; -} - -export function localeStore(forceLocale: string | undefined, fallbackLocale?: string): LocaleStore { - let currentLocale = writable(forceLocale ?? null); - let resolvedLocale = resolvedLocaleStore(currentLocale, fallbackLocale); - return { - ...resolvedLocale, - set(value: string | null) { - currentLocale.set(value); - }, - }; -} - -type ExcludeNone = T extends 'none' ? never : T; - -export type NumberPresetsOptions = Prettify< - { - defaults?: FormatNumberOptions; - } & { - [key in ExcludeNone]?: FormatNumberOptions; - } ->; -export type NumberPresets = Prettify< - { - defaults: FormatNumberOptions; - } & { - [key in ExcludeNone]?: FormatNumberOptions; - } ->; - -export interface LocaleSettingsInput { - locale: string; - formats?: { - numbers?: NumberPresetsOptions; - dates?: FormatDateLocaleOptions; - }; - dictionary?: DictionaryMessagesOptions; -} - -export interface LocaleSettings { - locale: string; - formats: { - numbers: NumberPresets; - dates: FormatDateLocalePresets; - }; - dictionary: DictionaryMessages; -} - -const defaultLocaleSettings: LocaleSettings = { - locale: 'en', - dictionary: { - Ok: 'Ok', - Cancel: 'Cancel', - Date: { - Start: 'Start', - End: 'End', - Empty: 'Empty', - - Day: 'Day', - DayTime: 'Day Time', - Time: 'Time', - Week: 'Week', - BiWeek: 'Bi-Week', - Month: 'Month', - Quarter: 'Quarter', - CalendarYear: 'Calendar Year', - FiscalYearOct: 'Fiscal Year (Oct)', - - PeriodDay: { - Current: 'Today', - Last: 'Yesterday', - LastX: 'Last {0} days', - }, - PeriodWeek: { - Current: 'This week', - Last: 'Last week', - LastX: 'Last {0} weeks', - }, - PeriodBiWeek: { - Current: 'This bi-week', - Last: 'Last bi-week', - LastX: 'Last {0} bi-weeks', - }, - PeriodMonth: { - Current: 'This month', - Last: 'Last month', - LastX: 'Last {0} months', - }, - PeriodQuarter: { - Current: 'This quarter', - Last: 'Last quarter', - LastX: 'Last {0} quarters', - }, - PeriodQuarterSameLastyear: 'Same quarter last year', - PeriodYear: { - Current: 'This year', - Last: 'Last year', - LastX: 'Last {0} years', - }, - PeriodFiscalYear: { - Current: 'This fiscal year', - Last: 'Last fiscal year', - LastX: 'Last {0} fiscal years', - }, - }, - }, - formats: { - numbers: { - defaults: { - currency: 'USD', - fractionDigits: 2, - currencyDisplay: 'symbol', - }, - }, - dates: { - baseParsing: 'MM/dd/yyyy', - weekStartsOn: DayOfWeek.Sunday, - ordinalSuffixes: { - one: 'st', - two: 'nd', - few: 'rd', - other: 'th', - }, - presets: { - day: { - short: [DateToken.DayOfMonth_numeric, DateToken.Month_numeric], - default: [DateToken.DayOfMonth_numeric, DateToken.Month_numeric, DateToken.Year_numeric], - long: [DateToken.DayOfMonth_numeric, DateToken.Month_short, DateToken.Year_numeric], - }, - dayTime: { - short: [ - DateToken.DayOfMonth_numeric, - DateToken.Month_numeric, - DateToken.Year_numeric, - DateToken.Hour_numeric, - DateToken.Minute_numeric, - ], - default: [ - DateToken.DayOfMonth_numeric, - DateToken.Month_numeric, - DateToken.Year_numeric, - DateToken.Hour_2Digit, - DateToken.Minute_2Digit, - ], - long: [ - DateToken.DayOfMonth_numeric, - DateToken.Month_numeric, - DateToken.Year_numeric, - DateToken.Hour_2Digit, - DateToken.Minute_2Digit, - DateToken.Second_2Digit, - ], - }, - - timeOnly: { - short: [DateToken.Hour_numeric, DateToken.Minute_numeric], - default: [DateToken.Hour_2Digit, DateToken.Minute_2Digit, DateToken.Second_2Digit], - long: [ - DateToken.Hour_2Digit, - DateToken.Minute_2Digit, - DateToken.Second_2Digit, - DateToken.MiliSecond_3, - ], - }, - - week: { - short: [DateToken.DayOfMonth_numeric, DateToken.Month_numeric], - default: [DateToken.DayOfMonth_numeric, DateToken.Month_numeric, DateToken.Year_numeric], - long: [DateToken.DayOfMonth_numeric, DateToken.Month_numeric, DateToken.Year_numeric], - }, - month: { - short: DateToken.Month_short, - default: DateToken.Month_short, - long: DateToken.Month_long, - }, - monthsYear: { - short: [DateToken.Month_short, DateToken.Year_2Digit], - default: [DateToken.Month_long, DateToken.Year_numeric], - long: [DateToken.Month_long, DateToken.Year_numeric], - }, - year: { - short: DateToken.Year_2Digit, - default: DateToken.Year_numeric, - long: DateToken.Year_numeric, - }, - }, - }, - }, -}; - -/** Creates a locale settings object, using the `base` locale settings as defaults. - * If omitted, the `en` locale is used as the base. */ -export function createLocaleSettings( - localeSettings: LocaleSettingsInput, - base = defaultLocaleSettings -): LocaleSettings { - // if ordinalSuffixes is specified, we want to make sure that all are empty first - if (localeSettings.formats?.dates?.ordinalSuffixes) { - localeSettings.formats.dates.ordinalSuffixes = { - one: '', - two: '', - few: '', - other: '', - zero: '', - many: '', - ...localeSettings.formats.dates.ordinalSuffixes, - }; - } - - // if weekStartsOn is not specified, let's default to the local one - if (localeSettings.formats?.dates?.weekStartsOn === undefined) { - localeSettings = defaultsDeep(localeSettings, { - formats: { dates: { weekStartsOn: getWeekStartsOnFromIntl(localeSettings.locale) } }, - }); - } - - return defaultsDeep(localeSettings, base); -} - -export const defaultLocale = createLocaleSettings({ locale: 'en' }); - -export function getAllKnownLocales( - additionalLocales?: Record -): Record { - const additional = additionalLocales - ? entries(additionalLocales).map( - ([key, value]) => [key, createLocaleSettings(value)] satisfies [string, LocaleSettings] - ) - : []; - return { en: defaultLocale, ...fromEntries(additional) }; -} diff --git a/packages/svelte-ux/src/lib/utils/logger.ts b/packages/svelte-ux/src/lib/utils/logger.ts deleted file mode 100644 index 26d664394..000000000 --- a/packages/svelte-ux/src/lib/utils/logger.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { BROWSER } from 'esm-env'; - -const logLevels = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] as const; -type LogLevel = (typeof logLevels)[number]; - -/** - * Enable: - * localStorage.logger = 'SelectField' - * localStorage.logger = 'SelectField:INFO' - * localStorage.logger = 'SelectField,Dialog' - * localStorage.logger = 'SelectField:INFO,Dialog' - */ - -export class Logger { - name: string; - - constructor(name: string) { - this.name = name; - } - - trace(...message: any[]) { - this.log('TRACE', ...message); - } - - debug(...message: any[]) { - this.log('DEBUG', ...message); - } - - info(...message: any[]) { - this.log('INFO', ...message); - } - - warn(...message: any[]) { - this.log('WARN', ...message); - } - - error(...message: any[]) { - this.log('ERROR', ...message); - } - - log(level: LogLevel, ...message: any) { - // TODO: Consider checking `env` for SSR support? - const enabledLoggers = BROWSER - ? (localStorage - .getItem('logger') - ?.split(',') - .map((x) => x.split(':') as [string, LogLevel?]) ?? []) - : []; - - const enabledLogger = enabledLoggers.find((x) => x[0] === this.name); - - const shouldLog = - enabledLogger != null && - logLevels.indexOf(level) >= logLevels.indexOf(enabledLogger[1] ?? 'DEBUG'); - - if (shouldLog) { - switch (level) { - case 'TRACE': - console.trace( - `%c${this.name} %c${level}`, - 'color: hsl(200deg, 10%, 50%)', - 'color: hsl(200deg, 40%, 50%)', - ...message - ); - break; - - case 'DEBUG': - console.log( - `%c${this.name} %c${level}`, - 'color: hsl(200deg, 10%, 50%)', - 'color: hsl(200deg, 40%, 50%)', - ...message - ); - break; - - case 'INFO': - console.log( - `%c${this.name} %c${level}`, - 'color: hsl(200deg, 10%, 50%)', - 'color: hsl(60deg, 100%, 50%)', - ...message - ); - break; - - case 'WARN': - console.warn( - `%c${this.name} %c${level}`, - 'color: hsl(200deg, 10%, 50%)', - 'color: hsl(30deg, 100%, 50%)', - ...message - ); - break; - - case 'ERROR': - console.warn( - `%c${this.name} %c${level}`, - 'color: hsl(200deg, 10%, 50%)', - 'color: hsl(0deg, 100%, 50%)', - ...message - ); - break; - } - } - } -} diff --git a/packages/svelte-ux/src/lib/utils/map.ts b/packages/svelte-ux/src/lib/utils/map.ts deleted file mode 100644 index ea580a455..000000000 --- a/packages/svelte-ux/src/lib/utils/map.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Get the value at path of Map. Useful for nested maps (d3-array group, etc). - * Similar to lodash get() but for Map instead of Object - */ -export function get(map: Map, path: K[]) { - let key: K | undefined = undefined; - let value: Map | V | undefined = map; - const currentPath = [...path]; // Copy since .shift() mutates original array - while ((key = currentPath.shift())) { - if (value instanceof Map && value.has(key)) { - value = value.get(key); - } else { - return undefined; - } - } - - return value; -} diff --git a/packages/svelte-ux/src/lib/utils/number.test.ts b/packages/svelte-ux/src/lib/utils/number.test.ts deleted file mode 100644 index f4efd639e..000000000 --- a/packages/svelte-ux/src/lib/utils/number.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { clamp, formatNumber, formatNumberWithLocale, round, step } from './number.js'; -import { createLocaleSettings } from './locale.js'; - -describe('clamp()', () => { - it('no change', () => { - const original = 15; - const actual = clamp(original, 10, 20); - expect(actual).equal(15); - }); - - it('clamp min', () => { - const original = 5; - const actual = clamp(original, 10, 20); - expect(actual).equal(10); - }); - - it('clamp max', () => { - const original = 25; - const actual = clamp(original, 10, 20); - expect(actual).equal(20); - }); -}); - -describe('round()', () => { - it('round to 0 digits (down)', () => { - const original = 123.456; - const actual = round(original, 0); - expect(actual).equal(123); - }); - - it('round to 0 digits (up)', () => { - const original = 123.56; - const actual = round(original, 0); - expect(actual).equal(124); - }); - - it('round to 1 digit', () => { - const original = 123.456; - const actual = round(original, 1); - expect(actual).equal(123.5); - }); - - it('round to 2 digits', () => { - const original = 123.456; - const actual = round(original, 2); - expect(actual).equal(123.46); - }); -}); - -describe('step()', () => { - it('integer (step up)', () => { - const actual = step(2, 1); - expect(actual).equal(3); - }); - - it('integer (step down)', () => { - const actual = step(2, -1); - expect(actual).equal(1); - }); - - it('decimal (step up)', () => { - const actual = step(0.2, 0.1); - expect(actual).equal(0.3); - }); - - it('decimal (step down)', () => { - const actual = step(0.2, -0.1); - expect(actual).equal(0.1); - }); - - it('decimal with integer step (step up)', () => { - const actual = step(0.2, 1); - expect(actual).equal(1); - }); - - it('decimal with integer step (step down)', () => { - const actual = step(0.2, -1); - expect(actual).equal(-1); - }); - - it('integer with decimal step (step up)', () => { - const actual = step(2, 0.1); - expect(actual).equal(2.1); - }); - - it('integer with decimal step (step down)', () => { - const actual = step(2, -0.1); - expect(actual).equal(1.9); - }); -}); - -describe('formatNumber()', () => { - it('returns empty string for null', () => { - const actual = formatNumber(null); - expect(actual).equal(''); - }); - - it('returns empty string for undefined', () => { - const actual = formatNumber(undefined); - expect(actual).equal(''); - }); - - it('returns value as string for style "none"', () => { - const actual = formatNumber(1234.5678, 'none'); - expect(actual).equal('1234.5678'); - }); - - it('formats number with integer default', () => { - const actual = formatNumber(1234.5678, 'integer'); - expect(actual).equal('1,235'); - }); - - it('formats number with integer fr', () => { - const actual = formatNumberWithLocale( - createLocaleSettings({ locale: 'fr' }), - 1234.5678, - 'integer' - ); - expect(actual).equal('1 235'); - }); - - it('formats number with default fraction digits', () => { - const actual = formatNumber(1234.5678); - expect(actual).equal('1,234.57'); - }); - - it('formats number with specified fraction digits', () => { - const actual = formatNumber(1234.5678, 'decimal', { fractionDigits: 3 }); - expect(actual).equal('1,234.568'); - }); - - it('returns value with significant digits', () => { - const actual = formatNumber(1234.5678, 'default', { - notation: 'compact', - maximumSignificantDigits: 2, - }); - expect(actual).equal('1.2K'); - }); - - it('returns value with significant digits', () => { - const actual = formatNumber(1000, 'default', { - notation: 'compact', - minimumSignificantDigits: 2, - }); - expect(actual).equal('1.0K'); - }); - - it('formats number with currency USD by style', () => { - const actual = formatNumber(1234.5678, 'currency'); - expect(actual).equal('$1,234.57'); - }); - - it('formats number with currency USD by currency', () => { - const actual = formatNumber(1234.5678, 'currency', { currency: 'USD' }); - expect(actual).equal('$1,234.57'); - }); - - it('formats number with currency GBP', () => { - const actual = formatNumber(1234.5678, 'currency', { currency: 'GBP' }); - expect(actual).equal('£1,234.57'); - }); - - it('formats number with currency EUR only currency', () => { - const actual = formatNumber(1234.5678, 'currency', { currency: 'EUR' }); - expect(actual).equal('€1,234.57'); - }); - - it('formats number with currency EUR with right local', () => { - const actual = formatNumberWithLocale( - createLocaleSettings({ locale: 'fr' }), - 1234.5678, - 'currency', - { - currency: 'EUR', - } - ); - expect(actual).equal('1 234,57 €'); - }); - - it('formats number with currencyRound', () => { - const actual = formatNumber(1234.5678, 'currencyRound'); - expect(actual).equal('$1,235'); - }); - - it('returns value with percent symbol for style "percent"', () => { - const actual = formatNumber(0.1234, 'percent'); - expect(actual).equal('12.34%'); - }); - - it('returns value with percent symbol and no decimal for style "percentRound"', () => { - const actual2 = formatNumber(0.1234, 'percentRound'); - expect(actual2).equal('12%'); - }); - - it('returns value with metric suffix for style "unit" & meters', () => { - const actual = formatNumber(1000, 'unit', { - unit: 'meter', - unitDisplay: 'narrow', - - notation: 'compact', - fractionDigits: 0, - }); - expect(actual).equal('1Km'); - }); - - it('byte 10B', () => { - const actual = formatNumber(10, 'unit', { - unit: 'byte', - unitDisplay: 'narrow', - notation: 'compact', - fractionDigits: 0, - }); - expect(actual).equal('10B'); - }); - - it('byte 200KB', () => { - const actual = formatNumber(200000, 'unit', { - unit: 'byte', - unitDisplay: 'narrow', - notation: 'compact', - fractionDigits: 0, - }); - expect(actual).equal('200KB'); - }); - - it('byte 50MB', () => { - const actual = formatNumber(50000000, 'unit', { - unit: 'byte', - unitDisplay: 'narrow', - notation: 'compact', - fractionDigits: 0, - }); - expect(actual).equal('50MB'); - }); - - it('dollar 0', () => { - const actual = formatNumber(0, 'metric', { - suffix: ' dollar', - }); - expect(actual).equal('0 dollar'); - }); - - it('dollars 10', () => { - const actual = formatNumber(10, 'metric', { - suffix: ' dollar', - }); - expect(actual).equal('10 dollars'); - }); - - it('dollars 200K', () => { - const actual = formatNumber(200000, 'metric', { - suffix: ' dollar', - }); - expect(actual).equal('200K dollars'); - }); - - it('dollars 50M', () => { - const actual = formatNumber(50000000, 'metric', { - suffix: ' dollar', - }); - expect(actual).equal('50M dollars'); - }); - - it('50M wo suffix', () => { - const actual = formatNumber(50000000, 'metric'); - expect(actual).equal('50M'); - }); - - it('200 m²', () => { - const actual = formatNumber(200, 'metric', { - suffix: ' m²', - suffixExtraIfMany: '', - }); - expect(actual).equal('200 m²'); - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/number.ts b/packages/svelte-ux/src/lib/utils/number.ts deleted file mode 100644 index 7e01a9430..000000000 --- a/packages/svelte-ux/src/lib/utils/number.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { defaultLocale, type LocaleSettings } from './locale.js'; -import { omitNil } from './object.js'; - -export type FormatNumberStyle = - | 'decimal' // from Intl.NumberFormat options.style NumberFormatOptions - | 'currency' // from Intl.NumberFormat options.style NumberFormatOptions - | 'percent' // from Intl.NumberFormat options.style NumberFormatOptions - | 'unit' // from Intl.NumberFormat options.style NumberFormatOptions - | 'none' - | 'integer' - | 'currencyRound' - | 'percentRound' - | 'metric' - | 'default'; - -export type FormatNumberOptions = Intl.NumberFormatOptions & { - fractionDigits?: number; - suffix?: string; - /** - * If number is >= 2, then this extraSuffix will be appended - * @default 's' - */ - suffixExtraIfMany?: string; -}; - -function getFormatNumber(settings: LocaleSettings, style: FormatNumberStyle | undefined) { - const { numbers } = settings.formats; - const styleSettings = style && style != 'none' ? numbers[style] : {}; - return { - ...numbers.defaults, - ...styleSettings, - }; -} - -export function formatNumber( - number: number | null | undefined, - style?: FormatNumberStyle, - options?: FormatNumberOptions -) { - return formatNumberWithLocale(defaultLocale, number, style, options); -} - -// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat -export function formatNumberWithLocale( - settings: LocaleSettings, - number: number | null | undefined, - style?: FormatNumberStyle, - options: FormatNumberOptions = {} -) { - if (number == null) { - return ''; - } - - if (style === 'none') { - return `${number}`; - } - - // Determine default style if not provided (undefined or null) - if (style == null) { - style = Number.isInteger(number) ? 'integer' : 'decimal'; - } - - const defaults = getFormatNumber(settings, style); - - // @ts-expect-error: Determine how to access `NumberFormatOptionsStyleRegistry` and check instead of just `style !=== 'default' below) - const formatter = Intl.NumberFormat(settings.locale, { - // Let's always starts with all defaults - ...defaults, - - ...(style !== 'default' && { - style, - }), - - // Let's shorten min / max with fractionDigits - ...{ - minimumFractionDigits: options.fractionDigits ?? defaults.fractionDigits, - maximumFractionDigits: options.fractionDigits ?? defaults.fractionDigits, - }, - - // now we bring in user specified options - ...omitNil(options), - - ...(style === 'currencyRound' && { - style: 'currency', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }), - - // Let's overwrite for style=percentRound - ...(style === 'percentRound' && { - style: 'percent', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }), - - // Let's overwrite for style=metric - ...(style === 'metric' && { - style: 'decimal', - notation: 'compact', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }), - - // Let's overwrite for style=integer - ...(style === 'integer' && { - style: 'decimal', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }), - }); - const value = formatter.format(number); - - let suffix = options.suffix ?? ''; - if (suffix && Math.abs(number) >= 2 && options.suffixExtraIfMany !== '') { - suffix += options.suffixExtraIfMany ?? 's'; - } - - return `${value}${suffix}`; -} - -/** - * Clamps value within min and max - */ -export function clamp(value: number, min: number, max: number) { - return value < min ? min : value > max ? max : value; -} - -/** - * Return the number of decimal positions (ex. 123.45 => 2, 123 => 0) - */ -export function decimalCount(value: number) { - return value?.toString().split('.')[1]?.length ?? 0; -} - -/** - * Round to the number of decimals (ex. round(123.45, 1) => 123.5) - */ -export function round(value: number, decimals: number) { - return Number(value.toFixed(decimals)); -} - -/** - * Step value while rounding to the nearest step precision (work around float issues such as `0.2` + `0.1`) - */ -export function step(value: number, step: number) { - return round(value + step, decimalCount(step)); -} - -/** - * Get random number between min and max (inclusive). See also d3.randomInt() - */ -export function randomInteger(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -/** - * Remainder (n % m) with support for negative numbers - * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder#description - */ -export function modulo(n: number, m: number) { - return ((n % m) + m) % m; -} diff --git a/packages/svelte-ux/src/lib/utils/object.test.ts b/packages/svelte-ux/src/lib/utils/object.test.ts deleted file mode 100644 index c4e0ac75d..000000000 --- a/packages/svelte-ux/src/lib/utils/object.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { addHours, subHours } from 'date-fns'; - -import { expireObject, omit, omitNil, pick } from './object.js'; - -describe('expireObject', () => { - it('simple value not expired', () => { - const original = 123; - const expiry = addHours(new Date(), 1); - - const actual = expireObject(original, expiry); - expect(actual).equal(original); - }); - - it('simple value expired', () => { - const original = 123; - const expiry = subHours(new Date(), 1); - - const actual = expireObject(original, expiry); - expect(actual).toBeNull(); - }); - - it('Date not expired', () => { - const original = new Date(); - const expiry = addHours(new Date(), 1); - - const actual = expireObject(original, expiry); - expect(actual).equal(original); - }); - - it('Date expired', () => { - const original = new Date(); - const expiry = subHours(new Date(), 1); - - const actual = expireObject(original, expiry); - expect(actual).toBeNull(); - }); - - it('full object not expired', () => { - const original = { - one: 1, - two: 2, - three: 3, - }; - const expiry = addHours(new Date(), 1); - - const actual = expireObject(original, expiry); - expect(actual).equal(original); - }); - - it('full object expired', () => { - const original = { - one: 1, - two: 2, - three: 3, - }; - const expiry = subHours(new Date(), 1); - - const actual = expireObject(original, expiry); - expect(actual).toBeNull(); - }); - - it('partial object expired', () => { - const original = { - one: 1, - two: 2, - three: 3, - }; - const expiry = { - two: subHours(new Date(), 1), - }; - - const actual = expireObject(original, expiry); - const expected = { - one: 1, - three: 3, - }; - expect(actual).equal(original); - - // Test cleaning up expiry - const expiryActual = expireObject(expiry, expiry); - expect(expiryActual).toBeNull(); - }); - - it('partial object expired with default', () => { - const original = { - one: 1, - two: 2, - three: 3, - }; - const expiry = { - one: subHours(new Date(), 3), - two: addHours(new Date(), 1), - $default: subHours(new Date(), 1), - }; - - const actual = expireObject(original, expiry); - const expected = { - two: 2, - }; - expect(actual).eql(expected); - - // Test cleaning up expiry - const expiryActual = expireObject(expiry, expiry); - expect(expiryActual).eql({ two: expiry.two }); - }); - - it('extra property expiry not in object', () => { - const original = { - one: 1, - two: 2, - three: 3, - }; - const expiry = { - one: subHours(new Date(), 3), - two: addHours(new Date(), 1), - four: subHours(new Date(), 1), - }; - - const actual = expireObject(original, expiry); - const expected = { - two: 2, - three: 3, - }; - expect(actual).eql(expected); - - // Test cleaning up expiry - const expiryActual = expireObject(expiry, expiry); - expect(expiryActual).eql({ two: expiry.two }); - }); - - it('expired object property', () => { - const original = { - one: { - foo: 1, - bar: 2, - }, - two: 2, - three: 3, - }; - const expiry = { - one: subHours(new Date(), 3), - two: addHours(new Date(), 1), - }; - - const actual = expireObject(original, expiry); - const expected = { - two: 2, - three: 3, - }; - expect(actual).eql(expected); - - // Test cleaning up expiry - const expiryActual = expireObject(expiry, expiry); - expect(expiryActual).eql({ two: expiry.two }); - }); - - it('expired nested object property', () => { - const original = { - one: { - foo: 1, - bar: 2, - }, - two: 2, - three: 3, - }; - const expiry = { - one: { - foo: subHours(new Date(), 3), - }, - two: addHours(new Date(), 1), - }; - - const actual = expireObject(original, expiry); - const expected = { - one: { - bar: 2, - }, - two: 2, - three: 3, - }; - expect(actual).eql(expected); - - // Test cleaning up expiry - const expiryActual = expireObject(expiry, expiry); - expect(expiryActual).eql({ two: expiry.two }); - }); - - it('removes $default expiry if expired', () => { - const expiry = { - one: addHours(new Date(), 1), - $default: subHours(new Date(), 1), - }; - - // Test cleaning up expiry - const expiryActual = expireObject(expiry, expiry); - expect(expiryActual).eql({ one: expiry.one }); - }); -}); - -describe('omit', () => { - it('remove single property', () => { - const original = { one: 1, two: 2, three: 3 }; - - const actual = omit(original, ['three']); - expect(actual).eql({ one: 1, two: 2 }); - }); -}); - -describe('omitNil', () => { - it('removes null values', () => { - const original = { one: 1, two: null, three: 3 }; - - const actual = omitNil(original); - expect(actual).eql({ one: 1, three: 3 }); - }); - - it('removes undefined values', () => { - const original = { one: 1, two: undefined, three: 3 }; - - const actual = omitNil(original); - expect(actual).eql({ one: 1, three: 3 }); - }); -}); - -describe('pick', () => { - it('pick single property', () => { - const original = { one: 1, two: 2, three: 3 }; - - const actual = pick(original, ['one']); - expect(actual).eql({ one: 1 }); - }); - - it('pick multiple properties', () => { - const original = { one: 1, two: 2, three: 3 }; - - const actual = pick(original, ['two', 'three']); - expect(actual).eql({ two: 2, three: 3 }); - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/object.ts b/packages/svelte-ux/src/lib/utils/object.ts deleted file mode 100644 index 1dc6c0b65..000000000 --- a/packages/svelte-ux/src/lib/utils/object.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { get, camelCase, mergeWith } from 'lodash-es'; -import { entries, fromEntries, keys } from '../types/typeHelpers.js'; - -export function isLiteralObject(obj: any): obj is object { - return obj && typeof obj === 'object' && obj.constructor === Object; -} - -export function isEmptyObject(obj: any) { - return isLiteralObject(obj) && keys(obj).length === 0; -} - -export function camelCaseKeys(obj: any) { - return keys(obj).reduce( - (acc, key) => ((acc[camelCase(key ? String(key) : undefined)] = obj[key]), acc), - {} as any - ); -} - -// https://codereview.stackexchange.com/questions/73714/find-a-nested-property-in-an-object -// https://github.com/dominik791/obj-traverse -export function nestedFindByPredicate( - obj: any, - predicate: (item: any) => boolean, - childrenProp?: PropAccessorArg -): any | undefined { - const getChildrenProp = propAccessor(childrenProp ?? 'children'); - - if (predicate(obj)) { - return obj; - } else { - const children = getChildrenProp(obj); - if (children) { - for (let o of children) { - const match = nestedFindByPredicate(o, predicate, childrenProp); - if (match) { - return match; - } - } - } - } -} - -export type PropAccessorArg = Parameters[0]; -export function propAccessor(prop?: string | ((x: any) => any) | null) { - return typeof prop === 'function' - ? prop - : typeof prop === 'string' - ? (d: { [key: string]: any }) => get(d, prop) - : (x: any) => x; -} - -/** - * Produce a unique Id for an object (helpful for debugging) - * See: https://stackoverflow.com/a/35306050/191902 - */ -var objIdMap = new WeakMap(), - objectCount = 0; -export function objectId(object: any) { - if (!objIdMap.has(object)) objIdMap.set(object, ++objectCount); - return objIdMap.get(object); -} - -export function distinctKeys(...objs: object[]) { - return [...new Set(flatten(objs.map((x: object) => keys(x as Record))))]; -} - -// Copied from `array.ts` to remove circular dependency -function flatten(items: T[][]): T[] { - return items.reduce((prev, next) => prev.concat(next), []); -} - -/** - * Recursive merge objects - * @param object The destination object - * @param source The source object - * @returns - */ -export function merge(object: TObject, source: TSource) { - return mergeWith(object, source, (objValue, srcValue) => { - if (Array.isArray(srcValue)) { - // Overwrite instead of merging by index with objValue (like standard lodash `merge` does) - return srcValue; - } - }); -} - -export type Expiry = Date | { [prop: string]: Date | { [prop: string]: Date } }; - -/** - * Remove properties from object based on expiration - */ -export function expireObject(object: TObject, expiry: Expiry): Partial | null { - const now = new Date(); - - if (expiry instanceof Date || typeof object !== 'object' || object == null) { - // Expired - if (expiry < now) { - return null; - } - // Not expired - return object; - } - - // LoopIterate over the properties in `object` - for (let [prop, propExpiry] of entries(expiry)) { - if (propExpiry instanceof Date) { - // Check if expired - if (propExpiry < now) { - if (prop === '$default') { - // Delete all properties which do not have explicit expiry to check - for (let objProp of keys(object) as Array) { - if (!(objProp in expiry)) { - delete object[objProp]; - } - } - - // Remove expired `$default` property - // @ts-expect-error it's fine if the property doesn't exist in object - delete object[prop]; - } else { - // Remove expired property - // @ts-expect-error it's fine if the property doesn't exist in object - delete object[prop]; - } - } else { - // Keep value - } - } else { - // Check expiry for each property in object. Skip if prop not in object (expiry only) - const value = object[prop as keyof TObject]; - if (value && typeof value === 'object') { - expireObject(value, propExpiry); - - // Remove property if empty object (all properties removed) - if (isEmptyObject(value)) { - delete object[prop as keyof TObject]; - } - } - } - } - - return isEmptyObject(object) ? null : object; -} - -/** - * Remove properties from an object. See also lodash `_.omit()` - */ -export function omit(obj: T, keys: (keyof T)[]): Partial { - if (keys.length === 0) { - return obj; - } else { - return fromEntries( - entries(obj).filter(([key]: [keyof T, T[keyof T]]) => !keys.includes(key)) - ) as Partial; - } -} - -/** - * Remove `null` or `undefined` properties from an object - */ -export function omitNil(obj: T): Partial { - if (keys.length === 0) { - return obj; - } else { - return fromEntries( - entries(obj).filter(([key, value]: [keyof T, T[keyof T]]) => value != null) - ) as Partial; - } -} - -/** - * Pick properties from an object. See also lodash `_.pick()` - */ -export function pick(obj: T, keys: string[]): Partial { - if (keys.length === 0) { - return obj; - } else { - return fromEntries( - keys.filter((key) => key in obj).map((key) => [key, obj[key as keyof T]]) - ) as Partial; - } -} - -/** - * Create new object with keys and values swapped. Last value's key is used if duplicated - */ -export function keysByValues(obj: T): Record { - return fromEntries(entries(obj).map(([key, value]) => [String(value), key])); -} diff --git a/packages/svelte-ux/src/lib/utils/promise.ts b/packages/svelte-ux/src/lib/utils/promise.ts deleted file mode 100644 index 98eaa4f54..000000000 --- a/packages/svelte-ux/src/lib/utils/promise.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/packages/svelte-ux/src/lib/utils/rollup.ts b/packages/svelte-ux/src/lib/utils/rollup.ts deleted file mode 100644 index c27f27598..000000000 --- a/packages/svelte-ux/src/lib/utils/rollup.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { rollup } from 'd3-array'; -import { get, isFunction } from 'lodash-es'; - -export default function ( - data: T[], - reduce: (values: T[]) => any, - keys: (((d: any) => any) | string | null)[] = [], - emptyKey = 'Unknown' -) { - // TODO: Fix object[] type if needed - // if (keys.length === 0) { - // return data; - // } - - const keyFuncs = keys.map((key) => { - if (isFunction(key)) { - return key; - } else if (typeof key === 'string') { - return (d: any) => get(d, key) || emptyKey; - } else { - return () => 'Overall'; - } - }); - - return rollup(data, reduce, ...keyFuncs); -} diff --git a/packages/svelte-ux/src/lib/utils/routing.test.ts b/packages/svelte-ux/src/lib/utils/routing.test.ts deleted file mode 100644 index 3eb15b92b..000000000 --- a/packages/svelte-ux/src/lib/utils/routing.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { isActive } from './routing.js'; - -describe('isActive()', () => { - it('identifical path', () => { - const currentUrl = new URL('http://localhost/foo'); - const path = '/foo'; - expect(isActive(currentUrl, path)).true; - }); - - it('identifical path with query string', () => { - const currentUrl = new URL('http://localhost/foo?one=1'); - const path = '/foo'; - expect(isActive(currentUrl, path)).true; - }); - - it('nested path', () => { - const currentUrl = new URL('http://localhost/foo/bar'); - const path = '/foo'; - expect(isActive(currentUrl, path)).true; - }); - - it('same prefix but different path', () => { - const currentUrl = new URL('http://localhost/fo'); - const path = '/foo'; - expect(isActive(currentUrl, path)).false; - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/routing.ts b/packages/svelte-ux/src/lib/utils/routing.ts deleted file mode 100644 index 27f14d83d..000000000 --- a/packages/svelte-ux/src/lib/utils/routing.ts +++ /dev/null @@ -1,39 +0,0 @@ -// See: routify's helpers: https://github.com/roxiness/routify/blob/9a1b7f5f8fc950a344cf20f7cbaa760593ded8fb/runtime/helpers.js#L244-L268 -export function url(currentUrl: URL, path: string) { - if (path == null) { - return path; - } else if (path.match(/^\.\.?\//)) { - // Relative path (starts wtih `./` or `../`) - // console.log('relative path'); - let [, breadcrumbs, relativePath] = path.match(/^([\.\/]+)(.*)/) as string[]; - let dir = currentUrl.pathname.replace(/\/$/, ''); - // console.log({ dir, breadcrumbs, relativePath }); - const traverse = breadcrumbs.match(/\.\.\//g) || []; - // if this is a page, we want to traverse one step back to its folder - // if (component.isPage) traverse.push(null) - traverse.forEach(() => (dir = dir.replace(/\/[^\/]+\/?$/, ''))); - path = `${dir}/${relativePath}`.replace(/\/$/, ''); - path = path || '/'; // empty means root - // console.groupEnd(); - } else if (path.match(/^\//)) { - // Absolute path (starts with `/`) - // console.log('absoute path'); - return path; - } else { - // Unknown (no named path) - return path; - } - - // console.log({ path }); - return path; -} - -export function isActive(currentUrl: URL, path: string) { - if (path === '/') { - // home must be direct match (otherwise matches all) - return currentUrl.pathname === path; - } else { - // Matches full path next character is `/` - return currentUrl.pathname.match(path + '($|\\/)') != null; - } -} diff --git a/packages/svelte-ux/src/lib/utils/serialize.ts b/packages/svelte-ux/src/lib/utils/serialize.ts deleted file mode 100644 index 806a5b9be..000000000 --- a/packages/svelte-ux/src/lib/utils/serialize.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { keys } from '../types/typeHelpers.js'; -import { parse, reviver, stringify } from './json.js'; -import { isEmptyObject } from './object.js'; - -// See: https://github.com/pbeshai/serialize-query-params/blob/master/src/serialize.ts - -/** - * Interprets an encoded string and returns either the string or null/undefined if not available. - * Ignores array inputs (takes just first element in array) - * @param input encoded string - */ -function getEncodedValue( - input: string | (string | null)[] | null | undefined, - allowEmptyString?: boolean -): string | null | undefined { - if (input == null) { - return input; - } - // '' or [] - if (input.length === 0 && (!allowEmptyString || (allowEmptyString && input !== ''))) { - return null; - } - - const str = input instanceof Array ? input[0] : input; - if (str == null) { - return str; - } - if (!allowEmptyString && str === '') { - return null; - } - - return str; -} - -/** - * Interprets an encoded string and return null/undefined or an array with - * the encoded string contents - * @param input encoded string - */ -function getEncodedValueArray( - input: string | (string | null)[] | null | undefined -): (string | null)[] | null | undefined { - if (input == null) { - return input; - } - - return input instanceof Array ? input : input === '' ? [] : [input]; -} - -/** - * Encodes a date as a string in YYYY-MM-DD format. - * - * @param {Date} date - * @return {String} the encoded date - */ -export function encodeDate(date: Date | null | undefined): string | null | undefined { - if (date == null) { - return date; - } - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return `${year}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day}`; -} - -/** - * Converts a date in the format 'YYYY-mm-dd...' into a proper date, because - * new Date() does not do that correctly. The date can be as complete or incomplete - * as necessary (aka, '2015', '2015-10', '2015-10-01'). - * It will not work for dates that have times included in them. - * - * If an array is provided, only the first entry is used. - * - * @param {String} input String date form like '2015-10-01' - * @return {Date} parsed date - */ -export function decodeDate( - input: string | (string | null)[] | null | undefined -): Date | null | undefined { - const dateString = getEncodedValue(input); - if (dateString == null) return dateString; - - const parts = dateString.split('-') as any; - // may only be a year so won't even have a month - if (parts[1] != null) { - parts[1] -= 1; // Note: months are 0-based - } else { - // just a year, set the month and day to the first - parts[1] = 0; - parts[2] = 1; - } - - const decoded = new Date(...(parts as [number, number, number])); - - if (isNaN(decoded.getTime())) { - return null; - } - - return decoded; -} - -/** - * Encodes a date as a string in ISO 8601 ("2019-05-28T10:58:40Z") format. - * - * @param {Date} date - * @return {String} the encoded date - */ -export function encodeDateTime(date: Date | null | undefined): string | null | undefined { - if (date == null) { - return date; - } - - return date.toISOString(); -} - -/** - * Converts a date in the https://en.wikipedia.org/wiki/ISO_8601 format. - * For allowed inputs see specs: - * - https://tools.ietf.org/html/rfc2822#page-14 - * - http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 - * - * If an array is provided, only the first entry is used. - * - * @param {String} input String date form like '1995-12-17T03:24:00' - * @return {Date} parsed date - */ -export function decodeDateTime( - input: string | (string | null)[] | null | undefined -): Date | null | undefined { - const dateString = getEncodedValue(input); - if (dateString == null) return dateString; - - const decoded = new Date(dateString); - - if (isNaN(decoded.getTime())) { - return null; - } - - return decoded; -} - -/** - * Encodes a boolean as a string. true -> "1", false -> "0". - * - * @param {Boolean} bool - * @return {String} the encoded boolean - */ -export function encodeBoolean(bool: boolean | null | undefined): string | null | undefined { - if (bool == null) { - return bool; - } - - return bool ? '1' : '0'; -} - -/** - * Decodes a boolean from a string. "1" -> true, "0" -> false. - * Everything else maps to undefined. - * - * If an array is provided, only the first entry is used. - * - * @param {String} input the encoded boolean string - * @return {Boolean} the boolean value - */ -export function decodeBoolean( - input: string | (string | null)[] | null | undefined -): boolean | null | undefined { - const boolStr = getEncodedValue(input); - if (boolStr == null) return boolStr; - - if (boolStr === '1') { - return true; - } else if (boolStr === '0') { - return false; - } - - return null; -} - -/** - * Encodes a number as a string. - * - * @param {Number} num - * @return {String} the encoded number - */ -export function encodeNumber(num: number | null | undefined): string | null | undefined { - if (num == null) { - return num; - } - - return String(num); -} - -/** - * Decodes a number from a string. If the number is invalid, - * it returns undefined. - * - * If an array is provided, only the first entry is used. - * - * @param {String} input the encoded number string - * @return {Number} the number value - */ -export function decodeNumber( - input: string | (string | null)[] | null | undefined -): number | null | undefined { - const numStr = getEncodedValue(input); - if (numStr == null) return numStr; - if (numStr === '') return null; - - const result = +numStr; - return result; -} - -/** - * Encodes a string while safely handling null and undefined values. - * - * @param {String} str a string to encode - * @return {String} the encoded string - */ -export function encodeString( - str: string | (string | null)[] | null | undefined -): string | null | undefined { - if (str == null) { - return str; - } - - return String(str); -} - -/** - * Decodes a string while safely handling null and undefined values. - * - * If an array is provided, only the first entry is used. - * - * @param {String} input the encoded string - * @return {String} the string value - */ -export function decodeString( - input: string | (string | null)[] | null | undefined -): string | null | undefined { - const str = getEncodedValue(input, true); - if (str == null) return str; - - return String(str); -} - -/** - * Decodes an enum value while safely handling null and undefined values. - * - * If an array is provided, only the first entry is used. - * - * @param {String} input the encoded string - * @param {String[]} enumValues allowed enum values - * @return {String} the string value from enumValues - */ -export function decodeEnum( - input: string | (string | null)[] | null | undefined, - enumValues: T[] -): T | null | undefined { - const str = decodeString(input); - if (str == null) return str; - return enumValues.includes(str as any) ? (str as T) : undefined; -} - -/** - * Encodes anything as a JSON string. - * - * @param {Any} any The thing to be encoded - * @return {String} The JSON string representation of any - */ -export function encodeJson(any: any | null | undefined): string | null | undefined { - if (any == null) { - return any; - } - - return stringify(any); -} - -/** - * Decodes a JSON string into javascript. - * - * If an array is provided, only the first entry is used. - * - * Restores Date strings to date objects - * - * @param {String} input The JSON string representation - * @return {Any} The javascript representation - */ -export function decodeJson( - input: string | (string | null)[] | null | undefined -): any | null | undefined { - const jsonStr = getEncodedValue(input); - if (jsonStr == null) return jsonStr; - - let result = null; - try { - result = parse(jsonStr); - } catch (e) { - /* ignore errors, returning undefined */ - } - - return result; -} - -/** - * Encodes an array as a JSON string. - * - * @param {Array} array The array to be encoded - * @return {String[]} The array of strings to be put in the URL - * as repeated query parameters - */ -export function encodeArray( - array: (string | null)[] | null | undefined -): (string | null)[] | null | undefined { - if (array == null) { - return array; - } - - return array; -} - -/** - * Decodes an array or singular value and returns it as an array - * or undefined if falsy. Filters out undefined values. - * - * @param {String | Array} input The input value - * @return {Array} The javascript representation - */ -export function decodeArray( - input: string | (string | null)[] | null | undefined -): (string | null)[] | null | undefined { - const arr = getEncodedValueArray(input); - if (arr == null) return arr; - - return arr; -} - -/** - * Encodes a numeric array as a JSON string. - * - * @param {Array} array The array to be encoded - * @return {String[]} The array of strings to be put in the URL - * as repeated query parameters - */ -export function encodeNumericArray( - array: (number | null)[] | null | undefined -): (string | null)[] | null | undefined { - if (array == null) { - return array; - } - - return array.map(String); -} - -/** - * Decodes an array or singular value and returns it as an array - * or undefined if falsy. Filters out undefined and NaN values. - * - * @param {String | Array} input The input value - * @return {Array} The javascript representation - */ -export function decodeNumericArray( - input: string | (string | null)[] | null | undefined -): (number | null)[] | null | undefined { - const arr = decodeArray(input); - if (arr == null) return arr; - - return arr.map((d) => (d === '' || d == null ? null : +d)); -} - -/** - * Encodes an array as a delimited string. For example, - * ['a', 'b'] -> 'a_b' with entrySeparator='_' - * - * @param array The array to be encoded - * @param entrySeparator The string used to delimit entries - * @return The array as a string with elements joined by the - * entry separator - */ -export function encodeDelimitedArray( - array: (string | null)[] | null | undefined, - entrySeparator = '_' -): string | null | undefined { - if (array == null) { - return array; - } - - return array.join(entrySeparator); -} - -/** - * Decodes a delimited string into javascript array. For example, - * 'a_b' -> ['a', 'b'] with entrySeparator='_' - * - * If an array is provided as input, only the first entry is used. - * - * @param {String} input The JSON string representation - * @param entrySeparator The array as a string with elements joined by the - * entry separator - * @return {Array} The javascript representation - */ -export function decodeDelimitedArray( - input: string | (string | null)[] | null | undefined, - entrySeparator = '_' -): (string | null)[] | null | undefined { - const arrayStr = getEncodedValue(input, true); - if (arrayStr == null) return arrayStr; - if (arrayStr === '') return []; - - return arrayStr.split(entrySeparator); -} - -/** - * Encodes a numeric array as a delimited string. (alias of encodeDelimitedArray) - * For example, [1, 2] -> '1_2' with entrySeparator='_' - * - * @param {Array} array The array to be encoded - * @return {String} The JSON string representation of array - */ -export const encodeDelimitedNumericArray = encodeDelimitedArray as ( - array: (number | null)[] | null | undefined, - entrySeparator?: string -) => string | null | undefined; - -/** - * Decodes a delimited string into javascript array where all entries are numbers - * For example, '1_2' -> [1, 2] with entrySeparator='_' - * - * If an array is provided as input, only the first entry is used. - * - * @param {String} jsonStr The JSON string representation - * @return {Array} The javascript representation - */ -export function decodeDelimitedNumericArray( - arrayStr: string | (string | null)[] | null | undefined, - entrySeparator = '_' -): (number | null)[] | null | undefined { - const decoded = decodeDelimitedArray(arrayStr, entrySeparator); - if (decoded == null) return decoded; - - return decoded.map((d) => (d === '' || d == null ? null : +d)); -} - -/** - * Encode simple objects as readable strings. - * - * For example { foo: bar, boo: baz } -> "foo-bar_boo-baz" - * - * @param {Object} object The object to encode - * @param {String} keyValSeparator="-" The separator between keys and values - * @param {String} entrySeparator="_" The separator between entries - * @return {String} The encoded object - */ -export function encodeObject( - obj: { [key: string]: any } | null | undefined, - keyValSeparator = '-', - entrySeparator = '_' -): string | null | undefined { - if (obj == null) return obj; // null or undefined - if (isEmptyObject(obj)) return ''; // {} case - - return keys(obj) - .map((key) => { - const value = encodeJson(obj[key]); - return `${key}${keyValSeparator}${value}`; - }) - .join(entrySeparator); -} - -/** - * Decodes a simple object to javascript. Currently works only for simple, - * flat objects where values are strings. - * - * For example "foo-bar_boo-baz" -> { foo: bar, boo: baz } - * - * If an array is provided as input, only the first entry is used. - * - * @param {String} input The object string to decode - * @param {String} keyValSeparator="-" The separator between keys and values - * @param {String} entrySeparator="_" The separator between entries - * @return {Object} The javascript object - */ -export function decodeObject( - input: string | (string | null)[] | null | undefined, - keyValSeparator = '-', - entrySeparator = '_' -): { [key: string]: any } | null | undefined { - const objStr = getEncodedValue(input, true); - if (objStr == null) return objStr; - if (objStr === '') return {}; - - const obj: { [key: string]: string } = {}; - - const keyValSeparatorRegExp = new RegExp(`${keyValSeparator}(.*)`); - objStr.split(entrySeparator).forEach((entryStr) => { - const [key, value] = entryStr.split(keyValSeparatorRegExp); - obj[key] = decodeJson(value); - }); - - return obj; -} - -/** - * Encode simple objects as readable strings. Alias of encodeObject. - * - * For example { foo: 123, boo: 521 } -> "foo-123_boo-521" - * - * @param {Object} object The object to encode - * @param {String} keyValSeparator="-" The separator between keys and values - * @param {String} entrySeparator="_" The separator between entries - * @return {String} The encoded object - */ -export const encodeNumericObject = encodeObject as ( - obj: { [key: string]: number | null | undefined } | null | undefined, - keyValSeparator?: string, - entrySeparator?: string -) => string | null | undefined; - -/** - * Decodes a simple object to javascript where all values are numbers. - * Currently works only for simple, flat objects. - * - * For example "foo-123_boo-521" -> { foo: 123, boo: 521 } - * - * If an array is provided as input, only the first entry is used. - * - * @param {String} input The object string to decode - * @param {String} keyValSeparator="-" The separator between keys and values - * @param {String} entrySeparator="_" The separator between entries - * @return {Object} The javascript object - */ -export function decodeNumericObject( - input: string | (string | null)[] | null | undefined, - keyValSeparator = '-', - entrySeparator = '_' -): { [key: string]: number | null | undefined } | null | undefined { - const decoded: { [key: string]: string } | null | undefined = decodeObject( - input, - keyValSeparator, - entrySeparator - ); - - if (decoded == null) return decoded; - - // convert to numbers - const decodedNumberObj: { [key: string]: number | null | undefined } = {}; - for (const key of keys(decoded)) { - decodedNumberObj[key] = decodeNumber(decoded[key]); - } - - return decodedNumberObj; -} diff --git a/packages/svelte-ux/src/lib/utils/sort.test.ts b/packages/svelte-ux/src/lib/utils/sort.test.ts deleted file mode 100644 index c55ea19a6..000000000 --- a/packages/svelte-ux/src/lib/utils/sort.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { nestedSort, sortFunc } from './sort.js'; - -describe('nestedSort', () => { - it('basic', () => { - const data = [ - { - key: 'Cat1', - values: [ - { - key: 'Cat1b', - values: [ - { - key: 'Account1b2', - Actual: 7, - Budget: 5, - Variance: 2, - }, - { - key: 'Account1b1', - Actual: 4, - Budget: 5, - Variance: -1, - }, - ], - }, - { - key: 'Cat1a', - values: [ - { - key: 'Account1a2', - Actual: 3, - Budget: 2, - Variance: 1, - }, - { - key: 'Account1a1', - Actual: 4, - Budget: 2, - Variance: 2, - }, - ], - }, - ], - }, - ]; - - const expected = [ - { - key: 'Cat1', - values: [ - { - key: 'Cat1a', - values: [ - { - key: 'Account1a1', - Actual: 4, - Budget: 2, - Variance: 2, - }, - { - key: 'Account1a2', - Actual: 3, - Budget: 2, - Variance: 1, - }, - ], - }, - { - key: 'Cat1b', - values: [ - { - key: 'Account1b1', - Actual: 4, - Budget: 5, - Variance: -1, - }, - { - key: 'Account1b2', - Actual: 7, - Budget: 5, - Variance: 2, - }, - ], - }, - ], - }, - ]; - - const actual = nestedSort(data, sortFunc('key')); - expect(actual).eql(expected); - }); - - it('basic 2', () => { - const data = [ - { - key: 'Cat1', - values: [ - { - key: 'Cat1a', - values: [ - { - key: 'Account1a1', - Actual: 4, - Budget: 2, - Variance: 2, - }, - { - key: 'Account1a2', - Actual: 3, - Budget: 2, - Variance: 1, - }, - ], - }, - { - key: 'Cat1b', - values: [ - { - key: 'Account1b1', - Actual: 4, - Budget: 5, - Variance: -1, - }, - { - key: 'Account1b2', - Actual: 7, - Budget: 5, - Variance: 2, - }, - ], - }, - ], - }, - ]; - - const expected = [ - { - key: 'Cat1', - values: [ - { - key: 'Cat1a', - values: [ - { - key: 'Account1a2', - Actual: 3, - Budget: 2, - Variance: 1, - }, - { - key: 'Account1a1', - Actual: 4, - Budget: 2, - Variance: 2, - }, - ], - }, - { - key: 'Cat1b', - values: [ - { - key: 'Account1b1', - Actual: 4, - Budget: 5, - Variance: -1, - }, - { - key: 'Account1b2', - Actual: 7, - Budget: 5, - Variance: 2, - }, - ], - }, - ], - }, - ]; - - const actual = nestedSort(data, sortFunc('Actual')); - expect(actual).eql(expected); - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/sort.ts b/packages/svelte-ux/src/lib/utils/sort.ts deleted file mode 100644 index 489371c72..000000000 --- a/packages/svelte-ux/src/lib/utils/sort.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { propAccessor, type PropAccessorArg } from './object.js'; - -export function sortFunc(value?: PropAccessorArg, direction: 'asc' | 'desc' = 'asc') { - const sortDirection = direction === 'asc' ? 1 : -1; - - return (a: any, b: any) => { - const valueFn = propAccessor(value); - const aValue = valueFn(a); - const bValue = valueFn(b); - - if (aValue == null || bValue == null) { - if (aValue == null && bValue != null) { - return -sortDirection; - } else if (aValue != null && bValue == null) { - return sortDirection; - } else { - // both `null` - return 0; - } - } - - return aValue < bValue ? -sortDirection : aValue > bValue ? sortDirection : 0; - }; -} - -export function compoundSortFunc(...sortFns: { (a: any, b: any): any }[]) { - return (a: any, b: any) => { - for (let i = 0; i < sortFns.length; i++) { - let result = sortFns[i](a, b); - if (result != 0) { - return result; - } - } - return 0; - }; -} - -/** Make a shallow copy and appy sort */ -export function sort( - data: Array, - value?: PropAccessorArg, - direction: 'asc' | 'desc' = 'asc' -) { - return [...data].sort(sortFunc(value, direction)); -} - -export function nestedSort( - data: Array<{ values?: object[] }>, - sortFunc: (a: object, b: object, depth: number) => number, - depth = 0 -) { - data.sort((a, b) => sortFunc(a, b, depth)); - data.forEach((d) => { - if (d.values) { - nestedSort(d.values, sortFunc, depth + 1); - } - }); - - return data; -} diff --git a/packages/svelte-ux/src/lib/utils/string.test.ts b/packages/svelte-ux/src/lib/utils/string.test.ts deleted file mode 100644 index 24f42674b..000000000 --- a/packages/svelte-ux/src/lib/utils/string.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { toTitleCase } from './string.js'; - -describe('toTitleCase()', () => { - it('basic', () => { - const original = 'this is a test'; - const expected = 'This is a Test'; - expect(toTitleCase(original)).equal(expected); - }); - - it('basic', () => { - const original = 'A long time ago'; - const expected = 'A Long Time Ago'; - expect(toTitleCase(original)).equal(expected); - }); -}); diff --git a/packages/svelte-ux/src/lib/utils/string.ts b/packages/svelte-ux/src/lib/utils/string.ts deleted file mode 100644 index d5fec415b..000000000 --- a/packages/svelte-ux/src/lib/utils/string.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { entries } from '../types/typeHelpers.js'; - -/** - * Check if str only contians upper case letters - */ -export function isUpperCase(str: string) { - return /^[A-Z]*$/.test(str); -} - -/** - * Returns string with the first letter of each word converted to uppercase (and remainder as lowercase) - */ -export function toTitleCase(str: string, ignore = ['a', 'an', 'is', 'the']) { - return str - .toLowerCase() - .split(' ') - .map((word, index) => { - if (index > 0 && ignore.includes(word)) { - return word; - } else { - return word.charAt(0).toUpperCase() + word.slice(1); - } - }) - .join(' '); -} - -/** - * Generates a unique Id, with prefix if provided - */ -const idMap = new Map(); -export function uniqueId(prefix = '') { - let id = (idMap.get(prefix) ?? 0) + 1; - idMap.set(prefix, id); - return prefix + id; -} - -/** - * Truncate text with option to keep a number of characters on end. Inserts ellipsis between parts - */ -export function truncate(text: string, totalChars: number, endChars: number = 0) { - endChars = Math.min(endChars, totalChars); - - const start = text.slice(0, totalChars - endChars); - const end = endChars > 0 ? text.slice(-endChars) : ''; - - if (start.length + end.length < text.length) { - return start + '…' + end; - } else { - return text; - } -} - -/** Get the roman numeral for the given value */ -export function romanize(value: number) { - const lookup = { - M: 1000, - CM: 900, - D: 500, - CD: 400, - C: 100, - XC: 90, - L: 50, - XL: 40, - X: 10, - IX: 9, - V: 5, - IV: 4, - I: 1, - }; - - let result = ''; - - for (let [numeral, numeralValue] of entries(lookup)) { - while (value >= numeralValue) { - result += numeral; - value -= numeralValue; - } - } - - return result; -} diff --git a/packages/svelte-ux/src/lib/utils/styles.ts b/packages/svelte-ux/src/lib/utils/styles.ts deleted file mode 100644 index f48f623b1..000000000 --- a/packages/svelte-ux/src/lib/utils/styles.ts +++ /dev/null @@ -1,56 +0,0 @@ -import clsx, { type ClassValue } from 'clsx'; -import { extendTailwindMerge } from 'tailwind-merge'; -import { range } from 'd3-array'; -import { entries } from '../types/typeHelpers.js'; -import { mergeWith } from 'lodash-es'; - -/** - * Convert object to style string - */ -export function objectToString(styleObj: { [key: string]: string }) { - return entries(styleObj) - .map(([key, value]) => { - if (value) { - // Convert camelCase into kaboob-case (ex. (transformOrigin => transform-origin)) - const propertyName = key.replace(/([A-Z])/g, '-$1').toLowerCase(); - return `${propertyName}: ${value};`; - } else { - return null; - } - }) - .filter((x) => x) - .join(' '); -} - -/** - * Wrapper around `tailwind-merge` and `clsx` - */ -const twMerge = extendTailwindMerge({ - extend: { - classGroups: { - shadow: [ - 'shadow-border-l', - 'shadow-border-r', - 'shadow-border-t', - 'shadow-border-b', - 'elevation-none', - ...range(1, 25).map((x) => `elevation-${x}`), - ], - }, - }, -}); - -type ClassFalsyValues = undefined | null | false; -type AnyClassValue = ClassValue | ClassFalsyValues; -type AnyClassCollection = Record | ClassFalsyValues; - -export const cls = (...inputs: AnyClassValue[]) => twMerge(clsx(...inputs)); - -export const clsMerge = ( - ...inputs: T[] -): Exclude => - mergeWith({}, ...inputs.filter(Boolean), (a: string, b: string) => twMerge(a, b)); - -export const normalizeClasses = (classes: string | ClassFalsyValues | T): T => { - return classes && typeof classes === 'object' ? classes : ({ root: classes } as T); -}; diff --git a/packages/svelte-ux/src/lib/utils/table.ts b/packages/svelte-ux/src/lib/utils/table.ts deleted file mode 100644 index 38cbeb594..000000000 --- a/packages/svelte-ux/src/lib/utils/table.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { isFunction, get } from 'lodash-es'; -import { parseISO } from 'date-fns'; - -import type { ColumnDef } from '../types/table.js'; -import { PeriodType } from '../utils/date_types.js'; - -/** Get columns representing headers with rowSpan/colSpan set based on structure */ -export function getHeaders(columns: ColumnDef[]) { - const maxDepth = getDepth(columns); - const result: ColumnDef[][] = Array.from({ - length: maxDepth, - }).map(() => []); - - function addItems(columns: ColumnDef[], depth: number) { - columns - .filter((c) => c.hidden !== true) - .forEach((column) => { - const columnDef: ColumnDef = { - ...column, - }; - delete columnDef.columns; - - if (column.columns) { - const colSpan = getWidth(column); - if (colSpan > 1) { - columnDef.colSpan = colSpan; - } - addItems(column.columns, depth + 1); - } else { - const rowSpan = maxDepth - depth; - if (rowSpan > 1) { - columnDef.rowSpan = maxDepth - depth; - } - } - result[depth].push(columnDef); - }); - } - addItems(columns, 0); - - return result; -} - -/** Get columns representing rows (i.e. flattened leaf columns if nested columns are used) */ -export function getRowColumns(columns: ColumnDef[]) { - const result: ColumnDef[] = []; - - function setColumns(column: ColumnDef) { - if (column.columns == null) { - result.push(column); - return; - } - - column.columns.filter((c) => c.hidden !== true).forEach((child) => setColumns(child)); - } - columns.filter((c) => c.hidden !== true).forEach((column) => setColumns(column)); - - return result; -} - -export function getDepth(columns: ColumnDef[] | undefined) { - if (columns == null) { - return 0; - } - - let depth = 0; - columns - .filter((c) => c.hidden !== true) - .forEach((item) => { - depth = Math.max(depth, getDepth(item.columns)); - }); - - return depth + 1; -} - -export function getWidth(column: ColumnDef) { - if (column.columns == null) { - return 1; - } - - let width = 0; - column.columns - .filter((c) => c.hidden !== true) - .forEach((child) => { - width += getWidth(child); - }); - - return width; -} - -export function getCellHeader(column: ColumnDef) { - if (column.header != null) { - return column.header; - } - - let header = column.name.split('.')[0]; // Use first section before dot (`Organization.Identifier` => `Organization`) - header = header.replace(/([a-z])([A-Z])/g, '$1 $2'); // Add space before capital letters - https://stackoverflow.com/questions/5582228/insert-space-before-capital-letters - return header; -} - -export function getCellValue(column: ColumnDef, rowData: any, rowIndex?: number) { - let value = undefined; - if (isFunction(column.value)) { - value = column.value?.(rowData, rowIndex); - } - - if (value === undefined) { - value = get(rowData, typeof column.value === 'string' ? column.value : column.name); - } - - if ( - typeof value === 'string' && - !isFunction(column.format) && - (column.format ?? 'none') in PeriodType - ) { - // Convert date string to Date instance - // TODO: Shoud dateFns.parseISO() be used? - // TODO: Should we handle date-only strings different? - // value = new Date(value); - // console.log({ column: column.name, value }); - value = parseISO(value); - } - - return value; -} diff --git a/packages/svelte-ux/src/routes/+layout.server.ts b/packages/svelte-ux/src/routes/+layout.server.ts index a9d3bad4d..0f92d0654 100644 --- a/packages/svelte-ux/src/routes/+layout.server.ts +++ b/packages/svelte-ux/src/routes/+layout.server.ts @@ -1,9 +1,9 @@ import { redirect } from '@sveltejs/kit'; +import { getThemeNames } from '@layerstack/tailwind'; import { env } from '$env/dynamic/private'; import themes from '../../themes.json' with { type: 'json' }; -import { getThemeNames } from '$lib/styles/theme.js'; export async function load({ url }) { // Redirect `svelte-ux.vercel.app` to `svelte-ux.techniq.dev` diff --git a/packages/svelte-ux/src/routes/+layout.svelte b/packages/svelte-ux/src/routes/+layout.svelte index c0e559af2..3f05d6b28 100644 --- a/packages/svelte-ux/src/routes/+layout.svelte +++ b/packages/svelte-ux/src/routes/+layout.svelte @@ -15,12 +15,12 @@ ThemeSwitch, Tooltip, settings, - lgScreen, - createLocaleSettings, - entries, } from 'svelte-ux'; import { DEV } from 'esm-env'; + import { entries, createLocaleSettings } from '@layerstack/utils'; + import { lgScreen } from '@layerstack/svelte-stores'; + import NavMenu from './_NavMenu.svelte'; import LanguageSelect from '$lib/components/LanguageSelect.svelte'; diff --git a/packages/svelte-ux/src/routes/+page.svelte b/packages/svelte-ux/src/routes/+page.svelte index 53f6bd771..68d481c91 100644 --- a/packages/svelte-ux/src/routes/+page.svelte +++ b/packages/svelte-ux/src/routes/+page.svelte @@ -49,7 +49,7 @@
Update tailwind.config.cjs
import { NavItem } from 'svelte-ux'; + import { entries } from '@layerstack/utils'; import { page } from '$app/stores'; - import { entries } from '$lib/types/typeHelpers.js'; - import { mdiCog, mdiFormatListBulleted, mdiHome, mdiPalette } from '@mdi/js'; + import { mdiCog, mdiFormatListBulleted, mdiHome, mdiPalette, mdiOpenInNew } from '@mdi/js'; const components = { App: ['AppBar', 'AppLayout', 'NavItem', 'Settings', 'ThemeInit', 'ThemeSelect', 'ThemeSwitch'], @@ -72,43 +72,6 @@ Effects: ['Gooey', 'Shine'], Other: ['CopyButton'], }; - - const actions = [ - 'dataBackground', - 'input', - 'layout', - 'mouse', - 'multi', - 'observer', - 'popover', - 'portal', - 'scroll', - 'spotlight', - 'sticky', - 'styleProps', - 'table', - ]; - - const stores = [ - 'changeStore', - 'debounceStore', - 'dirtyStore', - 'fetchStore', - 'formStore', - 'graphStore', - 'localStore', - 'mapStore', - 'matchMedia', - 'paginationStore', - 'promiseStore', - 'queryParamsStore', - 'selectionStore', - 'tableOrderStore', - 'timerStore', - 'uniqueStore', - ]; - - const utils = ['cls', 'duration', 'format', 'json', 'Logger', 'string']; @@ -128,17 +91,59 @@ {/each} {/each} +

Charts

+ +

Actions

-{#each actions as item} - -{/each} + +

Stores

-{#each stores as item} - -{/each} + +

Utils

-{#each utils as item} - -{/each} + + diff --git a/packages/svelte-ux/src/routes/customization/+page.md b/packages/svelte-ux/src/routes/customization/+page.md index 7e51438b8..8fc1df024 100644 --- a/packages/svelte-ux/src/routes/customization/+page.md +++ b/packages/svelte-ux/src/routes/customization/+page.md @@ -46,10 +46,10 @@ module.exports = { }; ``` -The Svelte UX tailwind plugin (`svelte-ux/plugins/tailwind.cjs`) will translate the defined colors to a common color space, which uses `hsl()` by default. If you would like to change the color space, for example use `oklch()` for an increased gamut of colors, simply call the plugin with the `colorSpace` option defined. +The Svelte UX tailwind plugin (`@layerstack/tailwind/plugin`) will translate the defined colors to a common color space, which uses `hsl()` by default. If you would like to change the color space, for example use `oklch()` for an increased gamut of colors, simply call the plugin with the `colorSpace` option defined. ```js -const svelteUx = require('svelte-ux/plugins/tailwind.cjs'); +const layerstack = require('@layerstack/tailwind/plugin'); module.exports = { // ... diff --git a/packages/svelte-ux/src/routes/docs/+layout.svelte b/packages/svelte-ux/src/routes/docs/+layout.svelte index 33c15ff31..ef0d5b02f 100644 --- a/packages/svelte-ux/src/routes/docs/+layout.svelte +++ b/packages/svelte-ux/src/routes/docs/+layout.svelte @@ -23,13 +23,14 @@ Icon, ListItem, TableOfContents, - toTitleCase, - cls, - xlScreen, settings, getSettings, } from 'svelte-ux'; + import { toTitleCase } from '@layerstack/utils'; + import { cls } from '@layerstack/tailwind'; + import { xlScreen } from '@layerstack/svelte-stores'; + import Code from '$lib/components/Code.svelte'; import ViewSourceButton from '$docs/ViewSourceButton.svelte'; diff --git a/packages/svelte-ux/src/routes/docs/actions/dataBackground/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/dataBackground/+page.svelte deleted file mode 100644 index 16314e2a0..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/dataBackground/+page.svelte +++ /dev/null @@ -1,157 +0,0 @@ - - -

Usage

- - - -
-
- - - Original - Derived - - - - - - - - - - -
- -
- -
- min: - - max: - -
-
- - -
- x: - - y: - -
-
-
- -
- - {duration} -
-
- - -
- -

Basic

- - - - - {#each sorted ? sort(values) : values as value} - - {#key duration} - - - - {/key} - {/each} - -
0 ? 'hsl(140 80% 80%)' : 'hsl(0 80% 80%)', - domain, - inset, - baseline, - tweened: { duration }, - }} - > - -
-
- -

Tailwind gradient

- - - - - {#each sorted ? sort(values) : values as value} - - {#key duration} - - - - {/key} - {/each} - -
0 ? 'from-success-300 to-success-500' : 'from-danger to-danger-300' - )} - use:dataBackground={{ - value, - domain, - inset, - baseline, - tweened: { duration }, - }} - > - -
-
diff --git a/packages/svelte-ux/src/routes/docs/actions/dataBackground/+page.ts b/packages/svelte-ux/src/routes/docs/actions/dataBackground/+page.ts deleted file mode 100644 index 7e82c23ed..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/dataBackground/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/actions/dataBackground.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Set background gradient based on data, similar to Excel. Typically used within a table', - related: ['components/Table', 'actions/table'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/input/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/input/+page.svelte deleted file mode 100644 index e86852d69..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/input/+page.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - -

Usage

- - - -

autoFocus Auto focus node when rendered

- - - - - -

selectOnFocus Selects the text inside a text node when the node is focused

- - - - - -

blurOnEscape Blurs the node when Escape is pressed

- - - - - -

autoHeight Automatically resize textarea based on content

- - - - - -

debounceEvent Debounce any event (input, change, etc)

- - - { - // @ts-expect-error - console.log(e.target.value); - }, - timeout: 1000, - }} - class="border" - /> - diff --git a/packages/svelte-ux/src/routes/docs/actions/input/+page.ts b/packages/svelte-ux/src/routes/docs/actions/input/+page.ts deleted file mode 100644 index 0ee297c77..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/input/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/actions/input.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - related: ['components/TextField', 'components/Input'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/layout/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/layout/+page.svelte deleted file mode 100644 index ba7ea564f..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/layout/+page.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - -

Usage

- - - -

- remainingViewportHeight Set `height` or `max-height` to viewport height excluding node's current viewport top -

- -
TODO
- -

- remainingViewportWidth Set `width` or `max-width` to viewport width excluding node's current viewport left -

- -
TODO
- -

- overflow Watch for overflow changes (x or y) and dispatch `overflow` event with amount -

- - -
- - -
-
{ - overflowX = e.detail.overflowX; - overflowY = e.detail.overflowY; - }} - > - {#each { length: overflowItems } as _} -
Resize the window to see text truncate and watch values
- {/each} -
-
overflowX: {overflowX}
-
overflowY: {overflowY}
-
diff --git a/packages/svelte-ux/src/routes/docs/actions/layout/+page.ts b/packages/svelte-ux/src/routes/docs/actions/layout/+page.ts deleted file mode 100644 index 7b81d0fa6..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/layout/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/actions/layout.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - related: ['components/Overflow'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/mouse/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/mouse/+page.svelte deleted file mode 100644 index 3fe4ce8d2..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/mouse/+page.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - -

Usage

- - - -

- longpress Dispatch event after element has been pressed for a duration of time -

- - - - {#if longpressed} - Success! Repeat to hide - {/if} - - -

movable Track mouse position changes from mouse down on node to mouse up

- - -
-
{ - coords.stiffness = 1; - coords.damping = 1; - }} - on:move={(e) => { - $coords.x += e.detail.dx; - $coords.y += e.detail.dy; - }} - on:moveend={() => { - coords.stiffness = 0.2; - coords.damping = 0.4; - coords.set({ x: 0, y: 0 }); - }} - style="transform: - translate({$coords.x}px,{$coords.y}px) - rotate({$coords.x * 0.2}deg)" - >
-
-
- -

With pixel steps / snapping

- - -
-
{ - coords.stiffness = 1; - coords.damping = 1; - }} - on:move={(e) => { - $coords.x += e.detail.dx; - $coords.y += e.detail.dy; - }} - on:moveend={() => { - coords.stiffness = 0.2; - coords.damping = 0.4; - coords.set({ x: 0, y: 0 }); - }} - style="transform: - translate({$coords.x}px,{$coords.y}px) - rotate({$coords.x * 0.2}deg)" - >
-
-
- -

With percentage of parent steps / snapping

- - -
-
{ - coords.stiffness = 1; - coords.damping = 1; - }} - on:move={(e) => { - $coords.x += e.detail.dx; - $coords.y += e.detail.dy; - }} - on:moveend={() => { - coords.stiffness = 0.2; - coords.damping = 0.4; - coords.set({ x: 0, y: 0 }); - }} - style="transform: - translate({$coords.x}px,{$coords.y}px) - rotate({$coords.x * 0.2}deg)" - >
-
-
- -

x-axis only

- - -
-
{ - coords.stiffness = 1; - coords.damping = 1; - }} - on:move={(e) => { - $coords.x += e.detail.dx; - $coords.y += e.detail.dy; - }} - on:moveend={() => { - coords.stiffness = 0.2; - coords.damping = 0.4; - coords.set({ x: 0, y: 0 }); - }} - style="transform: - translate({$coords.x}px,{$coords.y}px) - rotate({$coords.x * 0.2}deg) - " - >
-
-
diff --git a/packages/svelte-ux/src/routes/docs/actions/mouse/+page.ts b/packages/svelte-ux/src/routes/docs/actions/mouse/+page.ts deleted file mode 100644 index a55e3ffd1..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/mouse/+page.ts +++ /dev/null @@ -1,11 +0,0 @@ -import source from '$lib/actions/mouse.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/multi/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/multi/+page.svelte deleted file mode 100644 index dc9fbd0b1..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/multi/+page.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -

Usage

- - diff --git a/packages/svelte-ux/src/routes/docs/actions/multi/+page.ts b/packages/svelte-ux/src/routes/docs/actions/multi/+page.ts deleted file mode 100644 index 369c5aaa6..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/multi/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/actions/multi.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Helper action to handle multiple actions as a single action. Useful for adding actions for custom components', - related: ['components/Button', 'components/Input', 'components/TextField'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/observer/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/observer/+page.svelte deleted file mode 100644 index b63e931f4..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/observer/+page.svelte +++ /dev/null @@ -1,134 +0,0 @@ - - -

Usage

- - - -

use:resize

- -

Basic

- - -
{ - console.log(e.detail); - // @ts-expect-error - e.target.innerText = JSON.stringify(e.detail.contentRect, null, 2); - }} - class="resize overflow-auto whitespace-pre outline rounded" - >
-
- -

Full coordinates (using `getBoundingClientRect()`)

- - -
{ - // @ts-expect-error - e.target.innerText = JSON.stringify(e.target.getBoundingClientRect(), null, 2); - }} - class="resize overflow-auto whitespace-pre outline rounded" - >
-
- -

Setting CSS variable

- - -
{ - // @ts-expect-error - e.target.style.setProperty('--color', e.detail.contentRect.width % 255); - }} - style:background-color="hsl(var(--color), 100%, 70%)" - class="resize overflow-auto p-2 rounded" - > - Resize and watch me change colors -
-
- -

use:intersection

- -

Adding class when fully visible

- - -
- {#each { length: 10 } as _} -
Scroll down
- {/each} -
{ - if (e.detail.isIntersecting) { - // @ts-expect-error - e.target.classList.add('bg-danger'); - } else { - // @ts-expect-error - e.target.classList.remove('bg-danger'); - } - }} - class="transition-colors duration-500" - > - Watch me scroll away -
- {#each { length: 10 } as _} -
Scroll up
- {/each} -
-
- -

Show header on scroll away

- - - -
- {#if showHeader} -
- Header -
- {/if} -
- {#each { length: 10 } as _} -
Scroll down
- {/each} -
{ - if (e.detail.isIntersecting) { - // Visible - toggleOff(); - } else { - if (e.detail.boundingClientRect.top < (e.detail.rootBounds?.top ?? 0)) { - // Scrolled off top - toggleOn(); - } else { - // Scrolled off bottom - } - } - }} - > - Watch me scroll away -
- {#each { length: 10 } as _} -
Scroll up
- {/each} -
-
-
-
- -

use:mutate

- -
TODO
diff --git a/packages/svelte-ux/src/routes/docs/actions/observer/+page.ts b/packages/svelte-ux/src/routes/docs/actions/observer/+page.ts deleted file mode 100644 index 650267da0..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/observer/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/actions/observer.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Actions for ResizeObserver, IntersectionObserver, and MutationObserver', - related: ['components/InfiniteScroll', 'components/Lazy'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/popover/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/popover/+page.svelte deleted file mode 100644 index d7df7c6cc..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/popover/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -

Usage

- - diff --git a/packages/svelte-ux/src/routes/docs/actions/popover/+page.ts b/packages/svelte-ux/src/routes/docs/actions/popover/+page.ts deleted file mode 100644 index 4207b454c..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/popover/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/actions/popover.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Svelte action for floating-ui with simplier setup, especially for middlware', - related: ['components/Popover'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/portal/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/portal/+page.svelte deleted file mode 100644 index 7ab9d232e..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/portal/+page.svelte +++ /dev/null @@ -1,123 +0,0 @@ - - -

Usage

- - - -

Examples

- -

basic

- - -
- - - -
-
Portal content
- {#if enabled} - - {/if} -
-
-
-
- -

first/sibling .PortalTarget

- - - -
- -
-
Portal content
- {#if enabled} - - {/if} -
-
-
-
-
- -

anscestor .PortalTarget

- - - - -
-
- -
-
Portal content
- {#if enabled} - - {/if} -
-
-
-
-
- -

custom target

- - - -
- -
-
Portal content
- {#if enabled} - - {/if} -
-
-
-
-
- -

Destroyable

- - - - - {#if !destroyed} -
-
- - -
-
Portal content
- {#if enabled} - - {/if} -
-
-
-
- {:else} - - {/if} -
-
-
- - diff --git a/packages/svelte-ux/src/routes/docs/actions/portal/+page.ts b/packages/svelte-ux/src/routes/docs/actions/portal/+page.ts deleted file mode 100644 index b480a3b42..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/portal/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/actions/portal.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Render component outside current DOM hierarchy', - related: ['components/Dialog', 'components/Drawer', 'components/Backdrop'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/scroll/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/scroll/+page.svelte deleted file mode 100644 index 939d25e3f..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/scroll/+page.svelte +++ /dev/null @@ -1,153 +0,0 @@ - - -

Usage

- - - -

scrollIntoView Smootly scroll element into center of view

- - - - {scrolledIndex} -
- {#each items as item, i} -
- {item} -
- {/each} -
-
- -
- Only if needed -
- - - - {scrolledIndex} -
- {#each items as item, i} -
- {item} -
- {/each} -
-
- -

scrollShadow Add shadows to indicate scrolling available

- - -
- {#each items as item, i (item)} -
{item}
- {/each} -
-
- -

with flip'd children

- - -
- {#each items as item, i (item)} -
{item}
- {/each} -
-
- -

Horizontal scroll

- - -
-
- {#each items as item, i} -
{item}
- {/each} -
-
-
- -

Surface shadow on bottom (fade content)

- - -
- {#each items as item, i} -
{item}
- {/each} -
-
- -

with truncation

- - -
- {#each items as item, i} -
{item} with a really long description
- {/each} -
-
- -

scrollFade Add shadows to indicate scrolling available

- - -
- {#each items as item, i (item)} -
{item}
- {/each} -
-
- -

with flip'd children

- - -
- {#each items as item, i (item)} -
{item}
- {/each} -
-
- -

Horizontal scroll

- - -
-
- {#each items as item, i} -
{item}
- {/each} -
-
-
- -
- -
diff --git a/packages/svelte-ux/src/routes/docs/actions/scroll/+page.ts b/packages/svelte-ux/src/routes/docs/actions/scroll/+page.ts deleted file mode 100644 index 7ff1d2a74..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/scroll/+page.ts +++ /dev/null @@ -1,11 +0,0 @@ -import source from '$lib/actions/scroll.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/spotlight/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/spotlight/+page.svelte deleted file mode 100644 index 98170ba98..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/spotlight/+page.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - -

Usage

- - - - { - const body = window.document.body; - body.style.setProperty('--x', e.clientX + 'px'); - body.style.setProperty('--y', e.clientY + 'px'); - }} -/> - -

Using global context and options

- - -
- {#each items as item, i} -
- {item} -
- {/each} -
-
- -

Using global context and CSS variables

- - -
- {#each items as item, i} -
- {item} -
- {/each} -
-
- -

Line example

- - -
- {#each items as item, i} -
- {item} -
- {/each} -
-
diff --git a/packages/svelte-ux/src/routes/docs/actions/spotlight/+page.ts b/packages/svelte-ux/src/routes/docs/actions/spotlight/+page.ts deleted file mode 100644 index 8b9bed746..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/spotlight/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/actions/spotlight.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - related: ['components/Shine'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/sticky/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/sticky/+page.svelte deleted file mode 100644 index 6298c993d..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/sticky/+page.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -

Usage

- - - -

sticky

- -
TODO
- -

stickyContext

- -
TODO
diff --git a/packages/svelte-ux/src/routes/docs/actions/sticky/+page.ts b/packages/svelte-ux/src/routes/docs/actions/sticky/+page.ts deleted file mode 100644 index 14a7487d2..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/sticky/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/actions/sticky.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - related: ['components/Table', 'actions/table'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/styleProps/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/styleProps/+page.svelte deleted file mode 100644 index a9c653c15..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/styleProps/+page.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -

Usage

- - - - - {@const styles = { '--background': background, '--border': border }} -
-
-
- - -
-
-
diff --git a/packages/svelte-ux/src/routes/docs/actions/styleProps/+page.ts b/packages/svelte-ux/src/routes/docs/actions/styleProps/+page.ts deleted file mode 100644 index 08689da4e..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/styleProps/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/actions/styleProps.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Reactively set style properties using a single object.', - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/actions/table/+page.svelte b/packages/svelte-ux/src/routes/docs/actions/table/+page.svelte deleted file mode 100644 index eff886ea9..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/table/+page.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -

Usage

- - diff --git a/packages/svelte-ux/src/routes/docs/actions/table/+page.ts b/packages/svelte-ux/src/routes/docs/actions/table/+page.ts deleted file mode 100644 index fa3a927c2..000000000 --- a/packages/svelte-ux/src/routes/docs/actions/table/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/actions/table.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Apply ColumnDef to a table cell . Includes order by, dataBackground, and sticky support', - related: ['components/Table', 'actions/dataBackground'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/components/AppBar/+page.svelte b/packages/svelte-ux/src/routes/docs/components/AppBar/+page.svelte index f02fd5f9c..2ea9dc79c 100644 --- a/packages/svelte-ux/src/routes/docs/components/AppBar/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/AppBar/+page.svelte @@ -1,5 +1,7 @@ diff --git a/packages/svelte-ux/src/routes/docs/components/Gooey/+page.svelte b/packages/svelte-ux/src/routes/docs/components/Gooey/+page.svelte index c842bd2d5..358675db8 100644 --- a/packages/svelte-ux/src/routes/docs/components/Gooey/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/Gooey/+page.svelte @@ -2,7 +2,10 @@ import { blur } from 'svelte/transition'; import { circIn, circOut } from 'svelte/easing'; - import { Gooey, RangeField, cls, mouseCoords, timerStore } from 'svelte-ux'; + import { Gooey, RangeField } from 'svelte-ux'; + import { cls } from '@layerstack/tailwind'; + import { mouseCoords } from '@layerstack/svelte-actions'; + import { timerStore } from '@layerstack/svelte-stores'; import Preview from '$lib/components/Preview.svelte'; import Blockquote from '$docs/Blockquote.svelte'; diff --git a/packages/svelte-ux/src/routes/docs/components/ListItem/+page.svelte b/packages/svelte-ux/src/routes/docs/components/ListItem/+page.svelte index 671049cf7..08a08ed0d 100644 --- a/packages/svelte-ux/src/routes/docs/components/ListItem/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/ListItem/+page.svelte @@ -1,7 +1,9 @@ - -

Usage

- -```svelte - -``` - -

Example

- - - -
changed: {JSON.stringify($changed)}
-
- -

Pagination

- - -
{JSON.stringify($paginationChanged, null, 2)}
-
diff --git a/packages/svelte-ux/src/routes/docs/stores/changeStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/changeStore/+page.ts deleted file mode 100644 index 1a59e46db..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/changeStore/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/stores/changeStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Track previous value. Calls onChange callback only after first change (not initial value)', - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/debounceStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/debounceStore/+page.md deleted file mode 100644 index 4569bc61b..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/debounceStore/+page.md +++ /dev/null @@ -1,29 +0,0 @@ - - -

Usage

- -```svelte - -``` - -

Example

- - - -
value: {$value}
-
debouncedValue: {$debouncedValue}
-
diff --git a/packages/svelte-ux/src/routes/docs/stores/debounceStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/debounceStore/+page.ts deleted file mode 100644 index 514e01858..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/debounceStore/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/stores/debounceStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Delay store update until some time has passed since the last update', - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/dirtyStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/dirtyStore/+page.md deleted file mode 100644 index 15bb92564..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/dirtyStore/+page.md +++ /dev/null @@ -1,29 +0,0 @@ - - -

Usage

- -```svelte - -``` - -

Example

- - - -
isDirty: {$isDirty}
- -
diff --git a/packages/svelte-ux/src/routes/docs/stores/dirtyStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/dirtyStore/+page.ts deleted file mode 100644 index a101af6f7..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/dirtyStore/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/stores/dirtyStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Track when a store becomes dirty (changed), with ability to reset. Useful to enable an apply button, etc', - related: ['components/MultiSelect'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/fetchStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/fetchStore/+page.md deleted file mode 100644 index ee4587565..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/fetchStore/+page.md +++ /dev/null @@ -1,13 +0,0 @@ - - -

Usage

- -```js -import { fetchStore } from 'svelte-ux'; -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/fetchStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/fetchStore/+page.ts deleted file mode 100644 index d84e717f1..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/fetchStore/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/stores/fetchStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Fetch request as a store, with support for body parsing (json, text, arrayBuffer, etc), out of order responses, context configuration, and global errors', - related: ['stores/graphStore'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/formStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/formStore/+page.md deleted file mode 100644 index a5645868e..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/formStore/+page.md +++ /dev/null @@ -1,10 +0,0 @@ - - -

Usage

- -```js -import { formStore } from 'svelte-ux'; -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/formStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/formStore/+page.ts deleted file mode 100644 index e727184b7..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/formStore/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/stores/formStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Manage form state via immer draft and zod scehma, with undo history', - related: ['components/Form'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/graphStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/graphStore/+page.md deleted file mode 100644 index 29aa1b547..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/graphStore/+page.md +++ /dev/null @@ -1,13 +0,0 @@ - - -

Usage

- -```js -import { graphStore } from 'svelte-ux'; -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/graphStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/graphStore/+page.ts deleted file mode 100644 index 2d8e55044..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/graphStore/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/stores/graphStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: ['GraphQL requests powered by fetchStore'], - related: ['stores/fetchStore'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/localStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/localStore/+page.md deleted file mode 100644 index 8d966fa96..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/localStore/+page.md +++ /dev/null @@ -1,22 +0,0 @@ - - -

Usage

- -```svelte - -``` - -

Tests

- - diff --git a/packages/svelte-ux/src/routes/docs/stores/localStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/localStore/+page.ts deleted file mode 100644 index 2cd785ebe..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/localStore/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/stores/localStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Read and write to localStorage with expiration support', - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/mapStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/mapStore/+page.md deleted file mode 100644 index bbe78cb5a..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/mapStore/+page.md +++ /dev/null @@ -1,41 +0,0 @@ - - -

Usage

- -```js -import { mapStore } from 'svelte-ux'; - -const store = mapStore(); - -// Get a value -$store.get(key); - -// Set a value -store.set(key, value); - -// Update a value -store.update(key, (value) => value + 1); - -// Check if value exists -$store.has(key); - -// Delete a value -store.delete(key); - -// Delete all values -store.clear(); - -// Force a reactive update in case of internal changes to entries -store.refresh(); -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/mapStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/mapStore/+page.ts deleted file mode 100644 index f28134202..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/mapStore/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/stores/mapStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Store to wrap `Map` to simplify syncing state (set, delete, clear) with Svelte', - related: ['stores/uniqueStore'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/matchMedia/+page.md b/packages/svelte-ux/src/routes/docs/stores/matchMedia/+page.md deleted file mode 100644 index 3b5396e4f..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/matchMedia/+page.md +++ /dev/null @@ -1,149 +0,0 @@ - - -

Usage

- -

Full media query

- -```svelte - - -{#if $isLargeScreen} -
Only visible on 768px+ screens
-{/if} -``` - -

Convenient width media query

- -```svelte - - -{#if $isLargeScreen} -
Only visible on 768px+ screens
-{/if} -``` - -

Convenient presets (tailwind defaults)

- -```svelte - - -{#if $mdScreen} -
Only visible on 768px+ screens
-{/if} -``` - -```svelte - - -{#if $print} -
Only visable when printing
-{/if} -``` - -

Examples

- - -
- {#if $smScreen} - - {:else} - - {/if} - $smScreen (640px) - - {#if $mdScreen} - - {:else} - - {/if} - $mdScreen (768px) - - {#if $lgScreen} - - {:else} - - {/if} - $lgScreen (1024px) - - {#if $xlScreen} - - {:else} - - {/if} - $xlScreen (1280px) - - {#if $xxlScreen} - - {:else} - - {/if} - $xxlScreen (1536px) - - {#if $screen} - - {:else} - - {/if} - $screen - - {#if $print} - - {:else} - - {/if} - $print - - {#if $darkColorScheme} - - {:else} - - {/if} - $darkColorScheme - - {#if $motionReduce} - - {:else} - - {/if} - $motionReduce - -
- -
- current width: {innerWidth}px -
-
- - diff --git a/packages/svelte-ux/src/routes/docs/stores/matchMedia/+page.ts b/packages/svelte-ux/src/routes/docs/stores/matchMedia/+page.ts deleted file mode 100644 index 5dc107892..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/matchMedia/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/stores/matchMedia.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Store to monitor media query matching, including screen width/height, orientation, print media, prefers dark/light scheme, and prefers reduced motion', - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/paginationStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/paginationStore/+page.md deleted file mode 100644 index 4d164ee04..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/paginationStore/+page.md +++ /dev/null @@ -1,11 +0,0 @@ - - -

Usage

- -```js -import { paginationStore } from 'svelte-ux'; -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/paginationStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/paginationStore/+page.ts deleted file mode 100644 index d34b5703a..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/paginationStore/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/stores/paginationStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Manage pagination state including current page and page navigation (next/previous/first/last). See related Paginate/Pagination components', - related: ['components/Paginate', 'components/Pagination'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/promiseStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/promiseStore/+page.md deleted file mode 100644 index 91e9c7d7f..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/promiseStore/+page.md +++ /dev/null @@ -1,13 +0,0 @@ - - -

Usage

- -```js -import { promiseStore } from 'svelte-ux'; -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/promiseStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/promiseStore/+page.ts deleted file mode 100644 index fea044c06..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/promiseStore/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import source from '$lib/stores/promiseStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Wraps a Promise as a store. Useful for SvelteKit streamed data handling', - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/queryParamsStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/queryParamsStore/+page.md deleted file mode 100644 index 0b0feee2f..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/queryParamsStore/+page.md +++ /dev/null @@ -1,254 +0,0 @@ - - -

queryParamStore()

- -Manage a single query param - -```js -import { queryParamStore } from 'svelte-ux'; -import { page } from '$app/stores'; -import { goto } from '$app/navigation'; - -const dateRange = queryParamStore({ - name: 'range', - default: { - from: startOfToday(), - to: endOfToday(), - }, - paramType: 'object', - page, -}); - -$: setDataRange = (value) => { - const params = new URLSearchParams(location.search); - dateRange.apply(params, value); - const url = `${location.pathname}?${params.toString()}`; - goto(url, $page); -}; -``` - -If `goto` is passed, store can be set directly - -```js -import { queryParamsStore } from 'svelte-ux'; -import { page } from '$app/stores'; -import { goto } from '$app/navigation'; - -const filters = queryParamStore({ - name: 'range', - default: { - from: startOfToday(), - to: endOfToday(), - }, - paramType: 'object', - page, - goto, // <--- IMPORTANT -}); - -$dataRange = newValue; -``` - -

queryParamsStore()

- -Manage all query params as a single store - -```js -import { queryParamsStore } from 'svelte-ux'; -import { page } from '$app/stores'; -import { goto } from '$app/navigation'; - -const filters = queryParamsStore({ - defaults: { - name: null, - value: null, - range: { - from: startOfToday(), - to: endOfToday(), - }, - }, - paramTypes: (key) => { - switch (key) { - case 'name': - return 'string'; - case 'value': - return 'number'; - case 'range': - return 'object'; - } - }, - page, -}); - -$: setFilters = (newFilters) => { - const url = filters.createUrl(newFilters); - goto(url, $page); -}; -``` - -If `goto` is passed, store can be set directly - -```js -import { queryParamsStore } from 'svelte-ux'; -import { page } from '$app/stores'; -import { goto } from '$app/navigation'; - -const filters = queryParamsStore({ - defaults: { - name: null, - value: null, - range: { - from: startOfToday(), - to: endOfToday(), - }, - }, - paramTypes: (key) => { - switch (key) { - case 'name': - return 'string'; - case 'value': - return 'number'; - case 'range': - return 'object'; - } - }, - page, - goto, // <--- IMPORTANT -}); - -$filters = newFilters; -``` - -

Param types

-

string

-input - -```js -const value = 'example'; -``` - -output - -``` -?value=example -``` - -

string[]

-input - -```js -const value = ['one', 'two', 'three']; -``` - -output - -``` -?value=one_two_three -``` - -

number

-input - -```js -const value = 1234; -``` - -output - -``` -?value=1234 -``` - -

number[]

-input - -```js -const value = [1, 2, 3, 4]; -``` - -output - -``` -?value=1_2_3_4 -``` - -

boolean

-input - -```js -const value = true; -``` - -output - -``` -?value=1 -``` - -

date

-input - -```js -const value = new Date('1982-03-30T00:00:00'); // keep in local time -``` - -output - -``` -?value=1982-03-30 -``` - -

datetime

- -input - -```js -const value = new Date('1982-03-30T00:00:00-05:00'); -``` - -output - -``` -?value=1982-03-30T05:00:00.000Z -``` - -

json

- -input - -```js -const value = { - number: 1234, - string: 'example', - bool: true, - date: new Date('1982-03-30T00:00:00-05:00'), -}; -``` - -output - -``` -?value={"number":1234,"string":"example","bool":true,"date":"1982-03-30T05:00:00.000Z"} -``` - -

object

- -input - -```js -const value = { - number: 1234, - string: 'example', - bool: true, - date: new Date('1982-03-30T00:00:00-05:00'), -}; -``` - -output - -``` -?value=number-1234_string-"example"_bool-true_date-"1982-03-30T05:00:00.000Z" -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/queryParamsStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/queryParamsStore/+page.ts deleted file mode 100644 index 4f504e111..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/queryParamsStore/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/stores/queryParamsStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - title: 'Query params', - description: 'Manage query params as a store, with multiple serialization strategies', - hideUsage: true, - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/selectionStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/selectionStore/+page.md deleted file mode 100644 index ac5b59e3f..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/selectionStore/+page.md +++ /dev/null @@ -1,106 +0,0 @@ - - -

Usage

- -

Max

- -```js -const selection = selectionStore({ max: 3 }); -``` - - - {#each data as d} -
- $selection5.toggleSelected(d.id)} disabled={$selection5.isDisabled(d.id)}> - {d.id} - -
- {/each} - selected: {JSON.stringify($selection5.selected)} -
- -

Basic

- -```js -const selection = selectionStore(); -``` - - - {#each data as d} -
- $selection.toggleSelected(d.id)}> - {d.id} - -
- {/each} - selected: {JSON.stringify($selection.selected)} -
- -

Initial selection

- -```js -const selection2 = selectionStore({ initial: [3, 4, 5] }); -``` - - - {#each data as d} -
- $selection2.toggleSelected(d.id)}> - {d.id} - -
- {/each} - selected: {JSON.stringify($selection2.selected)} -
- -

Select all

- -```js -const selection3 = selectionStore({ all: data.map((d) => d.id) }); -``` - - - $selection3.toggleAll()}> - Select all - - {#each data as d} -
- $selection3.toggleSelected(d.id)}> - {d.id} - -
- {/each} - selected: {JSON.stringify($selection3.selected)} -
- -

Single

- -```js -const selection4 = selectionStore({ single: true }); -``` - - - {#each data as d} -
- $selection4.toggleSelected(d.id)}> - {d.id} - -
- {/each} - selected: {JSON.stringify($selection4.selected)} -
diff --git a/packages/svelte-ux/src/routes/docs/stores/selectionStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/selectionStore/+page.ts deleted file mode 100644 index 29e28be34..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/selectionStore/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/stores/selectionStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Manage item selection state including toggling values, selecting all, and clear or reset selection', - related: ['components/MultiSelect', 'components/Selection'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/tableOrderStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/tableOrderStore/+page.md deleted file mode 100644 index 6ac88f09b..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/tableOrderStore/+page.md +++ /dev/null @@ -1,11 +0,0 @@ - - -

Usage

- -```js -import { tableOrderStore } from 'svelte-ux'; -``` diff --git a/packages/svelte-ux/src/routes/docs/stores/tableOrderStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/tableOrderStore/+page.ts deleted file mode 100644 index 8ca2329e1..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/tableOrderStore/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/stores/tableOrderStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Manage table column sorting selection and direction. Compliments Table component', - related: ['actions/table', 'components/Table'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/timerStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/timerStore/+page.md deleted file mode 100644 index 3837e3687..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/timerStore/+page.md +++ /dev/null @@ -1,48 +0,0 @@ - - -

Usage

- -```js -const timer = timerStore(); -``` - -```ts -const timer = timerStore({ initial?: T, onTick?: (value: T) => {...}, delay?: number, disabled?: boolean }) -``` - -

Example

- -```svelte - -``` - - -
{$dateTimer}
- e.target.checked ? dateTimer.start() : dateTimer.stop()} /> -
- -

Tick count

- -```svelte - -``` - - -
{$tickTimer}
- e.target.checked ? tickTimer.start() : tickTimer.stop()} /> -
diff --git a/packages/svelte-ux/src/routes/docs/stores/timerStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/timerStore/+page.ts deleted file mode 100644 index 9138895a2..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/timerStore/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/stores/timerStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: 'Manage interval ticks, useful for timely updates and countdowns', - related: ['components/Duration', 'components/ScrollingValue'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/stores/uniqueStore/+page.md b/packages/svelte-ux/src/routes/docs/stores/uniqueStore/+page.md deleted file mode 100644 index b5b4ffca7..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/uniqueStore/+page.md +++ /dev/null @@ -1,40 +0,0 @@ - - -

Usage

- -```js -import { uniqueStore } from 'svelte-ux'; - -const store = uniqueStore(); -// $store.has(value) -// $store.size -// store.add(value); -// store.delete(value); -// store.toggle(value); -``` - -

Examples

- -

Basic

- - - {#each data as d} -
- selected.toggle(d.id)}> - {d.id} - -
- {/each} - selected: {JSON.stringify([...$selected])} -
diff --git a/packages/svelte-ux/src/routes/docs/stores/uniqueStore/+page.ts b/packages/svelte-ux/src/routes/docs/stores/uniqueStore/+page.ts deleted file mode 100644 index c90a22e96..000000000 --- a/packages/svelte-ux/src/routes/docs/stores/uniqueStore/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import source from '$lib/stores/uniqueStore.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Store to manage unique values using `Set` with improves ergonomics and better control of updates', - related: ['stores/selectionStore', 'stores/mapStore', 'components/MultiSelect'], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/utils/Logger/+page.md b/packages/svelte-ux/src/routes/docs/utils/Logger/+page.md deleted file mode 100644 index 3e7b12b5d..000000000 --- a/packages/svelte-ux/src/routes/docs/utils/Logger/+page.md +++ /dev/null @@ -1,27 +0,0 @@ - - -

Usage

- -```svelte - -``` - -to enable - -```js -window.localStorage.logger = 'MyComponent'; -window.localStorage.logger = 'MyComponent:INFO'; -window.localStorage.logger = 'MyComponent,OtherComponent'; -window.localStorage.logger = 'MyComponent:INFO,OtherComponent'; -``` diff --git a/packages/svelte-ux/src/routes/docs/utils/Logger/+page.ts b/packages/svelte-ux/src/routes/docs/utils/Logger/+page.ts deleted file mode 100644 index 55aa63daa..000000000 --- a/packages/svelte-ux/src/routes/docs/utils/Logger/+page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import source from '$lib/utils/logger.ts?raw'; -import pageSource from './+page.md?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - description: - 'Logging which can be granularly enabled/disabled via local storage and provides styled output', - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/utils/cls/+page.md b/packages/svelte-ux/src/routes/docs/utils/cls/+page.md deleted file mode 100644 index a1e13a9c2..000000000 --- a/packages/svelte-ux/src/routes/docs/utils/cls/+page.md +++ /dev/null @@ -1,13 +0,0 @@ - - -

Usage

- -```svelte - - -