@adia-ai/web-components 0.0.18 → 0.0.20

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.
Files changed (90) hide show
  1. package/components/accordion/accordion.css +101 -102
  2. package/components/agent-feedback-bar/agent-feedback-bar.js +8 -8
  3. package/components/agent-questions/agent-questions.css +2 -1
  4. package/components/agent-questions/agent-questions.js +6 -6
  5. package/components/agent-reasoning/agent-reasoning.js +20 -5
  6. package/components/agent-trace/agent-trace.a2ui.json +5 -5
  7. package/components/agent-trace/agent-trace.js +7 -5
  8. package/components/agent-trace/agent-trace.yaml +2 -2
  9. package/components/alert/alert.a2ui.json +1 -2
  10. package/components/alert/alert.css +4 -4
  11. package/components/alert/alert.yaml +1 -2
  12. package/components/avatar/avatar.a2ui.json +3 -3
  13. package/components/avatar/avatar.js +10 -0
  14. package/components/avatar/avatar.yaml +6 -6
  15. package/components/button/button.a2ui.json +14 -2
  16. package/components/button/button.css +19 -2
  17. package/components/button/button.js +1 -0
  18. package/components/button/button.yaml +20 -2
  19. package/components/calendar-picker/calendar-picker.css +2 -1
  20. package/components/calendar-picker/calendar-picker.js +12 -1
  21. package/components/chart/chart.css +11 -11
  22. package/components/chart/chart.js +26 -18
  23. package/components/chart-legend/chart-legend.a2ui.json +2 -2
  24. package/components/chart-legend/chart-legend.js +4 -1
  25. package/components/chart-legend/chart-legend.yaml +2 -2
  26. package/components/chat/chat-input.js +13 -5
  27. package/components/chat/chat.a2ui.json +2 -2
  28. package/components/chat/chat.js +14 -3
  29. package/components/chat/chat.yaml +2 -2
  30. package/components/code/code.css +16 -6
  31. package/components/command/command.js +9 -1
  32. package/components/field/field.a2ui.json +0 -5
  33. package/components/field/field.css +2 -2
  34. package/components/field/field.js +53 -5
  35. package/components/field/field.yaml +5 -8
  36. package/components/heatmap/heatmap.css +32 -23
  37. package/components/input/input.js +30 -1
  38. package/components/kbd/kbd.a2ui.json +5 -1
  39. package/components/kbd/kbd.yaml +5 -1
  40. package/components/menu/menu.css +20 -8
  41. package/components/menu/menu.js +9 -1
  42. package/components/modal/modal.css +101 -108
  43. package/components/noodles/noodles.js +25 -8
  44. package/components/pipeline-status/pipeline-status.css +4 -4
  45. package/components/pipeline-status/pipeline-status.js +6 -4
  46. package/components/popover/popover.js +4 -0
  47. package/components/progress-row/progress-row.a2ui.json +3 -2
  48. package/components/progress-row/progress-row.yaml +2 -1
  49. package/components/range/range.js +7 -0
  50. package/components/richtext/richtext.css +2 -2
  51. package/components/richtext/richtext.js +4 -1
  52. package/components/segment/segment.css +1 -1
  53. package/components/segmented/segmented.js +7 -1
  54. package/components/select/select.css +7 -4
  55. package/components/slider/slider.js +15 -8
  56. package/components/stepper/stepper.css +181 -144
  57. package/components/stepper/stepper.js +5 -2
  58. package/components/swiper/swiper.a2ui.json +3 -3
  59. package/components/swiper/swiper.css +11 -77
  60. package/components/swiper/swiper.js +6 -5
  61. package/components/swiper/swiper.yaml +3 -3
  62. package/components/switch/switch.a2ui.json +8 -1
  63. package/components/switch/switch.yaml +8 -1
  64. package/components/table/table.js +9 -1
  65. package/components/table-toolbar/table-toolbar.a2ui.json +21 -21
  66. package/components/table-toolbar/table-toolbar.css +32 -91
  67. package/components/table-toolbar/table-toolbar.js +219 -86
  68. package/components/table-toolbar/table-toolbar.yaml +21 -12
  69. package/components/tabs/tabs.css +3 -2
  70. package/components/tabs/tabs.js +7 -1
  71. package/components/tag/tag.a2ui.json +2 -2
  72. package/components/tag/tag.yaml +2 -2
  73. package/components/timeline/timeline.css +244 -204
  74. package/components/timeline/timeline.js +1 -3
  75. package/components/toast/toast.a2ui.json +2 -3
  76. package/components/toast/toast.yaml +5 -3
  77. package/components/toolbar/toolbar.css +6 -1
  78. package/components/toolbar/toolbar.js +10 -2
  79. package/components/tooltip/tooltip.css +8 -2
  80. package/components/tooltip/tooltip.js +12 -14
  81. package/components/tree/tree.css +21 -0
  82. package/core/icons.js +14 -0
  83. package/core/polyfills.js +17 -7
  84. package/package.json +1 -1
  85. package/patterns/a2ui-root/a2ui-root.js +21 -14
  86. package/patterns/app-shell/css/app-shell.main.css +30 -1
  87. package/patterns/app-shell/css/app-shell.tokens.css +1 -0
  88. package/patterns/gen-ui/gen-ui.js +1 -1
  89. package/styles/colors/semantics.css +59 -2
  90. package/styles/tokens.css +16 -12
