@camstack/ui-library 1.2.227 → 1.2.228

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/dist/index.cjs CHANGED
@@ -7596,8 +7596,12 @@ var useNodesExecuteQuery = trpc.nodes.executeQuery.useMutation;
7596
7596
  var useNotificationOutputListTargetKinds = trpc.notificationOutput.listTargetKinds.useQuery;
7597
7597
  /** Generated alias around `trpc.notificationOutput.listTargets.useQuery`. */
7598
7598
  var useNotificationOutputListTargets = trpc.notificationOutput.listTargets.useQuery;
7599
+ /** Generated alias around `trpc.notificationOutput.getTarget.useQuery`. */
7600
+ var useNotificationOutputGetTarget = trpc.notificationOutput.getTarget.useQuery;
7599
7601
  /** Generated alias around `trpc.notificationOutput.discoverTargets.useQuery`. */
7600
7602
  var useNotificationOutputDiscoverTargets = trpc.notificationOutput.discoverTargets.useQuery;
7603
+ /** Generated alias around `trpc.notificationOutput.probeTargetConfig.useMutation`. */
7604
+ var useNotificationOutputProbeTargetConfig = trpc.notificationOutput.probeTargetConfig.useMutation;
7601
7605
  /** Generated alias around `trpc.notificationOutput.send.useMutation`. */
7602
7606
  var useNotificationOutputSend = trpc.notificationOutput.send.useMutation;
7603
7607
  /** Generated alias around `trpc.notificationOutput.testTarget.useMutation`. */
@@ -12392,8 +12396,8 @@ function selectedDeviceOptions(options, selected) {
12392
12396
  */
12393
12397
  var INPUT_CLASS$1 = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
12394
12398
  var INPUT_SHELL$1 = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
12395
- var LABEL_CLASS$2 = "block text-[11px] font-medium text-foreground mb-1 break-words";
12396
- var DESC_CLASS$1 = "text-[10px] text-foreground-subtle mt-0.5 break-words";
12399
+ var LABEL_CLASS$3 = "block text-[11px] font-medium text-foreground mb-1 break-words";
12400
+ var DESC_CLASS$2 = "text-[10px] text-foreground-subtle mt-0.5 break-words";
12397
12401
  /** The compact projection of the listAll rows the device fields consume. */
12398
12402
  function toCandidates(rows) {
12399
12403
  if (!Array.isArray(rows)) return [];
@@ -12480,14 +12484,14 @@ function FacetDropdown$1({ label, values, chosen, onChange }) {
12480
12484
  })]
12481
12485
  })] });
12482
12486
  }
12483
- function FieldShell({ label, description, required, translationFn, children }) {
12487
+ function FieldShell$1({ label, description, required, translationFn, children }) {
12484
12488
  const resolvedLabel = resolveLabel(label, translationFn);
12485
12489
  const resolvedDescription = resolveLabel(description, translationFn);
12486
12490
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
12487
12491
  className: "min-w-0",
12488
12492
  children: [
12489
12493
  resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
12490
- className: LABEL_CLASS$2,
12494
+ className: LABEL_CLASS$3,
12491
12495
  children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
12492
12496
  className: "text-danger ml-0.5",
12493
12497
  children: "*"
@@ -12495,7 +12499,7 @@ function FieldShell({ label, description, required, translationFn, children }) {
12495
12499
  }),
12496
12500
  children,
12497
12501
  resolvedDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
12498
- className: DESC_CLASS$1,
12502
+ className: DESC_CLASS$2,
12499
12503
  children: resolvedDescription
12500
12504
  })
12501
12505
  ]
@@ -12505,7 +12509,7 @@ function DeviceSelectField({ field, value, onChange, disabled, translationFn })
12505
12509
  const { data } = useDeviceManagerListAll({});
12506
12510
  const options = (0, react$1.useMemo)(() => devicesToOptions(toCandidates(data), field.deviceFilter), [data, field.deviceFilter]);
12507
12511
  const current = value === void 0 || value === null ? "" : String(value);
12508
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldShell, {
12512
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldShell$1, {
12509
12513
  label: field.label,
12510
12514
  description: field.description,
12511
12515
  required: field.required,
@@ -12583,7 +12587,7 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
12583
12587
  className: "min-w-0",
12584
12588
  children: [
12585
12589
  label !== void 0 && label !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
12586
- className: LABEL_CLASS$2,
12590
+ className: LABEL_CLASS$3,
12587
12591
  children: label
12588
12592
  }),
12589
12593
  selectedOptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -12706,6 +12710,720 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
12706
12710
  })
12707
12711
  }),
12708
12712
  description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
