@xenosystem/blocks 0.13.14 → 0.13.16

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.
@@ -1,4 +1,4 @@
1
- import { PanelModule, PanelManifest } from '@xenosystem/block-sdk';
1
+ import { PanelModule, PanelManifest, BlockDeclaration } from '@xenosystem/block-sdk';
2
2
  import { ReactNode } from 'react';
3
3
 
4
4
  /**
@@ -750,31 +750,135 @@ declare const CONSENT_PANEL_ID = "xeno.core.consent";
750
750
  declare const consentManifest: PanelManifest;
751
751
 
752
752
  /**
753
- * The Consent panel view.
753
+ * The Consent panel AS A DECLARATION — the seventeenth declared block: one elicitation at a time,
754
+ * a PERMISSION (scopes + how long) or a QUESTION (options and/or a typed answer), never in bulk.
755
+ *
756
+ * Everything drawn is data: the header (risk and kind badges, the subject, the waiting count), the
757
+ * request (title, detail, its facts as `KeyValue`s), the permission's scope rows with a `Toggle`
758
+ * each and the duration control, or the question's option rows and the answer field with its
759
+ * length counter, Deny / Allow or Cancel / Submit, the status bar with the expiry. Every rule the
760
+ * code view carried survives: a question has no "deny" — refusing is a CANCELLATION, and the two
761
+ * are different outcomes for the host; no auto-focus on Allow, so a stray Enter grants nothing;
762
+ * the button's `disabled` is the SAME predicate `submit()` enforces. No runtime-bound unit.
763
+ */
764
+
765
+ type Tone = 'neutral' | 'warning' | 'error';
766
+ interface ConsentViewFact {
767
+ key: string;
768
+ value: string;
769
+ }
770
+ interface ConsentViewScope {
771
+ id: string;
772
+ label: string;
773
+ detail?: string; /** a two-line row needs its height — the unit's default row clips the detail */
774
+ height?: number;
775
+ risk?: string;
776
+ riskTone: Tone;
777
+ granted: boolean;
778
+ grantable: boolean;
779
+ toggleLabel: string;
780
+ selected: boolean;
781
+ }
782
+ interface ConsentViewOption {
783
+ id: string;
784
+ label: string;
785
+ detail?: string;
786
+ height?: number;
787
+ risk?: string;
788
+ riskTone: Tone;
789
+ toggleLabel: string;
790
+ selected: boolean;
791
+ }
792
+ interface ConsentViewModel {
793
+ idle: boolean;
794
+ emptyHint: string;
795
+ active: boolean;
796
+ riskLabel: string;
797
+ riskTone: Tone;
798
+ kindLabel: string;
799
+ subject?: string;
800
+ waiting?: string;
801
+ title: string;
802
+ detail?: string;
803
+ facts: ConsentViewFact[];
804
+ permission: boolean;
805
+ scopes: ConsentViewScope[];
806
+ durations: {
807
+ value: string;
808
+ label: string;
809
+ }[];
810
+ duration: string;
811
+ unrememberable: boolean;
812
+ allowDisabled: boolean;
813
+ question: boolean;
814
+ hasOptions: boolean;
815
+ options: ConsentViewOption[];
816
+ wantsText: boolean;
817
+ textLabel?: string;
818
+ multiline: boolean;
819
+ placeholder: string;
820
+ draft: string;
821
+ hasCounter: boolean;
822
+ counter: string;
823
+ overLimit: boolean;
824
+ submitLabel: string;
825
+ submitDisabled: boolean;
826
+ pendingLabel: string;
827
+ expiryLabel?: string;
828
+ }
829
+ type ConsentUiIntent = {
830
+ kind: 'scope';
831
+ id: string;
832
+ on: boolean;
833
+ } | {
834
+ kind: 'duration';
835
+ duration: XenoConsentDuration;
836
+ } | {
837
+ kind: 'option';
838
+ id: string;
839
+ on: boolean;
840
+ } | {
841
+ kind: 'text';
842
+ value: string;
843
+ } | {
844
+ kind: 'allow';
845
+ } | {
846
+ kind: 'deny';
847
+ } | {
848
+ kind: 'submit';
849
+ } | {
850
+ kind: 'dismiss';
851
+ };
852
+ interface ConsentViewOptions {
853
+ showQueueCount?: boolean;
854
+ emptyHint?: string;
855
+ }
856
+ /** Derive the view from the live controller — the runtime calls this on every draw. */
857
+ declare function consentViewState(c: ConsentController, options?: ConsentViewOptions): ConsentViewModel;
858
+ declare function applyConsentUi(c: ConsentController, intent: ConsentUiIntent): void;
859
+ declare const consentViewManifest: PanelManifest;
860
+ declare const BEHAVIOURS: {
861
+ 'trust/consentPanelView': (inputs: Record<string, unknown>) => ConsentViewModel;
862
+ };
863
+ declare const CONSENT_RUNTIME_UNITS: readonly [];
864
+ declare const CONSENT_DECLARATION: BlockDeclaration;
865
+
866
+ /**
867
+ * The Consent panel view — the CODE VIEW of `CONSENT_DECLARATION`, drawing the same
868
+ * `ConsentViewModel` byte-identical (the equivalence suite holds the two together). No runtime-bound
869
+ * unit; every intent goes through `applyConsentUi`.
754
870
  *
755
871
  * Composes `@xenosystem/components/chrome`; the host must ensure `@xenosystem/workbench/primitives.css` is
756
872
  * present.
757
873
  *
758
- * Two deliberate absences: **no "Allow all"** (bulk approval is how consent fatigue becomes a
759
- * security hole) and **no auto-focus on Allow** (a stray Enter must not grant anything).
760
- *
761
- * 🔴 **A third absence, added with the question kinds: nothing here submits on its own.** The
762
- * question forms carry no auto-submit, no debounce-then-send, and no Enter-to-answer on the
763
- * single-line field. The emitted answer is always the product of a click the user made, which is
764
- * the view's half of the controller's *"no value without a user action"* invariant — the guard
765
- * lives in `submit()`, but a view that fired it on a keystroke would satisfy the guard while
766
- * defeating the point.
767
- *
768
874
  * @module
769
875
  */