@@ -38,11 +38,21 @@ class AdiaField extends AdiaElement {
38
38
  static properties = {
39
39
  label: { type: String, default: '', reflect: true },
40
40
  hint: { type: String, default: '', reflect: true },
41
- error: { type: String, default: '', reflect: true },
42
41
  required: { type: Boolean, default: false, reflect: true },
43
42
  inline: { type: Boolean, default: false, reflect: true },
44
43
  };
45
44
 
45
+ // The validation message lives on the child control (AdiaFormElement.error
46
+ // is the single source of truth — validity is a property of the value, not
47
+ // of the layout wrapper). field-ui mirrors the child's error string into
48
+ // its own [data-field-error] slot so the validation message renders below
49
+ // the control. See docs/specs/component-token-contract.md "Toggle state
50
+ // naming" / open decisions resolution log.
51
+ get error() {
52
+ const ctrl = this.#findControl();
53
+ return ctrl?.error ?? '';
54
+ }
55
+
46
56
  static template = () => null;
47
57
 
48
58
  #labelEl = null;
@@ -69,17 +79,22 @@ class AdiaField extends AdiaElement {
69
79
  this.#ensureHintElement();
70
80
  this.#ensureErrorElement();
71
81
  this.#bindForToControl();
82
+ this.#bindErrorListener();
72
83
  this.#labelEl?.addEventListener('click', this.#onLabelClick);
73
84
  this.#mo = new MutationObserver(() => {
74
85
  this.#bindForToControl();
86
+ this.#bindErrorListener();
75
87
  this.#wireAriaDescribedBy();
88
+ this.#syncErrorMessage();
76
89
  });
77
90
  this.#mo.observe(this, { childList: true });
91
+ this.#syncErrorMessage();
78
92
  }
79
93
 
80
94
  disconnected() {
81
95
  this.#mo?.disconnect();
82
96
  this.#mo = null;
97
+ this.#unbindErrorListener();
83
98
  this.#labelEl?.removeEventListener('click', this.#onLabelClick);
84
99
  this.#labelEl = null;
85
100
  this.#labelMark = null;
@@ -96,11 +111,44 @@ class AdiaField extends AdiaElement {
96
111
  this.#hintEl.textContent = this.hint || '';
97
112
  this.#hintEl.hidden = !this.hint || Boolean(this.error);
98
113
  }
99
- if (this.#errorEl) {
100
- this.#errorEl.textContent = this.error || '';
101
- this.#errorEl.hidden = !this.error;
102
- }
114
+ this.#syncErrorMessage();
115
+ this.#wireAriaDescribedBy();
116
+ }
117
+
118
+ // Mirror child.error → #errorEl. Called on render, MutationObserver
119
+ // changes, and the child's `invalid`/`change`/`input` events.
120
+ #syncErrorMessage() {
121
+ if (!this.#errorEl) return;
122
+ const msg = this.error;
123
+ this.#errorEl.textContent = msg || '';
124
+ this.#errorEl.hidden = !msg;
125
+ }
126
+
127
+ #onChildEvent = () => {
128
+ this.#syncErrorMessage();
103
129
  this.#wireAriaDescribedBy();
