three-slicer 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-08-19
4
+
5
+ SLA (resin) printing lands as a second technology in the same kernel, routed by `printer_technology`.
6
+
7
+ ### Added
8
+
9
+ - SLA slicing in the WASM kernel: `slicer.sliceSla(stl, params)` on the engine handle,
10
+ `client.sliceSla` / `client.sliceSlaJob` on the worker client. Results carry per-layer mask segment
11
+ streams, generated support and pad meshes, `resin_ml` / `time_estimate` stats and `lift_layers`
12
+ (pad + elevation) — no G-code.
13
+ - The support chain is PrusaSlicer 2.9.6's own, ported verbatim: support-point generator
14
+ (SupportPointGenerator + SupportIslands), the default support tree, and the real pad geometry. Built
15
+ against vendored NLopt 2.5.0, Prusa's bundled libigl, SGI glu-libtess, and CGAL headers.
16
+ - `deriveSlaParams(settings)`, `printerTechnology(settings)`, `resinCatalog` / `resinSettingsFor(name)`
17
+ in `three-slicer/settings`; SLA machine profiles and the resin material catalog in the data files.
18
+ - Typed capability errors instead of silent approximation: `SLA_UNSUPPORTED_HOLLOWING` (hollowing/drain
19
+ holes), `SLA_UNSUPPORTED_ORGANIC` (organic trees), `SLA_PAD_AROUND_OBJECT_UNSUPPORTED` (zero-elevation
20
+ pad embedding). `SLA_CAPABILITIES` from `three-slicer/client` is the machine-readable map.
21
+ - Viewer: an SLA printer profile swaps the filament card for the resin card, previews the support/pad
22
+ meshes with the lifted layer frame, and exports `.sl1` archives — portrait PNG masks (the SL1 family's
23
+ panel mounting) plus upstream's `config.ini` field set.
24
+ - `<SettingsPanel/>` renders the SLA tabs for an SLA profile — no prop, it follows the settings map.
25
+ - `.3mf` SLA records (manual support points, drain holes) survive import/export round-trip.
26
+ - A runnable SLA example: `node node_modules/three-slicer/engine/examples/sla_headless.mjs`.
27
+ - `THIRD-PARTY-NOTICES.md` documenting the licenses compiled into the shipped kernels.
28
+
29
+ ### Notes
30
+
31
+ - The multithreaded kernel carries the SLA path too and is pinned byte-identical to the single-threaded
32
+ one over the same SLA slice.
33
+ - FFF output is unchanged: the golden byte-identical G-code check holds across this release.
34
+
35
+ Earlier releases (0.1.x) predate this changelog.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # three-slicer
2
2
 
3
- Browser/WASM 3D-printing slicer package derived from [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer). It can slice binary STL files to G-code in Node or the browser, and it also ships React UI pieces for building a browser slicer: a three.js viewport, GPU toolpath preview, and a schema-driven settings panel.
3
+ Browser/WASM 3D-printing slicer package derived from [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer). It can slice binary STL files to G-code in Node or the browser, and it also ships React UI pieces for building a browser slicer: a three.js viewport, GPU toolpath preview, and a schema-driven settings panel. SLA (resin) printers are a second technology in the same kernel — supports and pad from PrusaSlicer's ported chain, previewed in the same viewer, exported as an `.sl1` archive.
4
4
 
5
5
  The package is published as a single npm package, `three-slicer`, with subpath exports for the engine, viewer, components, worker, and extracted OrcaSlicer data.
6
6
 
@@ -23,6 +23,7 @@ The package is published as a single npm package, `three-slicer`, with subpath e
23
23
  - Support painting and material painting, plus per-tool filament and purge statistics.
24
24
  - 3MF **project** import in the viewer: a slicer-written `.3mf` (OrcaSlicer/BambuStudio save, MakerWorld download) restores its plate layout, project settings, and painted facets — not just the meshes.
25
25
  - Extracted data files for custom UIs: config schema, UI tree, toggle rules, invalidation map, and the printer/process/filament catalogs.
26
+ - SLA (resin) slicing: PrusaSlicer 2.9.6's ported support-point generator, support tree and pad, a resin material catalog, and `.sl1` mask export — routed by `printer_technology`, with typed capability errors for what the port does not cover.
26
27
  - Automatic multithreaded WASM selection on cross-origin-isolated browser pages.
27
28
 
28
29
  ## Installation
@@ -256,8 +257,9 @@ const result = slicer.slice(stlArrayBuffer, params)
256
257
  ## 3MF Projects
257
258
 
258
259
  A slicer-written `.3mf` is a project, not a mesh file: `Metadata/project_settings.config` holds the flattened
259
- preset its author sliced with. `<Viewport/>` reads all of it on import, and the two codecs behind that are exported
260
- for hosts doing it themselves.
260
+ preset its author sliced with. `<Viewport/>` reads all of it on import. The project parser/writer currently remain
261
+ viewer internals; there is no `three-slicer/viewer/project` public export yet. Hosts can normalize or serialize a
262
+ raw project settings object with the public settings helpers below, but should not import viewer source files.
261
263
 
262
264
  **Every value in that file is a string**, and reading it raw is not a cosmetic problem — a disabled option is the
263
265
  string `"0"`, and `"0"` is truthy, so a raw import turns every disabled option ON. A point is the string `"256x256"`
@@ -398,6 +400,53 @@ worker.postMessage({ cmd: 'erase', facet, hx, hy, hz, cx, cy, cz, radius })
398
400
 
399
401
  Because a single enum serves both jobs, a support blocker paint and an Extruder 2 paint are the same mark on a facet, and painted materials only take effect with support turned off (the painted multi-material path emits no support). Support painting is unaffected: a support-enabled slice stays on the single-material path.
400
402
 
