@camstack/ui-library 1.2.20 → 1.2.22

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.
@@ -6,17 +6,18 @@ import { DeviceControlProps } from './control-registry';
6
6
  * Drops into a 200 px control column without towering over sibling rows.
7
7
  */
8
8
  /**
9
- * The row/card-sized alarm control.
9
+ * The row/card-sized alarm control — ONE LINE, always.
10
10
  *
11
- * It leads with the STATE, and that is the fix rather than the decoration: the
12
- * first version rendered only the segment strip, so the sole indication that a
13
- * panel was armed was which of four buttons happened to be highlighted — and in
14
- * a grid card the strip overflowed and cut the highlighted one off entirely. A
15
- * security control whose state you have to infer from a button is not showing
16
- * you its state.
11
+ * Two earlier attempts were wrong in instructive ways. The first rendered only
12
+ * the segment strip, so the sole sign a panel was armed was which of four
13
+ * buttons happened to be highlighted — and in a grid card the strip overflowed
14
+ * and cut that button off. The second let the strip WRAP, which stopped the
15
+ * overflow by making the row two and three lines tall.
17
16
  *
18
- * The strip is allowed to WRAP for the same reason: four modes do not fit a
19
- * card at phone width, and a truncated alarm control is worse than a tall one.
17
+ * A state badge plus four buttons does not fit a phone-width row, and no amount
18
+ * of sizing makes it. So the row shows what a row is for **the state** — and
19
+ * offers the modes through a select, which is one line at any width. The four
20
+ * segments live in the hero, where there is room for them.
20
21
  */
21
22
  export declare function AlarmInlineControl({ trpc, deviceId, optimistic }: DeviceControlProps): ReactNode;
22
23
  /**
package/dist/index.cjs CHANGED
@@ -26770,7 +26770,7 @@ function AlarmGlyph({ icon, sizeClass = "h-8 w-8" }) {
26770
26770
  "aria-hidden": "true"
26771
26771
  });
26772
26772
  }
26773
- function Segment({ label, active, busy, onSelect, compact = false }) {
26773
+ function Segment({ label, active, busy, onSelect }) {
26774
26774
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
26775
26775
  type: "button",
26776
26776
  "aria-label": label,
@@ -26780,7 +26780,7 @@ function Segment({ label, active, busy, onSelect, compact = false }) {
26780
26780
  e.stopPropagation();
26781
26781
  if (!busy) onSelect();
26782
26782
  },
26783
- className: cn("rounded-md font-medium transition-colors", compact ? "px-2 py-1 text-xs" : "flex-1 px-3 py-2 text-sm", active ? "bg-primary text-primary-foreground shadow-sm" : "text-foreground-muted hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
26783
+ className: cn("flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors", active ? "bg-primary text-primary-foreground shadow-sm" : "text-foreground-muted hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
26784
26784
  children: label
26785
26785
  });
26786
26786
  }
@@ -26790,23 +26790,21 @@ function Segment({ label, active, busy, onSelect, compact = false }) {
26790
26790
  * segment row. Transitioning states (arming/disarming) leave NO segment
26791
26791
  * highlighted so the Disarm button does not look active mid-arm.
26792
26792
  */
26793
- function AlarmSegments({ state, availableModes, busy, onDisarm, onArm, compact = false }) {
26793
+ function AlarmSegments({ state, availableModes, busy, onDisarm, onArm }) {
26794
26794
  const armed = isArmedState(state);
26795
26795
  const transitioning = isTransitioningState(state);
26796
26796
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
26797
- className: cn("flex min-w-0 flex-wrap items-stretch gap-1 rounded-lg border border-border bg-surface-subtle", compact ? "p-0.5" : "p-1"),
26797
+ className: cn("flex min-w-0 items-stretch gap-1 rounded-lg border border-border bg-surface-subtle p-1"),
26798
26798
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Segment, {
26799
26799
  label: "Disarm",
26800
26800
  active: !armed && !transitioning,
26801
26801
  busy,
26802
- onSelect: onDisarm,
26803
- compact
26802
+ onSelect: onDisarm
26804
26803
  }), availableModes.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Segment, {
26805
26804
  label: ARM_MODE_LABEL[mode],
26806
26805
  active: !transitioning && state === `armed_${mode}`,
26807
26806
  busy,
26808
- onSelect: () => onArm(mode),
26809
- compact
26807
+ onSelect: () => onArm(mode)
26810
26808
  }, mode))]
