partial toolbar: basic writable store approach

This commit is contained in:
Elijah Duffy
2025-07-09 12:41:28 -07:00
parent e953c362cf
commit 414b63e8bc
5 changed files with 177 additions and 113 deletions

View File

@@ -1,94 +0,0 @@
<script lang="ts" module>
type ToolbarRenderable<P extends unknown[] = any[]> =
| { type: 'component'; component: Component; props?: P }
| { type: 'snippet'; snippet: Snippet<P>; props?: P }
| { type: 'string'; value: string };
interface ToolbarButton<P extends unknown[] = any[]> {
type: 'toggle' | 'button';
title: string;
render: ToolbarRenderable<P>;
}
interface ToolbarDivider {
type: 'divider';
}
interface ToolbarGroup {
type: 'group';
items: ToolbarItem[];
}
export type ToolbarItem = ToolbarButton<any> | ToolbarDivider | ToolbarGroup;
type SnippetArgs<T> = T extends Snippet<infer Args extends unknown[]> ? Args : never;
// For snippet-based buttons
export function toolbarItem<S extends Snippet<any>>(item: {
type: 'button' | 'toggle';
title: string;
render: {
type: 'snippet';
snippet: S;
props: SnippetArgs<S>;
};
}): typeof item;
// For component-based buttons (optional, for completeness)
export function toolbarItem<C extends Component>(item: {
type: 'button' | 'toggle';
title: string;
render: {
type: 'component';
component: C;
props: ComponentProps<C>;
};
}): typeof item;
// For other items (divider, group, etc)
export function toolbarItem<T extends ToolbarDivider | ToolbarGroup>(item: T): T;
// Implementation
export function toolbarItem(item: any): any {
return item;
}
</script>
<script lang="ts">
import { type Component, type Snippet, type ComponentProps } from 'svelte';
interface Props {
items: ToolbarItem[];
}
let { items }: Props = $props();
</script>
<div>
{#each items as toolbarItem}
{@render item(toolbarItem)}
{/each}
</div>
{#snippet item(i: ToolbarItem)}
{#if i.type === 'button' || i.type === 'toggle'}
<button class="toolbar-button">
{#if i.render.type === 'component'}
<item.render.component {...i.render.props} />
{:else if i.render.type === 'snippet'}
{@render i.render.snippet(i.render.props)}
{:else if i.render.type === 'string'}
<span>{i.render.value}</span>
{/if}
<span>{i.title}</span>
</button>
{:else if i.type === 'divider'}
<hr class="toolbar-divider" />
{:else if i.type === 'group'}
<div class="toolbar-group">
{#each i.items as groupItem}
{@render item(groupItem)}
{/each}
</div>
{/if}
{/snippet}

78
src/lib/Toolbar.svelte.ts Normal file
View File

@@ -0,0 +1,78 @@
import { writable, type Writable } from 'svelte/store';
export type ToolbarToggleState = 'on' | 'off';
export type ToolbarToggleOptions = {
state?: Writable<ToolbarToggleState>;
group?: string;
};
export class Toolbar {
private _groups: string[] = [];
constructor() {}
group(name: string) {
if (this._groups.includes(name)) {
throw new Error(`Toolbar group "${name}" already exists.`);
}
this._groups.push(name);
return {
['data-sui-toolbar-group']: name
};
}
toggle(opts: ToolbarToggleOptions = {}) {
if (opts.group && !this._groups.includes(opts.group)) {
throw new Error(`Toolbar group "${opts.group}" does not exist.`);
}
const toggleState = opts.state ?? writable<ToolbarToggleState>('off');
const bindDataState = (node: HTMLElement) => {
const unsubscribe = toggleState.subscribe((value) => {
console.log('state changed', value);
node.dataset.state = value;
});
return { destroy: unsubscribe };
};
return {
['data-sui-toolbar-toggle']: opts?.group || '',
onclick: (event: MouseEvent) => {
const target = event.currentTarget as HTMLElement;
if (!target) return;
// Toggle data-state
// const currentState = target.dataset.state || 'off';
// if (opts?.state) {
// opts.state = currentState === 'on' ? 'off' : 'on';
// } else {
// target.dataset.state = currentState === 'on' ? 'off' : 'on';
// }
toggleState.update((current) => (current === 'on' ? 'off' : 'on'));
// opts.state = opts.state === 'on' ? 'off' : 'on';
// If this is a member of a group, find the adjacent toggles and turn them off
if (opts?.group) {
const groupToggles = document.querySelectorAll(
`[data-sui-toolbar-toggle="${opts.group}"]`
) as NodeListOf<HTMLElement>;
groupToggles.forEach((toggle) => {
if (toggle !== target) {
toggle.dataset.state = 'off';
}
});
}
console.log('onlick', opts.group, toggleState);
},
bindDataState, // expose data-state attribute binder
state: toggleState // expose state to consumer
};
}
}

View File

@@ -23,4 +23,4 @@ export { default as TimeInput } from './TimeInput.svelte';
export { default as TimezoneInput } from './TimezoneInput.svelte';
export { default as ToggleGroup } from './ToggleGroup.svelte';
export { default as ToggleSelect } from './ToggleSelect.svelte';
export { type Option, getLabel, getValue } from './util';
export { type Option, type MaybeGetter, getLabel, getValue } from './util';

View File

@@ -1,3 +1,10 @@
/**
* MaybeGetter is a type that can either be a value of type T or a function that returns a value of type T.
* This is useful for cases where you might want to pass a value directly or a function that computes the
* value later, potentially taking advantage of reactivity.
*/
export type MaybeGetter<T> = T | (() => T);
/**
* Generates a unique identifier string unless an identifier is provided.
* If a prefix is provided, it will be prepended to the identifier.