@ggui-ai/ui-visual-tester 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ggui-ai/ui-visual-tester
2
2
 
3
- Behavioral validator for [ggui](https://ggui.ai)-generated component code.
3
+ Behavioral validator for [GGUI](https://ggui.ai)-generated component code.
4
4
 
5
5
  Structural validators (schema checks, wire-preservation, compile/lint)
6
6
  confirm that a component is _shaped_ correctly. They cannot tell you
@@ -20121,7 +20121,7 @@
20121
20121
  const inkAlias = "--ggui-surface-" + name + "-ink";
20122
20122
  const B = "var(" + bgAlias + ")";
20123
20123
  const I = "var(" + inkAlias + ")";
20124
- return `.${cls}{${bgAlias}:${bg};${inkAlias}:${ink};--ggui-color-onInverted:${ink}}.${cls}>*{--ggui-color-container:${B};--ggui-color-onContainer:${I};--ggui-color-elevated:${B};--ggui-color-onElevated:${I};--ggui-color-sunken:${B};--ggui-color-onSunken:${I};--ggui-color-neutral-500:${I};--ggui-color-ground:${B};--ggui-color-onGround:${I};--ggui-color-link:${accent}}@supports (color: color-mix(in srgb, red, blue)){.${cls}>*{--ggui-color-container:color-mix(in srgb, ${B} 94%, ${I});--ggui-color-elevated:color-mix(in srgb, ${B} 92%, ${I});--ggui-color-sunken:color-mix(in srgb, ${B} 88%, ${I});--ggui-color-onSunken:color-mix(in srgb, ${I} 76%, ${B});--ggui-color-neutral-500:color-mix(in srgb, ${I} 62%, ${B});--ggui-color-outline:${outlineOpen}color-mix(in srgb, ${I} 32%, ${B}));--ggui-color-outlineVariant:color-mix(in srgb, ${I} 18%, ${B})}}`;
20124
+ return `.${cls}{${bgAlias}:${bg};${inkAlias}:${ink};--ggui-color-onInverted:${ink}}.${cls}>*{--ggui-color-container:${B};--ggui-color-onContainer:${I};--ggui-color-elevated:${B};--ggui-color-onElevated:${I};--ggui-color-sunken:${B};--ggui-color-onSunken:${I};--ggui-color-neutral-500:${I};--ggui-color-ground:${B};--ggui-color-onGround:${I};--ggui-color-link:${accent};--ggui-color-controlAccentOutline:${accent}}@supports (color: color-mix(in srgb, red, blue)){.${cls}>*{--ggui-color-container:color-mix(in srgb, ${B} 94%, ${I});--ggui-color-elevated:color-mix(in srgb, ${B} 92%, ${I});--ggui-color-sunken:color-mix(in srgb, ${B} 88%, ${I});--ggui-color-onSunken:color-mix(in srgb, ${I} 76%, ${B});--ggui-color-neutral-500:color-mix(in srgb, ${I} 62%, ${B});--ggui-color-outline:${outlineOpen}color-mix(in srgb, ${I} 32%, ${B}));--ggui-color-controlOutline:${outlineOpen}color-mix(in srgb, ${I} 32%, ${B}));--ggui-color-outlineVariant:color-mix(in srgb, ${I} 18%, ${B})}}`;
20125
20125
  }
20126
20126
  var INVERTED_SCOPE_CSS = surfaceScopeCss(
20127
20127
  INVERTED_SCOPE_CLASS,
@@ -20657,7 +20657,8 @@
20657
20657
  },
20658
20658
  secondary: {
20659
20659
  backgroundColor: "var(--ggui-color-sunken, #f4f4f5)",
20660
- color: "var(--ggui-color-onSunken, #52525b)",
20660
+ // The label reads at full strength; the chip marks the rank (ggui#1566).
20661
+ color: "var(--ggui-color-onContainer, #18181b)",
20661
20662
  border: "none"
20662
20663
  },
20663
20664
  outline: {
@@ -20666,13 +20667,15 @@
20666
20667
  // label is the surface's own on-colour, never a brand-ladder stop
20667
20668
  // (ggui#1034): `primary-600` as the label read mid-grey on a monochrome
20668
20669
  // dark theme's ground — 2.41:1 / 3.5:1 on served reply chips. The brand
20669
- // stays on the border, where contrast is decorative.
20670
+ // stays on the border, and the border identifies the control, so it is the
20671
+ // first brand stop that clears 3:1 on the container (ggui#1494).
20670
20672
  color: "var(--ggui-color-onContainer, #18181b)",
20671
- border: "1px solid var(--ggui-color-primary-600, #0284c7)"
20673
+ border: "1px solid var(--ggui-color-controlAccentOutline, var(--ggui-color-primary-600, #0284c7))"
20672
20674
  },
20673
20675
  ghost: {
20674
20676
  backgroundColor: "transparent",
20675
- color: "var(--ggui-color-onSunken, #52525b)",
20677
+ // A dimmed label reads as disabled; ghost's rank is its missing chrome (ggui#1566).
20678
+ color: "var(--ggui-color-onContainer, #18181b)",
20676
20679
  border: "none"
20677
20680
  },
20678
20681
  danger: {
@@ -20738,7 +20741,7 @@
20738
20741
  ...sizeStyles2[size],
20739
20742
  width: "100%",
20740
20743
  borderRadius: "var(--ggui-shape-radius-control, var(--ggui-shape-radius-md, 8px))",
20741
- border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20744
+ border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d4d4d8))"}`,
20742
20745
  backgroundColor: disabled ? "var(--ggui-color-sunken, #fafafa)" : "var(--ggui-color-sunken, #ffffff)",
