@vectoriox/iox-builder 4.31.0 → 4.31.2

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.
@@ -1631,7 +1631,11 @@ class OverlayService {
1631
1631
  if (host.classList[i].startsWith('iox-node-'))
1632
1632
  return host;
1633
1633
  }
1634
- return host.firstElementChild || host;
1634
+ // The node's own styled element always precedes its children in document order; for the
1635
+ // Dialog it sits BELOW a classless fixed stage, where firstElementChild pointed the
1636
+ // outline at the stage (live find 2026-10-10 — "can't select the white box").
1637
+ const styled = host.querySelector('[class*="iox-node-"]');
1638
+ return styled || host.firstElementChild || host;
1635
1639
  }
1636
1640
  // ── Node → Element registry ──────────────────────────────
1637
1641
  registerNode(node, element, componentRef) {
@@ -8490,6 +8494,22 @@ class BuilderDialogComponent {
8490
8494
  this.selectSub?.unsubscribe();
8491
8495
  this.instanceRegistry.unregister(this.nodeId);
8492
8496
  }
8497
+ /** A click on the stage ITSELF is the backdrop click — children swallow everything else.
8498
+ * Closing clicks STOP here: the same click would bubble into canvas selection, and
8499
+ * select$ would reopen the dialog in the same breath (live find 2026-10-10 — "the X
8500
+ * doesn't work"). */
8501
+ onStageClick(event) {
8502
+ if (event.target !== event.currentTarget)
8503
+ return;
8504
+ if (this.closeOnBackdrop !== false && this.closeOnBackdrop !== 'false') {
8505
+ event.stopPropagation();
8506
+ this.designOpen = false;
8507
+ }
8508
+ }
8509
+ onCloseClick(event) {
8510
+ event.stopPropagation();
8511
+ this.designOpen = false;
8512
+ }
8493
8513
  onBackdrop() {
8494
8514
  if (this.closeOnBackdrop !== false && this.closeOnBackdrop !== 'false')
8495
8515
  this.designOpen = false;
@@ -8499,8 +8519,8 @@ class BuilderDialogComponent {
8499
8519
  }
8500
8520
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderDialogComponent, deps: [{ token: DragEngineService }, { token: i1.ComponentInstanceRegistryService }, { token: OverlayService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
8501
8521
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderDialogComponent, isStandalone: false, selector: "app-builder-dialog", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId", closeOnBackdrop: "closeOnBackdrop", showCloseButton: "showCloseButton", placement: "placement" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
8502
- <div *ngIf="designOpen" class="dialog-stage" [attr.data-placement]="placementValue">
8503
- <div class="dialog-backdrop" (click)="onBackdrop()"></div>
8522
+ <div *ngIf="designOpen" class="dialog-stage" [attr.data-placement]="placementValue"
8523
+ (click)="onStageClick($event)">
8504
8524
  <div [ngStyle]="style"
8505
8525
  [ngClass]="'iox-node-' + nodeId"
8506
8526
  class="dialog-surface"
@@ -8512,7 +8532,8 @@ class BuilderDialogComponent {
8512
8532
  type="button"
8513
8533
  class="dialog-close"
8514
8534
  aria-label="Close dialog"
8515
- (click)="designOpen = false">
8535
+ (mousedown)="$event.stopPropagation()"
8536
+ (click)="onCloseClick($event)">
8516
8537
  <i class="ph-thin ph-x"></i>
8517
8538
  </button>
8518
8539
 
@@ -8530,13 +8551,13 @@ class BuilderDialogComponent {
8530
8551
  </ng-container>
8531
8552
  </div>
8532
8553
  </div>
8533
- `, isInline: true, styles: [".dialog-stage{position:fixed;inset:0;z-index:50}.dialog-backdrop{position:absolute;inset:0;background:#0f172a73}.dialog-surface{position:absolute;inset-inline-start:50%;top:50%;transform:translate(-50%,-50%);box-sizing:border-box;max-width:calc(100% - 48px);max-height:calc(100% - 48px);overflow:auto}:host-context([dir=\"rtl\"]) .dialog-surface{transform:translate(50%,-50%)}.dialog-stage[data-placement=top] .dialog-surface{top:24px;transform:translate(-50%)}:host-context([dir=\"rtl\"]) .dialog-stage[data-placement=top] .dialog-surface{transform:translate(50%)}.dialog-stage[data-placement=bottom] .dialog-surface{top:auto;bottom:24px;transform:translate(-50%)}:host-context([dir=\"rtl\"]) .dialog-stage[data-placement=bottom] .dialog-surface{transform:translate(50%)}.dialog-stage[data-placement=start] .dialog-surface{inset-inline-start:24px;transform:translateY(-50%)}.dialog-stage[data-placement=end] .dialog-surface{inset-inline-start:auto;inset-inline-end:24px;transform:translateY(-50%)}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:999px;background:#0f172a0f;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
8554
+ `, isInline: true, styles: [".dialog-stage{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:24px;box-sizing:border-box;background:#0f172a73}.dialog-stage[data-placement=top]{align-items:start}.dialog-stage[data-placement=bottom]{align-items:end}.dialog-stage[data-placement=start]{justify-items:start}.dialog-stage[data-placement=end]{justify-items:end}.dialog-surface{box-sizing:border-box;max-width:100%;max-height:100%;overflow:auto;contain:layout}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:999px;background:#0f172a0f;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
8534
8555
  }
8535
8556
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderDialogComponent, decorators: [{
8536
8557
  type: Component,
8537
8558
  args: [{ selector: 'app-builder-dialog', template: `
8538
- <div *ngIf="designOpen" class="dialog-stage" [attr.data-placement]="placementValue">
8539
- <div class="dialog-backdrop" (click)="onBackdrop()"></div>
8559
+ <div *ngIf="designOpen" class="dialog-stage" [attr.data-placement]="placementValue"
8560
+ (click)="onStageClick($event)">
8540
8561
  <div [ngStyle]="style"
8541
8562
  [ngClass]="'iox-node-' + nodeId"
8542
8563
  class="dialog-surface"
@@ -8548,7 +8569,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
8548
8569
  type="button"
8549
8570
  class="dialog-close"
8550
8571
  aria-label="Close dialog"
8551
- (click)="designOpen = false">
8572
+ (mousedown)="$event.stopPropagation()"
8573
+ (click)="onCloseClick($event)">
8552
8574
  <i class="ph-thin ph-x"></i>
8553
8575
  </button>
8554
8576
 
@@ -8566,7 +8588,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
8566
8588
  </ng-container>
8567
8589
  </div>
8568
8590
  </div>
8569
- `, standalone: false, styles: [".dialog-stage{position:fixed;inset:0;z-index:50}.dialog-backdrop{position:absolute;inset:0;background:#0f172a73}.dialog-surface{position:absolute;inset-inline-start:50%;top:50%;transform:translate(-50%,-50%);box-sizing:border-box;max-width:calc(100% - 48px);max-height:calc(100% - 48px);overflow:auto}:host-context([dir=\"rtl\"]) .dialog-surface{transform:translate(50%,-50%)}.dialog-stage[data-placement=top] .dialog-surface{top:24px;transform:translate(-50%)}:host-context([dir=\"rtl\"]) .dialog-stage[data-placement=top] .dialog-surface{transform:translate(50%)}.dialog-stage[data-placement=bottom] .dialog-surface{top:auto;bottom:24px;transform:translate(-50%)}:host-context([dir=\"rtl\"]) .dialog-stage[data-placement=bottom] .dialog-surface{transform:translate(50%)}.dialog-stage[data-placement=start] .dialog-surface{inset-inline-start:24px;transform:translateY(-50%)}.dialog-stage[data-placement=end] .dialog-surface{inset-inline-start:auto;inset-inline-end:24px;transform:translateY(-50%)}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:999px;background:#0f172a0f;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"] }]
8591
+ `, standalone: false, styles: [".dialog-stage{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:24px;box-sizing:border-box;background:#0f172a73}.dialog-stage[data-placement=top]{align-items:start}.dialog-stage[data-placement=bottom]{align-items:end}.dialog-stage[data-placement=start]{justify-items:start}.dialog-stage[data-placement=end]{justify-items:end}.dialog-surface{box-sizing:border-box;max-width:100%;max-height:100%;overflow:auto;contain:layout}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:999px;background:#0f172a0f;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"] }]
8570
8592
  }], ctorParameters: () => [{ type: DragEngineService }, { type: i1.ComponentInstanceRegistryService }, { type: OverlayService }, { type: i0.ChangeDetectorRef }], propDecorators: { children: [{
8571
8593
  type: Input
8572
8594
  }], style: [{
@@ -9988,6 +10010,13 @@ function setTrait(node, name, value) {
9988
10010
  trait.default = value;
9989
10011
  }
9990
10012
 
10013
+ function text$1(content, styles, createConfig) {
10014
+ const node = textNode(content, createConfig);
10015
+ if (node)
10016
+ for (const [prop, value] of Object.entries(styles))
10017
+ setStyleTrait(node, prop, value);
10018
+ return node;
10019
+ }
9991
10020
  /**
9992
10021
  * THE CONSENT PRESET (consent plan C3) — the banner as builder elements, which is the whole bet:
9993
10022
  * Wix ships a banner you cannot design, Webflow ships a design system and no banner; ours is a
@@ -10014,16 +10043,50 @@ const click = (actions) => ({
10014
10043
  target: 'self', duration: 0, delay: 0, easing: 'ease', ...a,
10015
10044
  })),
10016
10045
  });
10017
- function button(label, createConfig, interaction) {
10046
+ const TOKEN$1 = (name, fallback) => `var(--iox-${name}, ${fallback})`;
10047
+ function button(label, createConfig, interaction, styles = {}) {
10018
10048
  const btn = createConfig('ButtonBlock');
10019
10049
  const text = textNode(label, createConfig);
10020
10050
  if (!btn || !text)
10021
10051
  return null;
10022
10052
  setTrait(btn, 'tag', 'button');
10053
+ setStyleTrait(text, 'fontSize', 'inherit');
10054
+ setStyleTrait(text, 'fontWeight', 'inherit');
10055
+ setStyleTrait(text, 'color', 'inherit');
10056
+ for (const [prop, value] of Object.entries(styles))
10057
+ setStyleTrait(btn, prop, value);
10023
10058
  btn.children = [text];
10024
10059
  btn.interactions = [interaction];
10025
10060
  return btn;
10026
10061
  }
10062
+ /** The C1/C2 board button voices — Swiss by fallback, the brand's the moment tokens seed. */
10063
+ const SOLID = {
10064
+ backgroundColor: TOKEN$1('brand-500', '#1f1d1b'), color: TOKEN$1('brandInk', '#ffffff'),
10065
+ fontSize: '12px', fontWeight: '600', padding: '9px 16px',
10066
+ borderRadius: TOKEN$1('radius-control', '4px'),
10067
+ };
10068
+ const OUTLINED = {
10069
+ backgroundColor: 'transparent', color: TOKEN$1('ink', '#1f1d1b'),
10070
+ borderWidth: '1px', borderStyle: 'solid', borderColor: TOKEN$1('ink', '#1f1d1b'),
10071
+ fontSize: '12px', fontWeight: '600', padding: '8px 16px',
10072
+ borderRadius: TOKEN$1('radius-control', '4px'),
10073
+ };
10074
+ const QUIET = {
10075
+ backgroundColor: 'transparent', color: TOKEN$1('inkMuted', '#4a453f'),
10076
+ fontSize: '11px', fontWeight: '400', padding: '2px 4px', textDecoration: 'underline',
10077
+ };
10078
+ const SMALL_ALLOW = {
10079
+ backgroundColor: 'transparent', color: TOKEN$1('ink', '#1f1d1b'),
10080
+ borderWidth: '1px', borderStyle: 'solid', borderColor: TOKEN$1('ink', '#1f1d1b'),
10081
+ fontSize: '11px', fontWeight: '600', padding: '5px 10px',
10082
+ borderRadius: TOKEN$1('radius-control', '4px'),
10083
+ };
10084
+ const SMALL_REFUSE = {
10085
+ backgroundColor: 'transparent', color: TOKEN$1('inkMuted', '#6b6560'),
10086
+ borderWidth: '1px', borderStyle: 'solid', borderColor: TOKEN$1('line', '#d8d4d1'),
10087
+ fontSize: '11px', fontWeight: '600', padding: '5px 10px',
10088
+ borderRadius: TOKEN$1('radius-control', '4px'),
10089
+ };
10027
10090
  function row(createConfig, styles, children) {
10028
10091
  const container = createConfig('Container');
10029
10092
  if (!container || children.some(c => !c))
@@ -10045,57 +10108,97 @@ function buildConsentPreset(createConfig) {
10045
10108
  setTrait(banner, 'closeOnBackdrop', false); // clicking away is NOT consent
10046
10109
  setTrait(banner, 'showCloseButton', true); // the runtime records X as a "no"
10047
10110
  setTrait(banner, 'placement', 'bottom'); // A5: placement is the Dialog's own choice
10048
- setStyleTrait(banner, 'width', 'min(560px, calc(100vw - 32px))');
10049
- const heading = textNode('Privacy choices', createConfig);
10050
- if (heading)
10051
- setStyleTrait(heading, 'fontWeight', '600');
10052
- const body = textNode('We use cookies to understand how this site is used. You choose what we may use, and you can change your mind at any time.', createConfig);
10053
- const categoriesLine = textNode('Necessary — always on · Analytics · Marketing', createConfig);
10111
+ // Board C1, Swiss column: a full-width bottom bar, hairline-ruled, surface-on-surface.
10112
+ setStyleTrait(banner, 'width', '100%');
10113
+ setStyleTrait(banner, 'borderRadius', '0px');
10114
+ setStyleTrait(banner, 'padding', '14px 20px');
10115
+ setStyleTrait(banner, 'backgroundColor', TOKEN$1('surface', '#ffffff'));
10116
+ setStyleTrait(banner, 'borderWidth', '1px');
10117
+ setStyleTrait(banner, 'borderStyle', 'solid');
10118
+ setStyleTrait(banner, 'borderColor', TOKEN$1('ink', '#1f1d1b'));
10119
+ setStyleTrait(banner, 'boxShadow', 'none');
10120
+ const heading = text$1('Cookies on this site', { fontSize: '13px', fontWeight: '600', color: TOKEN$1('ink', '#1f1d1b') }, createConfig);
10121
+ // The first layer NAMES the three categories (plan §7) — the board does it in the copy.
10122
+ const body = text$1('We use analytics and marketing cookies only if you allow them. Necessary ones always run.', { fontSize: '11px', color: TOKEN$1('inkMuted', '#4a453f'), lineHeight: '1.5', maxWidth: '330px' }, createConfig);
10123
+ const textCol = row(createConfig, { display: 'flex', flexDirection: 'column', gap: '4px', flexGrow: '1' }, [heading, body]);
10054
10124
  const accept = button('Accept all', createConfig, click([
10055
10125
  { type: 'grantConsent' },
10056
10126
  { type: 'closeDialog', target: banner.id },
10057
- ]));
10127
+ ]), SOLID);
10058
10128
  const reject = button('Reject all', createConfig, click([
10059
10129
  { type: 'denyConsent' },
10060
10130
  { type: 'closeDialog', target: banner.id },
10061
- ]));
10131
+ ]), OUTLINED);
10062
10132
  const prefs = button('Preferences', createConfig, click([
10063
10133
  { type: 'openConsentPreferences', target: preferences.id },
10064
- ]));
10065
- const buttonsRow = row(createConfig, { display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }, [accept, reject, prefs]);
10066
- if (!heading || !body || !categoriesLine || !buttonsRow)
10134
+ ]), QUIET);
10135
+ const acceptRejectRow = row(createConfig, { display: 'flex', gap: '7px' }, [accept, reject]);
10136
+ const buttonsCol = row(createConfig, { display: 'flex', flexDirection: 'column', gap: '7px', alignItems: 'center' }, [acceptRejectRow, prefs]);
10137
+ const bannerRow = row(createConfig, { display: 'flex', gap: '18px', alignItems: 'flex-start', flexWrap: 'wrap' }, [textCol, buttonsCol]);
10138
+ if (!bannerRow)
10067
10139
  return null;
10068
- banner.children = [heading, body, categoriesLine, buttonsRow];
10140
+ banner.children = [bannerRow];
10069
10141
  // ── the preferences modal ────────────────────────────────────────────────
10070
10142
  preferences.label = 'Consent preferences';
10071
10143
  setTrait(preferences, 'closeOnBackdrop', true);
10072
10144
  setTrait(preferences, 'showCloseButton', true);
10073
- const prefsHeading = textNode('Your privacy choices', createConfig);
10074
- if (prefsHeading)
10075
- setStyleTrait(prefsHeading, 'fontWeight', '600');
10076
- const categoryRow = (category, name) => {
10145
+ setStyleTrait(preferences, 'width', 'min(480px, calc(100vw - 48px))');
10146
+ setStyleTrait(preferences, 'padding', '18px 20px');
10147
+ setStyleTrait(preferences, 'display', 'flex');
10148
+ setStyleTrait(preferences, 'flexDirection', 'column');
10149
+ setStyleTrait(preferences, 'gap', '12px');
10150
+ const prefsHeading = text$1('Your privacy choices', { fontSize: '16px', fontWeight: '600', color: TOKEN$1('ink', '#1f1d1b') }, createConfig);
10151
+ const prefsSub = text$1('Change these any time from “Cookie settings” in the footer.', { fontSize: '12px', color: TOKEN$1('inkMuted', '#6b6560') }, createConfig);
10152
+ const CARD = {
10153
+ display: 'flex', gap: '12px', alignItems: 'flex-start',
10154
+ borderWidth: '1px', borderStyle: 'solid', borderColor: TOKEN$1('line', '#e6e3e1'),
10155
+ borderRadius: TOKEN$1('radius-surface', '8px'), padding: '12px 14px',
10156
+ };
10157
+ const nameText = (s) => text$1(s, { fontSize: '13px', fontWeight: '600', color: TOKEN$1('ink', '#1f1d1b') }, createConfig);
10158
+ const descText = (s) => text$1(s, { fontSize: '11px', color: TOKEN$1('inkMuted', '#6b6560'), lineHeight: '1.5' }, createConfig);
10159
+ const textColOf = (name, desc) => row(createConfig, { display: 'flex', flexDirection: 'column', gap: '2px', flexGrow: '1' }, [nameText(name), descText(desc)]);
10160
+ const alwaysOn = text$1('ALWAYS ON', {
10161
+ fontSize: '10px', fontWeight: '600', color: TOKEN$1('inkMuted', '#6b6560'),
10162
+ borderWidth: '1px', borderStyle: 'solid', borderColor: TOKEN$1('line', '#e6e3e1'),
10163
+ borderRadius: '999px', padding: '3px 8px',
10164
+ }, createConfig);
10165
+ const necessaryCard = row(createConfig, CARD, [
10166
+ textColOf('Necessary', 'Keeps the site working: your session, your language, this very choice. Always on.'),
10167
+ alwaysOn,
10168
+ ]);
10169
+ const categoryCard = (category, name, desc) => {
10077
10170
  const allow = button('Allow', createConfig, click([
10078
10171
  { type: 'setConsentCategory', params: { category, granted: true } },
10079
- ]));
10172
+ ]), SMALL_ALLOW);
10080
10173
  const refuse = button('Refuse', createConfig, click([
10081
10174
  { type: 'setConsentCategory', params: { category, granted: false } },
10082
- ]));
10083
- const pair = row(createConfig, { display: 'flex', gap: '8px' }, [allow, refuse]);
10084
- return row(createConfig, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }, [textNode(name, createConfig), pair]);
10175
+ ]), SMALL_REFUSE);
10176
+ const pair = row(createConfig, { display: 'flex', gap: '6px' }, [allow, refuse]);
10177
+ return row(createConfig, CARD, [textColOf(name, desc), pair]);
10085
10178
  };
10086
- const necessaryLine = textNode('Necessary — always on', createConfig);
10087
- const analyticsRow = categoryRow('analytics', 'Analytics');
10088
- const marketingRow = categoryRow('marketing', 'Marketing');
10089
- const done = button('Done', createConfig, click([
10179
+ const analyticsCard = categoryCard('analytics', 'Analytics', 'Anonymous counts of visits and pages. Off means the analytics script never loads.');
10180
+ const marketingCard = categoryCard('marketing', 'Marketing', 'Ads measurement and remarketing. Off means no ad cookies and no data shared.');
10181
+ // Board C2's footer: bulk exits live here too — Reject | (grow) | Done | Accept.
10182
+ const prefsReject = button('Reject all', createConfig, click([
10183
+ { type: 'denyConsent' },
10184
+ { type: 'closeDialog', target: preferences.id },
10185
+ ]), OUTLINED);
10186
+ const prefsDone = button('Done', createConfig, click([
10187
+ { type: 'closeDialog', target: preferences.id },
10188
+ ]), { ...OUTLINED, color: TOKEN$1('inkMuted', '#3d3833'), borderColor: TOKEN$1('line', '#d8d4d1') });
10189
+ const prefsAccept = button('Accept all', createConfig, click([
10190
+ { type: 'grantConsent' },
10090
10191
  { type: 'closeDialog', target: preferences.id },
10091
- ]));
10092
- if (!prefsHeading || !necessaryLine || !analyticsRow || !marketingRow || !done)
10192
+ ]), SOLID);
10193
+ const spacer = row(createConfig, { flexGrow: '1' }, []);
10194
+ const footer = row(createConfig, { display: 'flex', gap: '8px', alignItems: 'center' }, [prefsReject, spacer, prefsDone, prefsAccept]);
10195
+ if (!prefsHeading || !prefsSub || !necessaryCard || !analyticsCard || !marketingCard || !footer)
10093
10196
  return null;
10094
- preferences.children = [prefsHeading, necessaryLine, analyticsRow, marketingRow, done];
10197
+ preferences.children = [prefsHeading, prefsSub, necessaryCard, analyticsCard, marketingCard, footer];
10095
10198
  // ── the withdrawal path (§7.3) ───────────────────────────────────────────
10096
10199
  const withdrawal = button('Cookie settings', createConfig, click([
10097
10200
  { type: 'openConsentPreferences', target: preferences.id },
10098
- ]));
10201
+ ]), QUIET);
10099
10202
  if (!withdrawal)
10100
10203
  return null;
10101
10204
  withdrawal.label = 'Cookie settings';