1import { IsMobile } from '$lib/hooks/is-mobile.svelte.js';
2import { getContext, setContext } from 'svelte';
3import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js';
4
5type Getter<T> = () => T;
6
7export type SidebarStateProps = {
8 /**
9 * A getter function that returns the current open state of the sidebar.
10 * We use a getter function here to support `bind:open` on the `Sidebar.Provider`
11 * component.
12 */
13 open: Getter<boolean>;
14
15 /**
16 * A function that sets the open state of the sidebar. To support `bind:open`, we need
17 * a source of truth for changing the open state to ensure it will be synced throughout
18 * the sub-components and any `bind:` references.
19 */
20 setOpen: (open: boolean) => void;
21};
22
23class SidebarState {
24 readonly props: SidebarStateProps;
25 open = $derived.by(() => this.props.open());
26 openMobile = $state(false);
27 setOpen: SidebarStateProps['setOpen'];
28 #isMobile: IsMobile;
29 state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
30
31 constructor(props: SidebarStateProps) {
32 this.setOpen = props.setOpen;
33 this.#isMobile = new IsMobile();
34 this.props = props;
35 }
36
37 // Convenience getter for checking if the sidebar is mobile
38 // without this, we would need to use `sidebar.isMobile.current` everywhere
39 get isMobile() {
40 return this.#isMobile.current;
41 }
42
43 // Event handler to apply to the `<svelte:window>`
44 handleShortcutKeydown = (e: KeyboardEvent) => {
45 if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
46 e.preventDefault();
47 this.toggle();
48 }
49 };
50
51 setOpenMobile = (value: boolean) => {
52 this.openMobile = value;
53 };
54
55 toggle = () => {
56 return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
57 };
58}
59
60const SYMBOL_KEY = 'scn-sidebar';
61
62/**
63 * Instantiates a new `SidebarState` instance and sets it in the context.
64 *
65 * @param props The constructor props for the `SidebarState` class.
66 * @returns The `SidebarState` instance.
67 */
68export function setSidebar(props: SidebarStateProps): SidebarState {
69 return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
70}
71
72/**
73 * Retrieves the `SidebarState` instance from the context. This is a class instance,
74 * so you cannot destructure it.
75 * @returns The `SidebarState` instance.
76 */
77export function useSidebar(): SidebarState {
78 return getContext(Symbol.for(SYMBOL_KEY));
79}