20743
20746
  color: "var(--ggui-color-onContainer, #18181b)",
20744
20747
  transition: `border-color ${motionVar.duration.base} ${motionVar.easing.standard}, box-shadow ${motionVar.duration.base} ${motionVar.easing.standard}`,
@@ -20772,7 +20775,7 @@
20772
20775
  padding: "10px 12px",
20773
20776
  width: "100%",
20774
20777
  borderRadius: "var(--ggui-shape-radius-control, var(--ggui-shape-radius-md, 8px))",
20775
- border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20778
+ border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d4d4d8))"}`,
20776
20779
  backgroundColor: disabled ? "var(--ggui-color-sunken, #fafafa)" : "var(--ggui-color-sunken, #ffffff)",
20777
20780
  color: "var(--ggui-color-onContainer, #18181b)",
20778
20781
  fontSize: "var(--ggui-font-size-sm, 14px)",
@@ -20804,17 +20807,25 @@
20804
20807
  const messageId = `${uniqueId}-message`;
20805
20808
  const hasError = Boolean(error);
20806
20809
  const hasMessage = Boolean(error || helperText);
20810
+ const startsOnPlaceholder = Boolean(placeholder) && value === void 0 && rest.defaultValue === void 0;
20811
+ const [uncontrolledValue, setUncontrolledValue] = (0, import_react7.useState)(startsOnPlaceholder ? "" : typeof rest.defaultValue === "string" ? rest.defaultValue : void 0);
20812
+ const showingPlaceholder = Boolean(placeholder) && (value ?? uncontrolledValue) === "";
20807
20813
  return (0, import_jsx_runtime11.jsxs)("div", { className, style: { display: "flex", flexDirection: "column", gap: "var(--ggui-spacing-1, 4px)", ...style }, children: [label && (0, import_jsx_runtime11.jsxs)("label", { htmlFor: selectId, style: {
20808
20814
  fontSize: "var(--ggui-font-size-sm, 14px)",
20809
20815
  fontWeight: "var(--ggui-font-weight-medium, 500)",
20810
20816
  color: "var(--ggui-color-onSunken, #52525b)"
20811
- }, children: [label, required && (0, import_jsx_runtime11.jsx)("span", { "aria-hidden": "true", style: { color: "var(--ggui-color-error-500, #ef4444)", marginLeft: "2px" }, children: "*" })] }), (0, import_jsx_runtime11.jsxs)("select", { id: selectId, value, onChange: (e) => onChange?.(e.target.value), disabled, required, "aria-invalid": hasError || void 0, "aria-describedby": hasMessage ? messageId : void 0, style: {
20817
+ }, children: [label, required && (0, import_jsx_runtime11.jsx)("span", { "aria-hidden": "true", style: { color: "var(--ggui-color-error-500, #ef4444)", marginLeft: "2px" }, children: "*" })] }), (0, import_jsx_runtime11.jsxs)("select", { id: selectId, value, ...startsOnPlaceholder ? { defaultValue: "" } : {}, onChange: (e) => {
20818
+ if (value === void 0)
20819
+ setUncontrolledValue(e.target.value);
20820
+ onChange?.(e.target.value);
20821
+ }, disabled, required, "aria-invalid": hasError || void 0, "aria-describedby": hasMessage ? messageId : void 0, style: {
20812
20822
  ...sizeStyles3[size],
20813
20823
  width: "100%",
20814
20824
  borderRadius: "var(--ggui-shape-radius-control, var(--ggui-shape-radius-md, 8px))",
20815
- border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20825
+ border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d4d4d8))"}`,
20816
20826
  backgroundColor: disabled ? "var(--ggui-color-sunken, #fafafa)" : "var(--ggui-color-sunken, #ffffff)",
20817
- color: value ? "var(--ggui-color-onContainer, #18181b)" : "var(--ggui-color-onSunken, #52525b)",
20827
+ // Muted only while the placeholder is what shows (ggui#1566, #1569).
20828
+ color: showingPlaceholder ? "var(--ggui-color-onSunken, #52525b)" : "var(--ggui-color-onContainer, #18181b)",
20818
20829
  cursor: disabled ? "not-allowed" : "pointer",
20819
20830
  appearance: "none",
20820
20831
  backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M3 4.5L6 7.5L9 4.5'/%3E%3C/svg%3E")`,
@@ -20859,7 +20870,7 @@
20859
20870
  width: "18px",
20860
20871
  height: "18px",
20861
20872
  borderRadius: "var(--ggui-shape-radius-sm, 4px)",
20862
- border: `2px solid ${checked || indeterminate ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20873
+ border: `2px solid ${checked || indeterminate ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d4d4d8))"}`,
20863
20874
  backgroundColor: checked || indeterminate ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-sunken, #ffffff)",
20864
20875
  display: "flex",
20865
20876
  alignItems: "center",
@@ -20868,7 +20879,7 @@
20868
20879
  }, children: (checked || indeterminate) && (0, import_jsx_runtime12.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", style: { color: "var(--ggui-color-onPrimary, #ffffff)" }, children: indeterminate ? (0, import_jsx_runtime12.jsx)("path", { d: "M2.5 6H9.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) : (0, import_jsx_runtime12.jsx)("path", { d: "M2.5 6L5 8.5L9.5 3.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), (label || description) && (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [label && (0, import_jsx_runtime12.jsx)("span", { style: {
20869
20880
  fontSize: "var(--ggui-font-size-sm, 14px)",
20870
20881
  fontWeight: "var(--ggui-font-weight-medium, 500)",
20871
- color: "var(--ggui-color-onSunken, #52525b)"
20882
+ color: "var(--ggui-color-onContainer, #18181b)"
20872
20883
  }, children: label }), description && (0, import_jsx_runtime12.jsx)("span", { style: {
20873
20884
  fontSize: "var(--ggui-font-size-xs, 12px)",
20874
20885
  color: "var(--ggui-color-onSunken, #52525b)"
@@ -20904,7 +20915,7 @@
20904
20915
  width: config.width,
20905
20916
  height: config.height,
20906
20917
  borderRadius: "9999px",
20907
- backgroundColor: checked ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-outline, #d4d4d8)",
20918
+ backgroundColor: checked ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d4d4d8))",
20908
20919
  position: "relative",
20909
20920
  transition: `background-color ${motionVar.duration.base} ${motionVar.easing.standard}, box-shadow ${motionVar.duration.base} ${motionVar.easing.standard}`,
20910
20921
  flexShrink: 0,
@@ -20923,7 +20934,7 @@
20923
20934
  } }) }), label && (0, import_jsx_runtime13.jsx)("span", { style: {
20924
20935
  fontSize: "var(--ggui-font-size-sm, 14px)",
20925
20936
  fontWeight: "var(--ggui-font-weight-medium, 500)",
20926
- color: "var(--ggui-color-onSunken, #52525b)"
20937
+ color: "var(--ggui-color-onContainer, #18181b)"
20927
20938
  }, children: label })] });