26811
26809
  });
26812
26810
  }
@@ -26848,38 +26846,52 @@ function useAlarmControl(trpc, deviceId, optimistic) {
26848
26846
  * Drops into a 200 px control column without towering over sibling rows.
26849
26847
  */
26850
26848
  /**
26851
- * The row/card-sized alarm control.
26849
+ * The row/card-sized alarm control — ONE LINE, always.
26852
26850
  *
26853
- * It leads with the STATE, and that is the fix rather than the decoration: the
26854
- * first version rendered only the segment strip, so the sole indication that a
26855
- * panel was armed was which of four buttons happened to be highlighted — and in
26856
- * a grid card the strip overflowed and cut the highlighted one off entirely. A
26857
- * security control whose state you have to infer from a button is not showing
26858
- * you its state.
26851
+ * Two earlier attempts were wrong in instructive ways. The first rendered only
26852
+ * the segment strip, so the sole sign a panel was armed was which of four
26853
+ * buttons happened to be highlighted — and in a grid card the strip overflowed
26854
+ * and cut that button off. The second let the strip WRAP, which stopped the
26855
+ * overflow by making the row two and three lines tall.
26859
26856
  *
26860
- * The strip is allowed to WRAP for the same reason: four modes do not fit a
26861
- * card at phone width, and a truncated alarm control is worse than a tall one.
26857
+ * A state badge plus four buttons does not fit a phone-width row, and no amount
26858
+ * of sizing makes it. So the row shows what a row is for **the state** — and
26859
+ * offers the modes through a select, which is one line at any width. The four
26860
+ * segments live in the hero, where there is room for them.
26862
26861
  */
26863
26862
  function AlarmInlineControl({ trpc, deviceId, optimistic }) {
26864
26863
  const { display, capPresent, busy, handleDisarm, handleArm } = useAlarmControl(trpc, deviceId, optimistic);
26865
26864
  if (!capPresent) return null;
26866
26865
  if (display === void 0) return null;
26867
26866
  const config = ALARM_STATE_CONFIG[display.state];
26867
+ const current = isArmedState(display.state) ? display.state.replace("armed_", "") : "";
26868
26868
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
26869
- className: "flex min-w-0 flex-wrap items-center gap-2",
26869
+ className: "flex min-w-0 items-center gap-2",
26870
26870
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
26871
- className: cn("inline-flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] font-medium", config.colorClass),
26871
+ className: cn("inline-flex shrink-0 items-center gap-1 rounded-md border px-1.5 py-0.5 text-[11px] font-medium whitespace-nowrap", config.colorClass, isTransitioningState(display.state) && "animate-pulse"),
26872
26872
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlarmGlyph, {
26873
26873
  icon: config.icon,
26874
- sizeClass: "h-3.5 w-3.5"
26874
+ sizeClass: "h-3 w-3"
26875
26875
  }), config.label]
26876
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlarmSegments, {
26877
- state: display.state,
26878
- availableModes: display.availableModes,
26879
- busy,
26880
- onDisarm: handleDisarm,
26881
- onArm: handleArm,
26882
- compact: true
26876
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
26877
+ "aria-label": "Alarm mode",
26878
+ disabled: busy,
26879
+ value: current,
26880
+ onClick: (e) => e.stopPropagation(),
26881
+ onChange: (e) => {
26882
+ e.stopPropagation();
26883
+ const next = e.target.value;
26884
+ if (next === "") handleDisarm();
26885
+ else handleArm(next);
26886
+ },
26887
+ className: cn("min-w-0 shrink rounded-md border border-border bg-surface-subtle px-1.5 py-0.5 text-xs text-foreground-muted", busy && "cursor-not-allowed opacity-40"),
26888
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
26889
+ value: "",
26890
+ children: "Disarm"
26891
+ }), display.availableModes.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
26892
+ value: mode,
26893
+ children: ARM_MODE_LABEL[mode]
26894
+ }, mode))]
26883
26895
  })]
26884
26896
  });
26885
26897
  }
@@ -26910,10 +26922,10 @@ function AlarmHeroCard({ trpc, deviceId, optimistic }) {
26910
26922
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
26911
26923
  "data-testid": "alarm-state",
26912
26924
  "data-state": display.state,
26913
- className: cn("flex h-16 w-16 items-center justify-center rounded-full border-2", config.colorClass),
26925
+ className: cn("flex h-16 w-16 items-center justify-center rounded-full border-2", config.colorClass, isTransitioningState(display.state) && "animate-pulse"),
26914
26926
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlarmGlyph, { icon: config.icon })
26915
26927
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
26916
- className: "text-sm font-medium text-foreground-muted",
26928
+ className: cn("text-sm font-medium", config.colorClass, "border-0 bg-transparent"),
26917
26929
  children: config.label