12713
+ className: DESC_CLASS$2,
12714
+ children: description
12715
+ })
12716
+ ]
12717
+ });
12718
+ }
12719
+ //#endregion
12720
+ //#region ../../node_modules/class-variance-authority/dist/index.mjs
12721
+ /**
12722
+ * Copyright 2022 Joe Bell. All rights reserved.
12723
+ *
12724
+ * This file is licensed to you under the Apache License, Version 2.0
12725
+ * (the "License"); you may not use this file except in compliance with the
12726
+ * License. You may obtain a copy of the License at
12727
+ *
12728
+ * http://www.apache.org/licenses/LICENSE-2.0
12729
+ *
12730
+ * Unless required by applicable law or agreed to in writing, software
12731
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12732
+ * WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the
12733
+ * License for the specific language governing permissions and limitations under
12734
+ * the License.
12735
+ */ var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
12736
+ var cx = clsx;
12737
+ var cva = (base, config) => (props) => {
12738
+ var _config_compoundVariants;
12739
+ if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
12740
+ const { variants, defaultVariants } = config;
12741
+ const getVariantClassNames = Object.keys(variants).map((variant) => {
12742
+ const variantProp = props === null || props === void 0 ? void 0 : props[variant];
12743
+ const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
12744
+ if (variantProp === null) return null;
12745
+ const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
12746
+ return variants[variant][variantKey];
12747
+ });
12748
+ const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
12749
+ let [key, value] = param;
12750
+ if (value === void 0) return acc;
12751
+ acc[key] = value;
12752
+ return acc;
12753
+ }, {});
12754
+ return cx(base, getVariantClassNames, config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param) => {
12755
+ let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
12756
+ return Object.entries(compoundVariantOptions).every((param) => {
12757
+ let [key, value] = param;
12758
+ return Array.isArray(value) ? value.includes({
12759
+ ...defaultVariants,
12760
+ ...propsWithoutUndefined
12761
+ }[key]) : {
12762
+ ...defaultVariants,
12763
+ ...propsWithoutUndefined
12764
+ }[key] === value;
12765
+ }) ? [
12766
+ ...acc,
12767
+ cvClass,
12768
+ cvClassName
12769
+ ] : acc;
12770
+ }, []), props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
12771
+ };
12772
+ //#endregion
12773
+ //#region src/primitives/badge.tsx
12774
+ var badgeVariants = cva("inline-flex items-center rounded-full text-xs font-medium px-2 py-0.5", {
12775
+ variants: {
12776
+ variant: {
12777
+ default: "bg-surface-hover text-foreground",
12778
+ success: "bg-success/15 text-success",
12779
+ warning: "bg-warning/15 text-warning",
12780
+ danger: "bg-danger/15 text-danger",
12781
+ info: "bg-info/15 text-info"
12782
+ },
12783
+ styleVariant: {
12784
+ solid: "",
12785
+ outline: "border bg-transparent"
12786
+ }
12787
+ },
12788
+ defaultVariants: {
12789
+ variant: "default",
12790
+ styleVariant: "solid"
12791
+ }
12792
+ });
12793
+ var Badge = (0, react$1.forwardRef)(({ className, variant, style, ...props }, ref) => {
12794
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
12795
+ ref,
12796
+ className: cn(badgeVariants({
12797
+ variant,
12798
+ styleVariant: style
12799
+ }), className),
12800
+ ...props
12801
+ });
12802
+ });
12803
+ Badge.displayName = "Badge";
12804
+ //#endregion
12805
+ //#region src/composites/event-kind-visuals.tsx
12806
+ /**
12807
+ * Event-kind VISUALS — the UI-side half of the unified taxonomy dictionary
12808
+ * (`@camstack/types` `EVENT_TAXONOMY` owns the serializable data:
12809
+ * color / iconId / labelKey; this owns `iconId → lucide component` and
12810
+ * `labelKey → t()`).
12811
+ *
12812
+ * Every UI that renders an event kind (timeline, events tree, chips,
12813
+ * detection canvas/tree) resolves its glyph + translated label here — no
12814
+ * per-component hardcoded icon or label.
12815
+ */
12816
+ /**
12817
+ * `iconId` → lucide component. Built ONLY from statically-imported icons
12818
+ * (bundle-safe). Unknown ids resolve to the neutral `Tag` fallback.
12819
+ */
12820
+ var EVENT_KIND_ICONS = {
12821
+ motion: Activity,
12822
+ audio: Volume2,
12823
+ person: User,
12824
+ vehicle: Car,
12825
+ animal: PawPrint,
12826
+ package: Package,
12827
+ sensor: Radio,
12828
+ control: SlidersHorizontal,
12829
+ car: Car,
12830
+ truck: Truck,
12831
+ bus: Bus,
12832
+ motorcycle: Bike,
12833
+ bicycle: Bike,
12834
+ airplane: Plane,
12835
+ boat: Sailboat,
12836
+ train: TrainFront,
12837
+ bird: Bird,
12838
+ cat: Cat,
12839
+ dog: Dog,
12840
+ horse: PawPrint,
12841
+ sheep: PawPrint,
12842
+ cow: PawPrint,
12843
+ elephant: PawPrint,
12844
+ bear: PawPrint,
12845
+ zebra: PawPrint,
12846
+ giraffe: PawPrint,
12847
+ door: DoorOpen,
12848
+ pir: Radar,
12849
+ smoke: Flame,
12850
+ water: Droplet,
12851
+ gas: Wind,
12852
+ vibration: Vibrate,
12853
+ tamper: ShieldAlert,
12854
+ presence: UserCheck,
12855
+ generic: Tag,
12856
+ lock: Lock,
12857
+ siren: Siren,
12858
+ switch: ToggleRight,
12859
+ doorbell: BellRing,
12860
+ button: CircleDot,
12861
+ "audio-speech": MessageSquare,
12862
+ "audio-scream": Megaphone,
12863
+ "audio-crying": Baby,
12864
+ "audio-laughter": Laugh,
12865
+ "audio-music": Music,
12866
+ "audio-dog": Dog,
12867
+ "audio-cat": Cat,
12868
+ "audio-bird": Bird,
12869
+ "audio-animal": PawPrint,
12870
+ "audio-alarm": BellRing,
12871
+ "audio-doorbell": BellRing,
12872
+ "audio-glass_breaking": TriangleAlert,
12873
+ "audio-gunshot": Crosshair,
12874
+ "audio-vehicle": Car,
12875
+ "audio-siren": Siren,
12876
+ "audio-fire": Flame,
12877
+ "audio-water": Droplets,
12878
+ "audio-wind": Wind,
12879
+ "audio-door": DoorOpen,
12880
+ "audio-footsteps": Footprints,
12881
+ "audio-crowd": Users,
12882
+ "audio-telephone": Phone,
12883
+ "audio-engine": Cog,
12884
+ "audio-tools": Hammer,
12885
+ "audio-silence": VolumeX
12886
+ };
12887
+ /** Resolve an `iconId` to a lucide component (falls back to `Tag`). */
12888
+ function resolveEventKindIcon(iconId) {
12889
+ if (Object.hasOwn(EVENT_KIND_ICONS, iconId)) return EVENT_KIND_ICONS[iconId];
12890
+ if (iconId.startsWith("audio-")) return Volume2;
12891
+ return Tag;
12892
+ }
12893
+ /**
12894
+ * Translated label for an event kind — `t(labelKey)` when a translation
12895
+ * exists, else the English `label` fallback. Robust to a missing key: an
12896
+ * i18n backend that echoes the key unchanged (react-i18next default) is
12897
+ * treated as "no translation" and the fallback label is returned, so the UI
12898
+ * never renders a raw `eventKind.*` key.
12899
+ */
12900
+ function eventKindLabel(descriptor, t) {
12901
+ if (t === void 0) return descriptor.label;
12902
+ const translated = t(descriptor.labelKey);
12903
+ return translated === descriptor.labelKey || translated.length === 0 ? descriptor.label : translated;
12904
+ }
12905
+ /** Small convenience component: renders the lucide glyph for an `iconId`. */
12906
+ function EventKindGlyph({ iconId, size = 16, color, className }) {
12907
+ return (0, react$1.createElement)(resolveEventKindIcon(iconId), {
12908
+ size,
12909
+ color,
12910
+ className
12911
+ });
12912
+ }
12913
+ //#endregion
12914
+ //#region src/composites/device-picker/SelectedChip.tsx
12915
+ /**
12916
+ * Removable selection chip + the search-box shell shared by DevicePicker and
12917
+ * SearchableMultiSelectField (admin-ui). Lives here so addon MFEs can render
12918
+ * the same picker without importing admin-ui.
12919
+ */
12920
+ /** The search box's shell — shared with multiselect fields that mirror this picker. */
12921
+ var INPUT_SHELL = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
12922
+ /** One selected entry as a removable chip. */
12923
+ function SelectedChip({ label, onRemove, warning = false, iconId, iconUrl, testId }) {
12924
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
12925
+ "data-testid": testId,
12926
+ className: warning ? "inline-flex items-center gap-1 rounded-md bg-warning/10 px-1.5 py-0.5 text-[11px] text-warning max-w-full" : "inline-flex items-center gap-1 rounded-md bg-primary/10 px-1.5 py-0.5 text-[11px] text-primary max-w-full",
12927
+ children: [
12928
+ iconUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
12929
+ src: iconUrl,
12930
+ alt: "",
12931
+ className: "h-3 w-3 shrink-0 rounded-sm object-contain"
12932
+ }) : iconId !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventKindGlyph, {
12933
+ iconId,
12934
+ size: 12
12935
+ }) : null,
12936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
12937
+ className: "truncate",
12938
+ children: label
12939
+ }),
12940
+ onRemove !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
12941
+ type: "button",
12942
+ className: "shrink-0 hover:text-danger",
12943
+ "aria-label": `Remove ${label}`,
12944
+ onClick: onRemove,
12945
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
12946
+ })
12947
+ ]
12948
+ });
12949
+ }
12950
+ //#endregion
12951
+ //#region src/composites/option-select/option-select-model.ts
12952
+ /** Number of options at/above which search is enabled by default (unless creatable). */
12953
+ var OPTION_SELECT_SEARCH_MIN_OPTIONS = 8;
12954
+ /** Fold an option into a single lower-cased haystack (value + label + description). */
12955
+ function optionSearchText(opt) {
12956
+ return [
12957
+ opt.value,
12958
+ opt.label,
12959
+ opt.description
12960
+ ].filter((p) => typeof p === "string" && p.length > 0).join(" ").toLowerCase();
12961
+ }
12962
+ /**
12963
+ * Filter options by a free-text query. An empty / whitespace-only query
12964
+ * returns every option (identity, preserving order).
12965
+ */
12966
+ function filterOptions(options, query) {
12967
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
12968
+ if (tokens.length === 0) return options;
12969
+ return options.filter((opt) => {
12970
+ const hay = optionSearchText(opt);
12971
+ return tokens.every((t) => hay.includes(t));
12972
+ });
12973
+ }
12974
+ /**
12975
+ * Split options into the currently-selected set (in selection order) and the
12976
+ * rest. Selected values with no matching option are surfaced as synthetic
12977
+ * options whose label falls back to the raw value.
12978
+ */
12979
+ function partitionSelected(options, selected) {
12980
+ const byValue = new Map(options.map((o) => [o.value, o]));
12981
+ const selectedSet = new Set(selected);
12982
+ return {
12983
+ selectedOptions: selected.map((v) => byValue.get(v) ?? {
12984
+ value: v,
12985
+ label: v
12986
+ }),
12987
+ unselected: options.filter((o) => !selectedSet.has(o.value))
12988
+ };
12989
+ }
12990
+ function resolveSearchable(optionCount, creatable, searchable) {
12991
+ if (searchable !== void 0) return searchable;
12992
+ return optionCount >= 8 || creatable;
12993
+ }
12994
+ /** Trimmed non-empty draft from the search box, or null when absent. */
12995
+ function draftFromQuery(query) {
12996
+ const trimmed = query.trim();
12997
+ return trimmed.length > 0 ? trimmed : null;
12998
+ }
12999
+ /**
13000
+ * True when an existing catalog option already represents this draft (value or
13001
+ * label, case-insensitive). Used to hide the “Use «query»” row when redundant.
13002
+ */
13003
+ function catalogHasDraft(options, draft) {
13004
+ const lower = draft.toLowerCase();
13005
+ return options.some((o) => o.value.toLowerCase() === lower || o.label.toLowerCase() === lower);
13006
+ }
13007
+ /**
13008
+ * Show the footer “Add new…” action only when creatable AND the caller
13009
+ * provided `onCreateRequest` (e.g. open a dedicated create dialog).
13010
+ *
13011
+ * Without that hook the footer was a no-op (it only focused the search box),
13012
+ * which read as “Add new is broken”. Free-form create already works via the
13013
+ * “Use «query»” row once the operator types — no footer needed.
13014
+ */
13015
+ function shouldShowAddNewFooter(creatable, hasCreateRequest) {
13016
+ return creatable && hasCreateRequest;
13017
+ }
13018
+ /**
13019
+ * Show the “Use «query»” commit row: creatable, a non-empty draft, and no
13020
+ * catalog option that already matches the draft.
13021
+ */
13022
+ function shouldShowUseQueryRow(creatable, query, options) {
13023
+ if (!creatable) return false;
13024
+ const draft = draftFromQuery(query);
13025
+ if (draft === null) return false;
13026
+ return !catalogHasDraft(options, draft);
13027
+ }
13028
+ //#endregion
13029
+ //#region src/composites/option-select/OptionSelect.tsx
13030
+ var LISTBOX_GAP_PX = 4;
13031
+ var LISTBOX_MAX_HEIGHT_PX = 224;
13032
+ var VIEWPORT_MARGIN_PX = 8;
13033
+ var LABEL_CLASS$2 = "block text-[11px] font-medium text-foreground mb-1 break-words";
13034
+ var DESC_CLASS$1 = "text-[10px] text-foreground-subtle mt-0.5 break-words";
13035
+ function OptionIcon({ opt }) {
13036
+ if (opt.iconUrl !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
13037
+ src: opt.iconUrl,
13038
+ alt: "",
13039
+ className: "h-3.5 w-3.5 shrink-0 rounded-sm object-contain"
13040
+ });
13041
+ if (opt.iconId !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventKindGlyph, {
13042
+ iconId: opt.iconId,
13043
+ size: 13
13044
+ });
13045
+ return null;
13046
+ }
13047
+ function OptionSelect(props) {
13048
+ const { mode, options, placeholder = "Select…", disabled = false, emptyMessage = "No options available.", creatable = false, createLabel = "Add new…", onCreateRequest, label, description, testId, optionTestIdPrefix, listboxId: listboxIdProp } = props;
13049
+ const isDisabled = disabled;
13050
+ const searchable = resolveSearchable(options.length, creatable, props.searchable);
13051
+ const listboxId = listboxIdProp ?? testId ?? "option-select-listbox";
13052
+ const selectedValues = mode === "single" ? props.value.length > 0 ? [props.value] : [] : props.value;
13053
+ const maxItems = mode === "multi" ? props.maxItems : void 0;
13054
+ const atMax = mode === "multi" && maxItems !== void 0 && selectedValues.length >= maxItems;
13055
+ const [query, setQuery] = (0, react$1.useState)("");
13056
+ const [open, setOpen] = (0, react$1.useState)(false);
13057
+ const [highlight, setHighlight] = (0, react$1.useState)(0);
13058
+ const [coords, setCoords] = (0, react$1.useState)(null);
13059
+ const inputRef = (0, react$1.useRef)(null);
13060
+ const triggerRef = (0, react$1.useRef)(null);
13061
+ const listboxRef = (0, react$1.useRef)(null);
13062
+ const blurTimer = (0, react$1.useRef)(null);
13063
+ const computeListboxCoords = () => {
13064
+ const trigger = triggerRef.current;
13065
+ if (trigger === null) return null;
13066
+ const rect = trigger.getBoundingClientRect();
13067
+ const spaceBelow = window.innerHeight - rect.bottom - VIEWPORT_MARGIN_PX;
13068
+ const spaceAbove = rect.top - VIEWPORT_MARGIN_PX;
13069
+ const preferBelow = spaceBelow >= Math.min(LISTBOX_MAX_HEIGHT_PX, spaceAbove) || spaceBelow >= 120;
13070
+ const maxHeight = Math.max(96, Math.min(LISTBOX_MAX_HEIGHT_PX, preferBelow ? spaceBelow : spaceAbove));
13071
+ const left = Math.max(VIEWPORT_MARGIN_PX, Math.min(rect.left, window.innerWidth - rect.width - VIEWPORT_MARGIN_PX));
13072
+ if (preferBelow) return {
13073
+ top: rect.bottom + LISTBOX_GAP_PX,
13074
+ left,
13075
+ width: rect.width,
13076
+ maxHeight,
13077
+ placement: "below"
13078
+ };
13079
+ return {
13080
+ top: Math.max(VIEWPORT_MARGIN_PX, rect.top - LISTBOX_GAP_PX - maxHeight),
13081
+ left,
13082
+ width: rect.width,
13083
+ maxHeight,
13084
+ placement: "above"
13085
+ };
13086
+ };
13087
+ const { selectedOptions } = (0, react$1.useMemo)(() => partitionSelected(options, selectedValues), [options, selectedValues]);
13088
+ const filtered = (0, react$1.useMemo)(() => filterOptions(options, query), [options, query]);
13089
+ const showUseQuery = shouldShowUseQueryRow(creatable, query, options);
13090
+ const showAddNewFooter = shouldShowAddNewFooter(creatable, onCreateRequest !== void 0);
13091
+ const dropdownRows = (0, react$1.useMemo)(() => {
13092
+ const rows = filtered.map((opt, index) => ({
13093
+ kind: "option",
13094
+ opt,
13095
+ index
13096
+ }));
13097
+ if (showUseQuery) {
13098
+ const draft = draftFromQuery(query);
13099
+ if (draft !== null) rows.push({
13100
+ kind: "use-query",
13101
+ draft
13102
+ });
13103
+ }
13104
+ return rows;
13105
+ }, [
13106
+ filtered,
13107
+ showUseQuery,
13108
+ query
13109
+ ]);
13110
+ (0, react$1.useLayoutEffect)(() => {
13111
+ if (!open || isDisabled) {
13112
+ setCoords(null);
13113
+ return;
13114
+ }
13115
+ const update = () => {
13116
+ setCoords(computeListboxCoords());
13117
+ };
13118
+ update();
13119
+ window.addEventListener("resize", update);
13120
+ window.addEventListener("scroll", update, true);
13121
+ return () => {
13122
+ window.removeEventListener("resize", update);
13123
+ window.removeEventListener("scroll", update, true);
13124
+ };
13125
+ }, [
13126
+ open,
13127
+ isDisabled,
13128
+ selectedValues.length,
13129
+ query,
13130
+ dropdownRows.length
13131
+ ]);
13132
+ (0, react$1.useEffect)(() => {
13133
+ if (!open) return;
13134
+ const onPointer = (e) => {
13135
+ const target = e.target;
13136
+ if (listboxRef.current?.contains(target)) return;
13137
+ if (triggerRef.current?.contains(target)) return;
13138
+ setOpen(false);
13139
+ };
13140
+ document.addEventListener("mousedown", onPointer);
13141
+ return () => {
13142
+ document.removeEventListener("mousedown", onPointer);
13143
+ };
13144
+ }, [open]);
13145
+ const commitDraft = (draft) => {
13146
+ const trimmed = draft.trim();
13147
+ if (trimmed.length === 0) return;
13148
+ if (onCreateRequest !== void 0) {
13149
+ onCreateRequest(trimmed);
13150
+ setQuery("");
13151
+ setOpen(false);
13152
+ return;
13153
+ }
13154
+ if (mode === "single") {
13155
+ props.onChange(trimmed);
13156
+ setQuery("");
13157
+ setOpen(false);
13158
+ return;
13159
+ }
13160
+ if (props.value.includes(trimmed)) return;
13161
+ if (maxItems !== void 0 && props.value.length >= maxItems) return;
13162
+ props.onChange([...props.value, trimmed]);
13163
+ setQuery("");
13164
+ };
13165
+ const toggleMulti = (optValue) => {
13166
+ if (mode !== "multi" || isDisabled) return;
13167
+ if (props.value.includes(optValue)) props.onChange(props.value.filter((v) => v !== optValue));
13168
+ else {
13169
+ if (maxItems !== void 0 && props.value.length >= maxItems) return;
13170
+ props.onChange([...props.value, optValue]);
13171
+ }
13172
+ };
13173
+ const selectSingle = (optValue) => {
13174
+ if (mode !== "single" || isDisabled) return;
13175
+ props.onChange(optValue);
13176
+ setQuery("");
13177
+ setOpen(false);
13178
+ };
13179
+ const activateRow = (row) => {
13180
+ if (row.kind === "option") {
13181
+ if (row.opt.disabled === true) return;
13182
+ if (mode === "single") selectSingle(row.opt.value);
13183
+ else toggleMulti(row.opt.value);
13184
+ } else if (row.kind === "use-query") commitDraft(row.draft);
13185
+ };
13186
+ const rowCount = dropdownRows.length + (showAddNewFooter && !showUseQuery ? 1 : 0);
13187
+ const handleKeyDown = (e) => {
13188
+ if (e.key === "ArrowDown") {
13189
+ e.preventDefault();
13190
+ setOpen(true);
13191
+ setHighlight((h) => rowCount === 0 ? 0 : (h + 1) % rowCount);
13192
+ } else if (e.key === "ArrowUp") {
13193
+ e.preventDefault();
13194
+ setOpen(true);
13195
+ setHighlight((h) => rowCount === 0 ? 0 : (h - 1 + rowCount) % rowCount);
13196
+ } else if (e.key === "Enter") {
13197
+ e.preventDefault();
13198
+ if (showUseQuery && highlight === dropdownRows.length) {
13199
+ const draft = draftFromQuery(query);
13200
+ if (draft !== null) commitDraft(draft);
13201
+ return;
13202
+ }
13203
+ if (showAddNewFooter && !showUseQuery && highlight === dropdownRows.length) {
13204
+ inputRef.current?.focus();
13205
+ return;
13206
+ }
13207
+ const row = dropdownRows[highlight];
13208
+ if (row) activateRow(row);
13209
+ else if (creatable) {
13210
+ const draft = draftFromQuery(query);
13211
+ if (draft !== null) commitDraft(draft);
13212
+ }
13213
+ } else if (e.key === "Escape") setOpen(false);
13214
+ else if (e.key === "Backspace" && query === "" && mode === "multi" && props.value.length > 0) props.onChange(props.value.slice(0, -1));
13215
+ };
13216
+ const handleNonSearchKeyDown = (e) => {
13217
+ if (e.key === "Enter" || e.key === " ") {
13218
+ e.preventDefault();
13219
+ setOpen((o) => !o);
13220
+ } else if (e.key === "Escape") setOpen(false);
13221
+ };
13222
+ const removeChip = (value) => {
13223
+ if (mode === "single") props.onChange("");
13224
+ else props.onChange(props.value.filter((v) => v !== value));
13225
+ };
13226
+ const triggerPlaceholder = atMax === true ? `Max ${maxItems} selected` : placeholder;
13227
+ const showEmptyCatalog = options.length === 0 && !showUseQuery && dropdownRows.length === 0 && open;
13228
+ let dropdownBody;
13229
+ if (showEmptyCatalog) dropdownBody = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13230
+ className: "px-2.5 py-2 text-[11px] text-foreground-subtle",
13231
+ children: emptyMessage
13232
+ });
13233
+ else if (dropdownRows.length === 0 && !creatable) dropdownBody = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13234
+ className: "px-2.5 py-2 text-[11px] text-foreground-subtle italic",
13235
+ children: [
13236
+ "No matches for “",
13237
+ query,
13238
+ "”"
13239
+ ]
13240
+ });
13241
+ else dropdownBody = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: dropdownRows.map((row, i) => {
13242
+ if (row.kind === "use-query") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13243
+ type: "button",
13244
+ role: "option",
13245
+ "aria-selected": false,
13246
+ className: [
13247
+ "flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-xs transition-colors",
13248
+ i === highlight ? "bg-primary/10" : "hover:bg-surface-hover",
13249
+ "cursor-pointer"
13250
+ ].join(" "),
13251
+ onMouseEnter: () => setHighlight(i),
13252
+ onClick: () => commitDraft(row.draft),
13253
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3.5 w-3.5 shrink-0 text-primary" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
13254
+ className: "truncate text-foreground",
13255
+ children: [
13256
+ "Use “",
13257
+ row.draft,
13258
+ "”"
13259
+ ]
13260
+ })]
13261
+ }, "__use-query__");
13262
+ if (row.kind !== "option") return null;
13263
+ const { opt } = row;
13264
+ const checked = mode === "single" ? props.value === opt.value : props.value.includes(opt.value);
13265
+ const blockedByMax = mode === "multi" && !checked && atMax === true;
13266
+ const optionDisabled = opt.disabled === true || blockedByMax;
13267
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13268
+ type: "button",
13269
+ role: "option",
13270
+ "aria-selected": checked,
13271
+ disabled: optionDisabled,
13272
+ "data-testid": optionTestIdPrefix !== void 0 ? `${optionTestIdPrefix}-${opt.value}` : void 0,
13273
+ className: [
13274
+ "flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-xs transition-colors",
13275
+ i === highlight ? "bg-primary/10" : "hover:bg-surface-hover",
13276
+ optionDisabled ? "opacity-40 cursor-not-allowed" : "cursor-pointer"
13277
+ ].join(" "),
13278
+ onMouseEnter: () => setHighlight(i),
13279
+ onClick: () => activateRow(row),
13280
+ children: [
13281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13282
+ className: ["flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border", checked ? "border-primary bg-primary text-white" : "border-border"].join(" "),
13283
+ children: checked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-2.5 w-2.5" })
13284
+ }),
13285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionIcon, { opt }),
13286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13287
+ className: "min-w-0 flex-1 truncate text-foreground",
13288
+ children: opt.label
13289
+ }),
13290
+ opt.badge !== void 0 && opt.badge.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
13291
+ variant: "default",
13292
+ children: opt.badge
13293
+ }) : null
13294
+ ]
13295
+ }, opt.value);
13296
+ }) });
13297
+ const footerAddNewIndex = dropdownRows.length;
13298
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13299
+ className: "min-w-0 space-y-1",
13300
+ "data-testid": testId,
13301
+ children: [
13302
+ label !== void 0 && label.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
13303
+ className: LABEL_CLASS$2,
13304
+ children: label
13305
+ }),
13306
+ mode === "multi" && selectedOptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13307
+ className: "flex flex-wrap gap-1 mb-1.5",
13308
+ children: selectedOptions.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectedChip, {
13309
+ label: opt.label,
13310
+ ...opt.iconUrl !== void 0 ? { iconUrl: opt.iconUrl } : {},
13311
+ ...opt.iconId !== void 0 ? { iconId: opt.iconId } : {},
13312
+ ...!isDisabled ? { onRemove: () => removeChip(opt.value) } : {},
13313
+ ...optionTestIdPrefix !== void 0 ? { testId: `${optionTestIdPrefix}-chip-${opt.value}` } : {}
13314
+ }, opt.value))
13315
+ }),
13316
+ mode === "single" && selectedOptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13317
+ className: "flex flex-wrap gap-1 mb-1.5",
13318
+ children: selectedOptions.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectedChip, {
13319
+ label: opt.label,
13320
+ ...opt.iconUrl !== void 0 ? { iconUrl: opt.iconUrl } : {},
13321
+ ...opt.iconId !== void 0 ? { iconId: opt.iconId } : {},
13322
+ ...!isDisabled ? { onRemove: () => removeChip(opt.value) } : {}
13323
+ }, opt.value))
13324
+ }),
13325
+ searchable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13326
+ ref: (el) => {
13327
+ triggerRef.current = el;
13328
+ },
13329
+ className: INPUT_SHELL + (isDisabled ? " opacity-50" : ""),
13330
+ children: [
13331
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }),
13332
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
13333
+ ref: inputRef,
13334
+ type: "text",
13335
+ role: "combobox",
13336
+ "aria-expanded": open,
13337
+ "aria-controls": listboxId,
13338
+ className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-foreground-disabled",
13339
+ value: query,
13340
+ placeholder: triggerPlaceholder,
13341
+ disabled: isDisabled,
13342
+ onChange: (e) => {
13343
+ setQuery(e.target.value);
13344
+ setOpen(true);
13345
+ setHighlight(0);
13346
+ },
13347
+ onFocus: () => setOpen(true),
13348
+ onBlur: () => {
13349
+ blurTimer.current = setTimeout(() => {
13350
+ const active = document.activeElement;
13351
+ if (listboxRef.current?.contains(active)) return;
13352
+ setOpen(false);
13353
+ }, 120);
13354
+ },
13355
+ onKeyDown: handleKeyDown
13356
+ }),
13357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, {
13358
+ className: "h-3.5 w-3.5 shrink-0 cursor-pointer text-foreground-subtle",
13359
+ onMouseDown: (e) => {
13360
+ e.preventDefault();
13361
+ if (blurTimer.current) clearTimeout(blurTimer.current);
13362
+ setOpen((o) => !o);
13363
+ inputRef.current?.focus();
13364
+ }
13365
+ })
13366
+ ]
13367
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13368
+ ref: (el) => {
13369
+ triggerRef.current = el;
13370
+ },
13371
+ type: "button",
13372
+ role: "combobox",
13373
+ "aria-expanded": open,
13374
+ "aria-controls": listboxId,
13375
+ disabled: isDisabled,
13376
+ className: [
13377
+ INPUT_SHELL,
13378
+ "w-full justify-between text-xs text-foreground",
13379
+ isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
13380
+ ].join(" "),
13381
+ onClick: () => !isDisabled && setOpen((o) => !o),
13382
+ onKeyDown: handleNonSearchKeyDown,
13383
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13384
+ className: "truncate text-foreground-subtle",
13385
+ children: selectedOptions[0]?.label ?? triggerPlaceholder
13386
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" })]
13387
+ }),
13388
+ open && !isDisabled && coords !== null && typeof document !== "undefined" && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13389
+ ref: listboxRef,
13390
+ id: listboxId,
13391
+ role: "listbox",
13392
+ style: {
13393
+ position: "fixed",
13394
+ top: coords.top,
13395
+ left: coords.left,
13396
+ width: coords.width,
13397
+ maxHeight: coords.maxHeight,
13398
+ zIndex: 9999
13399
+ },
13400
+ className: "overflow-y-auto rounded-md border border-border bg-surface shadow-lg",
13401
+ onMouseDown: (e) => {
13402
+ e.preventDefault();
13403
+ if (blurTimer.current) clearTimeout(blurTimer.current);
13404
+ },
13405
+ children: [dropdownBody, showAddNewFooter && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13406
+ type: "button",
13407
+ className: [
13408
+ "flex w-full items-center gap-2 border-t border-border px-2.5 py-1.5 text-left text-xs transition-colors",
13409
+ footerAddNewIndex === highlight ? "bg-primary/10" : "hover:bg-surface-hover",
13410
+ "cursor-pointer text-primary"
13411
+ ].join(" "),
13412
+ onMouseEnter: () => setHighlight(footerAddNewIndex),
13413
+ onClick: () => {
13414
+ const draft = draftFromQuery(query);
13415
+ if (draft !== null) {
13416
+ onCreateRequest?.(draft);
13417
+ setQuery("");
13418
+ setOpen(false);
13419
+ return;
13420
+ }
13421
+ inputRef.current?.focus();
13422
+ },
13423
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3.5 w-3.5 shrink-0" }), createLabel]
13424
+ })]
13425
+ }), document.body),
13426
+ description !== void 0 && description.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
12709
13427
  className: DESC_CLASS$1,