130
+ };
131
+
132
+ #boundErrorTarget = null;
133
+
134
+ #bindErrorListener() {
135
+ const ctrl = this.#findControl();
136
+ if (ctrl === this.#boundErrorTarget) return;
137
+ this.#unbindErrorListener();
138
+ if (!ctrl) return;
139
+ this.#boundErrorTarget = ctrl;
140
+ ctrl.addEventListener('invalid', this.#onChildEvent, true);
141
+ ctrl.addEventListener('change', this.#onChildEvent);
142
+ ctrl.addEventListener('input', this.#onChildEvent);
143
+ }
144
+
145
+ #unbindErrorListener() {
146
+ const ctrl = this.#boundErrorTarget;
147
+ if (!ctrl) return;
148
+ ctrl.removeEventListener('invalid', this.#onChildEvent, true);
149
+ ctrl.removeEventListener('change', this.#onChildEvent);
150
+ ctrl.removeEventListener('input', this.#onChildEvent);
151
+ this.#boundErrorTarget = null;
104
152
  }
105
153
 
106
154
  // ── Private ───────────────────────────────────────────────────────
@@ -28,14 +28,11 @@ props:
28
28
  type: string
29
29
  default: ""
30
30
  reflect: true
31
- error:
32
- description: >-
33
- Validation error message rendered below the control in danger
34
- style. Takes precedence over `hint` in the same row, and
35
- carries role="alert" so screen readers announce changes.
36
- type: string
37
- default: ""
38
- reflect: true
31
+ # `error` is mirrored from the slotted control's `error` property.
32
+ # AdiaFormElement.error is the source of truth; field-ui renders the
33
+ # message below the control in danger style, role="alert" so screen
34
+ # readers announce changes. There is no field-level error attribute —
35
+ # set the message on the control: `<field-ui><input-ui error="…"></input-ui></field-ui>`.
39
36
  required:
40
37
  description: >-
41
38
  Renders a "*" marker on the label. Does not itself enforce
@@ -14,14 +14,15 @@
14
14
  --heatmap-label: var(--a-fg-subtle);
15
15
 
16
16
  /* ── Data ramp (5 buckets) ──
17
- Sequential tint scale from the current accent family (OKLCH, theme-
18
- aware via light-dark()). The `--a-{accent|success|warning|danger}-NN`
19
- tokens are already even-spaced, so no color-mix is needed. */
20
- --heatmap-bucket-0: var(--a-accent-10);
21
- --heatmap-bucket-1: var(--a-accent-30);
22
- --heatmap-bucket-2: var(--a-accent-50);
23
- --heatmap-bucket-3: var(--a-accent-70);
24
- --heatmap-bucket-4: var(--a-accent-90);
17
+ Sequential tint scale, sourced from the L2 `--a-{family}-bucket-N`
18
+ semantic aliases (see styles/colors/semantics.css §BUCKETS). The
19
+ L2 layer handles light-dark() polarity so step 0 reads as
20
+ low-intensity in either scheme without per-component math. */
21
+ --heatmap-bucket-0: var(--a-accent-bucket-0);
22
+ --heatmap-bucket-1: var(--a-accent-bucket-1);
23
+ --heatmap-bucket-2: var(--a-accent-bucket-2);
24
+ --heatmap-bucket-3: var(--a-accent-bucket-3);
25
+ --heatmap-bucket-4: var(--a-accent-bucket-4);
25
26
 
26
27
  /* ── Legend ── */
27
28
  --heatmap-legend-size: var(--a-caption-size);
@@ -121,26 +122,34 @@
121
122
  /* ── Color-scheme variants (token overrides only) ── */
122
123
 
123
124
  :scope[color-scheme="success"] {
124
- --heatmap-bucket-0: var(--a-success-10);
125
- --heatmap-bucket-1: var(--a-success-30);
126
- --heatmap-bucket-2: var(--a-success-50);
127
- --heatmap-bucket-3: var(--a-success-70);
128
- --heatmap-bucket-4: var(--a-success-90);
125
+ --heatmap-bucket-0: var(--a-success-bucket-0);
126
+ --heatmap-bucket-1: var(--a-success-bucket-1);
127
+ --heatmap-bucket-2: var(--a-success-bucket-2);
128
+ --heatmap-bucket-3: var(--a-success-bucket-3);
129
+ --heatmap-bucket-4: var(--a-success-bucket-4);
129
130
  }
130
131
 
131
132
  :scope[color-scheme="warning"] {
132
- --heatmap-bucket-0: var(--a-warning-10);
133
- --heatmap-bucket-1: var(--a-warning-30);
134
- --heatmap-bucket-2: var(--a-warning-50);
135
- --heatmap-bucket-3: var(--a-warning-70);
136
- --heatmap-bucket-4: var(--a-warning-90);
133
+ --heatmap-bucket-0: var(--a-warning-bucket-0);
134
+ --heatmap-bucket-1: var(--a-warning-bucket-1);
135
+ --heatmap-bucket-2: var(--a-warning-bucket-2);
136
+ --heatmap-bucket-3: var(--a-warning-bucket-3);
137
+ --heatmap-bucket-4: var(--a-warning-bucket-4);
137
138
  }
138
139
 
139
140
  :scope[color-scheme="danger"] {
140
- --heatmap-bucket-0: var(--a-danger-10);
141
- --heatmap-bucket-1: var(--a-danger-30);
142
- --heatmap-bucket-2: var(--a-danger-50);
143
- --heatmap-bucket-3: var(--a-danger-70);
144
- --heatmap-bucket-4: var(--a-danger-90);
141
+ --heatmap-bucket-0: var(--a-danger-bucket-0);
142
+ --heatmap-bucket-1: var(--a-danger-bucket-1);
143
+ --heatmap-bucket-2: var(--a-danger-bucket-2);
144
+ --heatmap-bucket-3: var(--a-danger-bucket-3);
145
+ --heatmap-bucket-4: var(--a-danger-bucket-4);
146
+ }
147
+
148
+ :scope[color-scheme="info"] {
149
+ --heatmap-bucket-0: var(--a-info-bucket-0);
150
+ --heatmap-bucket-1: var(--a-info-bucket-1);
151
+ --heatmap-bucket-2: var(--a-info-bucket-2);
152
+ --heatmap-bucket-3: var(--a-info-bucket-3);
153
+ --heatmap-bucket-4: var(--a-info-bucket-4);
145
154
  }
146
155
  }
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { AdiaFormElement } from '../../core/form.js';
19
- import { isIconName } from '../../core/icons.js';
19
+ import { isIconName, whenIconRegistryReady } from '../../core/icons.js';
20
20
 
21
21
  const renderAffix = (v) => isIconName(v)
22
22
  ? `<icon-ui name="${v}"></icon-ui>`
@@ -86,6 +86,35 @@ class AdiaInput extends AdiaFormElement {
86
86
  this.#textEl.addEventListener('blur', this.#onBlur);
87
87
  this.#textEl.addEventListener('paste', this.#onPaste);
88
88
  }
89
+
90
+ // In non-Vite static deploys, the icon registry loads asynchronously
91
+ // after the manifest fetch resolves. If our prefix/suffix were checked
92
+ // by isIconName() during that window, kebab-case icon names like
93
+ // "magnifying-glass" got baked into the DOM as literal text. Re-evaluate
94
+ // once the registry is ready and promote text-rendered affixes to
95
+ // <icon-ui>. (No-op on Vite dev where the promise resolves synchronously.)
96
+ if (this.prefix || this.suffix) {
97
+ whenIconRegistryReady.then(() => this.#promoteAffixes());
98
+ }
99
+ }
100
+
101
+ #promoteAffixes() {
102
+ if (!this.isConnected) return;
103
+ for (const which of ['prefix', 'suffix']) {
104
+ const value = this[which];
105
+ if (!value) continue;
106
+ const slot = this.querySelector(`:scope [slot="${which}"]`);
107
+ if (!slot) continue;
108
+ // Already an <icon-ui> — nothing to do.
109
+ if (slot.querySelector(':scope > icon-ui')) continue;
110
+ // Was rendered as text and the value is now a known icon — replace.
111
+ if (isIconName(value)) {
112
+ slot.replaceChildren();
113
+ const icon = document.createElement('icon-ui');
114
+ icon.setAttribute('name', value);
115
+ slot.appendChild(icon);
116
+ }
117
+ }
89
118
  }
90
119
 
91
120
  render() {
@@ -17,8 +17,12 @@
17
17
  "const": "Kbd"
18
18
  },
19
19
  "size": {
20
- "description": "Size variant",
20
+ "description": "Sizing scale (compact tier — sm / md only).",
21
21
  "type": "string",
22
+ "enum": [
23
+ "sm",
24
+ "md"
25
+ ],
22
26
  "default": ""
23
27
  }
24
28
  },
@@ -9,9 +9,13 @@ version: 1
9
9
  description: Keyboard key cap. Purely decorative, content from innerHTML.
10
10
  props:
11
11
  size:
12
- description: Size variant
12
+ description: Sizing scale (compact tier — sm / md only).
13
13
  type: string
14
14
  default: ""
15
+ enum:
16
+ - sm
17
+ - md
18
+ reflect: true
15
19
  events: {}
16
20
  slots:
17
21
  default:
@@ -1,16 +1,28 @@
1
1
  @scope (menu-ui) {
2
+ :where(:scope) {
3
+ --menu-popover-padding: var(--a-space-1);
4
+ --menu-popover-border: var(--a-border-subtle);
5
+ --menu-popover-radius: var(--a-radius-lg);
6
+ --menu-popover-bg: var(--a-bg-subtle);
7
+ --menu-popover-shadow: var(--a-shadow-lg);
8
+ --menu-popover-min-width: 10rem;
9
+ --menu-popover-font-size: var(--a-ui-size);
10
+ --menu-popover-fg: var(--a-fg);
11
+ }
12
+
2
13
  :scope {
3
14
  box-sizing: border-box;
4
15
  display: inline-flex;
5
16
  position: relative;
6
17
  }
7
- }
8
18
 
9
- /* Items/dividers in Light DOM are hidden unless they've been adopted
10
- into the popover on open. Popover API also hides the popover itself when closed. */
11
- menu-ui > menu-item-ui,
12
- menu-ui > menu-divider-ui {
13
- display: none !important;
19
+ /* Items/dividers in Light DOM are hidden unless they've been adopted
20
+ into the popover on open. Popover API also hides the popover itself
21
+ when closed. */
22
+ :scope > menu-item-ui,
23
+ :scope > menu-divider-ui {
24
+ display: none;
25
+ }
14
26
  }
15
27
 
16
28
  /* ── Top-layer: cannot inherit component tokens ── */
@@ -41,10 +53,10 @@ menu-ui [data-menu-popover] {
41
53
  --menu-item-fg: var(--a-fg-subtle);
42
54
  --menu-item-fg-hover: var(--a-fg);
43
55
  --menu-item-bg-hover: var(--a-bg-muted);
44
- --menu-item-fg-disabled: var(--a-fg-disabled, var(--a-fg-muted));
56
+ --menu-item-fg-disabled: var(--a-fg-disabled);
45
57
  --menu-item-icon-fg: var(--a-fg-muted);
46
58
  --menu-item-icon-fg-hover: var(--a-fg);
47
- --menu-item-icon-disabled: var(--a-fg-disabled, var(--a-fg-muted));
59
+ --menu-item-icon-disabled: var(--a-fg-disabled);
48
60
  --menu-item-danger-fg: var(--a-danger-bg);
49
61
  --menu-item-danger-bg: var(--a-danger-muted);
50
62
 
@@ -31,6 +31,7 @@ class AdiaMenu extends AdiaElement {
31
31
  #popover = null;
32
32
  #bound = false;
33
33
  #rafId = null;
34
+ #focusRaf = null;
34
35
 
35
36
  connected() {
36
37
  if (!this.#bound) {
@@ -86,7 +87,10 @@ class AdiaMenu extends AdiaElement {
86
87
  // Focus first enabled item (only when opened via keyboard).
87
88
  if (this.#openedByKeyboard) {
88
89
  this.#openedByKeyboard = false;
89
- requestAnimationFrame(() => this.#focusItem(0));
90
+ this.#focusRaf = requestAnimationFrame(() => {
91
+ this.#focusRaf = null;
92
+ this.#focusItem(0);
93
+ });
90
94
  }
91
95
 
92
96
  this.#rafId = requestAnimationFrame(() => {
@@ -112,6 +116,10 @@ class AdiaMenu extends AdiaElement {
112
116
  cancelAnimationFrame(this.#rafId);
113
117
  this.#rafId = null;
114
118
  }
119
+ if (this.#focusRaf != null) {
120
+ cancelAnimationFrame(this.#focusRaf);
121
+ this.#focusRaf = null;
122
+ }
115
123
  document.removeEventListener('pointerdown', this.#onOutside);
116
124
  document.removeEventListener('keydown', this.#onDocKey, true);
117
125
  }
@@ -1,5 +1,10 @@
1
1
  /* ═══════════════════════════════════════════════════════════════
2
- MODAL-N — Centered dialog overlay using native <dialog>.
2
+ MODAL-UI — Centered dialog overlay using native <dialog>.
3
+
4
+ <dialog> content stays in DOM tree → custom properties inherit
5
+ normally → all rules live INSIDE @scope (modal-ui), parity with
6
+ drawer-ui. See docs/specs/component-token-contract.md
7
+ §"Top-layer rules — when to keep them inside @scope".
3
8
  ═══════════════════════════════════════════════════════════════ */
4
9
 
5
10
  @scope (modal-ui) {
@@ -33,121 +38,109 @@
33
38
  :scope[size="sm"] { --modal-width: min(24rem, 90vw); }
34
39
  :scope[size="md"] { --modal-width: min(32rem, 90vw); }
35
40
  :scope[size="lg"] { --modal-width: min(48rem, 90vw); }
36
- }
37
-
38
- /* ═══════ Closed ═══════ */
39
-
40
- modal-ui > [slot="dialog"]:not([open]) { display: none; }
41
-
42
- /* ═══════ Full-viewport dialog shell ═══════ */
43
-
44
- modal-ui > [slot="dialog"][open] {
45
- box-sizing: border-box;
46
- position: fixed;
47
- inset: 0;
48
- width: 100%;
49
- height: 100%;
50
- max-width: none;
51
- max-height: none;
52
- margin: 0;
53
- padding: 0;
54
- border: none;
55
- background: transparent;
56
- overflow: clip;
57
- display: flex;
58
- align-items: center;
59
- justify-content: center;
60
- }
61
-
62
- /* ═══════ Backdrop ═══════ */
63
-
64
- modal-ui > [slot="dialog"]::backdrop {
65
- background: var(--modal-backdrop);
66
- opacity: 0;
67
- transition: opacity var(--modal-duration) var(--modal-easing);
68
- }
69
- modal-ui > [slot="dialog"][data-open]::backdrop { opacity: 1; }
70
- modal-ui > [slot="dialog"][data-closing]::backdrop { opacity: 0; }
71
-
72
- /* ── Top-layer: cannot inherit component tokens ── */
73
-
74
- /* ═══════ Content panel ═══════ */
75
-
76
- modal-ui > [slot="dialog"] > [slot="panel"] {
77
- box-sizing: border-box;
78
- display: flex;
79
- flex-direction: column;
80
- width: var(--modal-width);
81
- max-height: 85vh;
82
- background: var(--modal-bg);
83
- color: var(--modal-header-fg);
84
- font-family: var(--a-font-family); /* top-layer: --a-* required */
85
- border: 1px solid var(--modal-border);
86
- border-radius: var(--modal-radius);
87
- box-shadow: var(--modal-shadow);
88
- overflow: hidden;
89
- opacity: 0;
90
- transform: scale(0.95);
91
- }
92
-
93
- /* ═══════ Open animation ═══════ */
94
-
95
- modal-ui > [slot="dialog"][data-open] > [slot="panel"] {
96
- transition: transform var(--modal-duration) var(--modal-easing),
97
- opacity var(--modal-duration) var(--modal-easing);
98
- transform: scale(1);
99
- opacity: 1;
100
- }
101
41
 
102
- /* ═══════ Close animation ═══════ */
42
+ /* ═══════ Closed ═══════ */
43
+ :scope > [slot="dialog"]:not([open]) { display: none; }
103
44
 
104
- modal-ui > [slot="dialog"][data-closing] > [slot="panel"] {
105
- transition: transform var(--modal-duration) var(--modal-easing),
106
- opacity var(--modal-duration) var(--modal-easing);
107
- transform: scale(0.95);
108
- opacity: 0;
109
- }
45
+ /* ═══════ Full-viewport dialog shell ═══════ */
46
+ :scope > [slot="dialog"][open] {
47
+ box-sizing: border-box;
48
+ position: fixed;
49
+ inset: 0;
50
+ width: 100%;
51
+ height: 100%;
52
+ max-width: none;
53
+ max-height: none;
54
+ margin: 0;
55
+ padding: 0;
56
+ border: none;
57
+ background: transparent;
58
+ overflow: clip;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ }
110
63
 
111
- /* ═══════ Header ═══════ */
64
+ /* ═══════ Backdrop ═══════ */
65
+ :scope > [slot="dialog"]::backdrop {
66
+ background: var(--modal-backdrop);
67
+ opacity: 0;
68
+ transition: opacity var(--modal-duration) var(--modal-easing);
69
+ }
70
+ :scope > [slot="dialog"][data-open]::backdrop { opacity: 1; }
71
+ :scope > [slot="dialog"][data-closing]::backdrop { opacity: 0; }
112
72
 
113
- modal-ui [slot="header"] {
114
- display: flex;
115
- align-items: center;
116
- justify-content: space-between;
117
- padding: var(--modal-pad-header) var(--modal-pad-body);
118
- border-bottom: 1px solid var(--modal-border);
119
- font-size: var(--modal-heading-size);
120
- font-weight: var(--modal-heading-weight);
121
- color: var(--modal-header-fg);
122
- flex-shrink: 0;
123
- }
124
- modal-ui [slot="header"]::before {
125
- content: attr(text);
126
- flex: 1;
127
- }
73
+ /* ═══════ Content panel ═══════ */
74
+ :scope > [slot="dialog"] > [slot="panel"] {
75
+ box-sizing: border-box;
76
+ display: flex;
77
+ flex-direction: column;
78
+ width: var(--modal-width);
79
+ max-height: 85vh;
80
+ background: var(--modal-bg);
81
+ color: var(--modal-header-fg);
82
+ font-family: var(--modal-font-family);
83
+ border: 1px solid var(--modal-border);
84
+ border-radius: var(--modal-radius);
85
+ box-shadow: var(--modal-shadow);
86
+ overflow: hidden;
87
+ opacity: 0;
88
+ transform: scale(0.95);
89
+ }
128
90
 
129
- /* ═══════ Close button (button-ui) ═══════ */
91
+ /* ═══════ Open animation ═══════ */
92
+ :scope > [slot="dialog"][data-open] > [slot="panel"] {
93
+ transition: transform var(--modal-duration) var(--modal-easing),
94
+ opacity var(--modal-duration) var(--modal-easing);
95
+ transform: scale(1);
96
+ opacity: 1;
97
+ }
130
98
 
131
- modal-ui [slot="close"] {
132
- flex-shrink: 0;
133
- }
99
+ /* ═══════ Close animation ═══════ */
100
+ :scope > [slot="dialog"][data-closing] > [slot="panel"] {
101
+ transition: transform var(--modal-duration) var(--modal-easing),
102
+ opacity var(--modal-duration) var(--modal-easing);
103
+ transform: scale(0.95);
104
+ opacity: 0;
105
+ }
134
106
 
135
- /* ═══════ Body ═══════ */
107
+ /* ═══════ Header ═══════ */
108
+ :scope [slot="header"] {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: space-between;
112
+ padding: var(--modal-pad-header) var(--modal-pad-body);
113
+ border-bottom: 1px solid var(--modal-border);
114
+ font-size: var(--modal-heading-size);
115
+ font-weight: var(--modal-heading-weight);
116
+ color: var(--modal-header-fg);
117
+ flex-shrink: 0;
118
+ }
119
+ :scope [slot="header"]::before {
120
+ content: attr(text);
121
+ flex: 1;
122
+ }
136
123
 
137
- modal-ui [slot="body"] {
138
- padding: var(--modal-pad-body);
139
- flex: 1;
140
- overflow: auto;
141
- }
124
+ /* ═══════ Close button (button-ui) ═══════ */
125
+ :scope [slot="close"] {
126
+ flex-shrink: 0;
127
+ }
142
128
 
143
- /* ═══════ Footer ═══════ */
129
+ /* ═══════ Body ═══════ */
130
+ :scope [slot="body"] {
131
+ padding: var(--modal-pad-body);
132
+ flex: 1;
133
+ overflow: auto;
134
+ }
144
135
 
145
- modal-ui [slot="footer"] {
146
- display: flex;
147
- align-items: center;
148
- justify-content: flex-end;
149
- gap: var(--a-space-2); /* top-layer: --a-* required */
150
- padding: var(--modal-pad-footer) var(--modal-pad-body);
151
- border-top: 1px solid var(--modal-border);
152
- flex-shrink: 0;
136
+ /* ═══════ Footer ═══════ */
137
+ :scope [slot="footer"] {
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: flex-end;
141
+ gap: var(--modal-footer-gap);
142
+ padding: var(--modal-pad-footer) var(--modal-pad-body);
143
+ border-top: 1px solid var(--modal-border);
144
+ flex-shrink: 0;
145
+ }
153
146
  }
@@ -38,6 +38,7 @@ class AdiaNoodles extends AdiaElement {
38
38
  #bound = false;
39
39
  #dragState = null;
40
40
  #dragPath = null;
41
+ #dragCancel = null;
41
42
 
42
43
  // ── Public API ──────────────────────────────────────────────
43
44
 
@@ -124,6 +125,9 @@ class AdiaNoodles extends AdiaElement {
124
125
  }
125
126
 
126
127
  disconnected() {
128
+ // Flush any in-flight drag so per-port pointermove/up listeners + their
129
+ // closure references are released before the host detaches.
130
+ this.#dragCancel?.();
127
131
  this.#resizeObs?.disconnect();
128
132
  this.#mutationObs?.disconnect();
129
133
  if (this.#rafId) cancelAnimationFrame(this.#rafId);
@@ -385,9 +389,6 @@ class AdiaNoodles extends AdiaElement {
385
389
 
386
390
  const startPos = this.#getPortPosition(fromEl, fromSide);
387
391
 
388
- this.#dragState = { fromEl, fromSide, startPos, pointerId: e.pointerId };
389
- this.#dragPath = null;
390
-
391
392
  const onMove = (ev) => {
392
393
  if (!this.#dragState) return;
393
394
  const rect = this.getBoundingClientRect();
@@ -409,23 +410,39 @@ class AdiaNoodles extends AdiaElement {
409
410
  };
410
411
 
411
412
  const onUp = (ev) => {
412
- dot.releasePointerCapture(ev.pointerId);
413
+ try { dot.releasePointerCapture(ev.pointerId); } catch (_) { /* already released */ }
413
414
  dot.removeAttribute('data-noodle-dragging');
414
415
  dot.removeEventListener('pointermove', onMove);
415
416
  dot.removeEventListener('pointerup', onUp);
416
417
 
417
- // Check if dropped on a target port
418
- const target = this.#findDropTarget(ev.clientX, ev.clientY, fromEl);
419
- if (target) {
420
- this.connect(fromEl, target.el, fromSide, target.side);
418
+ // Check if dropped on a target port (skip on synthetic disconnect cancel).
419
+ if (ev.type === 'pointerup' && ev.clientX !== undefined) {
420
+ const target = this.#findDropTarget(ev.clientX, ev.clientY, fromEl);
421
+ if (target) this.connect(fromEl, target.el, fromSide, target.side);
421
422
  }
422
423
 
423
424
  this.#clearDropTargets();
424
425
  this.#dragState = null;
425
426
  this.#dragPath = null;
427
+ this.#dragCancel = null;
426
428
  this.#scheduleUpdate();
427
429
  };
428
430
 
431
+ // Capture the handlers so `disconnected()` can flush a mid-drag drag
432
+ // by calling the same onUp tear-down without a synthetic event.
433
+ this.#dragCancel = () => {
434
+ this.#dragState = null;
435
+ dot.removeAttribute('data-noodle-dragging');
436
+ dot.removeEventListener('pointermove', onMove);
437
+ dot.removeEventListener('pointerup', onUp);
438
+ this.#clearDropTargets();
439
+ this.#dragPath = null;
440
+ this.#dragCancel = null;
441
+ };
442
+
443
+ this.#dragState = { fromEl, fromSide, startPos, pointerId: e.pointerId };
444
+ this.#dragPath = null;
445
+
429
446
  dot.addEventListener('pointermove', onMove);
430
447
  dot.addEventListener('pointerup', onUp);
431
448
  };