20928
20939
  }
20929
20940
 
@@ -20971,7 +20982,7 @@
20971
20982
  width: "18px",
20972
20983
  height: "18px",
20973
20984
  borderRadius: "var(--ggui-shape-radius-full, 9999px)",
20974
- border: `2px solid ${isSelected ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20985
+ border: `2px solid ${isSelected ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d4d4d8))"}`,
20975
20986
  backgroundColor: "var(--ggui-color-sunken, #ffffff)",
20976
20987
  display: "flex",
20977
20988
  alignItems: "center",
@@ -20985,7 +20996,7 @@
20985
20996
  } }) })] }), (0, import_jsx_runtime14.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [(0, import_jsx_runtime14.jsx)("span", { style: {
20986
20997
  fontSize: "var(--ggui-font-size-sm, 14px)",
20987
20998
  fontWeight: "var(--ggui-font-weight-medium, 500)",
20988
- color: "var(--ggui-color-onSunken, #52525b)"
20999
+ color: "var(--ggui-color-onContainer, #18181b)"
20989
21000
  }, children: option.label }), option.description && (0, import_jsx_runtime14.jsx)("span", { style: {
20990
21001
  fontSize: "var(--ggui-font-size-xs, 12px)",
20991
21002
  color: "var(--ggui-color-onSunken, #52525b)"
@@ -26043,7 +26054,9 @@
26043
26054
  width: "100%",
26044
26055
  borderCollapse: "collapse",
26045
26056
  fontSize: "var(--ggui-font-size-sm, 14px)",
26046
- color: "var(--ggui-color-onSunken, #52525b)"
26057
+ // Body cells are the data and inherit this (ggui#1566); the header and
26058
+ // caption set the muted tier themselves.
26059
+ color: "var(--ggui-color-onContainer, #18181b)"
26047
26060
  }, children: [caption && (0, import_jsx_runtime25.jsx)("caption", { style: {
26048
26061
  padding: cellPadding,
26049
26062
  fontSize: "var(--ggui-font-size-sm, 14px)",
@@ -26509,7 +26522,7 @@
26509
26522
  }, children: isExpanded && (0, import_jsx_runtime28.jsx)("div", { style: {
26510
26523
  padding: "0 var(--ggui-spacing-4, 16px) var(--ggui-spacing-4, 16px)",
26511
26524
  fontSize: "var(--ggui-font-size-sm, 14px)",
26512
- color: "var(--ggui-color-onSunken, #52525b)",
26525
+ color: "var(--ggui-color-onContainer, #18181b)",
26513
26526
  lineHeight: "var(--ggui-font-lineHeight-normal, 1.5)"
26514
26527
  }, children: item.content }) })] }, item.key);
26515
26528
  }) });
