diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..07f92c8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + - run: npm ci + - run: npm run validate:config + - run: npm run build diff --git a/.gitignore b/.gitignore index ff96a01..962c43b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules .vscode build/*.map tmp/ + diff --git a/API.md b/API.md index 2f540d1..be52bc1 100644 --- a/API.md +++ b/API.md @@ -1,52 +1,447 @@ -""" -This file, `configuration.json`, contains the configuration settings for the application. It is used to store various parameters and options that control the behavior of the application. - -""" -# API - -The configuration file is `configuration.json` and it is a JSON file, it contains the configuration settings for the application. It is used to store various parameters and options that control the behavior of the application. It is located in src directory with the following fields: -1 - the tabs: the tabs that are displayed in the application -which are the following: - - the features tab, the default tab that is displayed when the application is launched, describes the features of the esp board - - the network tab, the tab that contains the network capabilities of the esp board - - the filesystem tab, the tab that contains the filesystem capabilities of the esp board - - the update tab, the tab that contains the update capabilities of the esp board - - the devices tab, the tab that contains the connected devices capabilities of the esp board - - the security tab, the tab that contains the security settings available - - the others tab, the tab that contains the other settings available that are not included in the previous tabs - -2 - the tab is a dictionary that contains the following fields: - - id: the id of the control, it is also used for the dependency between the controls - - description: the description of the control, it is used to display the control in the application - - setting: if the control is a setting or not, (true or false), if false it will be set as comment - - type: the type of the control, it can be one of the following: - - text: a text input - - select: a select input - - boolean: a boolean input - - group: a group of controls - - value: the content of the control, that a list of controls or groups of controls - -3 - the control is a dictionary that contains the following fields: - - id: the id of the control, it is also used for the dependency between the controls - - description: the description of the control, it is used to display the control in the application - - label: the label of the control, it is used to display the control in the application - - setting: if the control is a setting or not, (true or false), if false it will be set as comment - - define: if the control is a definition then set the label of the define - - type: the type of the control, it can be one of the following: - - text: a text input - - select: a select input - - boolean: a boolean input - - group: a group of controls - - depends: the id of the control that the current control depends on and the list of values of the control that the current control depends on (value), or the list of values of the control that the current control do not depends on (notvalue), if value is a text define then need to add also the `\"`` of begining and end of string - - - value: the content of the control, that a list of controls or groups of controls, if values is -1 it use default value - - options: if type is select, then the value is a list of options, defined by the following fields: - - label: the description of the option - - value: the value of the option - - depend: the id of the control that the current control depends on and value of the control that the current control depends on - - help: the help of the option - - isport: if the control is a port or not, (true or false), if true it will be set as port, and use the available port list accross all process, no need to set the possible options - - ispin: if the control is a pin or not, (true or false), if true it will be set as pin, and use the available pin list accross all process, no need to set the possible options - - usedescforoptions: if the description of the control is used for the options or not, (true or false), if true it will be used for the options, if false it will not be used for the options, so no need to repeat the description in the options - - disableiffalse: disable the control for the configuration if the value of the control is false - +# ESP3D-Configurator — Configuration schema + +This document describes how the configurator defines UI tabs, form fields, dependencies, and generated firmware files. + +Configuration is split per tab under `src/configuration/` (one JSON file per wizard section). + +## Documentation map + +| Document | Audience | Contents | +|----------|----------|----------| +| [README.md](README.md) | Everyone | Project overview, live demo link, dev quick start | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Developers | Clone, scripts, PR checklist, CI | +| **API.md** (this file) | Developers | Schema, modules, pins, export/import, presets | + +## Architecture + +``` +src/ + configuration/ + index.js # Aggregates all tab sections + visibility.js # depend / visibility (UI + generate) + applyBoardPreset.js # Apply preset or import snapshot by field id + boardPresets.json # Built-in board starting profiles + configurationSnapshot.js # JSON snapshot in configuration.h (export/import) + mcuPinDefaults.js # MCU default GPIO for SPI when pin = Default (-1) + pinCapabilities.js # Filter/validate GPIO by MCU + pinRole + pinUsageRoles.json # Field id → pinRole registry + psramReservedPins.js # PSRAM / camera reserved GPIO per MCU + serialPinDefaults.js # UART default GPIO labels for -1 + serialPins.js # Extra #defines for serial RX/TX in configuration.h + cameraPins.js # Camera model pin maps + #defines + cameraPinMaps.json + pinConflicts.js # Resolve effective GPIO + conflict audit + validateField.js # Per-field validation in step tabs + reservedResources.js # Cross-tab pin/port reservation lists + tabs/ # One JSON (+ .js re-export) per wizard section + components/ + BoardPresetPicker/ # Preset dropdown + Import configuration.h (Features) + ThemeToggle/ # Light / Auto / Dark (navbar) + theme/theme.js # Theme persistence (localStorage) + contexts/ + DatasContext.js # configuration ref, configRevision, importSnapshot + UiContext.js # Modals, toasts + pages/config/ + steps.js # Tab bar order, routes, icons + tabs/ + step/ # Step tabs; pins.json / ports.json + generate/ # configuration.h + platformio.ini + translations/ + en.json # Base strings for T() +tools/ + validate_configuration.js # npm run validate:config + sync_pin_roles.js # npm run sync:pin-roles + check_esp3d_reference.js # npm run check:esp3d (needs tmp/esp3d) +``` + +| Layer | Role | +|-------|------| +| `configTabs` (`steps.js`) | Wizard navigation: label, route, link id, icon, `section` key | +| `tabs/
.json` | Field definitions for one configurator step | +| `configuration/index.js` | `{ features: [...], network: [...], ... }` for the app | +| `BoardPresetPicker` | Features tab: presets + import; same confirm dialog as preset apply | +| `GenerateTab` | Reads configuration and exports `configuration.h` / `platformio.ini` | +| `DatasContext` | Mutable `configuration.current`; `configRevision` bumps UI after import | + +The `default` section exists in `configuration/index.js` (from `defaults.json`) and is included in generated headers, but it has **no dedicated tab** in the UI. + +## Development workflow + +### Prerequisites + +- **Node.js** 20+ (matches [CI](.github/workflows/ci.yml)) +- **npm** (lockfile: commit `package-lock.json` if present) + +### Install and run + +```bash +npm ci # or npm install +npm run dev # webpack dev server + express (see config/server.js) +npm run build # production bundle → build/ +``` + +Open the URL printed by the dev server (or serve `build/` statically for a production-like test). + +### npm scripts + +| Script | Command | Purpose | +|--------|---------|---------| +| `dev` | `concurrently` front + server | Local development | +| `front` | `webpack serve` | UI only | +| `server` | `nodemon config/server.js` | Dev backend | +| `build` | `webpack --config config/webpack.prod.js` | Release build | +| `validate:config` | `node tools/validate_configuration.js` | Unique ids, `depend` refs, pin roles | +| `sync:pin-roles` | `node tools/sync_pin_roles.js` | Sync `pinRole` from `pinUsageRoles.json` into tab JSON | +| `check:esp3d` | `node tools/check_esp3d_reference.js` | Camera models vs `tmp/esp3d` (optional) | + +**Before opening a PR:** `npm run validate:config` and `npm run build` (same as CI). + +### Continuous integration + +GitHub Actions workflow `.github/workflows/ci.yml` on push/PR to `main` / `master`: + +1. `npm ci` +2. `npm run validate:config` +3. `npm run build` + +### Internationalization (i18n) + +UI strings use `T("English key")` from `src/components/Translations/index.js`. + +- Base catalog: `src/translations/en.json` +- New user-visible text: add the **English sentence as key and value** in `en.json`, then use `T("…")` in components +- Toasts use the `content` property (not `message`); import feedback can pass `literal: true` to skip re-translation + +### UI theme + +- Toggle in navbar: **Light** / **Auto** / **Dark** (`src/components/ThemeToggle`, `src/theme/theme.js`) +- Stored in `localStorage` key `esp3d-configurator-ui-theme` (default `auto` → follows `prefers-color-scheme`) +- Resolved theme on ``; tokens in `src/style/_theme.scss` + +### Manual test checklist (config changes) + +1. Features → change MCU, preset, **Import configuration.h** (confirm dialog → file → green message + toast). +2. Walk affected tabs (pins, serial, camera). +3. Download → preview `configuration.h` (snapshot at end) and `platformio.ini`; fix pin conflicts if download is blocked. +4. Re-import the downloaded `configuration.h` and confirm values match. + +## Configurator tabs (UI) + +Defined in `src/pages/config/steps.js` (`configTabs`): + +| Tab | Route | Config section (`current`) | +|-----|-------|----------------------------| +| Features | `/config/features` | `features` | +| Network | `/config/network` | `network` | +| Filesystems | `/config/filesystems` | `filesystems` | +| Update | `/config/update` | `update` | +| Devices | `/config/devices` | `devices` | +| Security | `/config/security` | `security` | +| Others | `/config/others` | `others` | +| Download | `/config/generate` | (generation only) | + +To add a new wizard tab: + +1. Create `src/configuration/tabs/
.json` (array of groups). +2. Create `src/configuration/tabs/
.js` that default-exports that JSON. +3. Register the section in `src/configuration/index.js`. +4. Add an entry to `configTabs` in `src/pages/config/steps.js` (order = wizard order). +5. Routes are built automatically in `src/pages/config/index.js` from `configTabs`. + +## Tab file format + +Each `tabs/
.json` file is a **JSON array** of top-level **groups**: + +```json +[ + { + "id": "mygroup", + "description": "Group help text", + "label": "Group title", + "setting": true, + "type": "group", + "depend": { "id": "otherField", "value": [true] }, + "value": [ /* controls */ ] + } +] +``` + +## Control (field) format + +Each item inside `group.value` is a control: + +| Field | Description | +|-------|-------------| +| `id` | Unique identifier (used by `depend` and `getValueId()`). **Must be unique across the whole configuration.** | +| `label` | Short title in the UI | +| `description` | Longer help text | +| `type` | `text`, `number`, `select`, `boolean`, or `group` (nested) | +| `value` | Default value (`boolean`, string, or `-1` for “none” on selects) | +| `setting` | If `true`, emitted as `#define` in `configuration.h`; if `false`, comment only | +| `skipExport` | If `true` on a group, omitted from `configuration.h` (UI unchanged; values still in JSON snapshot) | +| `define` | C macro name (e.g. `WIFI_FEATURE`) | +| `depend` | Visibility rule (see below) | +| `options` | For `select`: list of `{ label, value, help?, depend? }` | +| `disableiffalse` | For booleans: omit define when value is false | +| `needquote` | Wrap string values in quotes in generated header | +| `hide` | Skip in generated output | +| `header` | Extra lines prepended before the define | +| `ispin` | Use shared pin list (`src/tabs/step/pins.json`) | +| `pinRole` | MCU-independent usage: `output`, `input`, `bidirectional`, `analog_input`, `optional_gpio` (see `pinUsageRoles.json`; run `node tools/sync_pin_roles.js` to sync from registry) | +| `pinRoleWhen` | Optional override map, e.g. `{ "dependId": "sensortype", "map": { "ANALOG_DEVICE": "analog_input" } }` on `sensorpin` | +| `footnote` | Optional note below the control (e.g. **Memory type** boot warning on ESP32-S3) | +| `footnoteWhenActive` | If `true` on a boolean, `footnote` is shown only when the value is enabled | +| `isport` | Use shared serial port list (`src/tabs/step/ports.json`) | +| `usedefault` | For pin/port selects: show “Default” instead of “None” | +| `usedescforoptions` | Repeat `description` as comment for select defines | + +### Dependencies (`depend`) + +Controls and options can be shown only when other fields match: + +```json +"depend": { "id": "wifi", "value": [true] } +``` + +```json +"depend": { "id": "targetmcu", "notvalue": ["esp8266", "esp8285"] } +``` + +Multiple conditions (all must match): + +```json +"depend": [ + { "id": "ethernet", "value": [true] }, + { "id": "ethernetboard", "notvalue": "TYPE_ETH_PHY_W5500" } +] +``` + +For string defines in `depend.value`, use the same quoting as in `value` (e.g. `"\"script\""`). + +**Note:** Some legacy entries use the string `"true"` instead of boolean `true` for ethernet-related fields. Prefer real booleans for new fields. + +### Pin and port allocation + +When `ispin` or `isport` is set, the step UI tracks used pins/ports **across all tabs** (not only the current step). Lists come from: + +- `reservedResources.js` — explicit GPIO values plus **resolved** defaults (see below) +- **Download** tab — `auditPinConflicts()` lists any GPIO still shared after resolution; while conflicts remain, **configuration.h** / **platformio.ini** downloads and file previews are hidden + +**Default pins (`value: "-1"` + `usedefault: true`)** +For shared SPI signals (MOSI/MISO/SCK/CS), `-1` is resolved to the MCU defaults in `mcuPinDefaults.js` (same numbers as `platformio.ini` / TFT build flags). In the UI, the default option is shown as `-1 (5)` (GPIO number for the selected MCU). Example: on ESP32, SD CS Default and TFT CS Default both resolve to GPIO 5 → reported as a conflict. + +Field ids mapped to defaults are listed in `defaultPinRoles` inside `mcuPinDefaults.js`. Other `-1` pins (optional / board-specific) are not resolved and do not participate in this check. + +Pin/port pick lists: + +- `src/tabs/step/pins.json` +- `src/tabs/step/ports.json` + +Per-field `options` can extend or override list entries (e.g. board-specific pins). + +## Generated outputs + +On the **Download** tab: + +- **configuration.h** — `#define` lines from all sections where `setting` is true and visibility/`depend` rules pass (`src/tabs/generate/index.js`). +- **platformio.ini** — Environment derived from MCU, flash size, WiFi/Ethernet/BT, camera, display, etc. + +Settings in the `default` section are included in `configuration.h` even without a UI tab. + +### Configuration snapshot (in `configuration.h` only) + +There is no separate settings file. **Download configuration.h** already embeds a JSON snapshot at the end of the file (block comment, markers `ESP3D_CONFIG_SNAPSHOT_BEGIN` … `ESP3D_CONFIG_SNAPSHOT_END`) with **all** wizard field ids and values (including `setting: false` fields such as serial/camera GPIO, `has_psram`, etc.). + +- **Save settings:** download `configuration.h` as today (`collectConfigurationSnapshot()` + `formatSnapshotComment()` in `src/configuration/configurationSnapshot.js`, appended in `configurationFile()`). +- **Restore:** **Features** tab → **Board preset** row → **Import configuration.h** (next to the preset list). The file name becomes a preset entry (selected by default after import). Re-selecting it uses the same confirmation flow as built-in presets. JSON is read from the comment block only (no `#define` parsing). UI refresh uses `configRevision` in `DatasContext`. + +Older `configuration.h` files without this block cannot be imported through this flow. + +**Groups omitted from the header (still in snapshot):** e.g. **Board description** (`features` group `mcu`) uses `skipExport: true` on the group — UI unchanged, values only in JSON snapshot, not as comment banner in `configuration.h`. + +## Maintenance notes + +- Avoid duplicate `id` values (e.g. multiple `ssdpmodelename` for different MCUs). Duplicates break `getValueId()` and can cause React key warnings; use distinct ids or a single control with `depend` on options. +- After editing JSON, run `npm run validate:config` (duplicate ids, broken `depend`) then `npm run build` or `npm run dev`. + +## Serial GPIO + +Two logical serial links (not three independent pin pairs): + +| Link | Port selector | GPIO overrides | +|------|---------------|----------------| +| **Main** (printer/TFT) | **Main serial port** → `ESP_SERIAL_OUTPUT`, then **RX** / **TX** | `ESP_RX_PIN`, `ESP_TX_PIN` | +| **Serial bridge** (optional, ESP32 family) | **Use serial bridge** + **Serial bridge port** → `ESP_SERIAL_BRIDGE_OUTPUT`, then **RX** / **TX** | `ESP_BRIDGE_RX_PIN`, `ESP_BRIDGE_TX_PIN` | + +All of these live in the **Serial Communications** group (pins directly under each port selector). + +Use **Default** (`-1`) on RX/TX selects to keep the board default for that UART (define not emitted; `esp3d_pins.h` supplies `-1` defaults). In the UI, Default is shown as **-1 (GPIO n)** using typical UART pins for the selected **Main serial port** or **Serial bridge port** (`serialPinDefaults.js`) so pin conflict tracking matches other tabs. + +Non-default GPIO values are appended to `configuration.h` with the same guards as ESP3D: + +- Main: `#ifndef ESP_RX_PIN` / `ESP_TX_PIN` then `#define` (always valid once `ESP_SERIAL_OUTPUT` is set). +- Bridge: wrapped in `#if defined(ESP_SERIAL_BRIDGE_OUTPUT)` … `#endif`, each pin under `#ifndef ESP_BRIDGE_RX_PIN` / `ESP_BRIDGE_TX_PIN` (only when **Use serial bridge** is enabled). `ESP_SERIAL_BRIDGE_OUTPUT` must appear earlier in the file (from **Serial bridge port**). + +**UART choices** depend on MCU (`src/tabs/step/ports.json`): ESP32/S3 may use UART 0/1/2 on the main port; bridge uses another UART from the same list when enabled. On **ESP32 with PSRAM**, UART 2 must not use GPIO 16/17 (module PSRAM lines); the configurator no longer suggests those as serial defaults when PSRAM (or a camera) is enabled. + +## PSRAM reserved GPIO + +When **Has PSRAM** is enabled, or a **camera** is selected (camera requires PSRAM), `psramReservedPins.js` adds module-reserved GPIO to pin tracking and conflict checks: + +| MCU | Reserved GPIO (typical module wiring) | +|-----|--------------------------------------| +| ESP32 | 16, 17 (PSRAM CS / clock on WROVER-class modules) | +| ESP32-S2 | 26–31 (SPI flash + PSRAM bus) | +| ESP32-S3 | 26–32; + 33–37 if **Memory type** is octal PSRAM (`dio_opi`, `qio_opi`, `opi_opi`) | + +ESP8266 has no PSRAM option. Exact pins depend on the physical module; do not assign these GPIO to SD, TFT, serial overrides, etc. + +## Camera + +On the **Devices** tab, the **Camera** group is visible only when **Has PSRAM** is enabled on Features (ESP32 / ESP32-S2 / ESP32-S3, not ESP-01). ESP3D only supports cameras on boards with PSRAM. + +- **Camera type** → `#define CAMERA_DEVICE` in `configuration.h`. +- **Camera GPIO map** (Devices): read-only for predefined models (`cameraPinMaps.json`). **Custom** enables editing; GPIO values are written to `configuration.h`. +- **Flip** options → `CAMERA_DEVICE_FLIP_*` defines. +- Choosing a camera model still turns **Has PSRAM** on automatically; `platformio.ini` adds `-DBOARD_HAS_PSRAM` and related flags when a model is selected. + +`platformio.ini` adds a `_cam` suffix to the environment name and uses `min_spiffs.csv` on 4 MB flash when a camera is enabled (aligned with `[env:esp32cam]` in ESP3D’s `platformio.ini`). + +### ESP3D reference sources (`tmp/`) + +Drop a copy of [ESP3D](https://github.com/luc-github/ESP3D) under `tmp/esp3d` (with `tmp/platformio.ini`) to compare against the upstream firmware. Then run: + +```bash +npm run check:esp3d +``` + +This checks that every `CAMERA_MODEL_*` in `tmp/esp3d/src/include/esp3d_defines.h` appears in the configurator UI. + +## Board presets and import + +UI: `src/components/BoardPresetPicker` on the **Features** tab (preset ` { + const file = e.target.files && e.target.files[0] + handleImportFile(file) + e.target.value = "" + }} + /> + } + label={T("Import configuration.h")} + onclick={confirmBeforeImport} + /> + + {importFeedback && ( +