12710
13428
  children: description
12711
13429
  })
@@ -12713,6 +13431,242 @@ function DeviceMultiSelectField({ field, value, onChange, disabled, translationF
12713
13431
  });
12714
13432
  }
12715
13433
  //#endregion
13434
+ //#region src/composites/notification-targets-options.ts
13435
+ function kindIconKey(addonId, kind) {
13436
+ return `${addonId}:${kind}`;
13437
+ }
13438
+ function targetsToOptions(targets, kindIconUrls, noteIds, noteLabel) {
13439
+ return targets.map((target) => {
13440
+ const iconUrl = target.addonId !== void 0 && target.kind !== void 0 ? kindIconUrls.get(kindIconKey(target.addonId, target.kind)) : void 0;
13441
+ let badge;
13442
+ if (noteIds?.has(target.id) === true) badge = noteLabel;
13443
+ else if (!target.enabled) badge = "disabled";
13444
+ return {
13445
+ value: target.id,
13446
+ label: target.name,
13447
+ ...iconUrl !== void 0 ? { iconUrl } : {},
13448
+ ...badge !== void 0 ? { badge } : {}
13449
+ };
13450
+ });
13451
+ }
13452
+ function buildKindIconMap(rows) {
13453
+ const map = /* @__PURE__ */ new Map();
13454
+ if (!Array.isArray(rows)) return map;
13455
+ for (const row of rows) {
13456
+ if (row === null || typeof row !== "object") continue;
13457
+ const kind = Reflect.get(row, "kind");
13458
+ const addonId = Reflect.get(row, "addonId");
13459
+ const iconUrl = Reflect.get(row, "iconUrl");
13460
+ if (typeof kind !== "string" || typeof addonId !== "string") continue;
13461
+ if (typeof iconUrl === "string" && iconUrl.length > 0) map.set(kindIconKey(addonId, kind), iconUrl);
13462
+ }
13463
+ return map;
13464
+ }
13465
+ function toTargetOptions(rows) {
13466
+ if (!Array.isArray(rows)) return [];
13467
+ const out = [];
13468
+ for (const row of rows) {
13469
+ if (row === null || typeof row !== "object") continue;
13470
+ const id = Reflect.get(row, "id");
13471
+ const name = Reflect.get(row, "name");
13472
+ const enabled = Reflect.get(row, "enabled");
13473
+ const kind = Reflect.get(row, "kind");
13474
+ const addonId = Reflect.get(row, "addonId");
13475
+ if (typeof id !== "string" || typeof name !== "string") continue;
13476
+ if (typeof kind !== "string" || typeof addonId !== "string") continue;
13477
+ out.push({
13478
+ id,
13479
+ name,
13480
+ enabled: enabled === true,
13481
+ kind,
13482
+ addonId
13483
+ });
13484
+ }
13485
+ return out;
13486
+ }
13487
+ //#endregion
13488
+ //#region src/composites/notification-targets-field.tsx
13489
+ /**
13490
+ * Shared notification-output target pickers.
13491
+ *
13492
+ * The Notification Center editors (Summaries, Timelapses, Rules, Calendar
13493
+ * events) and ConfigUISchema forms (script settings, addon settings) all
13494
+ * pick the same catalog — `notificationOutput.listTargets`. One presentational
13495
+ * control plus two ConfigUISchema wrappers so a script can declare
13496
+ * `notification-target-select` / `notification-target-multiselect` without
13497
+ * inventing a parallel picker.
13498
+ *
13499
+ * A DISABLED target is offered and marked, never hidden: it is a legitimate
13500
+ * choice (the operator re-enables it later), and hiding it would make a rule
13501
+ * whose only target is off look like a rule with no target at all.
13502
+ *
13503
+ * Single and multi both render through `OptionSelect` (dropdown + pills,
13504
+ * searchable, kind brand icons via `iconUrl`).
13505
+ */
13506
+ var DEFAULT_EMPTY = "No delivery target is configured. Add one on the Targets tab first — without one the notification is rendered and thrown away.";
13507
+ /**
13508
+ * Multi-select of delivery targets (NC Summaries / Timelapses / Rules shape).
13509
+ */
13510
+ function NotificationTargetsField({ targets, value, onChange, disabled = false, maxItems, testIdPrefix = "notification-target", emptyMessage = DEFAULT_EMPTY, noteIds, noteLabel = "note", label, description, kindIconUrls: kindIconUrlsProp }) {
13511
+ const kindsQuery = useNotificationOutputListTargetKinds({});
13512
+ const kindIconUrlsFromQuery = (0, react$1.useMemo)(() => buildKindIconMap(kindsQuery.data), [kindsQuery.data]);
13513
+ const kindIconUrls = kindIconUrlsProp ?? kindIconUrlsFromQuery;
13514
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
13515
+ mode: "multi",
13516
+ options: (0, react$1.useMemo)(() => targetsToOptions(targets, kindIconUrls, noteIds, noteLabel), [
13517
+ targets,
13518
+ kindIconUrls,
13519
+ noteIds,
13520
+ noteLabel
13521
+ ]),
13522
+ value,
13523
+ onChange,
13524
+ disabled,
13525
+ searchable: true,
13526
+ creatable: false,
13527
+ ...maxItems !== void 0 ? { maxItems } : {},
13528
+ emptyMessage,
13529
+ placeholder: "Search targets…",
13530
+ label,
13531
+ description,
13532
+ testId: `${testIdPrefix}s`,
13533
+ optionTestIdPrefix: testIdPrefix,
13534
+ listboxId: `${testIdPrefix}-listbox`
13535
+ });
13536
+ }
13537
+ /**
13538
+ * Single-select of one delivery target (Calendar events / script settings).
13539
+ */
13540
+ function NotificationTargetSelectField({ targets, value, onChange, disabled = false, emptyMessage = DEFAULT_EMPTY, placeholder = "Select…", testId = "notification-target-select", label, description, kindIconUrls: kindIconUrlsProp }) {
13541
+ const kindsQuery = useNotificationOutputListTargetKinds({});
13542
+ const kindIconUrlsFromQuery = (0, react$1.useMemo)(() => buildKindIconMap(kindsQuery.data), [kindsQuery.data]);
13543
+ const kindIconUrls = kindIconUrlsProp ?? kindIconUrlsFromQuery;
13544
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
13545
+ mode: "single",
13546
+ options: (0, react$1.useMemo)(() => targetsToOptions(targets, kindIconUrls, void 0, "note"), [targets, kindIconUrls]),
13547
+ value,
13548
+ onChange,
13549
+ disabled,
13550
+ searchable: true,
13551
+ creatable: false,
13552
+ emptyMessage,
13553
+ placeholder,
13554
+ label,
13555
+ description,
13556
+ testId,
13557
+ optionTestIdPrefix: testId,
13558
+ listboxId: `${testId}-listbox`
13559
+ });
13560
+ }
13561
+ function FieldShell({ label, description, required, translationFn, children }) {
13562
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13563
+ className: "space-y-1",
13564
+ children: [
13565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
13566
+ className: "block text-[11px] font-medium text-foreground mb-1 break-words",
13567
+ children: [resolveLabel(label, translationFn), required === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13568
+ className: "text-danger",
13569
+ children: " *"
13570
+ }) : null]
13571
+ }),
13572
+ children,
13573
+ description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
13574
+ className: "text-[10px] text-foreground-subtle mt-0.5 break-words",
13575
+ children: resolveLabel(description, translationFn)
13576
+ })
13577
+ ]
13578
+ });
13579
+ }
13580
+ /**
13581
+ * ConfigUISchema `notification-target-multiselect` — fetches listTargets and
13582
+ * stores a `string[]` of target ids.
13583
+ */
13584
+ function ConfigNotificationTargetMultiSelectFieldView({ field, value, onChange, disabled, translationFn }) {
13585
+ const targetsQuery = useNotificationOutputListTargets({});
13586
+ const kindsQuery = useNotificationOutputListTargetKinds({});
13587
+ const targets = (0, react$1.useMemo)(() => toTargetOptions(targetsQuery.data), [targetsQuery.data]);
13588
+ const kindIconUrls = (0, react$1.useMemo)(() => buildKindIconMap(kindsQuery.data), [kindsQuery.data]);
13589
+ const selected = Array.isArray(value) ? value.filter((v) => typeof v === "string") : typeof value === "string" && value.length > 0 ? [value] : [];
13590
+ const pending = targetsQuery.isPending || kindsQuery.isPending;
13591
+ const error = targetsQuery.isError ? targetsQuery.error : kindsQuery.isError ? kindsQuery.error : null;
13592
+ if (pending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldShell, {
13593
+ label: field.label,
13594
+ description: field.description,
13595
+ required: field.required,
13596
+ translationFn,
13597
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
13598
+ className: "text-[11px] text-foreground-subtle",
13599
+ children: "Loading targets…"
13600
+ })
13601
+ });
13602
+ if (error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldShell, {
13603
+ label: field.label,
13604
+ description: field.description,
13605
+ required: field.required,
13606
+ translationFn,
13607
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
13608
+ className: "text-[11px] text-danger",
13609
+ children: ["Could not load targets: ", error instanceof Error ? error.message : String(error)]
13610
+ })
13611
+ });
13612
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationTargetsField, {
13613
+ targets,
13614
+ value: selected,
13615
+ onChange: (next) => onChange(next),
13616
+ disabled: disabled === true || field.disabled === true,
13617
+ ...field.maxItems !== void 0 ? { maxItems: field.maxItems } : {},
13618
+ kindIconUrls,
13619
+ label: resolveLabel(field.label, translationFn),
13620
+ description: field.description !== void 0 ? resolveLabel(field.description, translationFn) : void 0,
13621
+ emptyMessage: "No delivery target is configured. Add one under Notification Center → Targets first.",
13622
+ testIdPrefix: `config-target-${field.key}`
13623
+ });
13624
+ }
13625
+ /**
13626
+ * ConfigUISchema `notification-target-select` — fetches listTargets and stores
13627
+ * one target id string.
13628
+ */
13629
+ function ConfigNotificationTargetSelectFieldView({ field, value, onChange, disabled, translationFn }) {
13630
+ const targetsQuery = useNotificationOutputListTargets({});
13631
+ const kindsQuery = useNotificationOutputListTargetKinds({});
13632
+ const targets = (0, react$1.useMemo)(() => toTargetOptions(targetsQuery.data), [targetsQuery.data]);
13633
+ const kindIconUrls = (0, react$1.useMemo)(() => buildKindIconMap(kindsQuery.data), [kindsQuery.data]);
13634
+ const current = typeof value === "string" ? value : "";
13635
+ const pending = targetsQuery.isPending || kindsQuery.isPending;
13636
+ const error = targetsQuery.isError ? targetsQuery.error : kindsQuery.isError ? kindsQuery.error : null;
13637
+ if (pending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldShell, {
13638
+ label: field.label,
13639
+ description: field.description,
13640
+ required: field.required,
13641
+ translationFn,
13642
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
13643
+ className: "text-[11px] text-foreground-subtle",
13644
+ children: "Loading targets…"
13645
+ })
13646
+ });
13647
+ if (error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldShell, {
13648
+ label: field.label,
13649
+ description: field.description,
13650
+ required: field.required,
13651
+ translationFn,
13652
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
13653
+ className: "text-[11px] text-danger",
13654
+ children: ["Could not load targets: ", error instanceof Error ? error.message : String(error)]
13655
+ })
13656
+ });
13657
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationTargetSelectField, {
13658
+ targets,
13659
+ value: current,
13660
+ onChange: (next) => onChange(next),
13661
+ disabled: disabled === true || field.disabled === true,
13662
+ kindIconUrls,
13663
+ label: resolveLabel(field.label, translationFn),
13664
+ description: field.description !== void 0 ? resolveLabel(field.description, translationFn) : void 0,
13665
+ emptyMessage: "No delivery target is configured. Add one under Notification Center → Targets first.",
13666
+ testId: `config-target-${field.key}`
13667
+ });
13668
+ }
13669
+ //#endregion
12716
13670
  //#region src/contexts/custom-field-renderers.tsx