@@ -26626,7 +26639,7 @@
26626
26639
  paddingLeft: "36px",
26627
26640
  fontSize: size === "lg" ? "16px" : "14px",
26628
26641
  borderRadius: "6px",
26629
- border: `1px solid var(--ggui-color-outline, #d1d5db)`,
26642
+ border: `1px solid var(--ggui-color-controlOutline, var(--ggui-color-outline, #d1d5db))`,
26630
26643
  backgroundColor: disabled ? "var(--ggui-color-sunken, #f9fafb)" : "var(--ggui-color-container, #ffffff)",
26631
26644
  color: "var(--ggui-color-onContainer, #111827)",
26632
26645
  outline: "none",
@@ -27824,14 +27837,17 @@
27824
27837
  padding: "10px 14px",
27825
27838
  borderRadius: radius.lg,
27826
27839
  backgroundColor: isOwn ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-sunken, #f3f4f6)",
27827
- color: isOwn ? "var(--ggui-color-onPrimary, #ffffff)" : "var(--ggui-color-onSunken, #111827)"
27840
+ color: isOwn ? "var(--ggui-color-onPrimary, #ffffff)" : "var(--ggui-color-onContainer, #111827)"
27828
27841
  }, children: [(0, import_jsx_runtime46.jsx)("p", { style: { margin: 0, fontSize: fontSize.sm }, children: message.content }), (0, import_jsx_runtime46.jsxs)("div", { style: {
27829
27842
  display: "flex",
27830
27843
  alignItems: "center",
27831
27844
  gap: "4px",
27832
27845
  marginTop: "4px",
27833
27846
  fontSize: fontSize.xs,
27834
- opacity: 0.7
27847
+ // Own messages derive the muted line from onPrimary; the other
27848
+ // party's message now reads at onContainer, so its timestamp
27849
+ // takes the muted tier itself (ggui#1566).
27850
+ ...isOwn ? { opacity: 0.7 } : { color: "var(--ggui-color-onSunken, #52525b)" }
27835
27851
  }, children: [(0, import_jsx_runtime46.jsx)("span", { children: timestamp }), isOwn && message.status && (0, import_jsx_runtime46.jsxs)("span", { children: [message.status === "sending" && "\u2022", message.status === "sent" && "\u2713", message.status === "delivered" && "\u2713\u2713", message.status === "read" && "\u2713\u2713", message.status === "error" && "!"] })] })] })] }, message.id);
