Skip to content
Twilight React Playground
ثيم رائدaren

DevSettingsWidget

componentBeginnerbrowserlive demo

A development-only floating panel that lists your twilight.json settings and home-component fields with defaults, and previews edited values in vite dev.

import { DevSettingsWidget, DevSettingsWidgetProps } from '@salla.sa/twilight-theme-engine/dev';

In plain words

While you develop, your theme's setting values come from the merchant's dashboard, which makes it slow to try "what if this switch were off?". DevSettingsWidget is a small panel for that. A gear button appears in the bottom-left corner; open it to see every setting and home-component field your twilight.json declares, filled with its default value.

Change a value and press Apply: during vite dev the page re-renders as if the merchant had chosen it. Nothing is sent to Salla; your choices stay in this browser.

It is a development tool, so mount it only when import.meta.env.DEV is true. Vite sets that value to true in vite dev and to false in a build, so the widget's code never reaches shoppers.

Signature

function DevSettingsWidget({ schema }: DevSettingsWidgetProps): ReactElement | null

interface DevSettingsWidgetProps {
  schema: DevSchema;   // normally: import devSchema from 'virtual:twilight/schema'
}

Try it live

The real widget, fed a hand-written schema. Beside it, the theme settings it overrides, read with useTheme().Try this: switch on Empty schema: that is what a production build passes, and the gear button disappears.
Storefront canvas · ar · RTL

Open the gear button in the bottom-left corner of the page, change a value, press Apply.

This page was built with import.meta.env.DEV = false: Apply saves the panel, and the theme ignores it.

useTheme().settings.playground_show_banner
undefined
useTheme().settings.playground_banner_text
undefined
useTheme().settings.playground_image_fit
undefined
Controls
Empty schemaWhat virtual:twilight/schema holds in a production build.
What a theme writes
import { lazy, Suspense } from 'react';
import devSchema from 'virtual:twilight/schema';

// Vite replaces import.meta.env.DEV with true in `vite dev` and false in a build.
const DevSettingsWidget = import.meta.env.DEV
  ? lazy(() =>
      import('@salla.sa/twilight-theme-engine/dev').then((m) => ({ default: m.DevSettingsWidget }))
    )
  : null;

/** Render once in app/routes/__root.tsx, inside <body> after <TwilightProvider>. */
export function DevTools() {
  if (!DevSettingsWidget) return null;
  return (
    <Suspense fallback={null}>
      <DevSettingsWidget schema={devSchema} />
    </Suspense>
  );
}

Example

app/components/DevTools.tsx
import { lazy, Suspense } from 'react';
import devSchema from 'virtual:twilight/schema';

// Vite replaces import.meta.env.DEV with true in `vite dev` and false in a build.
// With false this is just `null`: the import() and the widget's chunk are dropped.
const DevSettingsWidget = import.meta.env.DEV
  ? lazy(() =>
      import('@salla.sa/twilight-theme-engine/dev').then((m) => ({ default: m.DevSettingsWidget }))
    )
  : null;

/** Render once in app/routes/__root.tsx, inside <body> after <TwilightProvider>. */
export function DevTools() {
  if (!DevSettingsWidget) return null;
  return (
    <Suspense fallback={null}>
      <DevSettingsWidget schema={devSchema} />
    </Suspense>
  );
}

How it behaves

  • The two halves of the mount pattern do different jobs. import.meta.env.DEV is a literal the bundler folds, so a production build keeps null and never emits the widget. lazy(() => import(...)) puts the widget in a chunk of its own, fetched when it first renders, so even in development it stays out of the main bundle. /dev is a separate subpath for the same reason: nothing in the engine's main entry imports the widget.

  • It renders nothing on the server, before it has mounted, and when the schema has no settings and no components, which is what virtual:twilight/schema holds in every production build. Once mounted it portals to document.body, styles itself inline (no theme CSS needed) and sits fixed in the bottom-left corner above everything (z-index 2147483000).

  • The panel shows a checkbox for toggle fields, a select for select, a text box for text, and the reason a readonly field cannot be edited (such as remote (products) or collection/collection). Components are collapsible sections. Reset to twilight.json defaults restores every value and leaves Apply as it was.

  • Apply seeds every setting you have not edited with its twilight.json default and switches the overrides on; turning it off keeps your edits for next time. The state is saved in localStorage under twilight:dev-settings, in a store shared through globalThis, so the widget chunk and the engine read the same values.

  • What Apply changes, in vite dev only: TwilightProvider shallow-merges the setting values into the theme settings after hydration, so useTheme().settings returns them; the engine home page merges component-field values onto the home block whose path matches (home.<name> or <name>).

  • Only settings and components from twilight.json appear. Collections, variable lists, numbers and dropdowns whose options come from Salla (source) are read-only, and static entries are skipped. The field shapes are the DevSchema types.

Gotchas

  • In a production build Apply changes nothing, even when you hand the widget a schema: the merge in TwilightProvider and in the home page returns early unless import.meta.env.DEV is true (useDevSettingsOverrides in src/dev/dev-settings-store.ts). The live demo says which kind of build you are looking at.

  • Apply overrides the store's real values for every editable setting, not only the ones you touched, so a merchant value that differs from the default is hidden while Apply is on. It also fills keys a real store may not have saved at all: code that forgets its own fallback works under Apply and breaks on a real store. Fix: check your pages with Apply off as well.

  • Boolean defaults come from value, never from selected. Raed's topnav_is_dark has "value": true, "selected": false, so the widget previews it as on.

  • Component values reach only the engine's own home page. A home route you wrote yourself, or any other page, never sees them. Setting values reach every component that reads useTheme().

Related

Source and docs