403
+ ## SLA (Resin) Slicing
404
+
405
+ SLA is a second printer technology, not an FFF mode. `printer_technology` in the settings map routes it —
406
+ apply an SLA machine profile from the catalog (`printersByVendor` marks the technology) or set the key
407
+ yourself, then derive SLA params and call the SLA entry:
408
+
409
+ ```js
410
+ import { printerTechnology, deriveSlaParams, resinCatalog, resinSettingsFor } from 'three-slicer/settings'
411
+
412
+ printerTechnology(settings) // 'SLA' or 'FFF'
413
+ const params = deriveSlaParams(settings) // layers/exposure, display, supports, pad, raster orientation
414
+
415
+ const result = slicer.sliceSla(stl, params)
416
+ result.stats.sla // true — no G-code; stats carry resin_ml and time_estimate
417
+ result.stats.layers // includes the pad + elevation lift below the model
418
+ result.layers[0].paths // the same stride-8 segment stream the FFF preview reads
419
+ ```
420
+
421
+ In a worker, `client.sliceSla(stl, params, { onProgress, onLayer })` is the same call over the message
422
+ protocol, and `client.sliceSlaJob(job)` takes the typed job protocol — objects with manual support points,
423
+ drain-hole records and modifier volumes — validated structurally before slicing.
424
+
425
+ A runnable version ships with the package: `node node_modules/three-slicer/engine/examples/sla_headless.mjs`
426
+ slices a cube with supports and pad, then demonstrates the capability refusal below.
427
+
428
+ The support chain is PrusaSlicer 2.9.6's own, ported verbatim and compiled into the same WASM kernel: the
429
+ support-point generator, the default support tree, and the real pad geometry. Resin materials follow the
430
+ same catalog pattern as filaments: `resinCatalog` lists them, `resinSettingsFor(name)` is one preset.
431
+
432
+ **What the kernel cannot do it refuses with a typed code — it never approximates.** A request the port does
433
+ not cover fails with a stable error instead of silently producing something else:
434
+
435
+ | Code | Refused request |
436
+ | --- | --- |
437
+ | `SLA_UNSUPPORTED_HOLLOWING` | `hollowing_enable` or drain holes — a solid slice answered to a hollow request would be a mislabeled print |
438
+ | `SLA_UNSUPPORTED_ORGANIC` | organic support trees |
439
+ | `SLA_PAD_AROUND_OBJECT_UNSUPPORTED` | zero-elevation pad embedding (`pad_around_object`) |
440
+
441
+ `SLA_CAPABILITIES` (exported from `three-slicer/client`) is the machine-readable capability map. A `.3mf`
442
+ carrying SLA records survives round-trip regardless: manual support points and drain holes are preserved on
443
+ import and written back on export, even where slicing refuses to consume them.
444
+
445
+ The viewer follows the technology automatically: an SLA profile swaps the filament card for the resin card,
446
+ hides the FFF-only tools (prime tower, painting brushes), previews the generated support and pad meshes, and
447
+ exports an `.sl1` archive — per-layer PNG masks in the SL1 family's portrait orientation plus upstream's
448
+ `config.ini` — instead of G-code.
449
+
401
450
  ## Worker Usage
402
451
 
403
452
  Slicing blocks its thread for seconds, so in a browser it belongs in a worker. `three-slicer/client` wraps the
@@ -568,6 +617,7 @@ the layout by hand.
568
617
  | --- | --- |
569
618
  | `createSlicer()` | Loads the WASM kernel and returns a slicer handle |
570
619
  | `slicer.slice(stl, params, callbacks?)` | Slices binary STL input to G-code or streamed layers |
620
+ | `slicer.sliceSla(stl, params, onProgress?)` | SLA slice — layer masks, support/pad meshes, resin stats; typed `error` on a refused capability |
571
621
  | `slicer.paintPrepare(stl)` | Prepares painting against a model; returns the facet count |
572
622
  | `slicer.paint(args)` | Paints enforcer/blocker data (boolean pair; the state-addressed form is worker-only) |
573
623
  | `slicer.paintClear()` | Clears painting data for every state |
@@ -597,6 +647,9 @@ the layout by hand.
597
647
  | `machineLimitKeys` | The schema keys the kernel's machine limits are read from |
598
648
  | `processPresets()` | Lazy facade over the print (process) preset catalog |
599
649
  | `filamentPresets()` | Lazy facade over the material preset catalog |
650
+ | `printerTechnology(settings)` | `'SLA'` or `'FFF'` — what routes a slice to `slice_sla` |
651
+ | `deriveSlaParams(settings)` | Sparse settings → the SLA kernel's params (display, supports, pad, raster) |
652
+ | `resinCatalog` / `resinSettingsFor(name)` | The resin material catalog, and one preset's settings |
600
653
 
601
654
  ### `three-slicer/toggle`
602
655
 
@@ -648,6 +701,9 @@ the layout by hand.
648
701
  - Some vector settings are simplified to their first element.
649
702
  - Material painting and support cannot currently produce two materials on the same slice: the painted multi-material path emits no support, and one triangle selector serves both brushes.
650
703
  - The multi-material prime tower is a real ported wipe tower, but the fallback square ring used when it fails is not.
704
+ - SLA hollowing and drain-hole geometry are refused with `SLA_UNSUPPORTED_HOLLOWING` (the OpenVDB chain is not ported); the records still round-trip through `.3mf`.
705
+ - The `.sl1` export lives in the viewer (its masks need a canvas); there is no headless `.sl1` writer export yet.
706
+ - SLA mask edges are canvas anti-aliasing, not upstream's AGG rasterizer — same geometry, slightly different edge pixels.
651
707
  - Multithreaded WASM requires cross-origin isolation.
652
708
  - `three` is pinned as a peer dependency because viewer internals depend on the `TransformControls` API shape.
653
709
 