27836
27852
  }), typing && (0, import_jsx_runtime46.jsx)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: (0, import_jsx_runtime46.jsxs)("span", { style: { fontSize: fontSize.xs, color: "var(--ggui-color-neutral-500, #6b7280)" }, children: [typing.name, " is typing..."] }) }), (0, import_jsx_runtime46.jsx)("div", { ref: messagesEndRef })] }), (0, import_jsx_runtime46.jsxs)("div", { style: {
27837
27853
  padding: "12px 16px",
@@ -27841,7 +27857,7 @@
27841
27857
  }, children: [(0, import_jsx_runtime46.jsx)("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), onKeyDown: handleKeyDown, placeholder, style: {
27842
27858
  flex: 1,
27843
27859
  padding: "10px 14px",
27844
- border: `1px solid var(--ggui-color-outline, #d1d5db)`,
27860
+ border: `1px solid var(--ggui-color-controlOutline, var(--ggui-color-outline, #d1d5db))`,
27845
27861
  borderRadius: radius.full,
27846
27862
  fontSize: fontSize.sm,
27847
27863
  outline: "none"
@@ -27953,7 +27969,7 @@
27953
27969
  setIsDragOver(false);
27954
27970
  };
27955
27971
  return (0, import_jsx_runtime48.jsxs)("div", { className, style: { ...style }, children: [(0, import_jsx_runtime48.jsxs)("div", { onClick: () => !disabled && inputRef.current?.click(), onDrop: handleDrop, onDragOver: handleDragOver, onDragLeave: handleDragLeave, style: {
27956
- border: `2px dashed ${isDragOver ? "var(--ggui-color-primary-400, #38bdf8)" : "var(--ggui-color-outline, #d1d5db)"}`,
27972
+ border: `2px dashed ${isDragOver ? "var(--ggui-color-primary-400, #38bdf8)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d1d5db))"}`,
27957
27973
  borderRadius: radius.lg,
27958
27974
  padding: "32px",
27959
27975
  textAlign: "center",
@@ -28436,7 +28452,7 @@
28436
28452
  fontWeight: fontWeight.semibold,
28437
28453
  color: showOverlay ? "#ffffff" : "var(--ggui-color-onContainer, #374151)",
28438
28454
  backgroundColor: "transparent",
28439
- border: `1px solid ${showOverlay ? "rgba(255,255,255,0.3)" : "var(--ggui-color-outline, #d1d5db)"}`,
28455
+ border: `1px solid ${showOverlay ? "rgba(255,255,255,0.3)" : "var(--ggui-color-controlOutline, var(--ggui-color-outline, #d1d5db))"}`,
28440
28456
  borderRadius: "8px",
28441
28457
  cursor: "pointer",
28442
28458
  transition: "all 0.15s",
@@ -28577,6 +28593,7 @@
28577
28593
  buildWireConfig: () => buildWireConfig,
28578
28594
  connectionSource: () => connectionSource,
28579
28595
  useAction: () => useAction,
28596
+ useActionPending: () => useActionPending,
28580
28597
  useActionSpent: () => useActionSpent,
28581
28598
  useApp: () => useApp,
28582
28599
  useAuth: () => useAuth,
@@ -28679,8 +28696,19 @@
28679
28696
  return (0, import_react35.useSyncExternalStore)(subscribe, read, read);
28680
28697
  }
28681
28698
 
28682
- // ../wire/dist/useGguiContext.js
28699
+ // ../wire/dist/action-pending.js
28683
28700
  var import_react36 = __toESM(require_react(), 1);
28701
+ var ActionPendingContext = (0, import_react36.createContext)(null);
28702
+ var noSubscription2 = () => () => void 0;
28703
+ function useActionPending(actionName) {
28704
+ const source = (0, import_react36.useContext)(ActionPendingContext);
28705
+ const subscribe = (0, import_react36.useCallback)((listener) => source === null ? noSubscription2() : source.subscribe(listener), [source]);
28706
+ const read = (0, import_react36.useCallback)(() => source === null ? false : source.isPending(actionName), [source, actionName]);
28707
+ return (0, import_react36.useSyncExternalStore)(subscribe, read, read);
28708
+ }
28709
+
28710
+ // ../wire/dist/useGguiContext.js
28711
+ var import_react37 = __toESM(require_react(), 1);
28684
28712
  function useGguiContext(slotName) {
28685
28713
  const contextName = slotName.length === 0 ? "Context" : slotName.charAt(0).toUpperCase() + slotName.slice(1) + "Context";
28686
28714
  const registry = globalThis.__ggui__?.contexts;
@@ -28688,19 +28716,19 @@
28688
28716
  if (!ctx) {
28689
28717
  throw new Error(`[ggui] useGguiContext('${slotName}'): no Context registered. The contract must declare 'contextSpec.${slotName}' AND the runtime must have synthesized the per-slot React Context at boot. If you see this in production code generated by the LLM, the boilerplate's destructure line drifted from the contract.`);
28690
28718
  }
28691
- return (0, import_react36.useContext)(ctx);
28719
+ return (0, import_react37.useContext)(ctx);
28692
28720
  }
28693
28721
 
28694
28722
  // ../wire/dist/useStream.js
28695
- var import_react37 = __toESM(require_react(), 1);
28723
+ var import_react38 = __toESM(require_react(), 1);
28696
28724
  function useStream(channelName) {
28697
28725
  const { subscribe } = useWireContext();
28698
- const [latest, setLatest] = (0, import_react37.useState)(null);
28699
- const [all, setAll] = (0, import_react37.useState)([]);
28700
- const [isComplete, setIsComplete] = (0, import_react37.useState)(false);
28701
- const allRef = (0, import_react37.useRef)(all);
28726
+ const [latest, setLatest] = (0, import_react38.useState)(null);
28727
+ const [all, setAll] = (0, import_react38.useState)([]);
28728
+ const [isComplete, setIsComplete] = (0, import_react38.useState)(false);
28729
+ const allRef = (0, import_react38.useRef)(all);
28702
28730
  allRef.current = all;
28703
- (0, import_react37.useEffect)(() => {
28731
+ (0, import_react38.useEffect)(() => {
28704
28732
  return subscribe(channelName, (delivery) => {
28705
28733
  const typed = delivery.payload;
28706
28734
  setLatest(typed);
@@ -28738,7 +28766,7 @@
28738
28766
  }
28739
28767
 
28740
28768
  // ../wire/dist/useRender.js
28741
- var import_react38 = __toESM(require_react(), 1);
28769
+ var import_react39 = __toESM(require_react(), 1);
28742
28770
 
28743
28771
  // ../wire/dist/connection-store.js
28744
28772
  var ConnectionWriterConflictError = class extends Error {
@@ -28835,7 +28863,7 @@
28835
28863
  // ../wire/dist/useRender.js
28836
28864
  function useRender() {
28837
28865
  const { render } = useWireContext();
28838
- const isConnected = (0, import_react38.useSyncExternalStore)(connectionSource.subscribe, connectionSource.getSnapshot, connectionSource.getSnapshot);
28866
+ const isConnected = (0, import_react39.useSyncExternalStore)(connectionSource.subscribe, connectionSource.getSnapshot, connectionSource.getSnapshot);
28839
28867
  return { sessionId: render.sessionId, isConnected };
28840
28868
  }
28841
28869
 
@@ -29411,6 +29439,7 @@
29411
29439
  }
29412
29440
  }
29413
29441
  };
29442
+ var DEFAULT_ACTION_PENDING_BOUND_MS = 2e4;
29414
29443
  function buildWireConfig(opts) {
29415
29444
  let internalSeq = 0;
29416
29445
  const nextClientSeq = opts.nextClientSeq ?? (() => {
@@ -29435,8 +29464,43 @@
29435
29464
  }
29436
29465
  };
29437
29466
  opts.spentInputsChanged?.(notifySpent);
29467
+ const pendingBoundMs = opts.actionPendingBoundMs ?? DEFAULT_ACTION_PENDING_BOUND_MS;
29468
+ const pending = /* @__PURE__ */ new Map();
29469
+ const pendingListeners = /* @__PURE__ */ new Set();
29470
+ const notifyPending = () => {
29471
+ for (const listener of [...pendingListeners])
29472
+ listener();
29473
+ };
29474
+ const markPending = (actionName) => {
29475
+ const previous = pending.get(actionName);
29476
+ if (previous !== void 0)
29477
+ clearTimeout(previous);
29478
+ pending.set(actionName, setTimeout(() => {
29479
+ pending.delete(actionName);
29480
+ notifyPending();
29481
+ }, pendingBoundMs));
29482
+ notifyPending();
29483
+ };
29484
+ const actionPending = {
29485
+ isPending: (actionName) => pending.has(actionName),
29486
+ subscribe: (listener) => {
29487
+ pendingListeners.add(listener);
29488
+ return () => {
29489
+ pendingListeners.delete(listener);
29490
+ };
29491
+ }
29492
+ };
29493
+ opts.pendingInputsChanged?.(() => {
29494
+ if (pending.size === 0)
29495
+ return;
29496
+ for (const timer of pending.values())
29497
+ clearTimeout(timer);
29498
+ pending.clear();
29499
+ notifyPending();
29500
+ });
29438
29501
  return {
29439
29502
  actionSpent,
29503
+ actionPending,
29440
29504
  app: opts.app,
29441
29505
  render: opts.render,
29442
29506
  auth: opts.auth,
@@ -29469,6 +29533,7 @@
29469
29533
  const result = validateEnvelope(actionSpec, envelope);
29470
29534
  if (!result.valid) {
29471
29535
  opts.onViolation(new ClientContractViolationError("outbound-action", result.violations));
29536
+ opts.onDispatchRefused?.({ actionName, violations: result.violations });
29472
29537
  return;
29473
29538
  }
29474
29539
  if (isOneShot && !spentOneShots.has(actionName)) {
@@ -29476,6 +29541,7 @@
29476
29541
  notifySpent();
29477
29542
  }
29478
29543
  opts.emitEnvelope(envelope);
29544
+ markPending(actionName);
29479
29545
  },
29480
29546
  subscribe: (channelName, handler) => {
29481
29547
  return opts.streamBus.subscribe(channelName, (env) => {
@@ -29530,6 +29596,8 @@
29530
29596
  version: 2,
29531
29597
  tokens: [
29532
29598
  "--ggui-color-container",
29599
+ "--ggui-color-controlAccentOutline",
29600
+ "--ggui-color-controlOutline",
29533
29601
  "--ggui-color-elevated",
29534
29602
  "--ggui-color-error",
29535
29603
  "--ggui-color-error-100",
@@ -30161,8 +30229,10 @@
30161
30229
  // paper
30162
30230
  sunken: { $value: "#292929", $type: "color" },
30163
30231
  // ink
30164
- onSunken: { $value: "#d9d9d9", $type: "color" },
30165
- // chrome
30232
+ // Muted text, and text on sunken grounds (ggui#1566): the kit's dark muted
30233
+ // value, 6.57:1 on container and 5.80:1 on sunken. Light's onSunken is ink-3,
30234
+ // the same tier, so dark now reads as light already does.
30235
+ onSunken: { $value: "#a3a3a9", $type: "color" },
30166
30236
  container: { $value: "#1f1f1f", $type: "color" },
30167
30237
  // ink
30168
30238
  onContainer: { $value: "#f4f3ed", $type: "color" },
@@ -32469,12 +32539,18 @@
32469
32539
 
32470
32540
  // ../design/dist/themes/ThemeProvider.js
32471
32541
  var import_jsx_runtime58 = __toESM(require_jsx_runtime(), 1);
32472
- var import_react39 = __toESM(require_react(), 1);
32473
- var ThemeContext = (0, import_react39.createContext)(null);
32542
+ var import_react40 = __toESM(require_react(), 1);
32543
+ var ThemeContext = (0, import_react40.createContext)(null);
32474
32544
 
32475
32545
  // ../design/dist/rendering/css-tokens.js
32476
32546
  var EXPANDED_FRAME = { outerRadiusVar: "--ggui-shape-radius-xl", insetPx: 16 };
32477
32547
  var EXPANDED_FRAME_INNER_RADIUS = `max(0px, calc(var(${EXPANDED_FRAME.outerRadiusVar}) - ${EXPANDED_FRAME.insetPx}px))`;
32548
+ var EXPANDED_FRAME_CHROME = {
32549
+ radius: `var(${EXPANDED_FRAME.outerRadiusVar})`,
32550
+ hairline: "color-mix(in srgb, var(--ggui-color-onGround) 6%, transparent)",
32551
+ shadowVar: "--ggui-shape-shadow-sm",
32552
+ ground: "var(--ggui-color-neutral-50)"
32553
+ };
32478
32554
 
32479
32555
  // ../design/dist/rendering/rewrite-imports.js
32480
32556
  var WIRE_EXPORTS = [
@@ -32494,6 +32570,10 @@
32494
32570
  // its runtime's shim lacks fails at module link. A card generated to use it
32495
32571
  // must never meet a runtime without it.
32496
32572
  "useActionSpent",
32573
+ // ggui#1398: whether a dispatched action is waiting for the agent's answer,
32574
+ // as data. Served one release before the triad teaches it, for the same
32575
+ // reason as `useActionSpent` above.
32576
+ "useActionPending",
32497
32577
  // Provider component
32498
32578
  "GguiWireProvider"
32499
32579
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/ui-visual-tester",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Behavioral validator for ggui-generated component code. Renders a component in real Chromium via Playwright, clicks each action declared in its contract, and verifies the action actually does something — mutating bound state or dispatching to the agent. Catches buttons wired to no-op handlers that structural validators miss. Playwright is an optional peer dependency, injected by the caller.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -53,9 +53,9 @@
53
53
  "dependencies": {
54
54
  "react": "^19.0.0",
55
55
  "react-dom": "^19.0.0",
56
- "@ggui-ai/design": "0.24.0",
57
- "@ggui-ai/protocol": "0.24.0",
58
- "@ggui-ai/wire": "0.24.0"
56
+ "@ggui-ai/design": "0.26.0",
57
+ "@ggui-ai/wire": "0.26.0",
58
+ "@ggui-ai/protocol": "0.26.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@playwright/test": "^1.60.0",