12717
13671
  /**
12718
13672
  * Custom field renderer registry.
@@ -12766,115 +13720,6 @@ function useCustomFieldRenderer(type) {
12766
13720
  return (0, react$1.useContext)(CustomFieldRenderersContext)[type] ?? null;
12767
13721
  }
12768
13722
  //#endregion
12769
- //#region src/composites/event-kind-visuals.tsx
12770
- /**
12771
- * Event-kind VISUALS — the UI-side half of the unified taxonomy dictionary
12772
- * (`@camstack/types` `EVENT_TAXONOMY` owns the serializable data:
12773
- * color / iconId / labelKey; this owns `iconId → lucide component` and
12774
- * `labelKey → t()`).
12775
- *
12776
- * Every UI that renders an event kind (timeline, events tree, chips,
12777
- * detection canvas/tree) resolves its glyph + translated label here — no
12778
- * per-component hardcoded icon or label.
12779
- */
12780
- /**
12781
- * `iconId` → lucide component. Built ONLY from statically-imported icons
12782
- * (bundle-safe). Unknown ids resolve to the neutral `Tag` fallback.
12783
- */
12784
- var EVENT_KIND_ICONS = {
12785
- motion: Activity,
12786
- audio: Volume2,
12787
- person: User,
12788
- vehicle: Car,
12789
- animal: PawPrint,
12790
- package: Package,
12791
- sensor: Radio,
12792
- control: SlidersHorizontal,
12793
- car: Car,
12794
- truck: Truck,
12795
- bus: Bus,
12796
- motorcycle: Bike,
12797
- bicycle: Bike,
12798
- airplane: Plane,
12799
- boat: Sailboat,
12800
- train: TrainFront,
12801
- bird: Bird,
12802
- cat: Cat,
12803
- dog: Dog,
12804
- horse: PawPrint,
12805
- sheep: PawPrint,
12806
- cow: PawPrint,
12807
- elephant: PawPrint,
12808
- bear: PawPrint,
12809
- zebra: PawPrint,
12810
- giraffe: PawPrint,
12811
- door: DoorOpen,
12812
- pir: Radar,
12813
- smoke: Flame,
12814
- water: Droplet,
12815
- gas: Wind,
12816
- vibration: Vibrate,
12817
- tamper: ShieldAlert,
12818
- presence: UserCheck,
12819
- generic: Tag,
12820
- lock: Lock,
12821
- siren: Siren,
12822
- switch: ToggleRight,
12823
- doorbell: BellRing,
12824
- button: CircleDot,
12825
- "audio-speech": MessageSquare,
12826
- "audio-scream": Megaphone,
12827
- "audio-crying": Baby,
12828
- "audio-laughter": Laugh,
12829
- "audio-music": Music,
12830
- "audio-dog": Dog,
12831
- "audio-cat": Cat,
12832
- "audio-bird": Bird,
12833
- "audio-animal": PawPrint,
12834
- "audio-alarm": BellRing,
12835
- "audio-doorbell": BellRing,
12836
- "audio-glass_breaking": TriangleAlert,
12837
- "audio-gunshot": Crosshair,
12838
- "audio-vehicle": Car,
12839
- "audio-siren": Siren,
12840
- "audio-fire": Flame,
12841
- "audio-water": Droplets,
12842
- "audio-wind": Wind,
12843
- "audio-door": DoorOpen,
12844
- "audio-footsteps": Footprints,
12845
- "audio-crowd": Users,
12846
- "audio-telephone": Phone,
12847
- "audio-engine": Cog,
12848
- "audio-tools": Hammer,
12849
- "audio-silence": VolumeX
12850
- };
12851
- /** Resolve an `iconId` to a lucide component (falls back to `Tag`). */
12852
- function resolveEventKindIcon(iconId) {
12853
- if (Object.hasOwn(EVENT_KIND_ICONS, iconId)) return EVENT_KIND_ICONS[iconId];
12854
- if (iconId.startsWith("audio-")) return Volume2;
12855
- return Tag;
12856
- }
12857
- /**
12858
- * Translated label for an event kind — `t(labelKey)` when a translation
12859
- * exists, else the English `label` fallback. Robust to a missing key: an
12860
- * i18n backend that echoes the key unchanged (react-i18next default) is
12861
- * treated as "no translation" and the fallback label is returned, so the UI
12862
- * never renders a raw `eventKind.*` key.
12863
- */
12864
- function eventKindLabel(descriptor, t) {
12865
- if (t === void 0) return descriptor.label;
12866
- const translated = t(descriptor.labelKey);
12867
- return translated === descriptor.labelKey || translated.length === 0 ? descriptor.label : translated;
12868
- }
12869
- /** Small convenience component: renders the lucide glyph for an `iconId`. */
12870
- function EventKindGlyph({ iconId, size = 16, color, className }) {
12871
- return (0, react$1.createElement)(resolveEventKindIcon(iconId), {
12872
- size,
12873
- color,
12874
- className
12875
- });
12876
- }
12877
- //#endregion
12878
13723
  //#region src/composites/confirm-dialog.tsx