26918
26930
  })]
26919
26931
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlarmSegments, {
package/dist/index.js CHANGED
@@ -26746,7 +26746,7 @@ function AlarmGlyph({ icon, sizeClass = "h-8 w-8" }) {
26746
26746
  "aria-hidden": "true"
26747
26747
  });
26748
26748
  }
26749
- function Segment({ label, active, busy, onSelect, compact = false }) {
26749
+ function Segment({ label, active, busy, onSelect }) {
26750
26750
  return /* @__PURE__ */ jsx("button", {
26751
26751
  type: "button",
26752
26752
  "aria-label": label,
@@ -26756,7 +26756,7 @@ function Segment({ label, active, busy, onSelect, compact = false }) {
26756
26756
  e.stopPropagation();
26757
26757
  if (!busy) onSelect();
26758
26758
  },
26759
- className: cn("rounded-md font-medium transition-colors", compact ? "px-2 py-1 text-xs" : "flex-1 px-3 py-2 text-sm", active ? "bg-primary text-primary-foreground shadow-sm" : "text-foreground-muted hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
26759
+ className: cn("flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors", active ? "bg-primary text-primary-foreground shadow-sm" : "text-foreground-muted hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
26760
26760
  children: label
26761
26761
  });
26762
26762
  }
@@ -26766,23 +26766,21 @@ function Segment({ label, active, busy, onSelect, compact = false }) {
26766
26766
  * segment row. Transitioning states (arming/disarming) leave NO segment
26767
26767
  * highlighted so the Disarm button does not look active mid-arm.
26768
26768
  */
26769
- function AlarmSegments({ state, availableModes, busy, onDisarm, onArm, compact = false }) {
26769
+ function AlarmSegments({ state, availableModes, busy, onDisarm, onArm }) {
26770
26770
  const armed = isArmedState(state);
26771
26771
  const transitioning = isTransitioningState(state);
26772
26772
  return /* @__PURE__ */ jsxs("div", {
26773
- className: cn("flex min-w-0 flex-wrap items-stretch gap-1 rounded-lg border border-border bg-surface-subtle", compact ? "p-0.5" : "p-1"),
26773
+ className: cn("flex min-w-0 items-stretch gap-1 rounded-lg border border-border bg-surface-subtle p-1"),
26774
26774
  children: [/* @__PURE__ */ jsx(Segment, {
26775
26775
  label: "Disarm",
26776
26776
  active: !armed && !transitioning,
26777
26777
  busy,
26778
- onSelect: onDisarm,
26779
- compact
26778
+ onSelect: onDisarm
26780
26779
  }), availableModes.map((mode) => /* @__PURE__ */ jsx(Segment, {
26781
26780
  label: ARM_MODE_LABEL[mode],
26782
26781
  active: !transitioning && state === `armed_${mode}`,
26783
26782
  busy,
26784
- onSelect: () => onArm(mode),
26785
- compact
26783
+ onSelect: () => onArm(mode)
26786
26784
  }, mode))]
26787
26785
  });
26788
26786
  }
@@ -26824,38 +26822,52 @@ function useAlarmControl(trpc, deviceId, optimistic) {
26824
26822
  * Drops into a 200 px control column without towering over sibling rows.
26825
26823
  */
26826
26824
  /**
26827
- * The row/card-sized alarm control.
26825
+ * The row/card-sized alarm control — ONE LINE, always.
26828
26826
  *
26829
- * It leads with the STATE, and that is the fix rather than the decoration: the
26830
- * first version rendered only the segment strip, so the sole indication that a
26831
- * panel was armed was which of four buttons happened to be highlighted — and in
26832
- * a grid card the strip overflowed and cut the highlighted one off entirely. A
26833
- * security control whose state you have to infer from a button is not showing
26834
- * you its state.
26827
+ * Two earlier attempts were wrong in instructive ways. The first rendered only
26828
+ * the segment strip, so the sole sign a panel was armed was which of four
26829
+ * buttons happened to be highlighted — and in a grid card the strip overflowed
26830
+ * and cut that button off. The second let the strip WRAP, which stopped the
26831
+ * overflow by making the row two and three lines tall.
26835
26832
  *
26836
- * The strip is allowed to WRAP for the same reason: four modes do not fit a
26837
- * card at phone width, and a truncated alarm control is worse than a tall one.
26833
+ * A state badge plus four buttons does not fit a phone-width row, and no amount
26834
+ * of sizing makes it. So the row shows what a row is for **the state** — and
26835
+ * offers the modes through a select, which is one line at any width. The four
26836
+ * segments live in the hero, where there is room for them.
26838
26837
  */
26839
26838
  function AlarmInlineControl({ trpc, deviceId, optimistic }) {
26840
26839
  const { display, capPresent, busy, handleDisarm, handleArm } = useAlarmControl(trpc, deviceId, optimistic);
26841
26840
  if (!capPresent) return null;
26842
26841
  if (display === void 0) return null;
26843
26842
  const config = ALARM_STATE_CONFIG[display.state];
26843
+ const current = isArmedState(display.state) ? display.state.replace("armed_", "") : "";
26844
26844
  return /* @__PURE__ */ jsxs("div", {
26845
- className: "flex min-w-0 flex-wrap items-center gap-2",
26845
+ className: "flex min-w-0 items-center gap-2",
26846
26846
  children: [/* @__PURE__ */ jsxs("span", {
26847
- className: cn("inline-flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] font-medium", config.colorClass),
26847
+ className: cn("inline-flex shrink-0 items-center gap-1 rounded-md border px-1.5 py-0.5 text-[11px] font-medium whitespace-nowrap", config.colorClass, isTransitioningState(display.state) && "animate-pulse"),
26848
26848
  children: [/* @__PURE__ */ jsx(AlarmGlyph, {
26849
26849
  icon: config.icon,
26850
- sizeClass: "h-3.5 w-3.5"
26850
+ sizeClass: "h-3 w-3"
26851
26851
  }), config.label]
26852
- }), /* @__PURE__ */ jsx(AlarmSegments, {
26853
- state: display.state,
26854
- availableModes: display.availableModes,
26855
- busy,
26856
- onDisarm: handleDisarm,
26857
- onArm: handleArm,
26858
- compact: true
26852
+ }), /* @__PURE__ */ jsxs("select", {
26853
+ "aria-label": "Alarm mode",
26854
+ disabled: busy,
26855
+ value: current,
26856
+ onClick: (e) => e.stopPropagation(),
26857
+ onChange: (e) => {
26858
+ e.stopPropagation();
26859
+ const next = e.target.value;
26860
+ if (next === "") handleDisarm();
26861
+ else handleArm(next);
26862
+ },
26863
+ className: cn("min-w-0 shrink rounded-md border border-border bg-surface-subtle px-1.5 py-0.5 text-xs text-foreground-muted", busy && "cursor-not-allowed opacity-40"),
26864
+ children: [/* @__PURE__ */ jsx("option", {
26865
+ value: "",
26866
+ children: "Disarm"
26867
+ }), display.availableModes.map((mode) => /* @__PURE__ */ jsx("option", {
26868
+ value: mode,
26869
+ children: ARM_MODE_LABEL[mode]
26870
+ }, mode))]
26859
26871
  })]
26860
26872
  });
26861
26873
  }
@@ -26886,10 +26898,10 @@ function AlarmHeroCard({ trpc, deviceId, optimistic }) {
26886
26898
  children: [/* @__PURE__ */ jsx("div", {
26887
26899
  "data-testid": "alarm-state",
26888
26900
  "data-state": display.state,
26889
- className: cn("flex h-16 w-16 items-center justify-center rounded-full border-2", config.colorClass),
26901
+ className: cn("flex h-16 w-16 items-center justify-center rounded-full border-2", config.colorClass, isTransitioningState(display.state) && "animate-pulse"),
26890
26902
  children: /* @__PURE__ */ jsx(AlarmGlyph, { icon: config.icon })
26891
26903
  }), /* @__PURE__ */ jsx("span", {
26892
- className: "text-sm font-medium text-foreground-muted",
26904
+ className: cn("text-sm font-medium", config.colorClass, "border-0 bg-transparent"),
26893
26905
  children: config.label
26894
26906
  })]
26895
26907
  }), /* @__PURE__ */ jsx(AlarmSegments, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.20",
3
+ "version": "1.2.22",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",