@@ -0,0 +1,37 @@
1
+ # Third-party notices
2
+
3
+ `three-slicer` is licensed AGPL-3.0-or-later (see `LICENSE.txt`). The distributed artifacts — the WASM
4
+ kernels embedded in `engine/src/slicer_core.js` / `slicer_core.mt.js` and the bundled viewer/components —
5
+ contain compiled or bundled code from the following projects. Full license texts ship in the source
6
+ repository at the paths given (the package's `repository` field points there); the kernel's vendored copies
7
+ live under `packages/wasm-core/third_party/deps_src/`.
8
+
9
+ ## Compiled into the WASM kernels
10
+
11
+ | Component | License | Source of the vendored copy |
12
+ | --- | --- | --- |
13
+ | OrcaSlicer (the FFF slicing code this package is ported from) | AGPL-3.0-or-later | github.com/SoftFever/OrcaSlicer |
14
+ | PrusaSlicer 2.9.6 (the SLA support/pad chain, ported verbatim under `packages/wasm-core/slasupport_port/`) | AGPL-3.0-or-later | github.com/prusa3d/PrusaSlicer |
15
+ | Clipper (Angus Johnson) | BSL-1.0 | `deps_src/clipper/` |
16
+ | Clipper2 | BSL-1.0 | `deps_src/clipper2/` |
17
+ | Boost (header-only use) | BSL-1.0 | headers at build time |
18
+ | Eigen (header-only) | MPL-2.0 | headers at build time |
19
+ | CGAL 6 (header-only use, GMP/MPFR disabled) | GPL-3.0-or-later | headers at build time |
20
+ | libigl (headers; both the Orca-generation copy and Prusa's bundled copy) | MPL-2.0 | `deps_src/libigl/`, `deps_src/libigl_prusa/` |
21
+ | libnest2d (headers) | LGPL-3.0 | `deps_src/libnest2d/` (its `LICENSE.txt`) |
22
+ | NLopt 2.5.0 | LGPL-2.1-or-later for the combined library (`deps_src/nlopt/COPYING`); per-algorithm terms in each `algs/*/COPYRIGHT` — the most restrictive is `algs/luksan` (LGPL-2.1+), most others (e.g. `algs/newuoa`) are MIT-style Powell/MIT notices | `deps_src/nlopt/` |
23
+ | SGI glu-libtess (the GLU tessellator) | SGI Free Software License B 2.0 | `deps_src/glu-libtess/` (header of each source file) |
24
+ | Emscripten runtime (the generated JS glue around the kernels) | MIT | emscripten.org |
25
+
26
+ ## Bundled into the viewer/components builds
27
+
28
+ | Component | License | Note |
29
+ | --- | --- | --- |
30
+ | fflate (via three.js examples) | MIT | zip/unzip in the 3mf and SL1 codecs |
31
+
32
+ `three`, `react` and `react-dom` are peer dependencies and are not bundled or redistributed by this package.
33
+
34
+ The LGPL components (NLopt's luksan directory, libnest2d headers) are compiled into a work distributed under
35
+ AGPL-3.0-or-later, which satisfies their license terms; their complete license texts accompany the source
36
+ repository as noted above. Nothing in this file overrides a component's own license — where this summary and
37
+ a vendored license file disagree, the vendored file is authoritative.
@@ -6,6 +6,8 @@ Reusable React components for the browser slicer. Props-driven — no global sta
6
6
 
7
7
  An OrcaSlicer-style settings form generated from `three-slicer/data` (schema + UI tree + toggle rules): tab/page/group navigation, mode filter (simple/advanced/expert), search across 923 options, dirty markers with per-option reset, and enable/disable rules evaluation.
8
8
 
9
+ The tab set follows the printer profile's technology: a settings map whose `printer_technology` says SLA swaps the FFF tabs for the SLA ones (print/material/printer builders carry the technology in their upstream builder name, which is what the filter reads). No prop needed — change the profile in the shared settings map and the panel follows.
10
+
9
11
  ```bash
10
12
  npm i three-slicer # react is a peer dependency — npm installs it for you
11
13
  ```
@@ -1,63 +1,63 @@
1
- import e, { useRef as _, useState as h, useLayoutEffect as D, useEffect as P, useMemo as A } from "react";
2
- import { createPortal as F } from "react-dom";
3
- import { uiTree as S, schema as C } from "three-slicer/data";
4
- import { settingRaw as J } from "three-slicer/settings";
5
- import { disabledKeys as I, makeCfg as B } from "three-slicer/toggle";
6
- function L({ css: t, className: s, children: r }) {
7
- const i = _(null), [c] = h(() => {
8
- const l = document.createElement("div");
9
- return l.style.display = "contents", l;
1
+ import e, { useRef as L, useState as w, useLayoutEffect as P, useEffect as J, useMemo as F } from "react";
2
+ import { createPortal as B } from "react-dom";
3
+ import { uiTree as _, schema as S } from "three-slicer/data";
4
+ import { printerTechnology as I, settingRaw as T } from "three-slicer/settings";
5
+ import { disabledKeys as $, makeCfg as U } from "three-slicer/toggle";
6
+ function W({ css: t, className: s, children: n }) {
7
+ const i = L(null), [c] = w(() => {
8
+ const r = document.createElement("div");
9
+ return r.style.display = "contents", r;
10
10
  });
11
- return D(() => {
12
- const l = i.current, o = l.shadowRoot || l.attachShadow({ mode: "open" });
11
+ return P(() => {
12
+ const r = i.current, o = r.shadowRoot || r.attachShadow({ mode: "open" });
13
13
  let p = o.querySelector("style[data-ts-css]");
14
14
  p || (p = document.createElement("style"), p.setAttribute("data-ts-css", ""), o.append(p)), p.textContent = t, o.contains(c) || o.append(c);
15
- }, [t, c]), /* @__PURE__ */ e.createElement("div", { ref: i, className: s }, F(r, c));
15
+ }, [t, c]), /* @__PURE__ */ e.createElement("div", { ref: i, className: s }, B(n, c));
16
16
  }
17
- const $ = ':host{display:contents}*{box-sizing:border-box}.row{display:grid;grid-template-columns:260px 220px 70px 1fr;gap:8px;align-items:center;padding:3px 0;border-bottom:1px dashed #f0f0f0}.row .lbl{color:#222;text-decoration:none}.row .lbl:hover{color:#0a7d4f;text-decoration:underline}.row .key{font-size:11px;color:#999;overflow:hidden;text-overflow:ellipsis}.row input[type=text],.row input:not([type]),.row select,.row textarea{width:100%;max-width:260px;padding:2px 6px}.inputwrap{display:inline-flex;align-items:center;gap:4px}.unit{color:#888;font-size:12px;white-space:nowrap}.pts{font-size:11px;color:#666}.badge{font-size:10px;padding:1px 6px;border-radius:8px;color:#fff;text-align:center}.mode-simple{background:#2e9e5b}.mode-advanced{background:#3673c3}.mode-expert{background:#d9822b}.mode-develop{background:#c0392b}.mode-none,.mode-undefined{background:#aaa}.muted{color:#999;font-weight:400;font-size:12px}.row.custom{grid-template-columns:1fr}.row.disabled{opacity:.42}.row.disabled input,.row.disabled select,.row.disabled textarea{cursor:not-allowed;background:#f2f2f2}.lbl-cell{display:inline-flex;align-items:center;gap:5px;min-width:0}.lbl-cell .lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dirty-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:#e8873a;box-shadow:0 0 0 2px #fff3e6}.widget-cell{display:inline-flex;align-items:center;gap:6px;min-width:0}.reset-btn{flex:0 0 auto;border:1px solid #d8b48a;background:#fbf1e4;color:#b5701f;border-radius:5px;cursor:pointer;font-size:13px;line-height:1;padding:1px 5px}.reset-btn:hover{background:#f3e2cb}.settings-panel{flex:0 0 400px;width:400px;border-left:1px solid #e2e2e2;display:flex;flex-direction:column;min-height:0;background:#fff;color:#222;font:14px/1.5 -apple-system,Segoe UI,sans-serif}.settings-panel .sp-top{padding:10px 12px 8px;border-bottom:1px solid #eee}.sp-builder{display:flex;align-items:center;gap:8px;font-size:12px;color:#666}.sp-builder select{flex:1;padding:4px 6px}.sp-pages{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.sp-pages button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:4px 8px;font-size:12px;cursor:pointer}.sp-pages button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.sp-modes{display:flex;gap:4px;margin-top:8px}.sp-modes button{border:0;background:none;font-size:11px;color:#888;padding:2px 6px;border-radius:6px;cursor:pointer}.sp-modes button.on{background:#eef2f0;color:#0a7d4f;font-weight:700}.settings-panel .sp-body{flex:1 1 auto;overflow-y:auto;padding:8px 14px 24px}.settings-panel .sp-body h2{font-size:16px}.settings-panel .row{grid-template-columns:1fr auto;grid-template-areas:"lbl badge" "key widget";row-gap:2px;padding:6px 0}.settings-panel .row .lbl{grid-area:lbl;font-weight:600}.settings-panel .row .badge{grid-area:badge;justify-self:end}.settings-panel .row .key{grid-area:key}.settings-panel .row .inputwrap,.settings-panel .row>select,.settings-panel .row>input,.settings-panel .row>textarea,.settings-panel .row>.pts,.settings-panel .row>.muted{grid-area:widget;justify-self:end}.settings-panel .row input:not([type=checkbox]):not([type=color]),.settings-panel .row select,.settings-panel .row textarea{max-width:200px}.settings-panel .row.custom{grid-template-columns:1fr;grid-template-areas:"lbl"}.sp-search{width:100%;box-sizing:border-box;padding:6px 9px;border:1px solid #d6d6d6;border-radius:6px;font-size:13px;margin-bottom:8px}:host(.sp-embedded) .settings-panel{position:static;width:auto;flex:none;border:0;height:auto}:host(.sp-embedded) .sp-body{padding:4px 2px 8px;max-height:460px}.bse{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:220px}.bse-modes{display:flex;gap:4px}.bse-modes button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer}.bse-modes button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.bse-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 8px}.bse-grid label{display:flex;align-items:center;gap:4px;font-size:11px;color:#666;white-space:nowrap}.bse-grid input{width:56px;padding:2px 4px;font-size:12px}.bse-json{width:100%;font:11px ui-monospace,monospace;padding:4px 6px;border:1px solid #d6d6d6;border-radius:6px;box-sizing:border-box}.bse-json.err{border-color:#d0435a;background:#fdf3f4}', j = (t, s, r, i) => [[r, i], [t + r, i], [t + r, s + i], [r, s + i]], T = (t) => {
18
- const s = t / 2, r = 32;
19
- return Array.from({ length: r }, (i, c) => {
20
- const l = 2 * Math.PI * c / r;
21
- return [+(s + s * Math.cos(l)).toFixed(2), +(s + s * Math.sin(l)).toFixed(2)];
17
+ const q = ':host{display:contents}*{box-sizing:border-box}.row{display:grid;grid-template-columns:260px 220px 70px 1fr;gap:8px;align-items:center;padding:3px 0;border-bottom:1px dashed #f0f0f0}.row .lbl{color:#222;text-decoration:none}.row .lbl:hover{color:#0a7d4f;text-decoration:underline}.row .key{font-size:11px;color:#999;overflow:hidden;text-overflow:ellipsis}.row input[type=text],.row input:not([type]),.row select,.row textarea{width:100%;max-width:260px;padding:2px 6px}.inputwrap{display:inline-flex;align-items:center;gap:4px}.unit{color:#888;font-size:12px;white-space:nowrap}.pts{font-size:11px;color:#666}.badge{font-size:10px;padding:1px 6px;border-radius:8px;color:#fff;text-align:center}.mode-simple{background:#2e9e5b}.mode-advanced{background:#3673c3}.mode-expert{background:#d9822b}.mode-develop{background:#c0392b}.mode-none,.mode-undefined{background:#aaa}.muted{color:#999;font-weight:400;font-size:12px}.row.custom{grid-template-columns:1fr}.row.disabled{opacity:.42}.row.disabled input,.row.disabled select,.row.disabled textarea{cursor:not-allowed;background:#f2f2f2}.lbl-cell{display:inline-flex;align-items:center;gap:5px;min-width:0}.lbl-cell .lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dirty-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:#e8873a;box-shadow:0 0 0 2px #fff3e6}.widget-cell{display:inline-flex;align-items:center;gap:6px;min-width:0}.reset-btn{flex:0 0 auto;border:1px solid #d8b48a;background:#fbf1e4;color:#b5701f;border-radius:5px;cursor:pointer;font-size:13px;line-height:1;padding:1px 5px}.reset-btn:hover{background:#f3e2cb}.settings-panel{flex:0 0 400px;width:400px;border-left:1px solid #e2e2e2;display:flex;flex-direction:column;min-height:0;background:#fff;color:#222;font:14px/1.5 -apple-system,Segoe UI,sans-serif}.settings-panel .sp-top{padding:10px 12px 8px;border-bottom:1px solid #eee}.sp-builder{display:flex;align-items:center;gap:8px;font-size:12px;color:#666}.sp-builder select{flex:1;padding:4px 6px}.sp-pages{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.sp-pages button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:4px 8px;font-size:12px;cursor:pointer}.sp-pages button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.sp-modes{display:flex;gap:4px;margin-top:8px}.sp-modes button{border:0;background:none;font-size:11px;color:#888;padding:2px 6px;border-radius:6px;cursor:pointer}.sp-modes button.on{background:#eef2f0;color:#0a7d4f;font-weight:700}.settings-panel .sp-body{flex:1 1 auto;overflow-y:auto;padding:8px 14px 24px}.settings-panel .sp-body h2{font-size:16px}.settings-panel .row{grid-template-columns:1fr auto;grid-template-areas:"lbl badge" "key widget";row-gap:2px;padding:6px 0}.settings-panel .row .lbl{grid-area:lbl;font-weight:600}.settings-panel .row .badge{grid-area:badge;justify-self:end}.settings-panel .row .key{grid-area:key}.settings-panel .row .inputwrap,.settings-panel .row>select,.settings-panel .row>input,.settings-panel .row>textarea,.settings-panel .row>.pts,.settings-panel .row>.muted{grid-area:widget;justify-self:end}.settings-panel .row input:not([type=checkbox]):not([type=color]),.settings-panel .row select,.settings-panel .row textarea{max-width:200px}.settings-panel .row.custom{grid-template-columns:1fr;grid-template-areas:"lbl"}.sp-search{width:100%;box-sizing:border-box;padding:6px 9px;border:1px solid #d6d6d6;border-radius:6px;font-size:13px;margin-bottom:8px}:host(.sp-embedded) .settings-panel{position:static;width:auto;flex:none;border:0;height:auto}:host(.sp-embedded) .sp-body{padding:4px 2px 8px;max-height:460px}.bse{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:220px}.bse-modes{display:flex;gap:4px}.bse-modes button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer}.bse-modes button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.bse-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 8px}.bse-grid label{display:flex;align-items:center;gap:4px;font-size:11px;color:#666;white-space:nowrap}.bse-grid input{width:56px;padding:2px 4px;font-size:12px}.bse-json{width:100%;font:11px ui-monospace,monospace;padding:4px 6px;border:1px solid #d6d6d6;border-radius:6px;box-sizing:border-box}.bse-json.err{border-color:#d0435a;background:#fdf3f4}', O = (t, s, n, i) => [[n, i], [t + n, i], [t + n, s + i], [n, s + i]], G = (t) => {
18
+ const s = t / 2, n = 32;
19
+ return Array.from({ length: n }, (i, c) => {
20
+ const r = 2 * Math.PI * c / n;
21
+ return [+(s + s * Math.cos(r)).toFixed(2), +(s + s * Math.sin(r)).toFixed(2)];
22
22
  });
23
- }, O = (t) => {
24
- const s = t.map((i) => i[0]), r = t.map((i) => i[1]);
25
- return { x0: Math.min(...s), y0: Math.min(...r), w: Math.max(...s) - Math.min(...s), d: Math.max(...r) - Math.min(...r) };
23
+ }, R = (t) => {
24
+ const s = t.map((i) => i[0]), n = t.map((i) => i[1]);
25
+ return { x0: Math.min(...s), y0: Math.min(...n), w: Math.max(...s) - Math.min(...s), d: Math.max(...n) - Math.min(...n) };
26
26
  };
27
- function U({ value: t, onChange: s, disabled: r }) {
28
- const i = Array.isArray(t) && t.length >= 3 ? t : j(200, 200, 0, 0), c = O(i), [l, o] = h(i.length === 4 ? "rect" : i.length >= 12 ? "circle" : "custom"), [p, x] = h({ w: c.w, d: c.d, ox: c.x0, oy: c.y0 }), [w, u] = h(Math.round(c.w)), [d, f] = h(JSON.stringify(i)), [E, N] = h(!1);
29
- P(() => {
30
- const a = O(i);
31
- x({ w: a.w, d: a.d, ox: a.x0, oy: a.y0 }), u(Math.round(a.w)), f(JSON.stringify(i)), N(!1);
27
+ function H({ value: t, onChange: s, disabled: n }) {
28
+ const i = Array.isArray(t) && t.length >= 3 ? t : O(200, 200, 0, 0), c = R(i), [r, o] = w(i.length === 4 ? "rect" : i.length >= 12 ? "circle" : "custom"), [p, b] = w({ w: c.w, d: c.d, ox: c.x0, oy: c.y0 }), [E, g] = w(Math.round(c.w)), [d, x] = w(JSON.stringify(i)), [y, k] = w(!1);
29
+ J(() => {
30
+ const a = R(i);
31
+ b({ w: a.w, d: a.d, ox: a.x0, oy: a.y0 }), g(Math.round(a.w)), x(JSON.stringify(i)), k(!1);
32
32
  }, [JSON.stringify(t)]);
33
- const y = (a) => {
34
- x(a);
35
- const { w: m, d: n, ox: b, oy: v } = a;
36
- m > 0 && n > 0 && s(j(m, n, b, v));
37
- }, k = (a) => {
38
- u(a), a > 0 && s(T(a));
33
+ const v = (a) => {
34
+ b(a);
35
+ const { w: u, d: m, ox: N, oy: z } = a;
36
+ u > 0 && m > 0 && s(O(u, m, N, z));
39
37
  }, M = (a) => {
40
- f(a);
38
+ g(a), a > 0 && s(G(a));
39
+ }, A = (a) => {
40
+ x(a);
41
41
  try {
42
- const m = JSON.parse(a);
43
- if (!Array.isArray(m) || m.length < 3 || !m.every((n) => Array.isArray(n) && n.length === 2 && n.every(Number.isFinite))) throw 0;
44
- N(!1), s(m);
42
+ const u = JSON.parse(a);
43
+ if (!Array.isArray(u) || u.length < 3 || !u.every((m) => Array.isArray(m) && m.length === 2 && m.every(Number.isFinite))) throw 0;
44
+ k(!1), s(u);
45
45
  } catch {
46
- N(!0);
46
+ k(!0);
47
47
  }
48
- }, g = (a) => {
49
- const m = parseFloat(a);
50
- return Number.isFinite(m) ? m : 0;
48
+ }, h = (a) => {
49
+ const u = parseFloat(a);
50
+ return Number.isFinite(u) ? u : 0;
51
51
  };
52
- return /* @__PURE__ */ e.createElement("div", { className: "bse", "data-testid": "bed-shape-editor" }, /* @__PURE__ */ e.createElement("div", { className: "bse-modes" }, [["rect", "Rectangular"], ["circle", "Circular"], ["custom", "Custom"]].map(([a, m]) => /* @__PURE__ */ e.createElement("button", { key: a, type: "button", className: l === a ? "on" : "", disabled: r, onClick: () => o(a) }, m))), l === "rect" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Width ", /* @__PURE__ */ e.createElement(
52
+ return /* @__PURE__ */ e.createElement("div", { className: "bse", "data-testid": "bed-shape-editor" }, /* @__PURE__ */ e.createElement("div", { className: "bse-modes" }, [["rect", "Rectangular"], ["circle", "Circular"], ["custom", "Custom"]].map(([a, u]) => /* @__PURE__ */ e.createElement("button", { key: a, type: "button", className: r === a ? "on" : "", disabled: n, onClick: () => o(a) }, u))), r === "rect" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Width ", /* @__PURE__ */ e.createElement(
53
53
  "input",
54
54
  {
55
55
  type: "number",
56
56
  min: "1",
57
57
  value: p.w,
58
- disabled: r,
58
+ disabled: n,
59
59
  "data-testid": "bed-w",
60
- onChange: (a) => y({ ...p, w: g(a.target.value) })
60
+ onChange: (a) => v({ ...p, w: h(a.target.value) })
61
61
  }
62
62
  ), " mm"), /* @__PURE__ */ e.createElement("label", null, "Depth ", /* @__PURE__ */ e.createElement(
63
63
  "input",
@@ -65,84 +65,84 @@ function U({ value: t, onChange: s, disabled: r }) {
65
65
  type: "number",
66
66
  min: "1",
67
67
  value: p.d,
68
- disabled: r,
68
+ disabled: n,
69
69
  "data-testid": "bed-d",
70
- onChange: (a) => y({ ...p, d: g(a.target.value) })
70
+ onChange: (a) => v({ ...p, d: h(a.target.value) })
71
71
  }
72
72
  ), " mm"), /* @__PURE__ */ e.createElement("label", null, "Origin X ", /* @__PURE__ */ e.createElement(
73
73
  "input",
74
74
  {
75
75
  type: "number",
76
76
  value: p.ox,
77
- disabled: r,
78
- onChange: (a) => y({ ...p, ox: g(a.target.value) })
77
+ disabled: n,
78
+ onChange: (a) => v({ ...p, ox: h(a.target.value) })
79
79
  }
80
80
  )), /* @__PURE__ */ e.createElement("label", null, "Origin Y ", /* @__PURE__ */ e.createElement(
81
81
  "input",
82
82
  {
83
83
  type: "number",
84
84
  value: p.oy,
85
- disabled: r,
86
- onChange: (a) => y({ ...p, oy: g(a.target.value) })
85
+ disabled: n,
86
+ onChange: (a) => v({ ...p, oy: h(a.target.value) })
87
87
  }
88
- ))), l === "circle" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Diameter ", /* @__PURE__ */ e.createElement(
88
+ ))), r === "circle" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Diameter ", /* @__PURE__ */ e.createElement(
89
89
  "input",
90
90
  {
91
91
  type: "number",
92
92
  min: "1",
93
- value: w,
94
- disabled: r,
93
+ value: E,
94
+ disabled: n,
95
95
  "data-testid": "bed-dia",
96
- onChange: (a) => k(g(a.target.value))
96
+ onChange: (a) => M(h(a.target.value))
97
97
  }
98
- ), " mm")), l === "custom" && /* @__PURE__ */ e.createElement(
98
+ ), " mm")), r === "custom" && /* @__PURE__ */ e.createElement(
99
99
  "textarea",
100
100
  {
101
- className: "bse-json" + (E ? " err" : ""),
101
+ className: "bse-json" + (y ? " err" : ""),
102
102
  rows: 3,
103
103
  value: d,
104
- disabled: r,
104
+ disabled: n,
105
105
  "data-testid": "bed-json",
106
- onChange: (a) => M(a.target.value),
106
+ onChange: (a) => A(a.target.value),
107
107
  placeholder: "[[0,0],[200,0],[200,200],[0,200]]"
108
108
  }
109
109
  ));
110
110
  }
111
- const W = {
112
- printable_area: U
113
- }, R = Object.keys(S).filter((t) => S[t].length > 0), q = ["all", "simple", "advanced", "expert", "develop"], G = (t) => t.replace("::build", "").replace("Tab", "");
114
- function H(t, s) {
111
+ const Q = {
112
+ printable_area: H
113
+ }, D = Object.keys(_).filter((t) => _[t].length > 0), X = ["all", "simple", "advanced", "expert", "develop"], Y = (t) => t.replace("::build", "").replace("Tab", "").replace("_sla", " (SLA)"), V = (t) => /SLA|build_sla\b/.test(t) ? "SLA" : "FFF";
114
+ function Z(t, s) {
115
115
  var c;
116
116
  if (!t || !(s in t)) return !1;
117
- const r = (c = C[s]) == null ? void 0 : c.default, i = Array.isArray(r) ? r[0] : r;
117
+ const n = (c = S[s]) == null ? void 0 : c.default, i = Array.isArray(n) ? n[0] : n;
118
118
  return String(t[s]) !== String(i ?? "");
119
119
  }
120
- function Q(t, s) {
120
+ function K(t, s) {
121
121
  var i;
122
122
  if (s === "all" || t.startsWith("<")) return !0;
123
- const r = (i = C[t]) == null ? void 0 : i.mode;
124
- return s === "simple" ? r === "simple" : s === "advanced" ? r === "simple" || r === "advanced" : !0;
123
+ const n = (i = S[t]) == null ? void 0 : i.mode;
124
+ return s === "simple" ? n === "simple" : s === "advanced" ? n === "simple" || n === "advanced" : !0;
125
125
  }
126
- function X(t) {
126
+ function ee(t) {
127
127
  return t ? t.gui_type === "color" ? "color" : t.enum_values ? "select" : t.type === "coBool" || t.type === "coBools" ? "checkbox" : t.multiline || t.is_code ? "textarea" : t.type === "coPoints" || t.type === "coPoint" ? "points" : "input" : "unknown";
128
128
  }
129
- function Y({ mode: t }) {
129
+ function te({ mode: t }) {
130
130
  return /* @__PURE__ */ e.createElement("span", { className: `badge mode-${t ?? "none"}` }, t ?? "—");
131
131
  }
132
- function V({ def: t, optKey: s, settings: r, setSettings: i, disabled: c, customWidgets: l }) {
133
- const o = J(r, s), p = (d) => i((f) => ({ ...f, [s]: d })), x = (l == null ? void 0 : l[s]) ?? W[s];
134
- if (x) return /* @__PURE__ */ e.createElement(x, { def: t, value: o, onChange: p, disabled: c });
135
- const w = X(t), u = Array.isArray(o) ? o[0] : o;
136
- switch (w) {
132
+ function ae({ def: t, optKey: s, settings: n, setSettings: i, disabled: c, customWidgets: r }) {
133
+ const o = T(n, s), p = (d) => i((x) => ({ ...x, [s]: d })), b = (r == null ? void 0 : r[s]) ?? Q[s];
134
+ if (b) return /* @__PURE__ */ e.createElement(b, { def: t, value: o, onChange: p, disabled: c });
135
+ const E = ee(t), g = Array.isArray(o) ? o[0] : o;
136
+ switch (E) {
137
137
  case "select":
138
- return /* @__PURE__ */ e.createElement("select", { value: u ?? "", onChange: (d) => p(d.target.value), disabled: c }, t.enum_values.map((d, f) => {
139
- var E;
140
- return /* @__PURE__ */ e.createElement("option", { key: d, value: d }, ((E = t.enum_labels) == null ? void 0 : E[f]) ?? d);
138
+ return /* @__PURE__ */ e.createElement("select", { value: g ?? "", onChange: (d) => p(d.target.value), disabled: c }, t.enum_values.map((d, x) => {
139
+ var y;
140
+ return /* @__PURE__ */ e.createElement("option", { key: d, value: d }, ((y = t.enum_labels) == null ? void 0 : y[x]) ?? d);
141
141
  }));
142
142
  case "checkbox":
143
143
  return /* @__PURE__ */ e.createElement("input", { type: "checkbox", checked: Array.isArray(o) ? !!o[0] : !!o, onChange: (d) => p(d.target.checked), disabled: c });
144
144
  case "color":
145
- return /* @__PURE__ */ e.createElement("input", { type: "color", value: typeof u == "string" && u.startsWith("#") ? u : "#00ae42", onChange: (d) => p(d.target.value), disabled: c });
145
+ return /* @__PURE__ */ e.createElement("input", { type: "color", value: typeof g == "string" && g.startsWith("#") ? g : "#00ae42", onChange: (d) => p(d.target.value), disabled: c });
146
146
  case "textarea":
147
147
  return /* @__PURE__ */ e.createElement("textarea", { rows: 2, value: Array.isArray(o) ? o.join(`
148
148
  `) : o ?? "", onChange: (d) => p(d.target.value), disabled: c });
@@ -151,29 +151,29 @@ function V({ def: t, optKey: s, settings: r, setSettings: i, disabled: c, custom
151
151
  case "unknown":
152
152
  return /* @__PURE__ */ e.createElement("span", { className: "muted" }, "No schema");
153
153
  default:
154
- return /* @__PURE__ */ e.createElement("span", { className: "inputwrap" }, /* @__PURE__ */ e.createElement("input", { value: u ?? "", onChange: (d) => p(d.target.value), disabled: c }), t.sidetext && /* @__PURE__ */ e.createElement("span", { className: "unit" }, t.sidetext));
154
+ return /* @__PURE__ */ e.createElement("span", { className: "inputwrap" }, /* @__PURE__ */ e.createElement("input", { value: g ?? "", onChange: (d) => p(d.target.value), disabled: c }), t.sidetext && /* @__PURE__ */ e.createElement("span", { className: "unit" }, t.sidetext));
155
155
  }
156
156
  }
157
- function Z({ optKey: t, settings: s, setSettings: r, disabled: i, onOptionOpen: c, customWidgets: l }) {
157
+ function ne({ optKey: t, settings: s, setSettings: n, disabled: i, onOptionOpen: c, customWidgets: r }) {
158
158
  if (t.startsWith("<")) return /* @__PURE__ */ e.createElement("div", { className: "row custom" }, /* @__PURE__ */ e.createElement("span", { className: "muted" }, "⚙ Custom widget ", t));
159
- const o = C[t], p = i ? i[t] : void 0, x = p != null, w = H(s, t), u = () => r((f) => {
160
- const E = { ...f };
161
- return delete E[t], E;
159
+ const o = S[t], p = i ? i[t] : void 0, b = p != null, E = Z(s, t), g = () => n((x) => {
160
+ const y = { ...x };
161
+ return delete y[t], y;
162
162
  }), d = (o == null ? void 0 : o.label) || (o == null ? void 0 : o.full_label) || t;
163
- return /* @__PURE__ */ e.createElement("div", { className: "row" + (x ? " disabled" : ""), title: x ? `Disabled when: ${p}` : (o == null ? void 0 : o.tooltip) ?? "", "data-testid": `row-${t}` }, /* @__PURE__ */ e.createElement("span", { className: "lbl-cell" }, w && /* @__PURE__ */ e.createElement("span", { className: "dirty-dot", title: "Changed from default", "data-testid": `dirty-${t}` }), c ? /* @__PURE__ */ e.createElement("button", { type: "button", className: "lbl lbl-link", onClick: () => c(t), title: "Details" }, d) : /* @__PURE__ */ e.createElement("span", { className: "lbl" }, d)), /* @__PURE__ */ e.createElement("code", { className: "key" }, t), /* @__PURE__ */ e.createElement(Y, { mode: o == null ? void 0 : o.mode }), /* @__PURE__ */ e.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ e.createElement(V, { def: o, optKey: t, settings: s, setSettings: r, disabled: x, customWidgets: l }), w && /* @__PURE__ */ e.createElement("button", { className: "reset-btn", onClick: u, title: "Reset to default", "data-testid": `reset-${t}` }, "↺")));
163
+ return /* @__PURE__ */ e.createElement("div", { className: "row" + (b ? " disabled" : ""), title: b ? `Disabled when: ${p}` : (o == null ? void 0 : o.tooltip) ?? "", "data-testid": `row-${t}` }, /* @__PURE__ */ e.createElement("span", { className: "lbl-cell" }, E && /* @__PURE__ */ e.createElement("span", { className: "dirty-dot", title: "Changed from default", "data-testid": `dirty-${t}` }), c ? /* @__PURE__ */ e.createElement("button", { type: "button", className: "lbl lbl-link", onClick: () => c(t), title: "Details" }, d) : /* @__PURE__ */ e.createElement("span", { className: "lbl" }, d)), /* @__PURE__ */ e.createElement("code", { className: "key" }, t), /* @__PURE__ */ e.createElement(te, { mode: o == null ? void 0 : o.mode }), /* @__PURE__ */ e.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ e.createElement(ae, { def: o, optKey: t, settings: s, setSettings: n, disabled: b, customWidgets: r }), E && /* @__PURE__ */ e.createElement("button", { className: "reset-btn", onClick: g, title: "Reset to default", "data-testid": `reset-${t}` }, "↺")));
164
164
  }
165
- function re({ settings: t, setSettings: s, onOptionOpen: r, embedded: i = !1, customWidgets: c, only: l }) {
166
- const [o, p] = h((l == null ? void 0 : l.builder) ?? R[0] ?? ""), [x, w] = h(0), [u, d] = h("all"), [f, E] = h(""), N = S[(l == null ? void 0 : l.builder) ?? o] ?? [], y = l != null && l.page ? N.filter((n) => n.page === l.page) : N, k = y[Math.min(x, y.length - 1)], M = A(() => I(B(t)), [t]), g = f.trim().toLowerCase(), a = A(() => g ? Object.entries(C).filter(([n, b]) => {
167
- var v, z;
168
- return n.includes(g) || ((v = b.label) == null ? void 0 : v.toLowerCase().includes(g)) || ((z = b.full_label) == null ? void 0 : z.toLowerCase().includes(g));
169
- }).slice(0, 60).map(([n]) => n) : [], [g]), m = (n, b) => /* @__PURE__ */ e.createElement(Z, { key: n + b, optKey: n, settings: t, setSettings: s, disabled: M, onOptionOpen: r, customWidgets: c });
170
- return /* @__PURE__ */ e.createElement(L, { css: $, className: i ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, !l && /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: `Search options (${Object.keys(C).length})…`, value: f, onChange: (n) => E(n.target.value), "data-testid": "opt-search" }), !g && /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement("label", { className: "sp-builder" }, /* @__PURE__ */ e.createElement("span", null, "Settings group"), /* @__PURE__ */ e.createElement("select", { value: o, onChange: (n) => {
171
- p(n.target.value), w(0);
172
- } }, R.map((n) => /* @__PURE__ */ e.createElement("option", { key: n, value: n }, G(n))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, y.map((n, b) => /* @__PURE__ */ e.createElement("button", { key: n.page + b, className: b === Math.min(x, y.length - 1) ? "on" : "", onClick: () => w(b) }, n.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, q.map((n) => /* @__PURE__ */ e.createElement("button", { key: n, className: u === n ? "on" : "", onClick: () => d(n) }, n))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, g ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "Search: “", f, "” — ", a.length, " hit", a.length === 1 ? "" : "s", a.length === 60 ? "+" : ""), a.map(m), !a.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No matches")) : k ? /* @__PURE__ */ e.createElement(e.Fragment, null, !l && /* @__PURE__ */ e.createElement("h2", null, k.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", k.line)), k.groups.map((n) => {
173
- const b = n.options.filter((v) => Q(v, u) && !(l && v.startsWith("<")));
174
- return b.length ? /* @__PURE__ */ e.createElement("section", { key: n.group + n.line }, /* @__PURE__ */ e.createElement("h3", null, n.group || "(untitled group)", " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, ":", n.line)), b.map(m)) : null;
165
+ function ce({ settings: t, setSettings: s, onOptionOpen: n, embedded: i = !1, customWidgets: c, only: r }) {
166
+ const [o, p] = w((r == null ? void 0 : r.builder) ?? D[0] ?? ""), [b, E] = w(0), [g, d] = w("all"), [x, y] = w(""), k = I(t), v = D.filter((l) => V(l) === k), M = (r == null ? void 0 : r.builder) ?? (v.includes(o) ? o : v[0] ?? ""), A = _[(r == null ? void 0 : r.builder) ?? M] ?? [], h = r != null && r.page ? A.filter((l) => l.page === r.page) : A, a = h[Math.min(b, h.length - 1)], u = F(() => $(U(t)), [t]), m = x.trim().toLowerCase(), N = F(() => m ? Object.entries(S).filter(([l, f]) => {
167
+ var C, j;
168
+ return l.includes(m) || ((C = f.label) == null ? void 0 : C.toLowerCase().includes(m)) || ((j = f.full_label) == null ? void 0 : j.toLowerCase().includes(m));
169
+ }).slice(0, 60).map(([l]) => l) : [], [m]), z = (l, f) => /* @__PURE__ */ e.createElement(ne, { key: l + f, optKey: l, settings: t, setSettings: s, disabled: u, onOptionOpen: n, customWidgets: c });
170
+ return /* @__PURE__ */ e.createElement(W, { css: q, className: i ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, !r && /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: `Search options (${Object.keys(S).length})…`, value: x, onChange: (l) => y(l.target.value), "data-testid": "opt-search" }), !m && /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement("label", { className: "sp-builder" }, /* @__PURE__ */ e.createElement("span", null, "Settings group"), /* @__PURE__ */ e.createElement("select", { value: M, onChange: (l) => {
171
+ p(l.target.value), E(0);
172
+ } }, v.map((l) => /* @__PURE__ */ e.createElement("option", { key: l, value: l }, Y(l))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, h.map((l, f) => /* @__PURE__ */ e.createElement("button", { key: l.page + f, className: f === Math.min(b, h.length - 1) ? "on" : "", onClick: () => E(f) }, l.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, X.map((l) => /* @__PURE__ */ e.createElement("button", { key: l, className: g === l ? "on" : "", onClick: () => d(l) }, l))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, m ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "Search: “", x, "” — ", N.length, " hit", N.length === 1 ? "" : "s", N.length === 60 ? "+" : ""), N.map(z), !N.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No matches")) : a ? /* @__PURE__ */ e.createElement(e.Fragment, null, !r && /* @__PURE__ */ e.createElement("h2", null, a.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", a.line)), a.groups.map((l) => {
173
+ const f = l.options.filter((C) => K(C, g) && !(r && C.startsWith("<")));
174
+ return f.length ? /* @__PURE__ */ e.createElement("section", { key: l.group + l.line }, /* @__PURE__ */ e.createElement("h3", null, l.group || "(untitled group)", " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, ":", l.line)), f.map(z)) : null;
175
175
  })) : /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No builder"))));
176
176
  }
177
177
  export {
178
- re as default
178
+ ce as default
179
179
  };