import { assign, fromCallback, sendTo, setup } from "xstate"; import { os } from "../runtime/bus"; type ColorScheme = "light" | "dark"; type ColorSchemePreference = "system" | ColorScheme; /** * The system-preferences machine's context. Consumers read fields directly * (e.g. via `useSelector`) rather than going through a resolver utility — * the action handlers keep `colorScheme` in sync with `preferredColorScheme` * and `osColorScheme`. * * Note: `colorScheme` is intentionally stored as derived state in context so * consumers can read the resolved value directly without a utility. The * action handlers below are the single source of truth for the resolution. * @public */ export type SystemPreferencesContext = { /** User's color scheme choice; `system` defers to the OS preference. */ preferredColorScheme: ColorSchemePreference; /** Last reported OS color scheme. Used to resolve `colorScheme` when the * preference is `system`. */ osColorScheme: ColorScheme; /** Resolved color scheme — what the UI should render. Recomputed by the * action handlers whenever an input changes. */ colorScheme: ColorScheme; /** Whether the dock sidebar is pinned open. A plain stored boolean — * unlike `colorScheme` there is nothing to resolve. Defaults to locked. */ dockLocked: boolean; }; const DEFAULT_PREFERRED_COLOR_SCHEME: ColorSchemePreference = "system"; const DEFAULT_OS_COLOR_SCHEME: ColorScheme = "light"; const DEFAULT_DOCK_LOCKED = true; /** * Events the system-preferences machine accepts. All originate from the * host's adapter — `preferredColorScheme.set` and `dockLocked.set` are also * forwarded back to it so it can persist the user's choice. * @public */ export type SystemPreferencesEvent = | { type: "preferredColorScheme.set"; preferredColorScheme: ColorSchemePreference; } | { type: "osColorScheme.set"; osColorScheme: ColorScheme; } | { type: "dockLocked.set"; dockLocked: boolean; }; /** * Adapter interface the host implements to give the system-preferences * machine access to the user's environment (OS color scheme, persistent * storage, cross-context change notifications). * * The package owns all orchestration — synchronous seeding, idempotent * persistence, mapping cleared storage to `"system"`. The host's * implementation is pure DOM/storage glue: read, write, subscribe. * @public */ export type SystemPreferencesAdapter = { /** Read the user's stored preference. `null` means "no preference set" * (the machine treats this as `"system"`). */ getPreferredColorScheme: () => ColorSchemePreference | null; /** Write the user's preference to persistent storage. */ persistPreferredColorScheme: (value: ColorSchemePreference) => void; /** Subscribe to cross-context preference changes (e.g. another tab * writing to the shared storage). The callback receives the new stored * value (or `null` if it was cleared). Returns an unsubscribe function. */ subscribePreferredColorScheme: ( callback: (next: ColorSchemePreference | null) => void, ) => () => void; /** Read the current OS-detected color scheme. */ getOsColorScheme: () => ColorScheme; /** Subscribe to OS color-scheme changes (e.g. user flipping dark mode). * Returns an unsubscribe function. */ subscribeOsColorScheme: (callback: (next: ColorScheme) => void) => () => void; /** Read the user's stored dock-locked preference. `null` means "no * preference set" (the machine treats this as the default, locked). */ getDockLocked: () => boolean | null; /** Write the user's dock-locked preference to persistent storage. */ persistDockLocked: (value: boolean) => void; /** Subscribe to cross-context dock-locked changes (e.g. another tab writing * to the shared storage). The callback receives the new stored value (or * `null` if it was cleared). Returns an unsubscribe function. */ subscribeDockLocked: (callback: (next: boolean | null) => void) => () => void; }; const resolveColorScheme = ( preferred: ColorSchemePreference, osColorScheme: ColorScheme, ): ColorScheme => (preferred === "system" ? osColorScheme : preferred); // Internal bridge actor — subscribes to the host's adapter for runtime // changes and persists user-driven preference changes when the parent // forwards them. No-op if the host didn't pass an adapter (SSR, tests). type AdapterBridgeReceiveEvent = Extract< SystemPreferencesEvent, { type: "preferredColorScheme.set" } | { type: "dockLocked.set" } >; const adapterBridgeLogic = fromCallback< AdapterBridgeReceiveEvent, SystemPreferencesAdapter | undefined, SystemPreferencesEvent >(({ input: adapter, sendBack, receive }) => { if (!adapter) return; const unsubscribePreferredColorScheme = adapter.subscribePreferredColorScheme( (next) => { // Falls back to `"system"` so an external clear (another tab deleting // the key) resets the preference rather than silently keeping the // previous value. sendBack({ type: "preferredColorScheme.set", preferredColorScheme: next ?? "system", }); }, ); const unsubscribeOs = adapter.subscribeOsColorScheme((next) => { sendBack({ type: "osColorScheme.set", osColorScheme: next }); }); const unsubscribeDockLocked = adapter.subscribeDockLocked((next) => { // Falls back to the default so an external clear (another tab deleting // the key) resets the preference rather than silently keeping the // previous value. sendBack({ type: "dockLocked.set", dockLocked: next ?? DEFAULT_DOCK_LOCKED, }); }); receive((event) => { // Idempotent: skip writes that match the current stored value so a // `*.set` originating from a cross-context change doesn't loop back into // another write. if (event.type === "preferredColorScheme.set") { if (adapter.getPreferredColorScheme() === event.preferredColorScheme) return; adapter.persistPreferredColorScheme(event.preferredColorScheme); return; } if (adapter.getDockLocked() === event.dockLocked) return; adapter.persistDockLocked(event.dockLocked); }); return () => { unsubscribePreferredColorScheme(); unsubscribeOs(); unsubscribeDockLocked(); }; }); /** * @internal */ export const systemPreferencesLogic = setup({ types: { input: {} as { adapter?: SystemPreferencesAdapter }, context: {} as SystemPreferencesContext & { adapter: SystemPreferencesAdapter | undefined; }, events: {} as SystemPreferencesEvent, }, actors: { adapterBridge: adapterBridgeLogic, }, actions: { publishColorScheme: ({ context }) => { os.emit("preferences.color-scheme", context.colorScheme); }, publishDockLocked: ({ context }) => { os.emit("preferences.dock-locked", context.dockLocked); }, setDockLocked: assign({ dockLocked: (_, params: { dockLocked: boolean }) => params.dockLocked, }), setPreferredColorScheme: assign({ preferredColorScheme: ( _, params: { preferredColorScheme: ColorSchemePreference }, ) => params.preferredColorScheme, colorScheme: ( { context }, params: { preferredColorScheme: ColorSchemePreference }, ) => resolveColorScheme(params.preferredColorScheme, context.osColorScheme), }), setOsColorScheme: assign({ osColorScheme: (_, params: { osColorScheme: ColorScheme }) => params.osColorScheme, colorScheme: ({ context }, params: { osColorScheme: ColorScheme }) => resolveColorScheme(context.preferredColorScheme, params.osColorScheme), }), }, }).createMachine({ id: "system-preferences", initial: "ready", entry: [{ type: "publishColorScheme" }, { type: "publishDockLocked" }], context: ({ input }) => { const adapter = input.adapter; const preferredColorScheme = adapter?.getPreferredColorScheme() ?? DEFAULT_PREFERRED_COLOR_SCHEME; const osColorScheme = adapter?.getOsColorScheme() ?? DEFAULT_OS_COLOR_SCHEME; const dockLocked = adapter?.getDockLocked() ?? DEFAULT_DOCK_LOCKED; return { adapter, preferredColorScheme, osColorScheme, colorScheme: resolveColorScheme(preferredColorScheme, osColorScheme), dockLocked, }; }, invoke: { id: "adapter", src: "adapterBridge", input: ({ context }) => context.adapter, }, states: { ready: { on: { "preferredColorScheme.set": { actions: [ { type: "setPreferredColorScheme", params: ({ event }) => ({ preferredColorScheme: event.preferredColorScheme, }), }, { type: "publishColorScheme" }, // Forward to the adapter bridge so it can persist the user's // choice. The bridge guards against redundant writes, so // round-tripping a `preferredColorScheme.set` it sourced itself // (e.g. from a `storage` event in another tab) is a no-op. sendTo("adapter", ({ event }) => event), ], }, "osColorScheme.set": { actions: [ { type: "setOsColorScheme", params: ({ event }) => ({ osColorScheme: event.osColorScheme, }), }, { type: "publishColorScheme" }, ], }, "dockLocked.set": { actions: [ { type: "setDockLocked", params: ({ event }) => ({ dockLocked: event.dockLocked }), }, { type: "publishDockLocked" }, // Forward to the adapter bridge so it can persist the user's // choice. The bridge guards against redundant writes, so // round-tripping a `dockLocked.set` it sourced itself (e.g. from a // `storage` event in another tab) is a no-op. sendTo("adapter", ({ event }) => event), ], }, }, }, }, });