12879
13724
  /**
12880
13725
  * The ONE modal dialog of the product — confirm, alert and prompt.
@@ -19122,59 +19967,6 @@ function useCapabilityPresence(capabilityName) {
19122
19967
  return classifyCapability(query.data);
19123
19968
  }
19124
19969
  //#endregion
19125
- //#region ../../node_modules/class-variance-authority/dist/index.mjs
19126
- /**
19127
- * Copyright 2022 Joe Bell. All rights reserved.
19128
- *
19129
- * This file is licensed to you under the Apache License, Version 2.0
19130
- * (the "License"); you may not use this file except in compliance with the
19131
- * License. You may obtain a copy of the License at
19132
- *
19133
- * http://www.apache.org/licenses/LICENSE-2.0
19134
- *
19135
- * Unless required by applicable law or agreed to in writing, software
19136
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
19137
- * WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the
19138
- * License for the specific language governing permissions and limitations under
19139
- * the License.
19140
- */ var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
19141
- var cx = clsx;
19142
- var cva = (base, config) => (props) => {
19143
- var _config_compoundVariants;
19144
- if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
19145
- const { variants, defaultVariants } = config;
19146
- const getVariantClassNames = Object.keys(variants).map((variant) => {
19147
- const variantProp = props === null || props === void 0 ? void 0 : props[variant];
19148
- const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
19149
- if (variantProp === null) return null;
19150
- const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
19151
- return variants[variant][variantKey];
19152
- });
19153
- const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
19154
- let [key, value] = param;
19155
- if (value === void 0) return acc;
19156
- acc[key] = value;
19157
- return acc;
19158
- }, {});
19159
- return cx(base, getVariantClassNames, config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param) => {
19160
- let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
19161
- return Object.entries(compoundVariantOptions).every((param) => {
19162
- let [key, value] = param;
19163
- return Array.isArray(value) ? value.includes({
19164
- ...defaultVariants,
19165
- ...propsWithoutUndefined
19166
- }[key]) : {
19167
- ...defaultVariants,
19168
- ...propsWithoutUndefined
19169
- }[key] === value;
19170
- }) ? [
19171
- ...acc,
19172
- cvClass,
19173
- cvClassName
19174
- ] : acc;
19175
- }, []), props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
19176
- };
19177
- //#endregion
19178
19970
  //#region src/primitives/button.tsx
