@xenosystem/blocks 0.13.13 → 0.13.15

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.
@@ -661,234 +661,364 @@ 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
+ risk: scope.risk && scope.risk !== risk ? scope.risk : void 0,
719
+ riskTone: scope.risk ? RISK_TONE[scope.risk] : "neutral",
720
+ granted: scope.alreadyGranted === true,
721
+ grantable: scope.alreadyGranted !== true,
722
+ toggleLabel: `Grant ${scope.label}`,
723
+ selected: s.selectedScopeIds.includes(scope.id)
724
+ })) : [],
725
+ durations: permission ? allowedDurations(permission).map((d) => ({ value: d, label: DURATION_LABEL[d] })) : [],
726
+ duration: s.duration,
727
+ unrememberable: permission?.remember === "forbidden",
728
+ allowDisabled: !!permission && permission.scopes.some((sc) => !sc.alreadyGranted) && s.selectedScopeIds.length === 0,
729
+ question: !permission,
730
+ hasOptions: opts.length > 0,
731
+ options: opts.map((option) => ({
732
+ id: option.id,
733
+ label: option.label,
734
+ detail: option.detail,
735
+ risk: option.risk && option.risk !== risk ? option.risk : void 0,
736
+ riskTone: option.risk ? RISK_TONE[option.risk] : "neutral",
737
+ toggleLabel: `Choose ${option.label}`,
738
+ selected: s.selectedOptionId === option.id
739
+ })),
740
+ wantsText,
741
+ textLabel: extra.textLabel,
742
+ multiline: ask === "text" && extra.multiline === true,
743
+ placeholder: extra.placeholder ?? (ask === "path" ? "Type or paste a path\u2026" : "Type your answer\u2026"),
744
+ draft: s.draftText,
745
+ hasCounter: max !== null,
746
+ counter: max !== null ? `${s.draftText.length} / ${max}` : "",
747
+ overLimit: max !== null && s.draftText.length > max,
748
+ submitLabel: SUBMIT_LABEL[ask] ?? "Submit",
749
+ submitDisabled: !s.canSubmit,
750
+ pendingLabel: `${s.pending} pending`,
751
+ expiryLabel: s.expiresInMs !== null ? `expires in ${Math.ceil(s.expiresInMs / 1e3)}s \u2014 ${permission ? "denies" : "cancels"}` : void 0
752
+ };
753
+ }
754
+ function applyConsentUi(c, intent) {
755
+ switch (intent.kind) {
756
+ case "scope":
757
+ c.toggleScope(intent.id, intent.on);
758
+ break;
759
+ case "duration":
760
+ c.setDuration(intent.duration);
761
+ break;
762
+ case "option":
763
+ c.selectOption(intent.on ? intent.id : null);
764
+ break;
765
+ case "text":
766
+ c.setDraftText(intent.value);
767
+ break;
768
+ case "allow":
769
+ c.allow();
770
+ break;
771
+ case "deny":
772
+ c.deny();
773
+ break;
774
+ case "submit":
775
+ c.submit();
776
+ break;
777
+ case "dismiss":
778
+ c.dismiss();
779
+ break;
780
+ }
781
+ }
782
+ var consentViewManifest = {
783
+ id: consentManifest.id,
784
+ version: "1.0.0",
785
+ title: "Consent",
786
+ category: "trust",
787
+ layout: { slot: "side" },
788
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The ConsentViewModel the runtime derives from the controller on every draw." }],
789
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, \u2026 } \u2014 applied by the runtime to the controller." }],
790
+ commands: []
791
+ };
792
+ 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" };
793
+ var BEHAVIOURS = {
794
+ "trust/consentPanelView": (inputs) => inputs.view ?? EMPTY
795
+ };
796
+ var CONSENT_RUNTIME_UNITS = [];
797
+ var text = (b) => ({ component: "text", props: { text: b } });
798
+ var ui = (kind, fields = {}) => ({ emit: "ui", value: { object: { kind: { value: kind }, ...fields } } });
799
+ var twoLine = {
800
+ component: "chrome/Stack",
801
+ props: { gap: { value: 1 }, pad: { value: "none" } },
802
+ children: [{ component: "chrome/Text", children: [text({ item: "label" })] }, { component: "chrome/Text", when: { item: "detail" }, props: { role: { value: "meta" } }, children: [text({ item: "detail" })] }]
803
+ };
804
+ var riskMeta = { node: { component: "chrome/Badge", when: { item: "risk" }, props: { tone: { item: "riskTone" } }, children: [text({ item: "risk" })] } };
805
+ var CONSENT_DECLARATION = {
806
+ format: "xeno.block@1",
807
+ manifest: consentViewManifest,
808
+ behaviour: "trust/consentPanelView",
809
+ composition: {
810
+ component: "fragment",
811
+ children: [
812
+ { component: "chrome/EmptyState", when: { input: "view.idle" }, props: { title: { value: "Nothing pending" }, hint: { input: "view.emptyHint" } } },
722
813
  {
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)
814
+ component: "chrome/Column",
815
+ when: { input: "view.active" },
816
+ children: [
817
+ {
818
+ component: "chrome/Toolbar",
819
+ props: {
820
+ left: {
821
+ node: {
822
+ component: "fragment",
823
+ children: [
824
+ { component: "chrome/Badge", props: { tone: { input: "view.riskTone" } }, children: [text({ input: "view.riskLabel" })] },
825
+ { component: "chrome/Badge", children: [text({ input: "view.kindLabel" })] },
826
+ { component: "chrome/Text", when: { input: "view.subject" }, children: [text({ input: "view.subject" })] }
827
+ ]
828
+ }
829
+ },
830
+ 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
831
  }
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: [
832
+ },
833
+ {
834
+ component: "chrome/ScrollArea",
835
+ children: [
836
+ {
837
+ component: "chrome/Stack",
838
+ props: { gap: { value: 6 }, pad: { value: "gutter" } },
839
+ children: [
840
+ { component: "chrome/Text", props: { role: { value: "title" } }, children: [text({ input: "view.title" })] },
841
+ { component: "chrome/Text", when: { input: "view.detail" }, children: [text({ input: "view.detail" })] },
842
+ { component: "chrome/KeyValue", each: { input: "view.facts", key: "key" }, props: { name: { item: "key" }, value: { item: "value" } } }
843
+ ]
844
+ },
845
+ {
846
+ component: "chrome/RowList",
847
+ when: { input: "view.permission" },
848
+ children: [
849
+ {
850
+ component: "chrome/Row",
851
+ each: { input: "view.scopes", key: "id" },
852
+ props: {
853
+ noIcon: { value: true },
854
+ label: { node: twoLine },
855
+ meta: riskMeta,
856
+ trailing: {
857
+ node: {
858
+ component: "fragment",
859
+ children: [
860
+ { component: "chrome/Badge", when: { item: "granted" }, props: { title: { value: "Already granted" } }, children: [text({ value: "granted" })] },
861
+ { 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: "" } }) } }
862
+ ]
863
+ }
864
+ }
865
+ }
866
+ }
867
+ ]
868
+ },
869
+ {
870
+ component: "chrome/Stack",
871
+ when: { input: "view.question" },
872
+ props: { gap: { value: 6 }, pad: { value: "inset" } },
873
+ children: [
874
+ {
875
+ component: "chrome/RowList",
876
+ when: { input: "view.hasOptions" },
877
+ children: [
878
+ {
879
+ component: "chrome/Row",
880
+ each: { input: "view.options", key: "id" },
881
+ props: { noIcon: { value: true }, 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: "" } }) } } } }
882
+ }
883
+ ]
884
+ },
885
+ {
886
+ component: "chrome/Stack",
887
+ when: { input: "view.wantsText" },
888
+ props: { gap: { value: 3 }, pad: { value: "none" } },
889
+ children: [
890
+ { component: "chrome/Text", when: { input: "view.textLabel" }, props: { role: { value: "meta" } }, children: [text({ input: "view.textLabel" })] },
891
+ { 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: "" } }) } },
892
+ { 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: "" } }) } },
893
+ { component: "chrome/Text", when: { input: "view.hasCounter" }, props: { role: { value: "caption" } }, children: [text({ input: "view.counter" })] },
894
+ { component: "chrome/Badge", when: { input: "view.overLimit" }, props: { tone: { value: "error" } }, children: [text({ value: "too long" })] }
895
+ ]
896
+ }
897
+ ]
898
+ }
899
+ ]
900
+ },
901
+ {
902
+ component: "chrome/Toolbar",
903
+ when: { input: "view.permission" },
904
+ props: {
905
+ divided: { value: false },
906
+ // Duration is IN the decision, not buried in a settings page.
907
+ 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: "" } }) } } },
908
+ right: { node: { component: "chrome/Text", when: { input: "view.unrememberable" }, props: { role: { value: "caption" } }, children: [text({ value: "This grant cannot be remembered." })] } }
909
+ }
910
+ },
911
+ {
912
+ component: "chrome/Toolbar",
913
+ props: {
914
+ divided: { value: false },
915
+ right: {
916
+ node: {
917
+ component: "chrome/ToolbarGroup",
918
+ props: { end: { value: true } },
919
+ children: [
920
+ { component: "chrome/TextButton", when: { input: "view.permission" }, on: { onClick: ui("deny") }, children: [text({ value: "Deny" })] },
921
+ // No auto-focus: a stray Enter must not grant anything.
922
+ { component: "chrome/TextButton", when: { input: "view.permission" }, props: { strong: { value: true }, disabled: { input: "view.allowDisabled" } }, on: { onClick: ui("allow") }, children: [text({ value: "Allow" })] },
923
+ // A question has no "deny" — refusing to answer is a CANCELLATION.
924
+ { component: "chrome/TextButton", when: { input: "view.question" }, on: { onClick: ui("dismiss") }, children: [text({ value: "Cancel" })] },
925
+ { component: "chrome/TextButton", when: { input: "view.question" }, props: { strong: { value: true }, disabled: { input: "view.submitDisabled" } }, on: { onClick: ui("submit") }, children: [text({ input: "view.submitLabel" })] }
926
+ ]
927
+ }
928
+ }
929
+ }
930
+ },
931
+ { component: "chrome/StatusBar", props: { left: { input: "view.pendingLabel" }, right: { input: "view.expiryLabel" } } }
932
+ ]
933
+ }
934
+ ]
935
+ }
936
+ };
937
+
938
+ // src/trust/consent/react/ConsentPanelView.tsx
939
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
940
+ var TwoLine = ({ label, detail }) => /* @__PURE__ */ jsxs(Stack, { gap: 1, pad: "none", children: [
941
+ /* @__PURE__ */ jsx(Text, { children: label }),
942
+ detail ? /* @__PURE__ */ jsx(Text, { role: "meta", children: detail }) : null
943
+ ] });
944
+ function ConsentPanelView({ controller, showQueueCount, emptyHint }) {
945
+ useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
946
+ const view = consentViewState(controller, { showQueueCount, emptyHint });
947
+ const ui2 = (intent) => applyConsentUi(controller, intent);
948
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
949
+ view.idle ? /* @__PURE__ */ jsx(EmptyState, { title: "Nothing pending", hint: view.emptyHint }) : null,
950
+ view.active ? /* @__PURE__ */ jsxs(Column, { children: [
770
951
  /* @__PURE__ */ jsx(
771
- SegmentedControl,
952
+ Toolbar,
772
953
  {
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)
954
+ left: /* @__PURE__ */ jsxs(Fragment, { children: [
955
+ /* @__PURE__ */ jsx(Badge, { tone: view.riskTone, children: view.riskLabel }),
956
+ /* @__PURE__ */ jsx(Badge, { children: view.kindLabel }),
957
+ view.subject ? /* @__PURE__ */ jsx(Text, { children: view.subject }) : null
958
+ ] }),
959
+ 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
960
  }
782
961
  ),
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
962
+ /* @__PURE__ */ jsxs(ScrollArea, { children: [
963
+ /* @__PURE__ */ jsxs(Stack, { gap: 6, pad: "gutter", children: [
964
+ /* @__PURE__ */ jsx(Text, { role: "title", children: view.title }),
965
+ view.detail ? /* @__PURE__ */ jsx(Text, { children: view.detail }) : null,
966
+ view.facts.map((f) => /* @__PURE__ */ jsx(KeyValue, { name: f.key, value: f.value }, f.key))
840
967
  ] }),
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,
968
+ view.permission ? /* @__PURE__ */ jsx(RowList, { children: view.scopes.map((scope) => /* @__PURE__ */ jsx(
969
+ Row,
844
970
  {
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",
971
+ noIcon: true,
972
+ label: /* @__PURE__ */ jsx(TwoLine, { label: scope.label, detail: scope.detail }),
973
+ meta: scope.risk ? /* @__PURE__ */ jsx(Badge, { tone: scope.riskTone, children: scope.risk }) : null,
974
+ trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
975
+ scope.granted ? /* @__PURE__ */ jsx(Badge, { title: "Already granted", children: "granted" }) : null,
976
+ scope.grantable ? /* @__PURE__ */ jsx(Toggle, { size: "sm", label: scope.toggleLabel, checked: scope.selected, onChange: (on) => ui2({ kind: "scope", id: scope.id, on }) }) : null
977
+ ] })
978
+ },
979
+ scope.id
980
+ )) }) : null,
981
+ view.question ? /* @__PURE__ */ jsxs(Stack, { gap: 6, pad: "inset", children: [
982
+ view.hasOptions ? /* @__PURE__ */ jsx(RowList, { children: view.options.map((option) => /* @__PURE__ */ jsx(
983
+ Row,
984
+ {
985
+ noIcon: true,
986
+ label: /* @__PURE__ */ jsx(TwoLine, { label: option.label, detail: option.detail }),
987
+ meta: option.risk ? /* @__PURE__ */ jsx(Badge, { tone: option.riskTone, children: option.risk }) : null,
988
+ trailing: /* @__PURE__ */ jsx(Toggle, { size: "sm", label: option.toggleLabel, checked: option.selected, onChange: (on) => ui2({ kind: "option", id: option.id, on }) })
989
+ },
990
+ option.id
991
+ )) }) : null,
992
+ view.wantsText ? /* @__PURE__ */ jsxs(Stack, { gap: 3, pad: "none", children: [
993
+ view.textLabel ? /* @__PURE__ */ jsx(Text, { role: "meta", children: view.textLabel }) : null,
994
+ view.multiline ? /* @__PURE__ */ jsx(TextArea, { value: view.draft, placeholder: view.placeholder, rows: 3, label: "Answer", onChange: (value) => ui2({ kind: "text", value }) }) : null,
995
+ !view.multiline ? /* @__PURE__ */ jsx(Input, { value: view.draft, placeholder: view.placeholder, autoComplete: "off", label: "Answer", onChange: (value) => ui2({ kind: "text", value }) }) : null,
996
+ view.hasCounter ? /* @__PURE__ */ jsx(Text, { role: "caption", children: view.counter }) : null,
997
+ view.overLimit ? /* @__PURE__ */ jsx(Badge, { tone: "error", children: "too long" }) : null
998
+ ] }) : null
999
+ ] }) : null
1000
+ ] }),
1001
+ view.permission ? /* @__PURE__ */ jsx(
1002
+ Toolbar,
858
1003
  {
859
- value: state.draftText,
860
- onChange: (event) => controller.setDraftText(event.target.value),
861
- placeholder,
862
- rows: 3,
863
- spellCheck: false,
864
- style: FIELD_STYLE
1004
+ divided: false,
1005
+ left: /* @__PURE__ */ jsx(SegmentedControl, { size: "sm", label: "How long", options: view.durations, value: view.duration, onChange: (duration) => ui2({ kind: "duration", duration }) }),
1006
+ right: view.unrememberable ? /* @__PURE__ */ jsx(Text, { role: "caption", children: "This grant cannot be remembered." }) : null
865
1007
  }
866
- ) : /* @__PURE__ */ jsx(
867
- "input",
1008
+ ) : null,
1009
+ /* @__PURE__ */ jsx(
1010
+ Toolbar,
868
1011
  {
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
1012
+ divided: false,
1013
+ right: /* @__PURE__ */ jsxs(ToolbarGroup, { end: true, children: [
1014
+ view.permission ? /* @__PURE__ */ jsx(TextButton, { onClick: () => ui2({ kind: "deny" }), children: "Deny" }) : null,
1015
+ view.permission ? /* @__PURE__ */ jsx(TextButton, { strong: true, disabled: view.allowDisabled, onClick: () => ui2({ kind: "allow" }), children: "Allow" }) : null,
1016
+ view.question ? /* @__PURE__ */ jsx(TextButton, { onClick: () => ui2({ kind: "dismiss" }), children: "Cancel" }) : null,
1017
+ view.question ? /* @__PURE__ */ jsx(TextButton, { strong: true, disabled: view.submitDisabled, onClick: () => ui2({ kind: "submit" }), children: view.submitLabel }) : null
1018
+ ] })
876
1019
  }
877
1020
  ),
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
1021
+ /* @__PURE__ */ jsx(StatusBar, { left: view.pendingLabel, right: view.expiryLabel })
892
1022
  ] }) : null
893
1023
  ] });
894
1024
  }
@@ -979,16 +1109,22 @@ function createConsentPanel(options = {}) {
979
1109
  }
980
1110
  var consentPanel = createConsentPanel();
981
1111
  export {
1112
+ BEHAVIOURS as CONSENT_BEHAVIOURS,
1113
+ CONSENT_DECLARATION,
982
1114
  CONSENT_PANEL_ID,
1115
+ CONSENT_RUNTIME_UNITS,
983
1116
  ConsentController,
984
1117
  ConsentPanelView,
985
1118
  ELICITATION_RESULT_SCHEMA,
986
1119
  ELICITATION_SCHEMA,
987
1120
  allowedDurations,
1121
+ applyConsentUi,
988
1122
  autoCancel,
989
1123
  autoDeny,
990
1124
  consentManifest,
991
1125
  consentPanel,
1126
+ consentViewManifest,
1127
+ consentViewState,
992
1128
  createConsentPanel,
993
1129
  effectiveRisk,
994
1130
  elicitationAsk,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenosystem/blocks",
3
- "version": "0.13.13",
3
+ "version": "0.13.15",
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.23.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.23.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",