@vectoriox/iox-builder 4.31.1 → 4.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,10 +8494,21 @@ class BuilderDialogComponent {
8490
8494
  this.selectSub?.unsubscribe();
8491
8495
  this.instanceRegistry.unregister(this.nodeId);
8492
8496
  }
8493
- /** A click on the stage ITSELF is the backdrop click — children swallow everything else. */
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"). */
8494
8501
  onStageClick(event) {
8495
- if (event.target === event.currentTarget)
8496
- this.onBackdrop();
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;
8497
8512
  }
8498
8513
  onBackdrop() {
8499
8514
  if (this.closeOnBackdrop !== false && this.closeOnBackdrop !== 'false')
@@ -8517,7 +8532,8 @@ class BuilderDialogComponent {
8517
8532
  type="button"
8518
8533
  class="dialog-close"
8519
8534
  aria-label="Close dialog"
8520
- (click)="designOpen = false">
8535
+ (mousedown)="$event.stopPropagation()"
8536
+ (click)="onCloseClick($event)">
8521
8537
  <i class="ph-thin ph-x"></i>
8522
8538
  </button>
8523
8539
 
@@ -8553,7 +8569,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
8553
8569
  type="button"
8554
8570
  class="dialog-close"
8555
8571
  aria-label="Close dialog"
8556
- (click)="designOpen = false">
8572
+ (mousedown)="$event.stopPropagation()"
8573
+ (click)="onCloseClick($event)">
8557
8574
  <i class="ph-thin ph-x"></i>
8558
8575
  </button>
8559
8576
 
@@ -9993,6 +10010,13 @@ function setTrait(node, name, value) {
9993
10010
  trait.default = value;
9994
10011
  }
9995
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
+ }
9996
10020
  /**
9997
10021
  * THE CONSENT PRESET (consent plan C3) — the banner as builder elements, which is the whole bet:
9998
10022
  * Wix ships a banner you cannot design, Webflow ships a design system and no banner; ours is a
@@ -10019,16 +10043,50 @@ const click = (actions) => ({
10019
10043
  target: 'self', duration: 0, delay: 0, easing: 'ease', ...a,
10020
10044
  })),
10021
10045
  });
10022
- function button(label, createConfig, interaction) {
10046
+ const TOKEN$1 = (name, fallback) => `var(--iox-${name}, ${fallback})`;
10047
+ function button(label, createConfig, interaction, styles = {}) {
10023
10048
  const btn = createConfig('ButtonBlock');
10024
10049
  const text = textNode(label, createConfig);
10025
10050
  if (!btn || !text)
10026
10051
  return null;
10027
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);
10028
10058
  btn.children = [text];
10029
10059
  btn.interactions = [interaction];
10030
10060
  return btn;
10031
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
+ };
10032
10090
  function row(createConfig, styles, children) {
10033
10091
  const container = createConfig('Container');
10034
10092
  if (!container || children.some(c => !c))
@@ -10050,57 +10108,97 @@ function buildConsentPreset(createConfig) {
10050
10108
  setTrait(banner, 'closeOnBackdrop', false); // clicking away is NOT consent
10051
10109
  setTrait(banner, 'showCloseButton', true); // the runtime records X as a "no"
10052
10110
  setTrait(banner, 'placement', 'bottom'); // A5: placement is the Dialog's own choice
10053
- setStyleTrait(banner, 'width', 'min(560px, calc(100vw - 32px))');
10054
- const heading = textNode('Privacy choices', createConfig);
10055
- if (heading)
10056
- setStyleTrait(heading, 'fontWeight', '600');
10057
- 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);
10058
- 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]);
10059
10124
  const accept = button('Accept all', createConfig, click([
10060
10125
  { type: 'grantConsent' },
10061
10126
  { type: 'closeDialog', target: banner.id },
10062
- ]));
10127
+ ]), SOLID);
10063
10128
  const reject = button('Reject all', createConfig, click([
10064
10129
  { type: 'denyConsent' },
10065
10130
  { type: 'closeDialog', target: banner.id },
10066
- ]));
10131
+ ]), OUTLINED);
10067
10132
  const prefs = button('Preferences', createConfig, click([
10068
10133
  { type: 'openConsentPreferences', target: preferences.id },
10069
- ]));
10070
- const buttonsRow = row(createConfig, { display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }, [accept, reject, prefs]);
10071
- 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)
10072
10139
  return null;
10073
- banner.children = [heading, body, categoriesLine, buttonsRow];
10140
+ banner.children = [bannerRow];
10074
10141
  // ── the preferences modal ────────────────────────────────────────────────
10075
10142
  preferences.label = 'Consent preferences';
10076
10143
  setTrait(preferences, 'closeOnBackdrop', true);
10077
10144
  setTrait(preferences, 'showCloseButton', true);
10078
- const prefsHeading = textNode('Your privacy choices', createConfig);
10079
- if (prefsHeading)
10080
- setStyleTrait(prefsHeading, 'fontWeight', '600');
10081
- 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) => {
10082
10170
  const allow = button('Allow', createConfig, click([
10083
10171
  { type: 'setConsentCategory', params: { category, granted: true } },
10084
- ]));
10172
+ ]), SMALL_ALLOW);
10085
10173
  const refuse = button('Refuse', createConfig, click([
10086
10174
  { type: 'setConsentCategory', params: { category, granted: false } },
10087
- ]));
10088
- const pair = row(createConfig, { display: 'flex', gap: '8px' }, [allow, refuse]);
10089
- 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]);
10090
10178
  };
10091
- const necessaryLine = textNode('Necessary — always on', createConfig);
10092
- const analyticsRow = categoryRow('analytics', 'Analytics');
10093
- const marketingRow = categoryRow('marketing', 'Marketing');
10094
- 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' },
10095
10191
  { type: 'closeDialog', target: preferences.id },
10096
- ]));
10097
- 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)
10098
10196
  return null;
10099
- preferences.children = [prefsHeading, necessaryLine, analyticsRow, marketingRow, done];
10197
+ preferences.children = [prefsHeading, prefsSub, necessaryCard, analyticsCard, marketingCard, footer];
10100
10198
  // ── the withdrawal path (§7.3) ───────────────────────────────────────────
10101
10199
  const withdrawal = button('Cookie settings', createConfig, click([
10102
10200
  { type: 'openConsentPreferences', target: preferences.id },
10103
- ]));
10201
+ ]), QUIET);
10104
10202
  if (!withdrawal)
10105
10203
  return null;
10106
10204
  withdrawal.label = 'Cookie settings';
@@ -10526,7 +10624,9 @@ class FieldComponentConfig extends ComponentConfig {
10526
10624
  { label: 'Long text', value: 'textarea' },
10527
10625
  { label: 'Dropdown', value: 'select' },
10528
10626
  { label: 'Checkbox', value: 'checkbox' },
10627
+ { label: 'Switch', value: 'switch' },
10529
10628
  { label: 'Radio group', value: 'radio' },
10629
+ { label: 'Slider', value: 'range' },
10530
10630
  { label: 'Date', value: 'date' },
10531
10631
  { label: 'Time', value: 'time' },
10532
10632
  // ⚠️ 'file' is DELIBERATELY ABSENT until uploads are implemented. A file input's
@@ -10539,6 +10639,9 @@ class FieldComponentConfig extends ComponentConfig {
10539
10639
  new TraitConfig('placeholder', 'Hint inside the box', TraitInputType.Text, undefined, '', false, { trait: 'control', values: ['text', 'email', 'tel', 'url', 'number', 'password', 'textarea', 'select'] }),
10540
10640
  new TraitConfig('help', 'Help text', TraitInputType.Text, undefined, ''),
10541
10641
  new TraitConfig('rows', 'Rows', TraitInputType.Number, undefined, 4, false, { trait: 'control', values: 'textarea' }),
10642
+ new TraitConfig('rangeMin', 'Min', TraitInputType.Number, undefined, 0, false, { trait: 'control', values: 'range' }),
10643
+ new TraitConfig('rangeMax', 'Max', TraitInputType.Number, undefined, 100, false, { trait: 'control', values: 'range' }),
10644
+ new TraitConfig('rangeStep', 'Step', TraitInputType.Number, undefined, 1, false, { trait: 'control', values: 'range' }),
10542
10645
  new TraitConfig('labelHidden', 'Hide label (screen readers still read it)', TraitInputType.Checkbox, undefined, false),
10543
10646
  // The submitted key. Derived from the label; editable for external integrations.
10544
10647
  new TraitConfig('name', 'Submitted key', TraitInputType.Text, undefined, ''),