@camstack/addon-osd-manager 0.1.156 → 0.1.160

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.
Files changed (25) hide show
  1. package/dist/{MaskShapeCanvas-BEVxAjNq.mjs → MaskShapeCanvas-BT714ljC.mjs} +113 -113
  2. package/dist/MotionZonesSettings-gJgHYJJc.mjs +310 -0
  3. package/dist/{PrivacyMaskSettings-Ci65qYWB.mjs → PrivacyMaskSettings-HmUc5RFn.mjs} +35 -60
  4. package/dist/{SceneMonitorEditor-BG9mHyrC.mjs → SceneMonitorEditor-Dqc4eAZT.mjs} +307 -307
  5. package/dist/_stub.js +12141 -4504
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-BDKvyj3r.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-BX8u4O5k.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BzuGVFNp.mjs +26 -0
  8. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-CHxDN0gE.mjs +26 -0
  9. package/dist/{_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react__loadShare__.js-Bs1t18EM.mjs → _virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react__loadShare__.js-C5jUMd7X.mjs} +1 -1
  10. package/dist/{_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react_mf_2_dom__loadShare__.js-C0K4h_0e.mjs → _virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react_mf_2_dom__loadShare__.js-qB5uHZAd.mjs} +8 -7
  11. package/dist/addon-osd-manager.css +1 -1
  12. package/dist/{hostInit-DdhUwSkK.mjs → hostInit-o9ggbWOT.mjs} +2 -2
  13. package/dist/index.js +770 -50
  14. package/dist/index.mjs +750 -51
  15. package/dist/{player-overlays-C_Gmuzev.mjs → player-overlays-BgDqoYs3.mjs} +14 -14
  16. package/dist/remoteEntry.js +1 -1
  17. package/dist/{responsive-C-9Ko8kt.mjs → responsive-B7zCyVCX.mjs} +31 -27
  18. package/dist/{scene-monitor-copy-DudW1LZg.mjs → scene-monitor-copy-DWIcj1X6.mjs} +13 -13
  19. package/dist/{square-CpweDmax.mjs → square-D0YQUHMC.mjs} +29 -4
  20. package/dist/{trash-2-cb_YtxOy.mjs → trash-2-C_1H5SIl.mjs} +1 -1
  21. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-Dly_n9Qj.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-BAuPDG6y.mjs} +1 -1
  22. package/package.json +1 -1
  23. package/dist/MotionZonesSettings-DaDfau7A.mjs +0 -310
  24. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BTkfkwrM.mjs +0 -26
  25. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-DmzTg7Gl.mjs +0 -25
package/dist/index.js CHANGED
@@ -15445,6 +15445,51 @@ var SourceInfoSchema = object({
15445
15445
  raw: record(string(), unknown()).optional()
15446
15446
  });