19179
19971
  var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
19180
19972
  variants: {
@@ -20037,38 +20829,6 @@ function ClipPanel({ deviceId, clip, profile, onProfileChange }) {
20037
20829
  });
20038
20830
  }
20039
20831
  //#endregion
20040
- //#region src/primitives/badge.tsx
20041
- var badgeVariants = cva("inline-flex items-center rounded-full text-xs font-medium px-2 py-0.5", {
20042
- variants: {
20043
- variant: {
20044
- default: "bg-surface-hover text-foreground",
20045
- success: "bg-success/15 text-success",
20046
- warning: "bg-warning/15 text-warning",
20047
- danger: "bg-danger/15 text-danger",
20048
- info: "bg-info/15 text-info"
20049
- },
20050
- styleVariant: {
20051
- solid: "",
20052
- outline: "border bg-transparent"
20053
- }
20054
- },
20055
- defaultVariants: {
20056
- variant: "default",
20057
- styleVariant: "solid"
20058
- }
20059
- });
20060
- var Badge = (0, react$1.forwardRef)(({ className, variant, style, ...props }, ref) => {
20061
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
20062
- ref,
20063
- className: cn(badgeVariants({
20064
- variant,
20065
- styleVariant: style
20066
- }), className),
20067
- ...props
20068
- });
20069
- });
20070
- Badge.displayName = "Badge";
20071
- //#endregion
20072
20832
  //#region src/lib/format-duration.ts