+ {importFeedback.message} +

+ )} + {activePreset?.description && ( +

{activePreset.description}

+ )} + + ) +} + +export { BoardPresetPicker, IMPORTED_PRESET_ID } diff --git a/src/components/Helpers/components.js b/src/components/Helpers/components.js index 533334f..b83ee68 100644 --- a/src/components/Helpers/components.js +++ b/src/components/Helpers/components.js @@ -31,7 +31,7 @@ const generateUID = () => Math.random().toString(36).substr(2, 9) const createComponent = (is, className, classModifier = {}) => - ({ is: Tag = is, class: c = "", id = "", ...props }) => { + ({ is: Tag = is, class: c = "", id = "", children, ...props }) => { const splittedArgs = Object.keys(props).reduce( (acc, curr) => { if (Object.keys(classModifier).includes(curr)) @@ -49,7 +49,11 @@ const createComponent = const classNames = `${className} ${splittedArgs.classes.join( " " )} ${c}`.trim() - return + return ( + + {children} + + ) } /* diff --git a/src/components/Modal/confirmModal.js b/src/components/Modal/confirmModal.js index e57e3ea..c4613bf 100644 --- a/src/components/Modal/confirmModal.js +++ b/src/components/Modal/confirmModal.js @@ -19,6 +19,8 @@ import { h } from "preact" import { HelpCircle } from "preact-feather" import { showModal } from "./genericModal" +const CONFIRMATION_MODAL_ID = "confirmation" + const showConfirmationModal = ({ modals, title, @@ -26,13 +28,17 @@ const showConfirmationModal = ({ button1, button2, }) => { + const existing = modals.getModalIndex(CONFIRMATION_MODAL_ID) + if (existing !== -1) { + modals.removeModal(existing) + } showModal({ modals, title, content, button1, button2, - id: "confirmation", + id: CONFIRMATION_MODAL_ID, icon: , hideclose: true, }) diff --git a/src/components/Navbar/index.js b/src/components/Navbar/index.js index f215207..8e898f1 100644 --- a/src/components/Navbar/index.js +++ b/src/components/Navbar/index.js @@ -24,6 +24,7 @@ import { Settings } from "preact-feather" import { AppLogo } from "../Images/logo" import { Link } from "../Router" import { T } from "../Translations" +import { ThemeToggle } from "../ThemeToggle" /* * Local const @@ -73,6 +74,9 @@ const Navbar = () => { ) })}
+ ) } diff --git a/src/components/Router/index.js b/src/components/Router/index.js index 4ef6646..8c4141b 100644 --- a/src/components/Router/index.js +++ b/src/components/Router/index.js @@ -146,29 +146,24 @@ const Link = ({ ...rest }) => { const { activeRoute } = useRouterContext() - const [mergedClassName, setMergedClassName] = useState() - - useEffect(() => { - const route = window.location.hash.slice(1).toLowerCase() - if ( - (activeRoute == "/config" && href == route) || - (route.startsWith("/config") && href == "/config") - ) { - setMergedClassName(`${className} ${activeClassName}`) - } else - setMergedClassName( - activeRoute === href - ? `${className} ${activeClassName}` - : className - ) - }, [activeRoute]) + const route = + typeof window !== "undefined" + ? window.location.hash.slice(1).toLowerCase() + : "" + let mergedClassName = className + if ( + (activeRoute == "/config" && href == route) || + (route.startsWith("/config") && href == "/config") + ) { + mergedClassName = `${className} ${activeClassName}` + } else if (activeRoute === href) { + mergedClassName = `${className} ${activeClassName}` + } return ( - mergedClassName && ( - - {children} - - ) + + {children} + ) } diff --git a/src/components/TabBar/index.js b/src/components/TabBar/index.js index c5b44fd..144ebb3 100644 --- a/src/components/TabBar/index.js +++ b/src/components/TabBar/index.js @@ -21,89 +21,24 @@ TabBar.js - ESP3D WebUI Tabs bar file import { h } from "preact" import { Link } from "../Router" import { T } from "../Translations" -import { AppLogo } from "../Images/logo" -import { useUiContext, useUiContextFn } from "../../contexts" -import { - Camera, - Download, - HardDrive, - Tool, - Wifi, - Lock, - Upload, -} from "preact-feather" +import { configTabs } from "../../pages/config/steps" /* * Local const * */ -const defaultLinks = [ - { - label: "Features", - icon: , - href: "/config/features", - id: "featuresLink", - }, - { - label: "Network", - icon: , - href: "/config/network", - id: "networkLink", - }, - { - label: "Filesystems", - icon: , - href: "/config/filesystems", - id: "filesystemsLink", - }, - { - label: "Update", - icon: , - href: "/config/update", - id: "updateLink", - }, - { - label: "Devices", - icon: , - href: "/config/devices", - id: "devicesLink", - }, - - { - label: "Security", - icon: , - href: "/config/security", - id: "securityLink", - }, - { - label: "Others", - icon: , - href: "/config/others", - id: "othersLink", - }, - { - label: "Download", - icon: , - href: "/config/generate", - id: "generateLink", - }, -] const TabBar = () => { - const { uisettings } = useUiContext() return (
    - {defaultLinks && - defaultLinks.map(({ label, icon, href, id }) => { + {configTabs && + configTabs.map(({ label, icon, route, id }) => { return (
  • { - //TBD - }} + href={route} > {icon} diff --git a/src/components/ThemeToggle/index.js b/src/components/ThemeToggle/index.js new file mode 100644 index 0000000..844216e --- /dev/null +++ b/src/components/ThemeToggle/index.js @@ -0,0 +1,51 @@ +import { h } from "preact" +import { useState, useEffect } from "preact/hooks" +import { Sun, Moon, Monitor } from "preact-feather" +import { getStoredThemeMode, setThemeMode } from "../../theme/theme" + +const MODES = [ + { id: "light", Icon: Sun, label: "Light theme" }, + { id: "auto", Icon: Monitor, label: "System theme" }, + { id: "dark", Icon: Moon, label: "Dark theme" }, +] + +const ThemeToggle = () => { + const [mode, setMode] = useState(getStoredThemeMode) + + useEffect(() => { + const onStorage = (e) => { + if (e.key === "esp3d-configurator-ui-theme") { + setMode(getStoredThemeMode()) + } + } + window.addEventListener("storage", onStorage) + return () => window.removeEventListener("storage", onStorage) + }, []) + + const select = (next) => { + if (next === mode) return + setThemeMode(next) + setMode(next) + } + + return ( +
    + {MODES.map(({ id, Icon, label }) => ( + + ))} +
    + ) +} + +export { ThemeToggle } diff --git a/src/components/Toast/index.js b/src/components/Toast/index.js index bd4ccd2..2a80576 100644 --- a/src/components/Toast/index.js +++ b/src/components/Toast/index.js @@ -17,15 +17,18 @@ */ import { h } from "preact" import { useEffect } from "preact/hooks" -import { useUiContext } from "../../contexts" -import { Toast as SpectreToast } from "../Controls" +import { useUiContext, useUiContextFn } from "../../contexts" import { T } from "../Translations" -/* - * Local const - * - */ -const Toast = ({ index, type = "", children, timeout = 2000, remove }) => { +const modifierClass = (type) => { + if (type === "success") return "toast-success" + if (type === "error") return "toast-error" + if (type === "warning") return "toast-warning" + if (type === "primary") return "toast-primary" + return "" +} + +const Toast = ({ index, type = "", message = "", timeout = 2000, remove }) => { useEffect(() => { let timer if (timeout) { @@ -36,16 +39,21 @@ const Toast = ({ index, type = "", children, timeout = 2000, remove }) => { } }, []) + const variantClass = modifierClass(type) + return ( - - + {message} +