@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/composites/device-item/quick-detail-action.d.ts +6 -0
- package/dist/composites/device-list/generic-mode.d.ts +5 -0
- package/dist/composites/device-picker/SelectedChip.d.ts +3 -1
- package/dist/composites/index.d.ts +4 -0
- package/dist/composites/notification-targets-field.d.ts +66 -0
- package/dist/composites/notification-targets-options.d.ts +11 -0
- package/dist/composites/option-select/OptionSelect.d.ts +30 -0
- package/dist/composites/option-select/index.d.ts +4 -0
- package/dist/composites/option-select/option-select-model.d.ts +56 -0
- package/dist/contexts/device-quick-detail.d.ts +9 -0
- package/dist/generated/system-hooks.d.ts +4 -0
- package/dist/index.cjs +1086 -308
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1069 -309
- package/dist/primitives/badge.d.ts +1 -1
- package/dist/primitives/button.d.ts +1 -1
- package/dist/primitives/icon-button.d.ts +1 -1
- package/package.json +1 -1
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$
|
|
12396
|
-
var DESC_CLASS$
|
|
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$
|
|
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$
|
|
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$
|
|
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:
|
|
26874
|
+
value: text,
|
|
26114
26875
|
placeholder: field.placeholder,
|
|
26115
26876
|
disabled: disabled || field.disabled,
|
|
26116
|
-
|
|
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
|
|
26268
|
-
const
|
|
26269
|
-
|
|
26270
|
-
|
|
26271
|
-
|
|
26272
|
-
|
|
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.
|
|
26295
|
-
|
|
26296
|
-
|
|
26297
|
-
|
|
26298
|
-
|
|
26299
|
-
|
|
26300
|
-
|
|
26301
|
-
|
|
26302
|
-
|
|
26303
|
-
|
|
26304
|
-
|
|
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
|
|
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] :
|
|
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] :
|
|
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:
|
|
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
|
|
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 (!
|
|
48262
|
-
setState({ kind: "
|
|
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
|
-
|
|
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;
|