20073
20833
  /**
20074
20834
  * How long something lasted, the way this product says it.
@@ -26099,6 +26859,7 @@ function MultiSelectField({ field, value, onChange, disabled, translationFn }) {
26099
26859
  function PasswordField({ field, value, onChange, disabled, translationFn }) {
26100
26860
  const [show, setShow] = (0, react$1.useState)(false);
26101
26861
  const showToggle = field.showToggle !== false;
26862
+ const text = value === void 0 || value === null ? "" : String(value);
26102
26863
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
26103
26864
  label: field.label,
26104
26865
  description: field.description,
@@ -26110,14 +26871,20 @@ function PasswordField({ field, value, onChange, disabled, translationFn }) {
26110
26871
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
26111
26872
  type: show ? "text" : "password",
26112
26873
  className: INPUT_CLASS + (showToggle ? " pr-9" : ""),
26113
- value: value === void 0 || value === null ? "" : String(value),
26874
+ value: text,
26114
26875
  placeholder: field.placeholder,
26115
26876
  disabled: disabled || field.disabled,
26116
- onChange: (e) => onChange(e.target.value)
26877
+ autoComplete: "new-password",
26878
+ spellCheck: false,
26879
+ onChange: (e) => onChange(e.target.value),
26880
+ onFocus: (e) => {
26881
+ if (text.length > 0 && /^[•●*]{4,}$/u.test(text)) e.currentTarget.select();
26882
+ }
26117
26883
  }), showToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
26118
26884
  type: "button",
26119
26885
  className: "absolute right-2 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground",
26120
26886
  tabIndex: -1,
26887
+ "aria-label": show ? "Hide password" : "Show password",
26121
26888
  onClick: () => setShow((s) => !s),
26122
26889
  children: show ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eye, { className: "h-3.5 w-3.5" })
26123
26890
  })]
@@ -26263,70 +27030,30 @@ function SliderField({ field, value, onChange, disabled, translationFn }) {
26263
27030
  });
26264
27031
  }
26265
27032
  function TagsField({ field, value, onChange, disabled, translationFn }) {
26266
- const tags = Array.isArray(value) ? value : [];
26267
- const [input, setInput] = (0, react$1.useState)("");
26268
- const inputRef = (0, react$1.useRef)(null);
26269
- const addTag = (0, react$1.useCallback)((tag) => {
26270
- const trimmed = tag.trim();
26271
- if (!trimmed || tags.includes(trimmed)) return;
26272
- if (field.maxTags !== void 0 && tags.length >= field.maxTags) return;
26273
- onChange([...tags, trimmed]);
26274
- setInput("");
26275
- }, [
26276
- tags,
26277
- field.maxTags,
26278
- onChange
26279
- ]);
26280
- const removeTag = (tag) => onChange(tags.filter((t) => t !== tag));
26281
- const handleKeyDown = (e) => {
26282
- if (e.key === "Enter" || e.key === ",") {
26283
- e.preventDefault();
26284
- addTag(input);
26285
- } else if (e.key === "Backspace" && input === "" && tags.length > 0) removeTag(tags[tags.length - 1]);
26286
- };
26287
- const isDisabled = disabled || field.disabled;
27033
+ const tags = Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
27034
+ const creatable = field.creatable !== false;
27035
+ const options = (field.suggestions ?? []).map((s) => ({
27036
+ value: s,
27037
+ label: s
27038
+ }));
27039
+ const isDisabled = disabled === true || field.disabled === true;
26288
27040
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
26289
27041
  label: field.label,
26290
27042
  description: field.description,
26291
27043
  required: field.required,
26292
27044
  span: field.span,
26293
27045
  translationFn,
26294
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
26295
- className: [
26296
- "flex flex-wrap gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 min-h-[38px] cursor-text",
26297
- "focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30",
26298
- isDisabled ? "opacity-50 cursor-not-allowed" : ""
26299
- ].join(" "),
26300
- onClick: () => !isDisabled && inputRef.current?.focus(),
26301
- children: [
26302
- tags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
26303
- className: "inline-flex items-center gap-1 rounded-md bg-primary/10 px-1.5 py-0.5 text-xs text-primary",
26304
- children: [tag, !isDisabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
26305
- type: "button",
26306
- onClick: () => removeTag(tag),
26307
- className: "hover:text-danger",
26308
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
26309
- })]
26310
- }, tag)),
26311
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
26312
- ref: inputRef,
26313
- type: "text",
26314
- className: "flex-1 min-w-[80px] bg-transparent text-sm text-foreground outline-none placeholder:text-foreground-disabled",
26315
- value: input,
26316
- placeholder: tags.length === 0 ? field.placeholder ?? "Add tags…" : "",
26317
- disabled: isDisabled,
26318
- list: `tags-suggestions-${field.key}`,
26319
- onChange: (e) => setInput(e.target.value),
26320
- onKeyDown: handleKeyDown,
26321
- onBlur: () => {
26322
- if (input.trim()) addTag(input);
26323
- }
26324
- }),
26325
- field.suggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
26326
- id: `tags-suggestions-${field.key}`,
26327
- children: field.suggestions.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: s }, s))
26328
- })
26329
- ]
27046
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
27047
+ mode: "multi",
27048
+ options,
27049
+ value: tags,
27050
+ onChange: (next) => onChange(next),
27051
+ disabled: isDisabled,
27052
+ creatable,
27053
+ ...field.createLabel !== void 0 ? { createLabel: field.createLabel } : {},
27054
+ ...field.maxTags !== void 0 ? { maxItems: field.maxTags } : {},
27055
+ placeholder: field.placeholder ?? "Search or add…",
27056
+ testId: `config-tags-${field.key}`
26330
27057
  })
26331
27058
  });
26332
27059
  }
@@ -27558,6 +28285,20 @@ function FormField({ field, values, onChange, disabled, translationFn, onTestFie
27558
28285
  disabled,
27559
28286
  translationFn
27560
28287
  });
28288
+ case "notification-target-select": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigNotificationTargetSelectFieldView, {
28289
+ field,
28290
+ value,
28291
+ onChange: handleChange,
28292
+ disabled,
28293
+ translationFn
28294
+ });
28295
+ case "notification-target-multiselect": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigNotificationTargetMultiSelectFieldView, {
28296
+ field,
28297
+ value,
28298
+ onChange: handleChange,
28299
+ disabled,
28300
+ translationFn
28301
+ });
27561
28302
  case "probe": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeField, {
27562
28303
  field,
27563
28304
  value,
@@ -29090,9 +29831,6 @@ var DialogContent = (0, react$1.forwardRef)(({ className, width, placement, scro
29090
29831
  if (open && !el.open) el.showModal();
29091
29832
  if (!open && el.open) el.close();
29092
29833
  }, [open, dialogRef]);
29093
- const handleClick = (e) => {
29094
- if (e.target === e.currentTarget) setOpen(false);
29095
- };
29096
29834
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
29097
29835
  ref: dialogRef,
29098
29836
  id: contentId,
@@ -29101,7 +29839,6 @@ var DialogContent = (0, react$1.forwardRef)(({ className, width, placement, scro
29101
29839
  placement,
29102
29840
  scroll
29103
29841
  }), className),
29104
- onClick: handleClick,
29105
29842
  onClose: () => setOpen(false),
29106
29843
  ...props,
29107
29844
  children
@@ -39298,7 +40035,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
39298
40035
  value: e.target.value
39299
40036
  } });
39300
40037
  },
39301
- className: cn("w-full max-w-full min-w-0 truncate rounded border border-border bg-surface px-1 py-0.5 text-xs", busy && "opacity-40 cursor-not-allowed"),
40038
+ className: cn("w-full max-w-full min-w-0 truncate rounded bg-transparent px-0.5 py-0.5 text-xs", busy && "opacity-40 cursor-not-allowed"),
39302
40039
  children: slice.options.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
39303
40040
  value: opt,
39304
40041
  children: opt
@@ -40377,12 +41114,13 @@ function BatteryBar({ percent }) {
40377
41114
  });
40378
41115
  }
40379
41116
  function SensorHeroInner({ trpc, device }) {
40380
- const roleDescriptor = device.role != null ? ROLE_DESCRIPTOR[device.role] : void 0;
41117
+ const roleDescriptor = device.role != null ? ROLE_DESCRIPTOR[device.role] : ROLE_DESCRIPTOR["temperature-sensor"];
40381
41118
  const isBattery = isBatterySensorRole(device.role);
40382
41119
  const { data: roleDefaultsData } = useDeviceManagerGetRoleDisplayDefaults({}, { staleTime: 6e4 });
40383
41120
  const dev = useDeviceProxy(trpc, device.id);
40384
41121
  const numericSlice = useDeviceStateSlice(dev !== null && roleDescriptor !== void 0 ? roleDescriptor.getSliceHandle(dev.state) : void 0);
40385
41122
  const binaryBatterySlice = useDeviceStateSlice(dev !== null && isBattery ? dev.state.binary : void 0);
41123
+ const occupancySlice = useDeviceStateSlice(dev !== null && !isBattery ? dev.state.binary : void 0);
40386
41124
  const useBinaryBattery = isBattery && roleDescriptor?.kind === "numeric" && !hasBatteryNumericValue(numericSlice, roleDescriptor.field) && binaryBatterySlice !== void 0;
40387
41125
  const descriptor = useBinaryBattery ? BINARY_BATTERY_DESCRIPTOR : roleDescriptor;
40388
41126
  const slice = useBinaryBattery ? binaryBatterySlice : numericSlice;
@@ -40406,12 +41144,14 @@ function SensorHeroInner({ trpc, device }) {
40406
41144
  const resolved = resolveSensorDisplay({
40407
41145
  display: device.display,
40408
41146
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
40409
- roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : void 0,
41147
+ roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : roleDefaultsData?.defaults["temperature-sensor"],
40410
41148
  sliceUnit: liveUnit,
40411
41149
  slicePrecision,
40412
41150
  canonicalUnit: descriptor.canonicalUnit
40413
41151
  });
40414
41152
  const unit = resolved.unit;
41153
+ const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
41154
+ const occupancyOn = occupancySlice !== void 0 && isBinarySliceValue(occupancySlice) && typeof occupancySlice["on"] === "boolean" ? Boolean(occupancySlice["on"]) : null;
40415
41155
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
40416
41156
  className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
40417
41157
  children: [
@@ -40419,13 +41159,18 @@ function SensorHeroInner({ trpc, device }) {
40419
41159
  className: "flex items-baseline gap-1",
40420
41160
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
40421
41161
  className: "text-3xl font-semibold tabular-nums text-foreground",
40422
- children: formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision)
41162
+ children: formatted
40423
41163
  }), unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
40424
41164
  className: "text-base text-foreground-subtle",
40425
41165
  children: unit
40426
41166
  })]
40427
41167
  }),
40428
41168
  isBattery && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBar, { percent: rawValue }),
41169
+ occupancyOn !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
41170
+ className: cn("inline-flex items-center rounded px-2 py-0.5 text-xs font-medium", occupancyOn ? "bg-warning/15 text-warning" : "bg-surface-subtle text-foreground-subtle"),
41171
+ "data-testid": "sensor-hero-occupancy",
41172
+ children: occupancyOn ? "Occupied" : "Clear"
41173
+ }),
40429
41174
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
40430
41175
  className: "text-sm text-foreground-subtle",
40431
41176
  children: device.name
@@ -48255,11 +49000,11 @@ function useDeviceAdminLink(trpc, deviceId) {
48255
49000
  cap: (d) => d.deviceAdminLink,
48256
49001
  slice: () => void 0
48257
49002
  });
48258
- const bound = dev !== null && cap !== void 0 && cap !== null;
49003
+ const capBound = dev !== null && cap !== void 0 && cap !== null && (dev.binding?.entries.some((e) => e.capName === "device-admin-link") ?? true);
48259
49004
  const [state, setState] = (0, react$1.useState)({ kind: "pending" });
48260
49005
  (0, react$1.useEffect)(() => {
48261
- if (!bound || cap === void 0 || cap === null) {
48262
- setState({ kind: "pending" });
49006
+ if (!capBound || cap === void 0 || cap === null) {
49007
+ setState({ kind: "none" });
48263
49008
  return;
48264
49009
  }
48265
49010
  let live = true;
@@ -48280,7 +49025,7 @@ function useDeviceAdminLink(trpc, deviceId) {
48280
49025
  live = false;
48281
49026
  };
48282
49027
  }, [
48283
- bound,
49028
+ capBound,
48284
49029
  cap,
48285
49030
  deviceId
48286
49031
  ]);
@@ -49354,6 +50099,51 @@ function DeviceItemPreview({ trpc, device, status, enabled, showStatusPills = fa
49354
50099
  });
49355
50100
  }
49356
50101
  //#endregion
50102
+ //#region src/contexts/device-quick-detail.tsx
50103
+ /**
50104
+ * Optional host-provided "open quick detail for this device id" callback.
50105
+ *
50106
+ * The devices table (`DeviceItem` / `DeviceList`) draws a discreet quick-
50107
+ * detail glyph whenever a host has published a handler — admin-ui mounts
50108
+ * ONE provider at the app shell with the modal. Call sites never wire the
50109
+ * button themselves; absence of a provider means no glyph (unit tests,
50110
+ * embeds that do not host the modal).
50111
+ */
50112
+ var DeviceQuickDetailContext = createSharedContext("camstack:device-quick-detail", null);
50113
+ function DeviceQuickDetailProvider({ open, children }) {
50114
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceQuickDetailContext.Provider, {
50115
+ value: open,
50116
+ children
50117
+ });
50118
+ }
50119
+ /** The host's open callback, or null when no provider is mounted. */
50120
+ function useDeviceQuickDetailOpen() {
50121
+ return (0, react$1.useContext)(DeviceQuickDetailContext);
50122
+ }
50123
+ //#endregion
50124
+ //#region src/composites/device-item/quick-detail-action.tsx
50125
+ /**
50126
+ * Built-in row glyph for the host's quick-detail modal. Renders nothing
50127
+ * unless a `DeviceQuickDetailProvider` is mounted above — same "draw
50128
+ * nothing rather than something dead" rule as `AdminLinkQuickAction`.
50129
+ */
50130
+ function DeviceQuickDetailAction({ deviceId, className }) {
50131
+ const open = useDeviceQuickDetailOpen();
50132
+ if (open === null) return null;
50133
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
50134
+ type: "button",
50135
+ onClick: (e) => {
50136
+ e.stopPropagation();
50137
+ open(deviceId);
50138
+ },
50139
+ className: cn("inline-flex items-center justify-center rounded text-foreground-subtle hover:text-foreground transition-colors flex-shrink-0", className),
50140
+ title: "Quick detail",
50141
+ "aria-label": "Quick detail",
50142
+ "data-testid": "device-quick-detail",
50143
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Settings, { className: "h-3 w-3" })
50144
+ });
50145
+ }
50146
+ //#endregion
49357
50147
  //#region src/composites/device-item/status-dot.tsx
49358
50148
  /** The states that breathe (are "alive"). Offline + disabled are static. */
49359
50149
  function isAlive(status) {
@@ -49631,6 +50421,7 @@ function DeviceItem(props) {
49631
50421
  flags,
49632
50422
  controlPlacement: "right"
49633
50423
  }),
50424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceQuickDetailAction, { deviceId: device.id }),
49634
50425
  renderRowAction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49635
50426
  onClick: (e) => e.stopPropagation(),
49636
50427
  children: renderRowAction(device)
@@ -49713,6 +50504,7 @@ function DeviceItem(props) {
49713
50504
  flags,
49714
50505
  controlPlacement: "right"
49715
50506
  }),
50507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceQuickDetailAction, { deviceId: device.id }),
49716
50508
  renderRowAction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49717
50509
  onClick: (e) => e.stopPropagation(),
49718
50510
  children: renderRowAction(device)
@@ -49814,6 +50606,7 @@ function DeviceItemTableRow(props) {
49814
50606
  enabled: showSnapshot && isCamera,
49815
50607
  showStatusPills: true
49816
50608
  }),
50609
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceQuickDetailAction, { deviceId: device.id }),
49817
50610
  renderRowAction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49818
50611
  onClick: (e) => e.stopPropagation(),
49819
50612
  children: renderRowAction(device)
@@ -50911,10 +51704,10 @@ function useDeviceListRealtimeSync() {
50911
51704
  * `renderItem`.
50912
51705
  */
50913
51706
  function GenericModeList(props) {
50914
- const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize = 100, toolbar } = props;
51707
+ const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize = 100, toolbar } = props;
50915
51708
  const [searchInput, setSearchInput] = (0, react$1.useState)("");