770
876
 
771
877
  /** Props for {@link ConsentPanelView}. */
772
- interface ConsentPanelViewProps {
878
+ interface ConsentPanelViewProps extends ConsentViewOptions {
773
879
  controller: ConsentController;
774
- showQueueCount?: boolean;
775
- emptyHint?: string;
776
880
  }
777
881
  /** The Consent panel view. */
778
- declare function ConsentPanelView({ controller, showQueueCount, emptyHint, }: ConsentPanelViewProps): ReactNode;
882
+ declare function ConsentPanelView({ controller, showQueueCount, emptyHint }: ConsentPanelViewProps): ReactNode;
779
883
 
780
- export { CONSENT_PANEL_ID, ConsentController, type ConsentControllerOptions, type ConsentHostBridge, type ConsentPanelState, ConsentPanelView, type ConsentPanelViewProps, type ConsentRenderContext, type ConsentViewState, type CreateConsentPanelOptions, ELICITATION_RESULT_SCHEMA, ELICITATION_SCHEMA, type XenoChoiceAnswer, type XenoChoiceElicitation, type XenoConsentDecision, type XenoConsentDuration, type XenoConsentKind, type XenoConsentRequest, type XenoConsentRisk, type XenoConsentScope, type XenoElicitation, type XenoElicitationAnswer, type XenoElicitationAsk, type XenoElicitationBase, type XenoElicitationCancelled, type XenoElicitationOption, type XenoElicitationResult, type XenoPathAnswer, type XenoPathElicitation, type XenoQuestionElicitation, type XenoTextAnswer, type XenoTextElicitation, allowedDurations, autoCancel, autoDeny, consentManifest, consentPanel, createConsentPanel, effectiveRisk, elicitationAsk, elicitationOptions, findOption, isConsentRequest, isElicitation, isElicitationAnswer, isElicitationCancelled, isExpired, isPermissionElicitation, maxTextLength, selectableScopes };
884
+ export { BEHAVIOURS as CONSENT_BEHAVIOURS, CONSENT_DECLARATION, CONSENT_PANEL_ID, CONSENT_RUNTIME_UNITS, ConsentController, type ConsentControllerOptions, type ConsentHostBridge, type ConsentPanelState, ConsentPanelView, type ConsentPanelViewProps, type ConsentRenderContext, type ConsentUiIntent, type ConsentViewFact, type ConsentViewModel, type ConsentViewOption, type ConsentViewOptions, type ConsentViewScope, type ConsentViewState, type CreateConsentPanelOptions, ELICITATION_RESULT_SCHEMA, ELICITATION_SCHEMA, type XenoChoiceAnswer, type XenoChoiceElicitation, type XenoConsentDecision, type XenoConsentDuration, type XenoConsentKind, type XenoConsentRequest, type XenoConsentRisk, type XenoConsentScope, type XenoElicitation, type XenoElicitationAnswer, type XenoElicitationAsk, type XenoElicitationBase, type XenoElicitationCancelled, type XenoElicitationOption, type XenoElicitationResult, type XenoPathAnswer, type XenoPathElicitation, type XenoQuestionElicitation, type XenoTextAnswer, type XenoTextElicitation, allowedDurations, applyConsentUi, autoCancel, autoDeny, consentManifest, consentPanel, consentViewManifest, consentViewState, createConsentPanel, effectiveRisk, elicitationAsk, elicitationOptions, findOption, isConsentRequest, isElicitation, isElicitationAnswer, isElicitationCancelled, isExpired, isPermissionElicitation, maxTextLength, selectableScopes };
@@ -661,234 +661,369 @@ var consentManifest = {
661
661
  import { useSyncExternalStore } from "react";
662
662
  import {
663
663
  Badge,
664
+ Column,
664
665
  EmptyState,
666
+ Input,
667
+ KeyValue,
665
668
  Row,
666
669
  RowList,
667
670
  ScrollArea,
668
671
  SegmentedControl,
672
+ Stack,
669
673
  StatusBar,
674
+ Text,
675
+ TextArea,
670
676
  TextButton,
671
677
  Toggle,
672
678
  Toolbar,
673
679
  ToolbarGroup
674
680
  } from "@xenosystem/components/chrome";
675
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
676
- var RISK_TONE = {
677
- low: "neutral",
678
- medium: "warning",
679
- high: "error"
680
- };
681
+
682
+ // src/trust/consent/declaration.ts
683
+ var RISK_TONE = { low: "neutral", medium: "warning", high: "error" };
681
684
  var DURATION_LABEL = { once: "Once", session: "Session", always: "Always" };
682
- var FIELD_STYLE = {
683
- width: "100%",
684
- fontSize: 11,
685
- background: "var(--xeno-canvas)",
686
- border: "1px solid var(--xeno-border-subtle)",
687
- borderRadius: 2,
688
- padding: 4,
689
- color: "inherit",
690
- // 🔴 Text entry never takes the global focus ring: `outline-offset` draws it OUTSIDE the field,
691
- // so on dark chrome it reads as a stray stroke over the container and the control looks broken.
692
- outline: "none",
693
- resize: "none"
694
- };
695
- var SUBMIT_LABEL = {
696
- text: "Submit",
697
- path: "Use this path",
698
- choice: "Confirm"
699
- };
700
- function ConsentPanelView({
701
- controller,
702
- showQueueCount = true,
703
- emptyHint
704
- }) {
705
- const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
706
- const request = state.current;
685
+ var SUBMIT_LABEL = { text: "Submit", path: "Use this path", choice: "Confirm" };
686
+ function consentViewState(c, options = {}) {
687
+ const s = c.getState();
688
+ const request = s.current;
689
+ const showQueueCount = options.showQueueCount ?? true;
690
+ const emptyHint = options.emptyHint ?? "Requests needing your approval appear here.";
691
+ const base = { idle: !request, emptyHint, active: !!request };
707
692
  if (!request) {
708
- return /* @__PURE__ */ jsx(
709
- EmptyState,
710
- {
711
- title: "Nothing pending",
712
- hint: emptyHint ?? "Requests needing your approval appear here."
713
- }
714
- );
693
+ return { ...base, riskLabel: "", riskTone: "neutral", kindLabel: "", title: "", facts: [], permission: false, scopes: [], durations: [], duration: "once", unrememberable: false, allowDisabled: true, question: false, hasOptions: false, options: [], wantsText: false, multiline: false, placeholder: "", draft: "", hasCounter: false, counter: "", overLimit: false, submitLabel: "Submit", submitDisabled: true, pendingLabel: `${s.pending} pending` };
715
694
  }
716
695
  const risk = effectiveRisk(request);
717
696
  const ask = elicitationAsk(request);
718
697
  const permission = isPermissionElicitation(request) ? request : null;
719
- return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }, children: [
720
- /* @__PURE__ */ jsx(
721
- Toolbar,
698
+ const opts = permission ? [] : elicitationOptions(request);
699
+ const chosen = opts.find((o) => o.id === s.selectedOptionId);
700
+ const max = permission ? null : maxTextLength(request);
701
+ const extra = request;
702
+ const wantsText = !permission && (ask !== "choice" || chosen?.requiresText === true);
703
+ return {
704
+ ...base,
705
+ riskLabel: `${risk} risk`,
706
+ riskTone: RISK_TONE[risk],
707
+ kindLabel: permission ? permission.kind : ask,
708
+ subject: request.subject,
709
+ waiting: showQueueCount && s.queued.length > 0 ? `+${s.queued.length} waiting` : void 0,
710
+ title: request.title,
711
+ detail: request.detail,
712
+ facts: request.fields ? Object.entries(request.fields).map(([key, value]) => ({ key, value: String(value) })) : [],
713
+ permission: !!permission,
714
+ scopes: permission ? permission.scopes.map((scope) => ({
715
+ id: scope.id,
716
+ label: scope.label,
717
+ detail: scope.detail,
718
+ height: scope.detail ? 36 : void 0,
719
+ risk: scope.risk && scope.risk !== risk ? scope.risk : void 0,
720
+ riskTone: scope.risk ? RISK_TONE[scope.risk] : "neutral",
721
+ granted: scope.alreadyGranted === true,
722
+ grantable: scope.alreadyGranted !== true,
723
+ toggleLabel: `Grant ${scope.label}`,
724
+ selected: s.selectedScopeIds.includes(scope.id)
725
+ })) : [],
726
+ durations: permission ? allowedDurations(permission).map((d) => ({ value: d, label: DURATION_LABEL[d] })) : [],
727
+ duration: s.duration,
728
+ unrememberable: permission?.remember === "forbidden",
729
+ allowDisabled: !!permission && permission.scopes.some((sc) => !sc.alreadyGranted) && s.selectedScopeIds.length === 0,
730
+ question: !permission,
731
+ hasOptions: opts.length > 0,
732
+ options: opts.map((option) => ({
733
+ id: option.id,
734
+ label: option.label,
735
+ detail: option.detail,
736
+ height: option.detail ? 36 : void 0,
737
+ risk: option.risk && option.risk !== risk ? option.risk : void 0,
738
+ riskTone: option.risk ? RISK_TONE[option.risk] : "neutral",
739
+ toggleLabel: `Choose ${option.label}`,
740
+ selected: s.selectedOptionId === option.id
741
+ })),
742
+ wantsText,
743
+ textLabel: extra.textLabel,
744
+ multiline: ask === "text" && extra.multiline === true,
745
+ placeholder: extra.placeholder ?? (ask === "path" ? "Type or paste a path\u2026" : "Type your answer\u2026"),
746
+ draft: s.draftText,
747
+ hasCounter: max !== null,
748
+ counter: max !== null ? `${s.draftText.length} / ${max}` : "",
749
+ overLimit: max !== null && s.draftText.length > max,
750
+ submitLabel: SUBMIT_LABEL[ask] ?? "Submit",
751
+ submitDisabled: !s.canSubmit,
752
+ pendingLabel: `${s.pending} pending`,
753
+ expiryLabel: s.expiresInMs !== null ? `expires in ${Math.ceil(s.expiresInMs / 1e3)}s \u2014 ${permission ? "denies" : "cancels"}` : void 0
754
+ };
755
+ }
756
+ function applyConsentUi(c, intent) {
757
+ switch (intent.kind) {
758
+ case "scope":
759
+ c.toggleScope(intent.id, intent.on);
760
+ break;
761
+ case "duration":
762
+ c.setDuration(intent.duration);
763
+ break;
764
+ case "option":
765
+ c.selectOption(intent.on ? intent.id : null);
766
+ break;
767
+ case "text":
768
+ c.setDraftText(intent.value);
769
+ break;
770
+ case "allow":
771
+ c.allow();
772
+ break;
773
+ case "deny":
774
+ c.deny();
775
+ break;
776
+ case "submit":
777
+ c.submit();
778
+ break;
779
+ case "dismiss":
780
+ c.dismiss();
781
+ break;
782
+ }
783
+ }
784
+ var consentViewManifest = {
785
+ id: consentManifest.id,
786
+ version: "1.0.0",
787
+ title: "Consent",
788
+ category: "trust",
789
+ layout: { slot: "side" },
790
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The ConsentViewModel the runtime derives from the controller on every draw." }],
791
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, \u2026 } \u2014 applied by the runtime to the controller." }],
792
+ commands: []
793
+ };
794
+ var EMPTY = { idle: true, emptyHint: "Requests needing your approval appear here.", active: false, riskLabel: "", riskTone: "neutral", kindLabel: "", title: "", facts: [], permission: false, scopes: [], durations: [], duration: "once", unrememberable: false, allowDisabled: true, question: false, hasOptions: false, options: [], wantsText: false, multiline: false, placeholder: "", draft: "", hasCounter: false, counter: "", overLimit: false, submitLabel: "Submit", submitDisabled: true, pendingLabel: "0 pending" };
795
+ var BEHAVIOURS = {
796
+ "trust/consentPanelView": (inputs) => inputs.view ?? EMPTY
797
+ };
798
+ var CONSENT_RUNTIME_UNITS = [];
799
+ var text = (b) => ({ component: "text", props: { text: b } });
800
+ var ui = (kind, fields = {}) => ({ emit: "ui", value: { object: { kind: { value: kind }, ...fields } } });
801
+ var twoLine = {
802
+ component: "chrome/Stack",
803
+ props: { gap: { value: 1 }, pad: { value: "none" } },
804
+ children: [{ component: "chrome/Text", children: [text({ item: "label" })] }, { component: "chrome/Text", when: { item: "detail" }, props: { role: { value: "meta" } }, children: [text({ item: "detail" })] }]
805
+ };
806
+ var riskMeta = { node: { component: "chrome/Badge", when: { item: "risk" }, props: { tone: { item: "riskTone" } }, children: [text({ item: "risk" })] } };
807
+ var CONSENT_DECLARATION = {
808
+ format: "xeno.block@1",
809
+ manifest: consentViewManifest,
810
+ behaviour: "trust/consentPanelView",
811
+ composition: {
812
+ component: "fragment",
813
+ children: [
814
+ { component: "chrome/EmptyState", when: { input: "view.idle" }, props: { title: { value: "Nothing pending" }, hint: { input: "view.emptyHint" } } },
722
815
  {
723
- left: /* @__PURE__ */ jsxs(Fragment, { children: [
724
- /* @__PURE__ */ jsx(Badge, { tone: RISK_TONE[risk], children: `${risk} risk` }),
725
- /* @__PURE__ */ jsx(Badge, { children: permission ? permission.kind : ask }),
726
- request.subject ? /* @__PURE__ */ jsx("span", { style: { fontSize: 11 }, children: request.subject }) : null
727
- ] }),
728
- right: showQueueCount && state.queued.length > 0 ? /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(Badge, { title: "Answered one at a time \u2014 never in bulk.", children: `+${state.queued.length} waiting` }) }) : void 0
729
- }
730
- ),
731
- /* @__PURE__ */ jsxs(ScrollArea, { children: [
732
- /* @__PURE__ */ jsxs("div", { style: { padding: 8, display: "flex", flexDirection: "column", gap: 6 }, children: [
733
- /* @__PURE__ */ jsx("div", { style: { fontSize: 12, fontWeight: 600 }, children: request.title }),
734
- request.detail ? (
735
- // The HOST's sentence, verbatim. The panel never composes consent prose.
736
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.8, lineHeight: 1.45 }, children: request.detail })
737
- ) : null,
738
- request.fields ? Object.entries(request.fields).map(([key, value]) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, fontSize: 10 }, children: [
739
- /* @__PURE__ */ jsx("span", { style: { opacity: 0.55, minWidth: 72 }, children: key }),
740
- /* @__PURE__ */ jsx("code", { style: { userSelect: "all", wordBreak: "break-all" }, children: value })
741
- ] }, key)) : null
742
- ] }),
743
- permission ? /* @__PURE__ */ jsx(RowList, { children: permission.scopes.map((scope) => /* @__PURE__ */ jsx(
744
- Row,
745
- {
746
- noIcon: true,
747
- label: /* @__PURE__ */ jsxs("span", { style: { display: "flex", flexDirection: "column", gap: 1, minWidth: 0 }, children: [
748
- /* @__PURE__ */ jsx("span", { children: scope.label }),
749
- scope.detail ? /* @__PURE__ */ jsx("span", { style: { fontSize: 10, opacity: 0.6 }, children: scope.detail }) : null
750
- ] }),
751
- meta: scope.risk && scope.risk !== risk ? /* @__PURE__ */ jsx(Badge, { tone: RISK_TONE[scope.risk], children: scope.risk }) : null,
752
- trailing: scope.alreadyGranted ? (
753
- // Shown for context, not as a choice — hiding it would make the new ask look
754
- // bigger than it is.
755
- /* @__PURE__ */ jsx(Badge, { title: "Already granted", children: "granted" })
756
- ) : /* @__PURE__ */ jsx(
757
- Toggle,
758
- {
759
- size: "sm",
760
- label: `Grant ${scope.label}`,
761
- checked: state.selectedScopeIds.includes(scope.id),
762
- onChange: (next) => controller.toggleScope(scope.id, next)
816
+ component: "chrome/Column",
817
+ when: { input: "view.active" },
818
+ children: [
819
+ {
820
+ component: "chrome/Toolbar",
821
+ props: {
822
+ left: {
823
+ node: {
824
+ component: "fragment",
825
+ children: [
826
+ { component: "chrome/Badge", props: { tone: { input: "view.riskTone" } }, children: [text({ input: "view.riskLabel" })] },
827
+ { component: "chrome/Badge", children: [text({ input: "view.kindLabel" })] },
828
+ { component: "chrome/Text", when: { input: "view.subject" }, children: [text({ input: "view.subject" })] }
829
+ ]
830
+ }
831
+ },
832
+ right: { node: { component: "chrome/ToolbarGroup", when: { input: "view.waiting" }, props: { end: { value: true } }, children: [{ component: "chrome/Badge", props: { title: { value: "Answered one at a time \u2014 never in bulk." } }, children: [text({ input: "view.waiting" })] }] } }
763
833
  }
764
- )
765
- },
766
- scope.id
767
- )) }) : /* @__PURE__ */ jsx(QuestionForm, { controller, request, state, risk })
768
- ] }),
769
- permission ? /* @__PURE__ */ jsxs("div", { style: { padding: "4px 8px", display: "flex", alignItems: "center", gap: 8 }, children: [
834
+ },
835
+ {
836
+ component: "chrome/ScrollArea",
837
+ children: [
838
+ {
839
+ component: "chrome/Stack",
840
+ props: { gap: { value: 6 }, pad: { value: "gutter" } },
841
+ children: [
842
+ { component: "chrome/Text", props: { role: { value: "title" } }, children: [text({ input: "view.title" })] },
843
+ { component: "chrome/Text", when: { input: "view.detail" }, children: [text({ input: "view.detail" })] },
844
+ { component: "chrome/KeyValue", each: { input: "view.facts", key: "key" }, props: { name: { item: "key" }, value: { item: "value" } } }
845
+ ]
846
+ },
847
+ {
848
+ component: "chrome/RowList",
849
+ when: { input: "view.permission" },
850
+ children: [
851
+ {
852
+ component: "chrome/Row",
853
+ each: { input: "view.scopes", key: "id" },
854
+ props: {
855
+ noIcon: { value: true },
856
+ height: { item: "height" },
857
+ label: { node: twoLine },
858
+ meta: riskMeta,
859
+ trailing: {
860
+ node: {
861
+ component: "fragment",
862
+ children: [
863
+ { component: "chrome/Badge", when: { item: "granted" }, props: { title: { value: "Already granted" } }, children: [text({ value: "granted" })] },
864
+ { component: "chrome/Toggle", when: { item: "grantable" }, props: { size: { value: "sm" }, label: { item: "toggleLabel" }, checked: { item: "selected" } }, on: { onChange: ui("scope", { id: { item: "id" }, on: { arg: "" } }) } }
865
+ ]
866
+ }
867
+ }
868
+ }
869
+ }
870
+ ]
871
+ },
872
+ {
873
+ component: "chrome/Stack",
874
+ when: { input: "view.question" },
875
+ props: { gap: { value: 6 }, pad: { value: "inset" } },
876
+ children: [
877
+ {
878
+ component: "chrome/RowList",
879
+ when: { input: "view.hasOptions" },
880
+ children: [
881
+ {
882
+ component: "chrome/Row",
883
+ each: { input: "view.options", key: "id" },
884
+ props: { noIcon: { value: true }, height: { item: "height" }, label: { node: twoLine }, meta: riskMeta, trailing: { node: { component: "chrome/Toggle", props: { size: { value: "sm" }, label: { item: "toggleLabel" }, checked: { item: "selected" } }, on: { onChange: ui("option", { id: { item: "id" }, on: { arg: "" } }) } } } }
885
+ }
886
+ ]
887
+ },
888
+ {
889
+ component: "chrome/Stack",
890
+ when: { input: "view.wantsText" },
891
+ props: { gap: { value: 3 }, pad: { value: "none" } },
892
+ children: [
893
+ { component: "chrome/Text", when: { input: "view.textLabel" }, props: { role: { value: "meta" } }, children: [text({ input: "view.textLabel" })] },
894
+ { component: "chrome/TextArea", when: { input: "view.multiline" }, props: { value: { input: "view.draft" }, placeholder: { input: "view.placeholder" }, rows: { value: 3 }, label: { value: "Answer" } }, on: { onChange: ui("text", { value: { arg: "" } }) } },
895
+ { component: "chrome/Input", when: { not: { input: "view.multiline" } }, props: { value: { input: "view.draft" }, placeholder: { input: "view.placeholder" }, autoComplete: { value: "off" }, label: { value: "Answer" } }, on: { onChange: ui("text", { value: { arg: "" } }) } },
896
+ { component: "chrome/Text", when: { input: "view.hasCounter" }, props: { role: { value: "caption" } }, children: [text({ input: "view.counter" })] },
897
+ { component: "chrome/Badge", when: { input: "view.overLimit" }, props: { tone: { value: "error" } }, children: [text({ value: "too long" })] }
898
+ ]
899
+ }
900
+ ]
901
+ }
902
+ ]
903
+ },
904
+ {
905
+ component: "chrome/Toolbar",
906
+ when: { input: "view.permission" },
907
+ props: {
908
+ divided: { value: false },
909
+ // Duration is IN the decision, not buried in a settings page.
910
+ left: { node: { component: "chrome/SegmentedControl", props: { size: { value: "sm" }, label: { value: "How long" }, options: { input: "view.durations" }, value: { input: "view.duration" } }, on: { onChange: ui("duration", { duration: { arg: "" } }) } } },
911
+ right: { node: { component: "chrome/Text", when: { input: "view.unrememberable" }, props: { role: { value: "caption" } }, children: [text({ value: "This grant cannot be remembered." })] } }
912
+ }
913
+ },
914
+ {
915
+ component: "chrome/Toolbar",
916
+ props: {
917
+ divided: { value: false },
918
+ right: {
919
+ node: {
920
+ component: "chrome/ToolbarGroup",
921
+ props: { end: { value: true } },
922
+ children: [
923
+ { component: "chrome/TextButton", when: { input: "view.permission" }, on: { onClick: ui("deny") }, children: [text({ value: "Deny" })] },
924
+ // No auto-focus: a stray Enter must not grant anything.
925
+ { component: "chrome/TextButton", when: { input: "view.permission" }, props: { strong: { value: true }, disabled: { input: "view.allowDisabled" } }, on: { onClick: ui("allow") }, children: [text({ value: "Allow" })] },
926
+ // A question has no "deny" — refusing to answer is a CANCELLATION.
927
+ { component: "chrome/TextButton", when: { input: "view.question" }, on: { onClick: ui("dismiss") }, children: [text({ value: "Cancel" })] },
928
+ { component: "chrome/TextButton", when: { input: "view.question" }, props: { strong: { value: true }, disabled: { input: "view.submitDisabled" } }, on: { onClick: ui("submit") }, children: [text({ input: "view.submitLabel" })] }
929
+ ]
930
+ }
931
+ }
932
+ }
933
+ },
934
+ { component: "chrome/StatusBar", props: { left: { input: "view.pendingLabel" }, right: { input: "view.expiryLabel" } } }
935
+ ]
936
+ }
937
+ ]
938
+ }
939
+ };
940
+
941
+ // src/trust/consent/react/ConsentPanelView.tsx
942
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
943
+ var TwoLine = ({ label, detail }) => /* @__PURE__ */ jsxs(Stack, { gap: 1, pad: "none", children: [
944
+ /* @__PURE__ */ jsx(Text, { children: label }),
945
+ detail ? /* @__PURE__ */ jsx(Text, { role: "meta", children: detail }) : null
946
+ ] });
947
+ function ConsentPanelView({ controller, showQueueCount, emptyHint }) {
948
+ useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
949
+ const view = consentViewState(controller, { showQueueCount, emptyHint });
950
+ const ui2 = (intent) => applyConsentUi(controller, intent);
951
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
952
+ view.idle ? /* @__PURE__ */ jsx(EmptyState, { title: "Nothing pending", hint: view.emptyHint }) : null,
953
+ view.active ? /* @__PURE__ */ jsxs(Column, { children: [
770
954
  /* @__PURE__ */ jsx(
771
- SegmentedControl,
955
+ Toolbar,
772
956
  {
773
- size: "sm",
774
- label: "How long",
775
- options: allowedDurations(permission).map((d) => ({
776
- value: d,
777
- label: DURATION_LABEL[d]
778
- })),
779
- value: state.duration,
780
- onChange: (d) => controller.setDuration(d)
957
+ left: /* @__PURE__ */ jsxs(Fragment, { children: [
958
+ /* @__PURE__ */ jsx(Badge, { tone: view.riskTone, children: view.riskLabel }),
959
+ /* @__PURE__ */ jsx(Badge, { children: view.kindLabel }),
960
+ view.subject ? /* @__PURE__ */ jsx(Text, { children: view.subject }) : null
961
+ ] }),
962
+ right: view.waiting ? /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(Badge, { title: "Answered one at a time \u2014 never in bulk.", children: view.waiting }) }) : null
781
963
  }
782
964
  ),
783
- permission.remember === "forbidden" ? /* @__PURE__ */ jsx("span", { style: { fontSize: 9, opacity: 0.6 }, children: "This grant cannot be remembered." }) : null
784
- ] }) : null,
785
- /* @__PURE__ */ jsx(
786
- Toolbar,
787
- {
788
- divided: false,
789
- right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: permission ? /* @__PURE__ */ jsxs(Fragment, { children: [
790
- /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.deny(), children: "Deny" }),
791
- /* @__PURE__ */ jsx(
792
- TextButton,
793
- {
794
- strong: true,
795
- disabled: permission.scopes.some((s) => !s.alreadyGranted) && state.selectedScopeIds.length === 0,
796
- onClick: () => controller.allow(),
797
- children: "Allow"
798
- }
799
- )
800
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
801
- /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.dismiss(), children: "Cancel" }),
802
- /* @__PURE__ */ jsx(
803
- TextButton,
804
- {
805
- strong: true,
806
- disabled: !state.canSubmit,
807
- onClick: () => controller.submit(),
808
- children: SUBMIT_LABEL[ask] ?? "Submit"
809
- }
810
- )
811
- ] }) })
812
- }
813
- ),
814
- /* @__PURE__ */ jsx(
815
- StatusBar,
816
- {
817
- left: `${state.pending} pending`,
818
- right: state.expiresInMs !== null ? `expires in ${Math.ceil(state.expiresInMs / 1e3)}s \u2014 ${permission ? "denies" : "cancels"}` : void 0
819
- }
820
- )
821
- ] });
822
- }
823
- function QuestionForm({ controller, request, state, risk }) {
824
- const ask = elicitationAsk(request);
825
- const options = elicitationOptions(request);
826
- const max = maxTextLength(request);
827
- const chosen = options.find((o) => o.id === state.selectedOptionId);
828
- const multiline = ask === "text" && request.multiline === true;
829
- const placeholder = request.placeholder ?? (ask === "path" ? "Type or paste a path\u2026" : "Type your answer\u2026");
830
- const wantsText = ask !== "choice" || chosen?.requiresText === true;
831
- const textLabel = request.textLabel;
832
- return /* @__PURE__ */ jsxs("div", { style: { padding: "0 8px 8px", display: "flex", flexDirection: "column", gap: 6 }, children: [
833
- options.length > 0 ? /* @__PURE__ */ jsx(RowList, { children: options.map((option) => /* @__PURE__ */ jsx(
834
- Row,
835
- {
836
- noIcon: true,
837
- label: /* @__PURE__ */ jsxs("span", { style: { display: "flex", flexDirection: "column", gap: 1, minWidth: 0 }, children: [
838
- /* @__PURE__ */ jsx("span", { children: option.label }),
839
- option.detail ? /* @__PURE__ */ jsx("span", { style: { fontSize: 10, opacity: 0.6 }, children: option.detail }) : null
965
+ /* @__PURE__ */ jsxs(ScrollArea, { children: [
966
+ /* @__PURE__ */ jsxs(Stack, { gap: 6, pad: "gutter", children: [
967
+ /* @__PURE__ */ jsx(Text, { role: "title", children: view.title }),
968
+ view.detail ? /* @__PURE__ */ jsx(Text, { children: view.detail }) : null,
969
+ view.facts.map((f) => /* @__PURE__ */ jsx(KeyValue, { name: f.key, value: f.value }, f.key))
840
970
  ] }),
841
- meta: option.risk && option.risk !== risk ? /* @__PURE__ */ jsx(Badge, { tone: RISK_TONE[option.risk], children: option.risk }) : null,
842
- trailing: /* @__PURE__ */ jsx(
843
- Toggle,
971
+ view.permission ? /* @__PURE__ */ jsx(RowList, { children: view.scopes.map((scope) => /* @__PURE__ */ jsx(
972
+ Row,
844
973
  {
845
- size: "sm",
846
- label: `Choose ${option.label}`,
847
- checked: state.selectedOptionId === option.id,
848
- onChange: (next) => controller.selectOption(next ? option.id : null)
849
- }
850
- )
851
- },
852
- option.id
853
- )) }) : null,
854
- wantsText ? /* @__PURE__ */ jsxs("label", { style: { display: "flex", flexDirection: "column", gap: 3 }, children: [
855
- textLabel ? /* @__PURE__ */ jsx("span", { style: { fontSize: 10, opacity: 0.6 }, children: textLabel }) : null,
856
- multiline ? /* @__PURE__ */ jsx(
857
- "textarea",
974
+ noIcon: true,
975
+ height: scope.height,
976
+ label: /* @__PURE__ */ jsx(TwoLine, { label: scope.label, detail: scope.detail }),
977
+ meta: scope.risk ? /* @__PURE__ */ jsx(Badge, { tone: scope.riskTone, children: scope.risk }) : null,
978
+ trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
979
+ scope.granted ? /* @__PURE__ */ jsx(Badge, { title: "Already granted", children: "granted" }) : null,
980
+ scope.grantable ? /* @__PURE__ */ jsx(Toggle, { size: "sm", label: scope.toggleLabel, checked: scope.selected, onChange: (on) => ui2({ kind: "scope", id: scope.id, on }) }) : null
981
+ ] })
982
+ },
983
+ scope.id
984
+ )) }) : null,
985
+ view.question ? /* @__PURE__ */ jsxs(Stack, { gap: 6, pad: "inset", children: [
986
+ view.hasOptions ? /* @__PURE__ */ jsx(RowList, { children: view.options.map((option) => /* @__PURE__ */ jsx(
987
+ Row,
988
+ {
989
+ noIcon: true,
990
+ height: option.height,
991
+ label: /* @__PURE__ */ jsx(TwoLine, { label: option.label, detail: option.detail }),
992
+ meta: option.risk ? /* @__PURE__ */ jsx(Badge, { tone: option.riskTone, children: option.risk }) : null,
993
+ trailing: /* @__PURE__ */ jsx(Toggle, { size: "sm", label: option.toggleLabel, checked: option.selected, onChange: (on) => ui2({ kind: "option", id: option.id, on }) })
994
+ },
995
+ option.id
996
+ )) }) : null,
997
+ view.wantsText ? /* @__PURE__ */ jsxs(Stack, { gap: 3, pad: "none", children: [
998
+ view.textLabel ? /* @__PURE__ */ jsx(Text, { role: "meta", children: view.textLabel }) : null,
999
+ view.multiline ? /* @__PURE__ */ jsx(TextArea, { value: view.draft, placeholder: view.placeholder, rows: 3, label: "Answer", onChange: (value) => ui2({ kind: "text", value }) }) : null,
1000
+ !view.multiline ? /* @__PURE__ */ jsx(Input, { value: view.draft, placeholder: view.placeholder, autoComplete: "off", label: "Answer", onChange: (value) => ui2({ kind: "text", value }) }) : null,
1001
+ view.hasCounter ? /* @__PURE__ */ jsx(Text, { role: "caption", children: view.counter }) : null,
1002
+ view.overLimit ? /* @__PURE__ */ jsx(Badge, { tone: "error", children: "too long" }) : null
1003
+ ] }) : null
1004
+ ] }) : null
1005
+ ] }),
1006
+ view.permission ? /* @__PURE__ */ jsx(
1007
+ Toolbar,
858
1008
  {
859
- value: state.draftText,
860
- onChange: (event) => controller.setDraftText(event.target.value),
861
- placeholder,
862
- rows: 3,
863
- spellCheck: false,
864
- style: FIELD_STYLE
1009
+ divided: false,
1010
+ left: /* @__PURE__ */ jsx(SegmentedControl, { size: "sm", label: "How long", options: view.durations, value: view.duration, onChange: (duration) => ui2({ kind: "duration", duration }) }),
1011
+ right: view.unrememberable ? /* @__PURE__ */ jsx(Text, { role: "caption", children: "This grant cannot be remembered." }) : null
865
1012
  }
866
- ) : /* @__PURE__ */ jsx(
867
- "input",
1013
+ ) : null,
1014
+ /* @__PURE__ */ jsx(
1015
+ Toolbar,
868
1016
  {
869
- type: "text",
870
- value: state.draftText,
871
- onChange: (event) => controller.setDraftText(event.target.value),
872
- placeholder,
873
- spellCheck: false,
874
- autoComplete: "off",
875
- style: FIELD_STYLE
1017
+ divided: false,
1018
+ right: /* @__PURE__ */ jsxs(ToolbarGroup, { end: true, children: [
1019
+ view.permission ? /* @__PURE__ */ jsx(TextButton, { onClick: () => ui2({ kind: "deny" }), children: "Deny" }) : null,
1020
+ view.permission ? /* @__PURE__ */ jsx(TextButton, { strong: true, disabled: view.allowDisabled, onClick: () => ui2({ kind: "allow" }), children: "Allow" }) : null,
1021
+ view.question ? /* @__PURE__ */ jsx(TextButton, { onClick: () => ui2({ kind: "dismiss" }), children: "Cancel" }) : null,
1022
+ view.question ? /* @__PURE__ */ jsx(TextButton, { strong: true, disabled: view.submitDisabled, onClick: () => ui2({ kind: "submit" }), children: view.submitLabel }) : null
1023
+ ] })
876
1024
  }
877
1025
  ),
878
- max !== null ? /* @__PURE__ */ jsx(
879
- "span",
880
- {
881
- style: {
882
- fontSize: 9,
883
- opacity: 0.6,
884
- alignSelf: "flex-end",
885
- // Over the limit the answer is REFUSED, not truncated — so the counter has to say
886
- // so rather than implying the extra characters are merely ignored.
887
- color: state.draftText.length > max ? "var(--xeno-danger)" : void 0
888
- },
889
- children: `${state.draftText.length} / ${max}`
890
- }
891
- ) : null
1026
+ /* @__PURE__ */ jsx(StatusBar, { left: view.pendingLabel, right: view.expiryLabel })
892
1027
  ] }) : null
893
1028
  ] });
894
1029
  }