15447
15447
  /**
15448
+ * Coerce common unit-of-measurement string variants to a canonical form
15449
+ * so the UI's display templates can rely on stable strings. Pure
15450
+ * passthrough for unknown units — kept conservative on purpose, the goal
15451
+ * is to normalise the long tail of HA `unit_of_measurement` values that
15452
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
15453
+ *
15454
+ * Extend the map as we encounter real-world data. Returning the raw
15455
+ * input for unknown values is intentional: providers should never
15456
+ * lose information silently here.
15457
+ */
15458
+ function normalizeUnit(raw) {
15459
+ if (raw == null) return void 0;
15460
+ const trimmed = raw.trim();
15461
+ if (trimmed === "") return void 0;
15462
+ const lower = trimmed.toLowerCase();
15463
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
15464
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
15465
+ if (lower === "k" || lower === "kelvin") return "K";
15466
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
15467
+ if (lower === "lx" || lower === "lux") return "lx";
15468
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
15469
+ if (lower === "kpa") return "kPa";
15470
+ if (lower === "pa") return "Pa";
15471
+ if (lower === "mbar" || lower === "millibar") return "mbar";
15472
+ if (lower === "w" || lower === "watt") return "W";
15473
+ if (lower === "kw" || lower === "kilowatt") return "kW";
15474
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
15475
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
15476
+ if (lower === "v" || lower === "volt") return "V";
15477
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
15478
+ if (lower === "ma" || lower === "milliampere") return "mA";
15479
+ if (lower === "va") return "VA";
15480
+ if (lower === "ppm") return "ppm";
15481
+ if (lower === "ppb") return "ppb";
15482
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
15483
+ if (lower === "km/h" || lower === "kph") return "km/h";
15484
+ if (lower === "mph") return "mph";
15485
+ if (lower === "m/s") return "m/s";
15486
+ if (lower === "m") return "m";
15487
+ if (lower === "km") return "km";
15488
+ if (lower === "kg") return "kg";
15489
+ if (lower === "g") return "g";
15490
+ return trimmed;
15491
+ }
15492
+ /**
15448
15493
  * device-discovery — device-scoped capability for parents that host /
15449
15494
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
15450
15495
  * integrations). The parent advertises a list of discoverable children,
@@ -39296,28 +39341,50 @@ setOverlay: method(object({
39296
39341
  *
39297
39342
  * This cap owns the other half: a per-(camera, slot) BINDING that says
39298
39343
  * which value feeds the slot, how it is formatted, and under which
39299
- * conditions it is shown at all. One addon renders every binding on every
39300
- * camera, so a new source costs zero driver code.
39301
- *
39302
- * Three deliberate choices, each with a rejected alternative:
39303
- *
39304
- * 1. A source is `(capName, valuePath)` over the kernel's device
39305
- * runtime-state mirror — NOT a closed enum of source kinds. Every
39306
- * cap-keyed slice a device publishes is bindable the day the cap
39307
- * ships. The rejected alternative (one enum member per source, with
39308
- * a resolver branch each) is what makes "add the humidity too" a
39309
- * code change.
39310
- * 2. The display gate reuses `NcConditionsSchema` verbatim — the
39311
- * notification centre's condition vocabulary — rather than a parallel
39312
- * model. An operator who has learned one condition editor has learned
39313
- * both.
39314
- * 3. Because the renderer's facts are device STATE and not a detection
39315
- * record, only a SUBSET of that vocabulary can be answered here.
39316
- * `setSlotBinding` REJECTS the rest at write time (see
39317
- * `getConditionSupport`). It does not accept-then-fail-closed: a
39318
- * condition that can never be true renders a permanently blank
39319
- * overlay, and a blank overlay looks exactly like a broken camera.
39344
+ * conditions it is shown at all. Named TEMPLATES combine many device
39345
+ * fields into one overlay string (Scrypted OSD Manager parity).
39346
+ */
39347
+ /** What `{alias}` expands to for one template binding. Exactly one choice. */
39348
+ var OsdTemplateFormatSchema = _enum([
39349
+ "value+unit",
39350
+ "value",
39351
+ "unit"
39352
+ ]);
39353
+ /**
39354
+ * How the operator reached the leaf device. `deviceId` is ALWAYS the
39355
+ * OSD-supported leaf that is read; `containerId` is set when the pick
39356
+ * went through a container (gateway) first.
39357
+ */
39358
+ var OsdTemplatePickSchema = object({
39359
+ containerId: number().int().optional(),
39360
+ deviceId: number().int()
39361
+ });
39362
+ /**
39363
+ * One named binding inside a multi-device template. The parser only
39364
+ * sees `{alias}`; format / unit / decimals live here structurally.
39365
+ */
39366
+ var OsdTemplateBindingSchema = object({
39367
+ /** Token in the parser, e.g. `t` → `{t}`. Unique within the template. */
39368
+ alias: string().min(1).max(32).regex(/^[A-Za-z][A-Za-z0-9_-]*$/, "alias must start with a letter"),
39369
+ pick: OsdTemplatePickSchema,
39370
+ capName: string().min(1).max(64),
39371
+ valuePath: string().min(1).max(64),
39372
+ format: OsdTemplateFormatSchema.default("value+unit"),
39373
+ /** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
39374
+ displayUnit: string().max(16).optional(),
39375
+ maxDecimals: number().int().min(0).max(4).default(1)
39376
+ });
39377
+ /**
39378
+ * A named template: bindings (alias → leaf field) + a parser that only
39379
+ * references `{alias}` tokens.
39320
39380
  */
39381
+ var OsdNamedTemplateSchema = object({
39382
+ id: string().min(1).max(64),
39383
+ name: string().min(1).max(64),
39384
+ bindings: array(OsdTemplateBindingSchema).max(32).default([]),
39385
+ /** Parser with alias placeholders, e.g. `{t} / {h}`. */
39386
+ parser: string().max(256).default("")
39387
+ });
39321
39388
  /** Where a slot's value comes from. */
39322
39389
  var OsdSourceSchema = discriminatedUnion("kind", [
39323
39390
  object({
@@ -39336,11 +39403,17 @@ var OsdSourceSchema = discriminatedUnion("kind", [
39336
39403
  deviceId: number().int().optional(),
39337
39404
  capName: string().min(1).max(64),
39338
39405
  /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
39339
- valuePath: string().min(1).max(64)
39406
+ valuePath: string().min(1).max(64),
39407
+ /** Convert TO this unit at render. Absent = no conversion. */
39408
+ displayUnit: string().max(16).optional()
39340
39409
  }),
39341
39410
  object({
39342
39411
  kind: literal("latest-recognition"),
39343
39412
  recognition: _enum(["person", "plate"])
39413
+ }),
39414
+ object({
39415
+ kind: literal("template"),
39416
+ templateId: string().min(1).max(64)
39344
39417
  })
39345
39418
  ]);
39346
39419
  var OsdSlotBindingSchema = object({
@@ -39356,7 +39429,10 @@ var OsdSlotBindingSchema = object({
39356
39429
  * documented workaround for firmwares that reject `.` in overlay text.
39357
39430
  */
39358
39431
  maxDecimals: number().int().min(0).max(4).default(1),
39359
- /** Appended via `${unit}`. The state mirror does not carry units. */
39432
+ /**
39433
+ * Appended via `${unit}` when no unit conversion produces a label.
39434
+ * Prefer `source.displayUnit` for converted device-state values.
39435
+ */
39360
39436
  unitLabel: string().max(8).optional(),
39361
39437
  /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
39362
39438
  valueMap: record(string(), string()).optional(),
@@ -39510,6 +39586,21 @@ var osdManagerCapability = {
39510
39586
  renderDevice: method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
39511
39587
  kind: "mutation",
39512
39588
  auth: "admin"
39589
+ }),
39590
+ /** Every named multi-device template. */
39591
+ listTemplates: method(object({}), object({ templates: array(OsdNamedTemplateSchema) }), { auth: "admin" }),
39592
+ /**
39593
+ * Create or replace a named template by `id`. Returns the stored record
39594
+ * after Zod normalisation.
39595
+ */
39596
+ upsertTemplate: method(object({ template: OsdNamedTemplateSchema }), object({ template: OsdNamedTemplateSchema }), {
39597
+ kind: "mutation",
39598
+ auth: "admin"
39599
+ }),
39600
+ /** Drop a named template. Bindings that still reference it render unreadable. */
39601
+ deleteTemplate: method(object({ templateId: string().min(1).max(64) }), object({ success: literal(true) }), {
39602
+ kind: "mutation",
39603
+ auth: "admin"
39513
39604
  })
39514
39605
  }
39515
39606
  };
@@ -45260,6 +45351,265 @@ var zoneRulesCapability = {
45260
45351
  */
45261
45352
  durability: "restored"
45262
45353
  };
45354
+ /**
45355
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
45356
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
45357
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
45358
+ * decimals). K derives from `°C = K − 273.15`.
45359
+ */
45360
+ var UNIT_TABLE = {
45361
+ "°C": {
45362
+ dimension: "temperature",
45363
+ factor: 1,
45364
+ offset: 0
45365
+ },
45366
+ "°F": {
45367
+ dimension: "temperature",
45368
+ factor: 5 / 9,
45369
+ offset: -160 / 9
45370
+ },
45371
+ K: {
45372
+ dimension: "temperature",
45373
+ factor: 1,
45374
+ offset: -273.15
45375
+ },
45376
+ hPa: {
45377
+ dimension: "pressure",
45378
+ factor: 1,
45379
+ offset: 0
45380
+ },
45381
+ kPa: {
45382
+ dimension: "pressure",
45383
+ factor: 10,
45384
+ offset: 0
45385
+ },
45386
+ Pa: {
45387
+ dimension: "pressure",
45388
+ factor: .01,
45389
+ offset: 0
45390
+ },
45391
+ mbar: {
45392
+ dimension: "pressure",
45393
+ factor: 1,
45394
+ offset: 0
45395
+ },
45396
+ bar: {
45397
+ dimension: "pressure",
45398
+ factor: 1e3,
45399
+ offset: 0
45400
+ },
45401
+ inHg: {
45402
+ dimension: "pressure",
45403
+ factor: 33.8639,
45404
+ offset: 0
45405
+ },
45406
+ mmHg: {
45407
+ dimension: "pressure",
45408
+ factor: 1.33322,
45409
+ offset: 0
45410
+ },
45411
+ psi: {
45412
+ dimension: "pressure",
45413
+ factor: 68.9476,
45414
+ offset: 0
45415
+ },
45416
+ "m/s": {
45417
+ dimension: "speed",
45418
+ factor: 1,
45419
+ offset: 0
45420
+ },
45421
+ "km/h": {
45422
+ dimension: "speed",
45423
+ factor: 1 / 3.6,
45424
+ offset: 0
45425
+ },
45426
+ mph: {
45427
+ dimension: "speed",
45428
+ factor: .44704,
45429
+ offset: 0
45430
+ },
45431
+ kn: {
45432
+ dimension: "speed",
45433
+ factor: .514444,
45434
+ offset: 0
45435
+ },
45436
+ "mm/h": {
45437
+ dimension: "precipitation-rate",
45438
+ factor: 1,
45439
+ offset: 0
45440
+ },
45441
+ "in/h": {
45442
+ dimension: "precipitation-rate",
45443
+ factor: 25.4,
45444
+ offset: 0
45445
+ },
45446
+ m: {
45447
+ dimension: "length",
45448
+ factor: 1,
45449
+ offset: 0
45450
+ },
45451
+ mm: {
45452
+ dimension: "length",
45453
+ factor: .001,
45454
+ offset: 0
45455
+ },
45456
+ cm: {
45457
+ dimension: "length",
45458
+ factor: .01,
45459
+ offset: 0
45460
+ },
45461
+ km: {
45462
+ dimension: "length",
45463
+ factor: 1e3,
45464
+ offset: 0
45465
+ },
45466
+ in: {
45467
+ dimension: "length",
45468
+ factor: .0254,
45469
+ offset: 0
45470
+ },
45471
+ ft: {
45472
+ dimension: "length",
45473
+ factor: .3048,
45474
+ offset: 0
45475
+ },
45476
+ mi: {
45477
+ dimension: "length",
45478
+ factor: 1609.344,
45479
+ offset: 0
45480
+ },
45481
+ lx: {
45482
+ dimension: "illuminance",
45483
+ factor: 1,
45484
+ offset: 0
45485
+ },
45486
+ "W/m²": {
45487
+ dimension: "irradiance",
45488
+ factor: 1,
45489
+ offset: 0
45490
+ },
45491
+ W: {
45492
+ dimension: "power",
45493
+ factor: 1,
45494
+ offset: 0
45495
+ },
45496
+ kW: {
45497
+ dimension: "power",
45498
+ factor: 1e3,
45499
+ offset: 0
45500
+ },
45501
+ VA: {
45502
+ dimension: "apparent-power",
45503
+ factor: 1,
45504
+ offset: 0
45505
+ },
45506
+ Wh: {
45507
+ dimension: "energy",
45508
+ factor: 1,
45509
+ offset: 0
45510
+ },
45511
+ kWh: {
45512
+ dimension: "energy",
45513
+ factor: 1e3,
45514
+ offset: 0
45515
+ },
45516
+ MWh: {
45517
+ dimension: "energy",
45518
+ factor: 1e6,
45519
+ offset: 0
45520
+ },
45521
+ V: {
45522
+ dimension: "voltage",
45523
+ factor: 1,
45524
+ offset: 0
45525
+ },
45526
+ mV: {
45527
+ dimension: "voltage",
45528
+ factor: .001,
45529
+ offset: 0
45530
+ },
45531
+ A: {
45532
+ dimension: "current",
45533
+ factor: 1,
45534
+ offset: 0
45535
+ },
45536
+ mA: {
45537
+ dimension: "current",
45538
+ factor: .001,
45539
+ offset: 0
45540
+ },
45541
+ ppm: {
45542
+ dimension: "concentration-volume",
45543
+ factor: 1,
45544
+ offset: 0
45545
+ },
45546
+ ppb: {
45547
+ dimension: "concentration-volume",
45548
+ factor: .001,
45549
+ offset: 0
45550
+ },
45551
+ "µg/m³": {
45552
+ dimension: "concentration-mass",
45553
+ factor: 1,
45554
+ offset: 0
45555
+ },
45556
+ "mg/m³": {
45557
+ dimension: "concentration-mass",
45558
+ factor: 1e3,
45559
+ offset: 0
45560
+ },
45561
+ kg: {
45562
+ dimension: "mass",
45563
+ factor: 1,
45564
+ offset: 0
45565
+ },
45566
+ g: {
45567
+ dimension: "mass",
45568
+ factor: .001,
45569
+ offset: 0
45570
+ },
45571
+ lb: {
45572
+ dimension: "mass",
45573
+ factor: .453592,
45574
+ offset: 0
45575
+ },
45576
+ oz: {
45577
+ dimension: "mass",
45578
+ factor: .0283495,
45579
+ offset: 0
45580
+ },
45581
+ "%": {
45582
+ dimension: "percentage",
45583
+ factor: 1,
45584
+ offset: 0
45585
+ }
45586
+ };
45587
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
45588
+ function specFor(unit) {
45589
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
45590
+ }
45591
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
45592
+ function toCanonical(value, spec) {
45593
+ return value * spec.factor + spec.offset;
45594
+ }
45595
+ /** Canonical-unit value → the unit described by `spec`. */
45596
+ function fromCanonical(canonical, spec) {
45597
+ return (canonical - spec.offset) / spec.factor;
45598
+ }
45599
+ /**
45600
+ * Graceful variant for the render path: returns the converted value, or
45601
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
45602
+ * Never throws — a render caller falls back to the raw value + source unit.
45603
+ */
45604
+ function tryConvertUnit(value, from, to) {
45605
+ if (from === to) return value;
45606
+ const fromSpec = specFor(from);
45607
+ if (!fromSpec) return void 0;
45608
+ const toSpec = specFor(to);
45609
+ if (!toSpec) return void 0;
45610
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
45611
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
45612
+ }
45263
45613
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
45264
45614
  /**
45265
45615
  * Most specific first. Extending this list is how a new device kind becomes
@@ -50628,6 +50978,12 @@ Object.freeze({
50628
50978
  addonId: null,
50629
50979
  access: "create"
50630
50980
  },
50981
+ "osdManager.deleteTemplate": {
50982
+ capName: "osd-manager",
50983
+ capScope: "system",
50984
+ addonId: null,
50985
+ access: "delete"
50986
+ },
50631
50987
  "osdManager.getConditionSupport": {
50632
50988
  capName: "osd-manager",
50633
50989
  capScope: "system",
@@ -50646,6 +51002,12 @@ Object.freeze({
50646
51002
  addonId: null,
50647
51003
  access: "view"
50648
51004
  },
51005
+ "osdManager.listTemplates": {
51006
+ capName: "osd-manager",
51007
+ capScope: "system",
51008
+ addonId: null,
51009
+ access: "view"
51010
+ },
50649
51011
  "osdManager.previewSlot": {
50650
51012
  capName: "osd-manager",
50651
51013
  capScope: "system",
@@ -50664,6 +51026,12 @@ Object.freeze({
50664
51026
  addonId: null,
50665
51027
  access: "create"
50666
51028
  },
51029
+ "osdManager.upsertTemplate": {
51030
+ capName: "osd-manager",
51031
+ capScope: "system",
51032
+ addonId: null,
51033
+ access: "create"
51034
+ },
50667
51035
  "petFeeder.callPet": {
50668
51036
  capName: "pet-feeder",
50669
51037
  capScope: "device",
@@ -57912,10 +58280,10 @@ function limitText(text, max) {
57912
58280
  return `${text.slice(0, max - 3)}...`;
57913
58281
  }
57914
58282
  /** Turn a raw state value into the string that goes into `${value}`. */
57915
- function displayValue(raw, binding) {
58283
+ function displayValue(raw, binding, maxDecimals = binding.maxDecimals) {
57916
58284
  const mapped = binding.valueMap?.[String(raw)];
57917
58285
  if (mapped !== void 0) return mapped;
57918
- if (typeof raw === "number") return truncateDecimals(raw, binding.maxDecimals);
58286
+ if (typeof raw === "number") return truncateDecimals(raw, maxDecimals);
57919
58287
  if (typeof raw === "boolean") return raw ? "ON" : "OFF";
57920
58288
  if (raw === null || raw === void 0) return "";
57921
58289
  if (typeof raw === "string") return raw;
@@ -57929,9 +58297,10 @@ function applyTemplate(template, value, unit) {
57929
58297
  return template.replaceAll("${value}", value).replaceAll("${unit}", unit);
57930
58298
  }
57931
58299
  /** The whole formatting chain: raw value → the text to burn into the slot. */
57932
- function formatSlotText(raw, binding) {
57933
- const value = displayValue(raw, binding);
57934
- return limitText(applyTemplate(binding.template, value, binding.unitLabel ?? "").trim(), binding.maxCharacters);
58300
+ function formatSlotText(raw, binding, options = {}) {
58301
+ const value = displayValue(options.displayRaw ?? raw, binding);
58302
+ const unit = options.unit ?? binding.unitLabel ?? "";
58303
+ return limitText(applyTemplate(binding.template, value, unit).trim(), binding.maxCharacters);
57935
58304
  }
57936
58305
  //#endregion
57937
58306
  //#region src/state-leaves.ts
@@ -58019,6 +58388,144 @@ function readStatePath(slice, path) {
58019
58388
  return current;
58020
58389
  }
58021
58390
  //#endregion
58391
+ //#region src/template-tokens.ts
58392
+ /** Build the placeholder token for one binding alias. */
58393
+ function aliasToken(alias) {
58394
+ return `{${alias}}`;
58395
+ }
58396
+ /** Chip / parser token for a binding. */
58397
+ function bindingToken(binding) {
58398
+ return aliasToken(binding.alias);
58399
+ }
58400
+ /** Parse `{alias}` tokens from a parser string (letters, digits, `_`, `-`). */
58401
+ function parseAliasTokens(parser) {
58402
+ const out = [];
58403
+ for (const match of parser.matchAll(/\{([A-Za-z][A-Za-z0-9_-]*)\}/g)) {
58404
+ const alias = match[1];
58405
+ if (alias === void 0 || alias === "") continue;
58406
+ if (!out.includes(alias)) out.push(alias);
58407
+ }
58408
+ return out;
58409
+ }
58410
+ /** Replace every occurrence of `token` in `parser` with `value`. */
58411
+ function replaceToken(parser, token, value) {
58412
+ return parser.replaceAll(token, value);
58413
+ }
58414
+ /**
58415
+ * Suggest a short unique alias from a device/field name.
58416
+ * Falls back to `b1`, `b2`, … when the slug collides.
58417
+ */
58418
+ function suggestAlias(seed, taken, fallbackPrefix = "b") {
58419
+ const slug = seed.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/^(\d)/, "a$1").slice(0, 24);
58420
+ const base = slug.length > 0 ? slug : fallbackPrefix;
58421
+ if (!taken.has(base)) return base;
58422
+ for (let i = 2; i < 1e3; i += 1) {
58423
+ const candidate = `${base}${i}`.slice(0, 32);
58424
+ if (!taken.has(candidate)) return candidate;
58425
+ }
58426
+ return `${fallbackPrefix}${Date.now().toString(36)}`;
58427
+ }
58428
+ //#endregion
58429
+ //#region src/unit-display.ts
58430
+ /**
58431
+ * Convert a numeric state value for OSD display using `@camstack/types`
58432
+ * UNIT_TABLE / tryConvertUnit (same set as D699).
58433
+ */
58434
+ /**
58435
+ * Convert `raw` from `sourceUnit` to `displayUnit` when both are known and
58436
+ * same-dimension. On refusal, keep the raw value and the source unit so the
58437
+ * label never lies about the number.
58438
+ */
58439
+ function convertForDisplay(raw, sourceUnit, displayUnit) {
58440
+ const from = sourceUnit !== void 0 ? normalizeUnit(sourceUnit) : void 0;
58441
+ const to = displayUnit !== void 0 ? normalizeUnit(displayUnit) : void 0;
58442
+ if (from === void 0 || to === void 0 || from === to) return {
58443
+ value: raw,
58444
+ unit: to ?? from ?? ""
58445
+ };
58446
+ const converted = tryConvertUnit(raw, from, to);
58447
+ if (converted === void 0) return {
58448
+ value: raw,
58449
+ unit: from
58450
+ };
58451
+ return {
58452
+ value: converted,
58453
+ unit: to
58454
+ };
58455
+ }
58456
+ /**
58457
+ * Best-effort unit from a state slice: sibling `unit` at the same depth as
58458
+ * `valuePath`, else top-level `unit`.
58459
+ */
58460
+ function inferSourceUnit(slice, valuePath) {
58461
+ if (typeof slice !== "object" || slice === null || Array.isArray(slice)) return void 0;
58462
+ const root = { ...slice };
58463
+ const segments = valuePath.split(".");
58464
+ if (segments.length >= 1) {
58465
+ let parent = root;
58466
+ for (const segment of segments.slice(0, -1)) {
58467
+ if (typeof parent !== "object" || parent === null || Array.isArray(parent)) {
58468
+ parent = void 0;
58469
+ break;
58470
+ }
58471
+ parent = { ...parent }[segment];
58472
+ }
58473
+ if (typeof parent === "object" && parent !== null && !Array.isArray(parent)) {
58474
+ const unit = { ...parent }.unit;
58475
+ if (typeof unit === "string" && unit.length > 0) return unit;
58476
+ }
58477
+ }
58478
+ const top = root.unit;
58479
+ return typeof top === "string" && top.length > 0 ? top : void 0;
58480
+ }
58481
+ //#endregion
58482
+ //#region src/named-template-render.ts
58483
+ /** Combine value/unit according to the binding's single format choice. */
58484
+ function applyTemplateFormat(value, unit, format) {
58485
+ if (format === "value") return value;
58486
+ if (format === "unit") return unit;
58487
+ const v = value.trim();
58488
+ const u = unit.trim();
58489
+ if (v === "") return u;
58490
+ if (u === "") return v;
58491
+ return `${v} ${u}`;
58492
+ }
58493
+ /** Resolve one binding to live value/unit/formatted parts (or a reason). */
58494
+ function resolveBindingLive(binding, facts) {
58495
+ const deviceId = binding.pick.deviceId;
58496
+ const deviceState = facts.stateByDevice.get(deviceId);
58497
+ if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
58498
+ const slice = deviceState[binding.capName];
58499
+ if (slice === void 0) return { reason: `device ${deviceId} publishes no "${binding.capName}" state` };
58500
+ const raw = readStatePath(slice, binding.valuePath);
58501
+ if (raw === void 0) return { reason: `"${binding.capName}.${binding.valuePath}" is absent on device ${deviceId}` };
58502
+ let value;
58503
+ let unit = "";
58504
+ if (typeof raw === "number") {
58505
+ const converted = convertForDisplay(raw, inferSourceUnit(slice, binding.valuePath), binding.displayUnit);
58506
+ value = truncateDecimals(converted.value, binding.maxDecimals);
58507
+ unit = converted.unit;
58508
+ } else if (typeof raw === "boolean") value = raw ? "ON" : "OFF";
58509
+ else if (raw === null) value = "";
58510
+ else value = String(raw);
58511
+ return {
58512
+ value,
58513
+ unit,
58514
+ formatted: applyTemplateFormat(value, unit, binding.format)
58515
+ };
58516
+ }
58517
+ /** Render one named template's parser from live states. */
58518
+ function renderNamedTemplate(template, facts) {
58519
+ let text = template.parser;
58520
+ if (template.bindings.length === 0) return text.trim() === "" ? { reason: `template "${template.id}" has no bindings` } : { text };
58521
+ for (const binding of template.bindings) {
58522
+ const live = resolveBindingLive(binding, facts);
58523
+ if ("reason" in live) return live;
58524
+ text = replaceToken(text, aliasToken(binding.alias), live.formatted);
58525
+ }
58526
+ return { text };
58527
+ }
58528
+ //#endregion
58022
58529
  //#region src/render.ts
58023
58530
  /**
58024
58531
  * Read the source's value. `null` means "unreadable" — a missing device, a
@@ -58033,6 +58540,16 @@ function resolveSource(binding, cameraDeviceId, facts) {
58033
58540
  const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
58034
58541
  return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
58035
58542
  }
58543
+ if (source.kind === "template") {
58544
+ const named = facts.templatesById.get(source.templateId);
58545
+ if (named === void 0) return { reason: `named template "${source.templateId}" is missing` };
58546
+ const rendered = renderNamedTemplate(named, { stateByDevice: facts.stateByDevice });
58547
+ if ("reason" in rendered) return rendered;
58548
+ return { value: {
58549
+ raw: rendered.text,
58550
+ preformatted: rendered.text
58551
+ } };
58552
+ }
58036
58553
  const deviceId = source.deviceId ?? cameraDeviceId;
58037
58554
  const deviceState = facts.stateByDevice.get(deviceId);
58038
58555
  if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
@@ -58040,6 +58557,14 @@ function resolveSource(binding, cameraDeviceId, facts) {
58040
58557
  if (slice === void 0) return { reason: `device ${deviceId} publishes no "${source.capName}" state` };
58041
58558
  const raw = readStatePath(slice, source.valuePath);
58042
58559
  if (raw === void 0) return { reason: `"${source.capName}.${source.valuePath}" is absent on device ${deviceId}` };
58560
+ if (typeof raw === "number") {
58561
+ const converted = convertForDisplay(raw, inferSourceUnit(slice, source.valuePath), source.displayUnit);
58562
+ return { value: {
58563
+ raw,
58564
+ displayRaw: converted.value,
58565
+ unit: converted.unit || binding.unitLabel
58566
+ } };
58567
+ }
58043
58568
  return { value: { raw } };
58044
58569
  }
58045
58570
  /**
@@ -58072,7 +58597,12 @@ function renderSlot(slotId, binding, cameraDeviceId, facts, previousText) {
58072
58597
  if (!gate.open) return settle(slotId, fallback, previousText, "gated", `condition "${gate.failedCondition}"`);
58073
58598
  const resolved = resolveSource(binding, cameraDeviceId, facts);
58074
58599
  if ("reason" in resolved) return settle(slotId, fallback, previousText, "unreadable", resolved.reason);
58075
- return settle(slotId, formatSlotText(resolved.value.raw, binding), previousText, "written");
58600
+ if (resolved.value.preformatted !== void 0 && binding.source.kind === "template") return settle(slotId, limitText(resolved.value.preformatted.trim(), binding.maxCharacters), previousText, "written");
58601
+ const unit = resolved.value.unit ?? binding.unitLabel;
58602
+ return settle(slotId, formatSlotText(resolved.value.raw, binding, {
58603
+ ...resolved.value.displayRaw !== void 0 ? { displayRaw: resolved.value.displayRaw } : {},
58604
+ ...unit !== void 0 ? { unit } : {}
58605
+ }), previousText, "written");
58076
58606
  }
58077
58607
  function settle(slotId, text, previousText, outcome, reason) {
58078
58608
  if (previousText !== void 0 && previousText === text) return {
@@ -58093,20 +58623,98 @@ function settle(slotId, text, previousText, outcome, reason) {
58093
58623
  };
58094
58624
  }
58095
58625
  //#endregion
58626
+ //#region src/osd-source-fields.ts
58627
+ /** Caps the OSD source picker may bind. */
58628
+ var OSD_SOURCE_CAP_NAMES = new Set([
58629
+ "temperature-sensor",
58630
+ "humidity-sensor",
58631
+ "climate-control",
58632
+ "numeric-sensor",
58633
+ "weather",
58634
+ "air-quality-sensor",
58635
+ "lock-control",
58636
+ "contact",
58637
+ "binary",
58638
+ "battery"
58639
+ ]);
58640
+ /**
58641
+ * Per-cap leaf allowlist. Absent entry ⇒ every leaf from `stateLeavesOf`
58642
+ * that survives bookkeeping (single-metric sensors). Present ⇒ only these
58643
+ * paths (climate / weather / battery).
58644
+ */
58645
+ var OSD_SOURCE_PATHS = new Map([
58646
+ ["climate-control", new Set([
58647
+ "currentTemp",
58648
+ "currentHumidity",
58649
+ "target",
58650
+ "targetHumidity",
58651
+ "mode"
58652
+ ])],
58653
+ ["weather", new Set([
58654
+ "condition",
58655
+ "temperature",
58656
+ "humidity",
58657
+ "pressure",
58658
+ "windSpeed",
58659
+ "windBearing"
58660
+ ])],
58661
+ ["battery", new Set(["percentage"])],
58662
+ ["temperature-sensor", new Set(["celsius"])],
58663
+ ["humidity-sensor", new Set(["percent"])],
58664
+ ["numeric-sensor", new Set(["value"])],
58665
+ ["air-quality-sensor", new Set([
58666
+ "co2Ppm",
58667
+ "vocPpb",
58668
+ "pm25",
58669
+ "pm10",
58670
+ "aqi"
58671
+ ])],
58672
+ ["binary", new Set(["on"])],
58673
+ ["contact", new Set(["entryOpen"])],
58674
+ ["lock-control", new Set(["state"])]
58675
+ ]);
58676
+ function isOsdSourceCap(capName) {
58677
+ return OSD_SOURCE_CAP_NAMES.has(capName);
58678
+ }
58679
+ /** Bindable OSD leaves for one cap — empty when the cap is not OSD-relevant. */
58680
+ function osdStateLeavesOf(cap) {
58681
+ if (!isOsdSourceCap(cap.name)) return [];
58682
+ const leaves = stateLeavesOf(cap);
58683
+ const allowed = OSD_SOURCE_PATHS.get(cap.name);
58684
+ if (allowed === void 0) return leaves;
58685
+ return leaves.filter((leaf) => allowed.has(leaf.path));
58686
+ }
58687
+ /** Device types that never contribute their OWN catalog leaves. */
58688
+ var OSD_NON_LEAF_TYPES = new Set(["container"]);
58689
+ /** True when this device type may expose OSD leaves (not a container gateway). */
58690
+ function isOsdLeafDeviceType(type) {
58691
+ if (type === null || type === void 0) return true;
58692
+ return !OSD_NON_LEAF_TYPES.has(type);
58693
+ }
58694
+ /** @deprecated Prefer {@link isOsdLeafDeviceType}. */
58695
+ var OSD_PICKER_EXCLUDED_TYPES = OSD_NON_LEAF_TYPES;
58696
+ /** @deprecated Prefer {@link isOsdLeafDeviceType}. */
58697
+ function isOsdPickerDeviceType(type) {
58698
+ return isOsdLeafDeviceType(type);
58699
+ }
58700
+ function isContainerDeviceType(type) {
58701
+ return type === "container";
58702
+ }
58703
+ //#endregion
58096
58704
  //#region src/source-catalog.ts
58097
58705
  /**
58098
- * Build the editor's source picker from the CAP DEFINITIONS — never a
58099
- * hand-written list. A cap that ships a new runtime-state field becomes
58100
- * bindable the moment it deploys, with no change here.
58706
+ * Build the editor's source picker from OSD-relevant cap definitions.
58707
+ * Gateway / infrastructure caps (broker, mqtt, deviceExport, …) never
58708
+ * appear — see `osd-source-fields.ts`.
58101
58709
  */
58102
58710
  /** Turn `air-quality-sensor` + `pm25` into `Air Quality Sensor · pm25`. */
58103
58711
  function humanizeCapName(capName) {
58104
58712
  return capName.split("-").map((part) => part.length === 0 ? part : part[0].toUpperCase() + part.slice(1)).join(" ");
58105
58713
  }
58106
58714
  /**
58107
- * Every bindable value across the cluster, with the camera's own state
58108
- * first — that is what an operator reaches for, and a picker that buries
58109
- * it under 200 other devices is a picker nobody uses.
58715
+ * Every OSD-bindable value across the cluster, with the camera's own
58716
+ * state first — that is what an operator reaches for, and a picker that
58717
+ * buries it under 200 other devices is a picker nobody uses.
58110
58718
  */
58111
58719
  function sourceOptionsFor(cameraDeviceId, devices, capsByDevice) {
58112
58720
  const capsByName = new Map(ALL_CAPABILITY_DEFINITIONS.map((cap) => [cap.name, cap]));
@@ -58116,22 +58724,44 @@ function sourceOptionsFor(cameraDeviceId, devices, capsByDevice) {
58116
58724
  return a.id - b.id;
58117
58725
  });
58118
58726
  const options = [];
58119
- for (const device of ordered) for (const capName of capsByDevice.get(device.id) ?? []) {
58120
- const cap = capsByName.get(capName);
58121
- if (cap === void 0) continue;
58122
- for (const leaf of stateLeavesOf(cap)) options.push({
58123
- deviceId: device.id,
58124
- deviceName: device.name,
58125
- capName,
58126
- valuePath: leaf.path,
58127
- label: `${humanizeCapName(capName)} · ${leaf.path}`,
58128
- valueType: leaf.valueType,
58129
- ...leaf.enumValues !== void 0 ? { enumValues: leaf.enumValues } : {}
58130
- });
58727
+ for (const device of ordered) {
58728
+ if (!isOsdLeafDeviceType(device.type ?? null)) continue;
58729
+ for (const capName of capsByDevice.get(device.id) ?? []) {
58730
+ if (!isOsdSourceCap(capName)) continue;
58731
+ const cap = capsByName.get(capName);
58732
+ if (cap === void 0) continue;
58733
+ for (const leaf of osdStateLeavesOf(cap)) options.push({
58734
+ deviceId: device.id,
58735
+ deviceName: device.name,
58736
+ capName,
58737
+ valuePath: leaf.path,
58738
+ label: `${humanizeCapName(capName)} · ${leaf.path}`,
58739
+ valueType: leaf.valueType,
58740
+ ...leaf.enumValues !== void 0 ? { enumValues: leaf.enumValues } : {}
58741
+ });
58742
+ }
58131
58743
  }
58132
58744
  return options;
58133
58745
  }
58134
58746
  //#endregion
58747
+ //#region src/templates-store.ts
58748
+ /**
58749
+ * Named OSD templates — durable in the addon's own (global) store.
58750
+ */
58751
+ var OSD_TEMPLATES_KEY = "namedTemplates";
58752
+ var OsdNamedTemplatesSchema = array(OsdNamedTemplateSchema);
58753
+ var EMPTY_TEMPLATES = [];
58754
+ function upsertNamedTemplate(prev, template) {
58755
+ const parsed = OsdNamedTemplateSchema.parse(template);
58756
+ return [...prev.filter((t) => t.id !== parsed.id), parsed].toSorted((a, b) => a.name.localeCompare(b.name));
58757
+ }
58758
+ function deleteNamedTemplate(prev, templateId) {
58759
+ return prev.filter((t) => t.id !== templateId);
58760
+ }
58761
+ function findNamedTemplate(templates, templateId) {
58762
+ return templates.find((t) => t.id === templateId);
58763
+ }
58764
+ //#endregion
58135
58765
  //#region src/osd-manager.ts
58136
58766
  /**
58137
58767
  * The `osd-manager` provider — the render loop and the operator surface.
@@ -58495,6 +59125,26 @@ var OsdManager = class {
58495
59125
  renderDevice = async ({ deviceId }) => {
58496
59126
  return { results: [...await this.renderDeviceInternal(deviceId, null)] };
58497
59127
  };
59128
+ listTemplates = async () => {
59129
+ return { templates: [...await this.deps.templates.get()] };
59130
+ };
59131
+ upsertTemplate = async ({ template }) => {
59132
+ const next = upsertNamedTemplate(await this.deps.templates.get(), template);
59133
+ await this.deps.templates.set(next);
59134
+ const stored = findNamedTemplate(next, template.id);
59135
+ if (stored === void 0) throw new Error(`osd-manager: template "${template.id}" missing after upsert`);
59136
+ this.logger.info("osd-manager: named template upserted", { meta: {
59137
+ templateId: stored.id,
59138
+ bindingCount: stored.bindings.length
59139
+ } });
59140
+ return { template: stored };
59141
+ };
59142
+ deleteTemplate = async ({ templateId }) => {
59143
+ const prev = await this.deps.templates.get();
59144
+ await this.deps.templates.set(deleteNamedTemplate(prev, templateId));
59145
+ this.logger.info("osd-manager: named template deleted", { meta: { templateId } });
59146
+ return { success: true };
59147
+ };
58498
59148
  async renderAllBound() {
58499
59149
  for (const deviceId of [...this.boundDevices]) try {
58500
59150
  await this.renderDeviceInternal(deviceId, null);
@@ -58562,11 +59212,19 @@ var OsdManager = class {
58562
59212
  async collectFacts(cameraDeviceId, bindings) {
58563
59213
  const wanted = new Set([cameraDeviceId]);
58564
59214
  const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
59215
+ const templateIds = /* @__PURE__ */ new Set();
58565
59216
  for (const binding of Object.values(bindings)) {
58566
59217
  if (binding.source.kind === "device-state" && binding.source.deviceId !== void 0) wanted.add(binding.source.deviceId);
59218
+ if (binding.source.kind === "template") templateIds.add(binding.source.templateId);
58567
59219
  const gated = binding.conditions?.deviceState?.deviceId;
58568
59220
  if (gated !== void 0) wanted.add(gated);
58569
59221
  }
59222
+ const allTemplates = await this.deps.templates.get();
59223
+ const templatesById = /* @__PURE__ */ new Map();
59224
+ for (const named of allTemplates) {
59225
+ templatesById.set(named.id, named);
59226
+ if (templateIds.has(named.id)) for (const binding of named.bindings) wanted.add(binding.pick.deviceId);
59227
+ }
58570
59228
  const stateByDevice = /* @__PURE__ */ new Map();
58571
59229
  const gateState = /* @__PURE__ */ new Map();
58572
59230
  for (const id of wanted) try {
@@ -58591,7 +59249,8 @@ var OsdManager = class {
58591
59249
  atMs: this.now(),
58592
59250
  stateByDevice,
58593
59251
  recognitionByDevice,
58594
- deviceState: (id) => gateState.get(id)
59252
+ deviceState: (id) => gateState.get(id),
59253
+ templatesById
58595
59254
  };
58596
59255
  }
58597
59256
  async hydrateRecognitions(deviceId) {
@@ -58732,6 +59391,7 @@ var OsdManagerAddon = class extends BaseAddon {
58732
59391
  const ctx = this.ctx;
58733
59392
  const store = new OsdBindingsStore((deviceId, key, schema, fallback) => this.deviceState(deviceId, key, schema, fallback));
58734
59393
  const boundIndex = this.state(OSD_BOUND_INDEX_KEY, OsdBoundIndexSchema, null);
59394
+ const templates = this.state(OSD_TEMPLATES_KEY, OsdNamedTemplatesSchema, EMPTY_TEMPLATES);
58735
59395
  const manager = new OsdManager({
58736
59396
  logger: ctx.logger,
58737
59397
  readOsdStatus: async (deviceId) => {
@@ -58765,7 +59425,8 @@ var OsdManagerAddon = class extends BaseAddon {
58765
59425
  listDevices: async () => {
58766
59426
  return (await ctx.api.deviceManager.listAll.query({ projection: "slim" })).map((d) => ({
58767
59427
  id: d.id,
58768
- name: d.name
59428
+ name: d.name,
59429
+ type: d.type ?? null
58769
59430
  }));
58770
59431
  },
58771
59432
  listCameras: async () => {
@@ -58805,7 +59466,8 @@ var OsdManagerAddon = class extends BaseAddon {
58805
59466
  };
58806
59467
  },
58807
59468
  bindings: (deviceId) => store.forDevice(deviceId),
58808
- boundIndex
59469
+ boundIndex,
59470
+ templates
58809
59471
  });
58810
59472
  this.manager = manager;
58811
59473
  this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {
@@ -58862,28 +59524,86 @@ var OsdManagerAddon = class extends BaseAddon {
58862
59524
  }
58863
59525
  };
58864
59526
  //#endregion
59527
+ //#region src/osd-picker-devices.ts
59528
+ /**
59529
+ * Devices offered to the OSD DevicePicker — containers INCLUDED so the
59530
+ * operator can pick a gateway, then an OSD-supported child.
59531
+ */
59532
+ function toOsdPickerDevices(devices) {
59533
+ return devices.map((d) => ({
59534
+ id: d.id,
59535
+ name: d.name,
59536
+ type: d.type ?? null,
59537
+ location: d.location ?? null,
59538
+ parentDeviceId: d.parentDeviceId ?? null
59539
+ }));
59540
+ }
59541
+ /** Children of a container that have at least one OSD catalog field. */
59542
+ function osdSupportedChildrenOf(containerId, devices, deviceIdsWithOsdFields) {
59543
+ return devices.filter((d) => d.parentDeviceId === containerId && isOsdLeafDeviceType(d.type) && deviceIdsWithOsdFields.has(d.id)).toSorted((a, b) => a.name.localeCompare(b.name));
59544
+ }
59545
+ function deviceById(devices, id) {
59546
+ return devices.find((d) => d.id === id);
59547
+ }
59548
+ //#endregion
59549
+ //#region src/template-id.ts
59550
+ /**
59551
+ * Named-template ids are system-generated — never an operator text field.
59552
+ * Create uses a uuid; the editable surface is the template name.
59553
+ */
59554
+ function generateTemplateId() {
59555
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
59556
+ return `tpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
59557
+ }
59558
+ /** Optional slug helper when a stable short id from the name is preferred. */
59559
+ function templateIdFromName(name) {
59560
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
59561
+ return `${slug.length > 0 ? slug : "template"}-${Date.now().toString(36)}`;
59562
+ }
59563
+ //#endregion
58865
59564
  exports.DEFAULT_TICK_MS = DEFAULT_TICK_MS;
58866
59565
  exports.EMPTY_BINDINGS = EMPTY_BINDINGS;
58867
59566
  exports.OSD_BINDINGS_KEY = OSD_BINDINGS_KEY;
59567
+ exports.OSD_NON_LEAF_TYPES = OSD_NON_LEAF_TYPES;
59568
+ exports.OSD_PICKER_EXCLUDED_TYPES = OSD_PICKER_EXCLUDED_TYPES;
59569
+ exports.OSD_SOURCE_CAP_NAMES = OSD_SOURCE_CAP_NAMES;
58868
59570
  exports.OSD_SUPPORTED_CONDITIONS = OSD_SUPPORTED_CONDITIONS;
58869
59571
  exports.OsdBindingsStore = OsdBindingsStore;
58870
59572
  exports.OsdDeviceBindingsSchema = OsdDeviceBindingsSchema;
58871
59573
  exports.OsdManager = OsdManager;
58872
59574
  exports.OsdManagerAddon = OsdManagerAddon;
59575
+ exports.aliasToken = aliasToken;
58873
59576
  exports.applyTemplate = applyTemplate;
59577
+ exports.applyTemplateFormat = applyTemplateFormat;
58874
59578
  exports.assertSupportedConditions = assertSupportedConditions;
59579
+ exports.bindingToken = bindingToken;
58875
59580
  exports.default = OsdManagerAddon;
59581
+ exports.deviceById = deviceById;
58876
59582
  exports.displayValue = displayValue;
58877
59583
  exports.evaluateGate = evaluateGate;
58878
59584
  exports.formatClock = formatClock;
58879
59585
  exports.formatSlotText = formatSlotText;
59586
+ exports.generateTemplateId = generateTemplateId;
58880
59587
  exports.humanizeCapName = humanizeCapName;
59588
+ exports.isContainerDeviceType = isContainerDeviceType;
59589
+ exports.isOsdLeafDeviceType = isOsdLeafDeviceType;
59590
+ exports.isOsdPickerDeviceType = isOsdPickerDeviceType;
59591
+ exports.isOsdSourceCap = isOsdSourceCap;
58881
59592
  exports.limitText = limitText;
59593
+ exports.osdStateLeavesOf = osdStateLeavesOf;
59594
+ exports.osdSupportedChildrenOf = osdSupportedChildrenOf;
59595
+ exports.parseAliasTokens = parseAliasTokens;
58882
59596
  exports.readStatePath = readStatePath;
59597
+ exports.renderNamedTemplate = renderNamedTemplate;
58883
59598
  exports.renderSlot = renderSlot;
59599
+ exports.replaceToken = replaceToken;
59600
+ exports.resolveBindingLive = resolveBindingLive;
58884
59601
  exports.resolveSource = resolveSource;
58885
59602
  exports.sourceOptionsFor = sourceOptionsFor;
58886
59603
  exports.stateLeavesOf = stateLeavesOf;
59604
+ exports.suggestAlias = suggestAlias;
59605
+ exports.templateIdFromName = templateIdFromName;
59606
+ exports.toOsdPickerDevices = toOsdPickerDevices;
58887
59607
  exports.truncateDecimals = truncateDecimals;
58888
59608
  exports.unsupportedConditionKeys = unsupportedConditionKeys;
58889
59609
  exports.withBinding = withBinding;