Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-needles-spend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fuzdev/fuz_ui': minor
---

change some APIs to getters to improve reactivity
4 changes: 3 additions & 1 deletion src/lib/ColorSchemeInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import {themer_context} from './themer.svelte.js';

const get_themer = themer_context.get();

const {
value = themer_context.get(),
value = get_themer(),
...rest
}: SvelteHTMLElements['menu'] & {
value?: {color_scheme: ColorScheme};
Expand Down
6 changes: 4 additions & 2 deletions src/lib/ContextmenuEntry.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
disabled?: boolean;
} = $props();

const contextmenu = contextmenu_context.get();
const get_contextmenu = contextmenu_context.get();
const contextmenu = $derived(get_contextmenu());

const entry = contextmenu.add_entry(
// add_entry registers on the current instance at init — not reactive to contextmenu getter changes
const entry = get_contextmenu().add_entry(
() => run,
() => disabled_prop,
);
Expand Down
6 changes: 4 additions & 2 deletions src/lib/ContextmenuLinkEntry.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
external_rel?: string;
} = $props();

const contextmenu = contextmenu_context.get();
const get_contextmenu = contextmenu_context.get();
const contextmenu = $derived(get_contextmenu());

let anchor_el: HTMLAnchorElement | undefined = $state();

// Register with state management for keyboard navigation
// When activated via keyboard, programmatically click the anchor to trigger navigation
const entry = contextmenu.add_entry(
// add_entry registers on the current instance at init — not reactive to contextmenu getter changes
const entry = get_contextmenu().add_entry(
() => () => {
if (anchor_el) anchor_el.click();
},
Expand Down
10 changes: 1 addition & 9 deletions src/lib/ContextmenuRoot.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,6 @@
separator_entry = separator_entry_default,
children,
}: {
/**
* The `contextmenu` prop is not reactive because that's a rare corner case and
* it's easier to put the `contextmenu` directly in the context
* rather than wrapping with a store or other reactivity.
* If you need to change the contextmenu prop for some reason, use a `{#key contextmenu}` block:
* https://svelte.dev/docs#template-syntax-key
* @nonreactive
*/
contextmenu?: ContextmenuState;
/**
* The number of pixels to offset from the pointer X position when opened.
Expand Down Expand Up @@ -117,7 +109,7 @@
children: Snippet;
} = $props();

contextmenu_context.set(contextmenu);
contextmenu_context.set(() => contextmenu);

if (DEV) contextmenu_check_global_root(() => scoped); // TODO @many is this import tree-shaken?

Expand Down
10 changes: 1 addition & 9 deletions src/lib/ContextmenuRootForSafariCompatibility.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,6 @@
separator_entry = separator_entry_default,
children,
}: {
/**
* The `contextmenu` prop is not reactive because that's a rare corner case and
* it's easier to put the `contextmenu` directly in the context
* rather than wrapping with a store or other reactivity.
* If you need to change the contextmenu prop for some reason, use a `{#key contextmenu}` block:
* https://svelte.dev/docs#template-syntax-key
* @nonreactive
*/
contextmenu?: ContextmenuState;
/**
* The number of pixels the pointer can be moved without canceling `longpress`.
Expand Down Expand Up @@ -131,7 +123,7 @@
children: Snippet;
} = $props();

contextmenu_context.set(contextmenu);
contextmenu_context.set(() => contextmenu);

if (DEV) contextmenu_check_global_root(() => scoped); // TODO @many is this import tree-shaken?

Expand Down
10 changes: 6 additions & 4 deletions src/lib/ContextmenuSubmenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
children: Snippet;
} = $props();

const contextmenu = contextmenu_context.get();
const get_contextmenu = contextmenu_context.get();
const contextmenu = $derived(get_contextmenu());

const submenu = contextmenu.add_submenu();
// add_submenu registers on the current instance at init — not reactive to contextmenu getter changes
const submenu = get_contextmenu().add_submenu();

const {layout} = contextmenu;
const {layout} = $derived(contextmenu);

const selected = $derived(submenu.selected);
const {selected} = $derived(submenu);

let el: HTMLElement | undefined = $state();

Expand Down
4 changes: 2 additions & 2 deletions src/lib/Docs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
} = $props();

// TODO this API is messy, inconsistent usage of props/context
const tomes_by_name = new Map(tomes.map((t) => [t.name, t]));
tomes_context.set(tomes_by_name);
const tomes_by_name = $derived(new Map(tomes.map((t) => [t.name, t])));
tomes_context.set(() => tomes_by_name);

// TODO @many dialog navs - this is messy to satisfy SSR with the current design that puts the secondary nav in a dialog
const TERTIARY_NAV_BREAKPOINT = 1000;
Expand Down
14 changes: 6 additions & 8 deletions src/lib/Redirect.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import {page} from '$app/state';
import {strip_start} from '@fuzdev/fuz_util/string.js';
import {goto} from '$app/navigation';
import type {Snippet} from 'svelte';
import {BROWSER} from 'esm-env';
import {onMount, type Snippet} from 'svelte';

const {
host = '',
Expand All @@ -12,26 +11,25 @@
children,
}: {
/**
* The target host to redirect to. Defaults to the current `location.host`.
* @nonreactive
* The target host to redirect to. Defaults to `''` (relative URL).
*/
host?: string;
/**
* The target path to redirect to. Defaults to the current `location.pathname`.
* @nonreactive
*/
path?: string;
/**
* Should the redirect happen automatically without user input? Defaults to `true`.
* @nonreactive
*/
auto?: boolean;
children?: Snippet<[url: string]>;
} = $props();

const url = host + path;
const url = $derived(host + path);

if (auto && BROWSER) void goto(url, {replaceState: true}); // eslint-disable-line svelte/no-navigation-without-resolve
onMount(() => {
if (auto) void goto(url, {replaceState: true}); // eslint-disable-line svelte/no-navigation-without-resolve
});
</script>

<svelte:head>
Expand Down
4 changes: 3 additions & 1 deletion src/lib/ThemeInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import {themer_context} from './themer.svelte.js';

const get_themer = themer_context.get();

const {
selected_theme = themer_context.get(),
selected_theme = get_themer(),
themes = default_themes,
enable_editing = false,
select = (theme) => {
Expand Down
6 changes: 1 addition & 5 deletions src/lib/Themed.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
load_theme = default_load_theme,
save_theme = default_save_theme,
theme_fallback,
// TODO make reactive? by passing getters as options?
themer = new Themer({theme: load_theme(theme_fallback), color_scheme: load_color_scheme()}),
children,
}: {
Expand All @@ -39,9 +38,6 @@
/**
* A reactive class containing the selected theme and color scheme.
* Defaults to the first default theme.
* The class reference is not reactive
* because it's set in context without a wrapper, use `{#key theme}` if it changes.
* @nonreactive
*/
themer?: Themer;
children: Snippet<[themer: Themer, style: string | null, theme_style_html: string | null]>;
Expand All @@ -67,7 +63,7 @@
* @module
*/

themer_context.set(themer);
themer_context.set(() => themer);

const selected_theme_name = $derived(themer.theme.name);
const style = $derived(
Expand Down
2 changes: 1 addition & 1 deletion src/lib/TomeContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

const docs_links = docs_links_context.get();

tome_context.set(tome); // TODO make reactive?
tome_context.set(() => tome);

const fragment = $derived(docs_slugify(tome.name));

Expand Down
17 changes: 8 additions & 9 deletions src/lib/TomeHeader.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import {page} from '$app/state';
import {onDestroy} from 'svelte';
import {DEV} from 'esm-env';
import type {SvelteHTMLElements} from 'svelte/elements';

Expand All @@ -11,17 +10,17 @@
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
const props: SvelteHTMLElements['h1'] | SvelteHTMLElements['h2'] = $props();

const tome = tome_context.get(); // TODO make reactive?
if (DEV && !tome) throw Error('TomeHeader expects a tome in context'); // eslint-disable-line @typescript-eslint/no-unnecessary-condition
const get_tome = tome_context.get();
if (DEV && !get_tome) throw Error('TomeHeader expects a tome in context'); // eslint-disable-line @typescript-eslint/no-unnecessary-condition
const tome = $derived(get_tome());

const docs_links = docs_links_context.get();

const fragment = docs_slugify(tome.name);
const path_slug = docs_slugify(tome.name);
const id = docs_links.add(fragment, tome.name, page.url.pathname);

onDestroy(() => {
docs_links.remove(id);
const fragment = $derived(docs_slugify(tome.name));
const path_slug = $derived(docs_slugify(tome.name));
$effect(() => {
const id = docs_links.add(fragment, tome.name, page.url.pathname);
return () => docs_links.remove(id);
});

const {path, path_is_selected} = $derived(to_docs_path_info(path_slug, page.url.pathname));
Expand Down
13 changes: 7 additions & 6 deletions src/lib/TomeSection.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" module>
import {create_context} from './context_helpers.js';

export type RegisterSectionHeader = (fragment: string) => string | undefined;
export type RegisterSectionHeader = (get_fragment: () => string) => string | undefined;
export const register_section_header_context = create_context<RegisterSectionHeader>();
export const section_depth_context = create_context(() => 0);
export const section_id_context = create_context<string | undefined>();
Expand Down Expand Up @@ -39,22 +39,23 @@
// Provide own section ID to direct children (header) via context
section_id_context.set(section_id);

let fragment: string | undefined;
let get_fragment: (() => string) | undefined = $state();

register_section_header_context.set((f) => {
if (DEV && fragment !== undefined) {
register_section_header_context.set((gf) => {
if (DEV && get_fragment !== undefined) {
throw Error(
`TomeSection already has header "${fragment}", cannot add "${f}". Did you forget to wrap a TomeSectionHeader in its own TomeSection?`,
`TomeSection already has header "${get_fragment()}", cannot add "${gf()}". Did you forget to wrap a TomeSectionHeader in its own TomeSection?`,
);
}
fragment = f;
get_fragment = gf;
return parent_section_id; // Return parent section ID to header
});
</script>

<section
{...rest}
{@attach intersect(() => ({intersecting}) => {
const fragment = get_fragment?.();
if (!fragment) {
if (DEV) console.error('TomeSectionHeader must be a child of TomeSection'); // eslint-disable-line no-console
return;
Expand Down
18 changes: 7 additions & 11 deletions src/lib/TomeSectionHeader.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import {onDestroy, type Snippet} from 'svelte';
import type {Snippet} from 'svelte';
import type {SvelteHTMLElements} from 'svelte/elements';
import {page} from '$app/state';
import {resolve} from '$app/paths';
Expand Down Expand Up @@ -41,12 +41,12 @@
const my_section_id = section_id_context.get();

// Register with parent section to get parent's ID back
const parent_section_id = register_section_header(fragment);
const parent_section_id = register_section_header(() => fragment);

// Register with docs_links using own section ID and parent section ID
let id: string | undefined;
if (page.url.pathname !== resolve(docs_links.root_path as any)) {
id = docs_links.add(
// Register with docs_links — re-registers when reactive values change
$effect(() => {
if (page.url.pathname === resolve(docs_links.root_path as any)) return;
const id = docs_links.add(
fragment,
text,
page.url.pathname,
Expand All @@ -55,11 +55,7 @@
parent_section_id,
my_section_id,
);
}

// Cleanup on unmount
onDestroy(() => {
if (id) docs_links.remove(id);
return () => docs_links.remove(id);
});
</script>

Expand Down
2 changes: 1 addition & 1 deletion src/lib/contextmenu_state.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ const contextmenu_query_params = (
return params;
};

export const contextmenu_context = create_context<ContextmenuState>();
export const contextmenu_context = create_context<() => ContextmenuState>();

export const contextmenu_submenu_context = create_context<SubmenuState>();

Expand Down
2 changes: 1 addition & 1 deletion src/lib/themer.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class Themer {
};
}
}
export const themer_context = create_context<Themer>();
export const themer_context = create_context<() => Themer>();

export const sync_color_scheme = (color_scheme: ColorScheme | null): void => {
if (!BROWSER) return;
Expand Down
8 changes: 4 additions & 4 deletions src/lib/tome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ export const to_tome_pathname = (
return resolve((hash ? path + ensure_start(hash, '#') : path) as any);
};

export const tomes_context = create_context<Map<string, Tome>>();
export const tomes_context = create_context<() => Map<string, Tome>>();

export const get_tome_by_name = (name: string): Tome => {
const tomes = tomes_context.get();
const tome = tomes.get(name);
const get_tomes = tomes_context.get();
const tome = get_tomes().get(name);
if (!tome) throw Error(`unable to find tome "${name}"`);
return tome;
};

export const tome_context = create_context<Tome>();
export const tome_context = create_context<() => Tome>();
4 changes: 2 additions & 2 deletions src/routes/docs/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import {tomes_context} from '$lib/tome.js';
import {library_context} from '$lib/library.svelte.js';

const tomes_by_name = tomes_context.get();
const get_tomes_by_name = tomes_context.get();

const tomes = Array.from(tomes_by_name.values());
const tomes = $derived(Array.from(get_tomes_by_name().values()));

const library = library_context.get();
</script>
Expand Down
3 changes: 2 additions & 1 deletion src/routes/docs/Themed/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

const themes = default_themes.slice();

const themer = themer_context.get();
const get_themer = themer_context.get();
const themer = $derived(get_themer());

// let show_create_theme_dialog = false;
let editing_theme: null | Theme = $state(null);
Expand Down
Loading
Loading