@@ -979,16 +1114,22 @@ function createConsentPanel(options = {}) {
979
1114
  }
980
1115
  var consentPanel = createConsentPanel();
981
1116
  export {
1117
+ BEHAVIOURS as CONSENT_BEHAVIOURS,
1118
+ CONSENT_DECLARATION,
982
1119
  CONSENT_PANEL_ID,
1120
+ CONSENT_RUNTIME_UNITS,
983
1121
  ConsentController,
984
1122
  ConsentPanelView,
985
1123
  ELICITATION_RESULT_SCHEMA,
986
1124
  ELICITATION_SCHEMA,
987
1125
  allowedDurations,
1126
+ applyConsentUi,
988
1127
  autoCancel,
989
1128
  autoDeny,
990
1129
  consentManifest,
991
1130
  consentPanel,
1131
+ consentViewManifest,
1132
+ consentViewState,
992
1133
  createConsentPanel,
993
1134
  effectiveRisk,
994
1135
  elicitationAsk,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenosystem/blocks",
3
- "version": "0.13.14",
3
+ "version": "0.13.16",
4
4
  "description": "XENO Blocks — rung 3 of the front ladder: components composed into a contract-bearing unit (manifest: ports, intents, commands, agent surface), rendered by @xenosystem/workbench. ONE package, one subpath per family (/auth, /edit, /data, /time, /ops, /trust, /agent), one named export per block — XENO PACKAGE NAMING - STANDARD.md.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  "peerDependencies": {
65
65
  "@duckdb/duckdb-wasm": ">=1.28.0",
66
66
  "@xenosystem/block-sdk": ">=0.3.4 <1",
67
- "@xenosystem/components": ">=0.24.0 <1",
67
+ "@xenosystem/components": ">=0.25.0 <1",
68
68
  "@xenosystem/data-core": "^0.1.0",
69
69
  "@xenosystem/elements": ">=0.0.1",
70
70
  "@xenosystem/elements-react": ">=0.0.1",
@@ -81,7 +81,7 @@
81
81
  "@types/react": "^19.0.0",
82
82
  "@types/react-dom": "^19.0.0",
83
83
  "@xenosystem/block-sdk": "^0.3.4",
84
- "@xenosystem/components": "^0.24.0",
84
+ "@xenosystem/components": "^0.25.0",
85
85
  "@xenosystem/data-core": "^0.1.0",
86
86
  "@xenosystem/elements": "0.0.1",
87
87
  "@xenosystem/elements-react": "0.0.1",