50916
51709
  const debouncedSearch = useDebouncedString(searchInput, 300);
50917
- const [typeFilters, setTypeFilters] = (0, react$1.useState)([]);
51710
+ const [typeFilters, setTypeFilters] = (0, react$1.useState)(() => [...initialTypeFilters]);
50918
51711
  const [page, setPage] = (0, react$1.useState)(1);
50919
51712
  (0, react$1.useEffect)(() => {
50920
51713
  setPage(1);
@@ -55780,39 +56573,6 @@ function VersionBadge({ version, preRelease, className }) {
55780
56573
  });
55781
56574
  }
55782
56575
  //#endregion
55783
- //#region src/composites/device-picker/SelectedChip.tsx
55784
- /**
55785
- * Removable selection chip + the search-box shell shared by DevicePicker and
55786
- * SearchableMultiSelectField (admin-ui). Lives here so addon MFEs can render
55787
- * the same picker without importing admin-ui.
55788
- */
55789
- /** The search box's shell — shared with multiselect fields that mirror this picker. */
55790
- var INPUT_SHELL = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
55791
- /** One selected entry as a removable chip. */
55792
- function SelectedChip({ label, onRemove, warning = false, iconId, testId }) {
55793
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
55794
- "data-testid": testId,
55795
- className: warning ? "inline-flex items-center gap-1 rounded-md bg-warning/10 px-1.5 py-0.5 text-[11px] text-warning max-w-full" : "inline-flex items-center gap-1 rounded-md bg-primary/10 px-1.5 py-0.5 text-[11px] text-primary max-w-full",
55796
- children: [
55797
- iconId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventKindGlyph, {
55798
- iconId,
55799
- size: 12
55800
- }),
55801
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55802
- className: "truncate",
55803
- children: label
55804
- }),
55805
- onRemove !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55806
- type: "button",
55807
- className: "shrink-0 hover:text-danger",
55808
- "aria-label": `Remove ${label}`,
55809
- onClick: onRemove,
55810
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
55811
- })
55812
- ]
55813
- });
55814
- }
55815
- //#endregion
55816
56576
  //#region src/composites/device-picker/device-picker-model.ts
55817
56577
  /** Numbers numerically, anything else by its text — the name tie-break. */
55818
56578
  function compareIds(a, b) {
@@ -58190,6 +58950,8 @@ exports.CodeBlock = CodeBlock;
58190
58950
  exports.CollapsibleCard = CollapsibleCard;
58191
58951
  exports.ConfigFormBuilder = ConfigFormBuilder;
58192
58952
  exports.ConfigFormField = FormField;
58953
+ exports.ConfigNotificationTargetMultiSelectFieldView = ConfigNotificationTargetMultiSelectFieldView;
58954
+ exports.ConfigNotificationTargetSelectFieldView = ConfigNotificationTargetSelectFieldView;
58193
58955
  exports.ConfigSchemaField = ConfigSchemaField;
58194
58956
  exports.ConfirmActionButton = ConfirmActionButton;
58195
58957
  exports.ConfirmDialogProvider = ConfirmDialogProvider;
@@ -58231,6 +58993,7 @@ exports.DeviceItem = DeviceItem;
58231
58993
  exports.DeviceList = DeviceList;
58232
58994
  exports.DeviceMultiSelectField = DeviceMultiSelectField;
58233
58995
  exports.DevicePicker = DevicePicker;
58996
+ exports.DeviceQuickDetailProvider = DeviceQuickDetailProvider;
58234
58997
  exports.DeviceSelectField = DeviceSelectField;
58235
58998
  exports.DeviceSelectorPicker = DeviceSelectorPicker;
58236
58999
  exports.DeviceStepMatrix = DeviceStepMatrix;
@@ -58324,7 +59087,11 @@ exports.NetworkLinkBadge = NetworkLinkBadge;
58324
59087
  exports.NodeMultiSelectField = NodeMultiSelectField;
58325
59088
  exports.NodePicker = NodePicker;
58326
59089
  exports.NodeSelectField = NodeSelectField;
59090
+ exports.NotificationTargetSelectField = NotificationTargetSelectField;
59091
+ exports.NotificationTargetsField = NotificationTargetsField;
59092
+ exports.OPTION_SELECT_SEARCH_MIN_OPTIONS = OPTION_SELECT_SEARCH_MIN_OPTIONS;
58327
59093
  exports.OfflineBadge = OfflineBadge;
59094
+ exports.OptionSelect = OptionSelect;
58328
59095
  exports.PHASE_CONFIG = PHASE_CONFIG;
58329
59096
  exports.PICKER_CHIP_LIMIT = PICKER_CHIP_LIMIT;
58330
59097
  exports.PICKER_DEFAULT_GROUP_BY = PICKER_DEFAULT_GROUP_BY;
@@ -58462,6 +59229,7 @@ exports.ariaSortForColumn = ariaSortForColumn;
58462
59229
  exports.badgeFor = badgeFor;
58463
59230
  exports.badgeFor$1 = badgeFor;
58464
59231
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
59232
+ exports.catalogHasDraft = catalogHasDraft;
58465
59233
  exports.childEntityId = childEntityId;
58466
59234
  exports.childListName = childListName;
58467
59235
  exports.chipOverflow = chipOverflow;
@@ -58494,11 +59262,13 @@ exports.deviceTypeLabel = deviceTypeLabel;
58494
59262
  exports.deviceTypeMeta = deviceTypeMeta;
58495
59263
  exports.deviceTypeMetaOf = deviceTypeMetaOf;
58496
59264
  exports.devicesToOptions = devicesToOptions;
59265
+ exports.draftFromQuery = draftFromQuery;
58497
59266
  exports.emptyDevicesWidens = emptyDevicesWidens;
58498
59267
  exports.ensureMfHostInit = ensureMfHostInit;
58499
59268
  exports.eventKindLabel = eventKindLabel;
58500
59269
  exports.filterDataTableRows = filterDataTableRows;
58501
59270
  exports.filterDeviceOptions = filterDeviceOptions;
59271
+ exports.filterOptionSelectOptions = filterOptions;
58502
59272
  exports.filterPickerDevices = filterPickerDevices;
58503
59273
  exports.findTimezone = _camstack_types.findTimezone;
58504
59274
  exports.flattenPickerGrouping = flattenPickerGrouping;
@@ -58546,11 +59316,13 @@ exports.nextTableSort = nextTableSort;
58546
59316
  exports.normalizeForSearch = normalizeForSearch;
58547
59317
  exports.offListSelections = offListSelections;
58548
59318
  exports.openTopLayer = openTopLayer;
59319
+ exports.optionSelectSearchText = optionSearchText;
58549
59320
  exports.orphanDeviceLabel = orphanDeviceLabel;
58550
59321
  exports.overrideEntityIdFromLink = overrideEntityIdFromLink;
58551
59322
  exports.pageRows = pageRows;
58552
59323
  exports.parsePickerGroupBy = parsePickerGroupBy;
58553
59324
  exports.parseRecordedServerMessage = parseRecordedServerMessage;
59325
+ exports.partitionOptionSelectSelected = partitionSelected;
58554
59326
  exports.pickerFacetFilterShown = pickerFacetFilterShown;
58555
59327
  exports.pickerFacetKey = pickerFacetKey;
58556
59328
  exports.pickerFacetOptions = pickerFacetOptions;
@@ -58574,6 +59346,7 @@ exports.resolveDisplayIcon = resolveDisplayIcon;
58574
59346
  exports.resolveEffectiveDefaultModel = resolveEffectiveDefaultModel;
58575
59347
  exports.resolveEffectiveStepModel = resolveEffectiveStepModel;
58576
59348
  exports.resolveEventKindIcon = resolveEventKindIcon;
59349
+ exports.resolveOptionSelectSearchable = resolveSearchable;
58577
59350
  exports.resolvePrimaryChild = resolvePrimaryChild;
58578
59351
  exports.resolveSelectedDevices = resolveSelectedDevices;
58579
59352
  exports.resolveSensorDisplay = resolveSensorDisplay;
@@ -58588,6 +59361,8 @@ exports.selectedDeviceOptions = selectedDeviceOptions;
58588
59361
  exports.serializeRecordedCommand = serializeRecordedCommand;
58589
59362
  exports.shouldCommit = shouldCommit;
58590
59363
  exports.shouldEmit = shouldEmit;
59364
+ exports.shouldShowAddNewFooter = shouldShowAddNewFooter;
59365
+ exports.shouldShowUseQueryRow = shouldShowUseQueryRow;
58591
59366
  exports.shouldUseSingleNode = shouldUseSingleNode;
58592
59367
  exports.showsNoCamerasHint = showsNoCamerasHint;
58593
59368
  exports.sortRows = sortRows;
@@ -58970,6 +59745,7 @@ exports.useDeviceProviderSupportsManualCreation = useDeviceProviderSupportsManua
58970
59745
  exports.useDeviceProviderTestCreationField = useDeviceProviderTestCreationField;
58971
59746
  exports.useDeviceProxy = useDeviceProxy;
58972
59747
  exports.useDeviceProxy$1 = useDeviceProxy;
59748
+ exports.useDeviceQuickDetailOpen = useDeviceQuickDetailOpen;
58973
59749
  exports.useDeviceSnapshot = useDeviceSnapshot;
58974
59750
  exports.useDeviceSnapshotImage = useDeviceSnapshotImage;
58975
59751
  exports.useDeviceSnapshotImage$1 = useDeviceSnapshotImage;
@@ -59186,8 +59962,10 @@ exports.useNodesTopology = useNodesTopology;
59186
59962
  exports.useNodesUndeployAddon = useNodesUndeployAddon;
59187
59963
  exports.useNotificationOutputDeleteTarget = useNotificationOutputDeleteTarget;
59188
59964
  exports.useNotificationOutputDiscoverTargets = useNotificationOutputDiscoverTargets;
59965
+ exports.useNotificationOutputGetTarget = useNotificationOutputGetTarget;
59189
59966
  exports.useNotificationOutputListTargetKinds = useNotificationOutputListTargetKinds;
59190
59967
  exports.useNotificationOutputListTargets = useNotificationOutputListTargets;
59968
+ exports.useNotificationOutputProbeTargetConfig = useNotificationOutputProbeTargetConfig;
59191
59969
  exports.useNotificationOutputSend = useNotificationOutputSend;
59192
59970
  exports.useNotificationOutputSetTargetEnabled = useNotificationOutputSetTargetEnabled;
59193
59971
  exports.useNotificationOutputTestTarget = useNotificationOutputTestTarget;