@aparte/core 0.16.8 → 0.16.9

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 (31) hide show
  1. package/dist/client/aparte-client.d.ts +57 -2
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/lifecycle-events.d.ts +8 -1
  4. package/dist/client/lifecycle-events.d.ts.map +1 -1
  5. package/dist/client/stream-adapter.d.ts +8 -0
  6. package/dist/client/stream-adapter.d.ts.map +1 -1
  7. package/dist/components/bubble/aparte-chat-bubble.d.ts +6 -0
  8. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  9. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-input.d.ts +1 -1
  11. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  12. package/dist/components/conversation-list/aparte-conversation-list.d.ts +5 -0
  13. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  14. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +3 -2
  15. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
  16. package/dist/components/status/aparte-chat-status.d.ts +41 -3
  17. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  18. package/dist/custom-elements.json +12484 -12344
  19. package/dist/{dialog-triggers-D8wNICrT.js → dialog-triggers-CApC7K1I.js} +96 -26
  20. package/dist/{dialog-triggers-D8wNICrT.js.map → dialog-triggers-CApC7K1I.js.map} +1 -1
  21. package/dist/elicitation/panel.d.ts.map +1 -1
  22. package/dist/index.css +119 -31
  23. package/dist/index.js +157 -47
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.node.js +1 -1
  26. package/dist/interop/element-props.d.ts.map +1 -1
  27. package/dist/primitives/select/aparte-select.d.ts +25 -3
  28. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  29. package/dist/types/events.d.ts +4 -2
  30. package/dist/types/events.d.ts.map +1 -1
  31. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/elicitation/panel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACR,uBAAuB,EAK1B,MAAM,YAAY,CAAC;AAWpB,MAAM,WAAW,qBAAqB;IAClC,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,gEAAgE;IAChE,UAAU,IAAI,OAAO,CAAC;IACtB,yDAAyD;IACzD,UAAU,IAAI,OAAO,CAAC;IACtB,kDAAkD;IAClD,KAAK,IAAI,IAAI,CAAC;IACd;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,IAAI,QAAQ,GAAG,MAAM,CAAC;IAC1B,wEAAwE;IACxE,UAAU,IAAI,OAAO,CAAC;IACtB,mGAAmG;IACnG,OAAO,IAAI,IAAI,CAAC;IAChB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,OAAO,IAAI,IAAI,CAAC;CACnB;AAyCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,WAAW,CAalG;AA6ZD,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,EAChC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,IAAI,GACrB,qBAAqB,CAevB"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/elicitation/panel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACR,uBAAuB,EAK1B,MAAM,YAAY,CAAC;AAWpB,MAAM,WAAW,qBAAqB;IAClC,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,gEAAgE;IAChE,UAAU,IAAI,OAAO,CAAC;IACtB,yDAAyD;IACzD,UAAU,IAAI,OAAO,CAAC;IACtB,kDAAkD;IAClD,KAAK,IAAI,IAAI,CAAC;IACd;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,IAAI,QAAQ,GAAG,MAAM,CAAC;IAC1B,wEAAwE;IACxE,UAAU,IAAI,OAAO,CAAC;IACtB,mGAAmG;IACnG,OAAO,IAAI,IAAI,CAAC;IAChB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,OAAO,IAAI,IAAI,CAAC;CACnB;AAyCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,WAAW,CAalG;AA8aD,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,EAChC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,IAAI,GACrB,qBAAqB,CAevB"}
package/dist/index.css CHANGED
@@ -326,8 +326,12 @@
326
326
  and a 14px mark are a minimap's measures, not spacing steps. */
327
327
  --aparte-scroll-rail-tick-size: 14px;
328
328
  --aparte-scroll-rail-tick-thickness: 2px;
329
-
330
- --aparte-scroll-rail-gap: 8px;
329
+ /* What a tick is to a POINTER, as opposed to what it is to the eye: WCAG 2.5.8's
330
+ 24px minimum. The drawn line stays 14×2 — this sizes the pseudo-element around it,
331
+ and the gap below derives from it so the pitch rises with the zone. The rail is
332
+ `display: none` under `(pointer: coarse)`, so 2.5.8's 24px is the bar here and not
333
+ 2.5.5's 44px. */
334
+ --aparte-scroll-rail-hit-size: 24px;
331
335
 
332
336
  /* Context gauge, ring variant — a diameter, and a stroke in the ring's own 36-unit
333
337
  box. Literals like the rail's: a 22px ring is a toolbar's measure, not a spacing step. */
@@ -721,7 +725,12 @@ aparte-chat {
721
725
  --aparte-viewport-padding: var(--aparte-space-8);
722
726
  --aparte-scrollbar-thumb: var(--aparte-neutral);
723
727
  --aparte-input-padding-x: var(--aparte-space-6);
724
- --aparte-input-padding-y: var(--aparte-space-5);
728
+ /* The editor at rest is the control height: half of what the control size leaves once
729
+ one line of its own text is taken out. It used to be a flat 10px, which made the editor
730
+ 44px beside 36px buttons and, the row aligning at the end, sat the send button 4px
731
+ below the editor's centre. Derived from the editor's own line (1lh), so a coarse
732
+ pointer's 44 and a changed font both keep the editor and its buttons on one centre. */
733
+ --aparte-input-padding-y: calc((var(--aparte-composer-control-size) - 1lh) / 2);
725
734
  --aparte-conv-list-gap: var(--aparte-space-1);
726
735
  --aparte-conv-item-gap: var(--aparte-space-3);
727
736
  --aparte-elic-option-radius: var(--aparte-radius-md);
@@ -841,8 +850,14 @@ aparte-chat {
841
850
  --aparte-track: color-mix(in srgb, var(--aparte-text) 8%, var(--aparte-bg));
842
851
  --aparte-skeleton-base: var(--aparte-track);
843
852
  /* The rail is as wide as its widest tick (the current one is 1.6× the resting size);
844
- it used to be a 20px literal clipping a 22.4px tick. */
845
- --aparte-scroll-rail-width: calc(var(--aparte-scroll-rail-tick-size) * 1.6);
853
+ it used to be a 20px literal clipping a 22.4px tick. The floor is one tick's
854
+ pressable zone: the rail clips, so a column narrower than the zone would cut the
855
+ target back below 24px on the very edge a reader aims at. */
856
+ --aparte-scroll-rail-width: max(var(--aparte-scroll-rail-hit-size), calc(var(--aparte-scroll-rail-tick-size) * 1.6));
857
+ /* The distance between two ticks IS the zone minus the line: gap + thickness = the
858
+ 24px pitch, so the zones tile edge to edge. Growing the zone alone would have
859
+ overlapped them by 14px and left the z-order to decide every press. */
860
+ --aparte-scroll-rail-gap: calc(var(--aparte-scroll-rail-hit-size) - var(--aparte-scroll-rail-tick-thickness));
846
861
  --aparte-skeleton-shine: var(--aparte-surface-3);
847
862
  /* The line it stands for: as tall as the content type, and a gap that is the rest of
848
863
  the content line, so the text that replaces it lands where it was (T21). It was 12px
@@ -2932,9 +2947,15 @@ aparte-icon {
2932
2947
  --aparte-thumbnail-border: var(--aparte-attachment-chip-border);
2933
2948
  }
2934
2949
 
2935
- /* Only an image tile the app declared it can preview is a button — see
2936
- aparteGlobalConfig.setHostHandlers(). Undeclared, it is a picture, not a control. */
2937
- .aparte-thumb--image[role='button'] {
2950
+ /* Only an image the app declared it can preview is a button — see
2951
+ aparteGlobalConfig.setHostHandlers(). Undeclared, it is a picture, not a control.
2952
+
2953
+ Two selectors because the two strips carry the role in two places, and both are
2954
+ right. The sent-message strip has no ✕, so its TILE is the whole control. The
2955
+ composer's tile wraps the remove button, so the control there is the IMAGE — a
2956
+ button inside a button is what the role on the tile used to make. */
2957
+ .aparte-thumb--image[role='button'],
2958
+ .aparte-thumbnail__image[role='button'] {
2938
2959
  cursor: pointer;
2939
2960
  }
2940
2961
 
@@ -2943,6 +2964,15 @@ aparte-icon {
2943
2964
  outline-offset: var(--aparte-focus-outline-offset);
2944
2965
  }
2945
2966
 
2967
+ /* Inset, unlike the tile's. The image fills the tile edge to edge and the tile is the
2968
+ frame (`overflow: hidden` above), so an outline drawn outward is cropped away — the
2969
+ focus ring would simply not exist. Pulled in by its own width, it lands just inside
2970
+ the border and stays whole. */
2971
+ .aparte-thumbnail__image[role='button']:focus-visible {
2972
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
2973
+ outline-offset: calc(-1 * var(--aparte-focus-outline-width));
2974
+ }
2975
+
2946
2976
  .aparte-thumb--file {
2947
2977
  display: flex;
2948
2978
  align-items: center;
@@ -3507,11 +3537,12 @@ aparte-icon {
3507
3537
  * script the caller owns: a <details open> toggling itself, a positioning library placing
3508
3538
  * a menu next to its trigger. Nothing here reaches for position:fixed on your behalf.
3509
3539
  *
3510
- * <div class="aparte-tabs" role="tablist">
3511
- * <button class="aparte-tabs__tab" role="tab" aria-selected="true">Preview</button>
3512
- * <button class="aparte-tabs__tab" role="tab" aria-selected="false">Code</button>
3540
+ * <div class="aparte-tabs" role="tablist" aria-label="Result view">
3541
+ * <button class="aparte-tabs__tab" role="tab" type="button" id="tabs-a-preview" aria-controls="tabs-a-preview-panel" aria-selected="true" tabindex="0">Preview</button>
3542
+ * <button class="aparte-tabs__tab" role="tab" type="button" id="tabs-a-code" aria-controls="tabs-a-code-panel" aria-selected="false" tabindex="-1">Code</button>
3513
3543
  * </div>
3514
- * <div class="aparte-tabs__panel" role="tabpanel">The panel for the selected tab.</div>
3544
+ * <div class="aparte-tabs__panel" role="tabpanel" id="tabs-a-preview-panel" aria-labelledby="tabs-a-preview">The panel for the selected tab.</div>
3545
+ * <div class="aparte-tabs__panel" role="tabpanel" id="tabs-a-code-panel" aria-labelledby="tabs-a-code" hidden>The panel the other tab opens.</div>
3515
3546
  *
3516
3547
  * <div class="aparte-accordion">
3517
3548
  * <details class="aparte-accordion__item" open>
@@ -3558,17 +3589,29 @@ aparte-icon {
3558
3589
  the current tab, which reads as a control rather than as navigation. Neither moves
3559
3590
  anything: `aria-selected` is what says which tab is current, and the app owns it.
3560
3591
 
3561
- <div class="aparte-tabs aparte-tabs--underline" role="tablist">
3562
- <button class="aparte-tabs__tab" role="tab" aria-selected="true">Preview</button>
3563
- <button class="aparte-tabs__tab" role="tab" aria-selected="false">Code</button>
3592
+ The markup below is the whole contract of the role, and all of it is yours to write:
3593
+ ONE tab stop for the list (`tabindex="0"` on the selected tab, `-1` on the rest),
3594
+ `aria-controls` from each tab to its `role="tabpanel"`, and `aria-labelledby` back.
3595
+ The app also owns the ArrowLeft/ArrowRight/Home/End handler that moves the selection
3596
+ and the stop together — `@aparte/plugin-artifacts`' card has a working one to copy.
3597
+ A tablist without those is louder than plain buttons and says less: every tab a tab
3598
+ stop, and none of them naming what it opens.
3599
+
3600
+ <div class="aparte-tabs aparte-tabs--underline" role="tablist" aria-label="Result view">
3601
+ <button class="aparte-tabs__tab" role="tab" type="button" id="tabs-u-preview" aria-controls="tabs-u-preview-panel" aria-selected="true" tabindex="0">Preview</button>
3602
+ <button class="aparte-tabs__tab" role="tab" type="button" id="tabs-u-code" aria-controls="tabs-u-code-panel" aria-selected="false" tabindex="-1">Code</button>
3564
3603
  </div>
3565
- <div class="aparte-tabs__panel" role="tabpanel">What the selected tab shows.</div>
3604
+ <div class="aparte-tabs__panel" role="tabpanel" id="tabs-u-preview-panel" aria-labelledby="tabs-u-preview">What the selected tab shows.</div>
3605
+ <div class="aparte-tabs__panel" role="tabpanel" id="tabs-u-code-panel" aria-labelledby="tabs-u-code" hidden>What the other tab shows.</div>
3566
3606
 
3567
- <div class="aparte-tabs aparte-tabs--segmented" role="tablist">
3568
- <button class="aparte-tabs__tab" role="tab" aria-selected="true">Light</button>
3569
- <button class="aparte-tabs__tab" role="tab" aria-selected="false">Dark</button>
3570
- <button class="aparte-tabs__tab" role="tab" aria-selected="false">System</button>
3607
+ <div class="aparte-tabs aparte-tabs--segmented" role="tablist" aria-label="Theme">
3608
+ <button class="aparte-tabs__tab" role="tab" type="button" id="tabs-s-light" aria-controls="tabs-s-light-panel" aria-selected="true" tabindex="0">Light</button>
3609
+ <button class="aparte-tabs__tab" role="tab" type="button" id="tabs-s-dark" aria-controls="tabs-s-dark-panel" aria-selected="false" tabindex="-1">Dark</button>
3610
+ <button class="aparte-tabs__tab" role="tab" type="button" id="tabs-s-system" aria-controls="tabs-s-system-panel" aria-selected="false" tabindex="-1">System</button>
3571
3611
  </div>
3612
+ <div class="aparte-tabs__panel" role="tabpanel" id="tabs-s-light-panel" aria-labelledby="tabs-s-light">The light theme's settings.</div>
3613
+ <div class="aparte-tabs__panel" role="tabpanel" id="tabs-s-dark-panel" aria-labelledby="tabs-s-dark" hidden>The dark theme's settings.</div>
3614
+ <div class="aparte-tabs__panel" role="tabpanel" id="tabs-s-system-panel" aria-labelledby="tabs-s-system" hidden>Whatever the system asks for.</div>
3572
3615
  ========================================= */
3573
3616
 
3574
3617
  .aparte-tabs {
@@ -4792,7 +4835,10 @@ aparte-progress-spinner:not([value]) svg {
4792
4835
 
4793
4836
  /* Both host shapes core recognises get the same layout: the vanilla
4794
4837
  <aparte-chat> element AND a framework wrapper's `[data-aparte-chat]` root
4795
- (React/Vue/Svelte/Angular render `<div data-aparte-chat>`). `[data-aparte-chat]`
4838
+ (React/Vue/Svelte render `<div data-aparte-chat class="aparte-chat-container">`;
4839
+ Angular's host IS `<aparte-chat>` and its shell is the inner
4840
+ `.aparte-chat-container`, which carries no attribute here — the overlay rules
4841
+ below list that third shape for exactly that reason). `[data-aparte-chat]`
4796
4842
  is the SAME selector core uses to resolve the host (see aparte-client /
4797
4843
  aparte-chat-bubble), so one rule covers both — no wrapper-specific class. */
4798
4844
  aparte-chat,
@@ -4870,12 +4916,14 @@ aparte-chat[overlay-composer] > aparte-chat-viewport,
4870
4916
  z-index: 0;
4871
4917
  }
4872
4918
 
4873
- /* The bottom stack paints over the absolutely positioned viewport. `:not()` is
4874
- exact here: the only child that is not the viewport IS the stack (the scroll
4875
- button lives inside the viewport). */
4876
- aparte-chat[overlay-composer] > :not(aparte-chat-viewport),
4877
- [data-aparte-chat][overlay-composer] > :not(aparte-chat-viewport),
4878
- .aparte-chat-container[overlay-composer] > :not(aparte-chat-viewport) {
4919
+ /* The bottom stack paints over the absolutely positioned viewport. The `:not()` names
4920
+ what is NOT the stack: the viewport (the scroll button lives inside it), and the
4921
+ scroll rail, which is a child of the shell too. `position: relative` here beat the
4922
+ rail's own `absolute`, so in overlay mode the floating rail dropped into the flow
4923
+ and pushed the composer down — the one child that floats by design has to be named. */
4924
+ aparte-chat[overlay-composer] > :not(aparte-chat-viewport, aparte-scroll-rail),
4925
+ [data-aparte-chat][overlay-composer] > :not(aparte-chat-viewport, aparte-scroll-rail),
4926
+ .aparte-chat-container[overlay-composer] > :not(aparte-chat-viewport, aparte-scroll-rail) {
4879
4927
  position: relative;
4880
4928
  z-index: var(--aparte-z-raised);
4881
4929
  }
@@ -7221,12 +7269,30 @@ aparte-context[data-level="danger"] .aparte-context__value {
7221
7269
  * chat host. `:has()` gives the host its containing block only while a rail is inside
7222
7270
  * it — a bare `position: relative` on every chat would have moved any absolutely
7223
7271
  * positioned child a consumer already placed there.
7272
+ *
7273
+ * The three shell shapes shell.css already lays out, because an absolute element lands
7274
+ * in the nearest POSITIONED ancestor and a shape missing from this list is a rail that
7275
+ * escaped to the page: the vanilla `<aparte-chat>`, the `[data-aparte-chat]` div
7276
+ * React/Vue/Svelte render, and Angular's inner `.aparte-chat-container` (its host IS
7277
+ * `<aparte-chat>`, but the rail is a child of that inner div). The first two shapes are
7278
+ * both true of the wrapper div, which carries the class and the attribute — listing all
7279
+ * three costs one selector and closes the Angular hole.
7224
7280
  */
7225
7281
  aparte-chat:has(> aparte-scroll-rail),
7226
- [data-aparte-chat]:has(> aparte-scroll-rail) {
7282
+ [data-aparte-chat]:has(> aparte-scroll-rail),
7283
+ .aparte-chat-container:has(> aparte-scroll-rail) {
7227
7284
  position: relative;
7228
7285
  }
7229
7286
 
7287
+ /* The block padding is the room the END zones need, and it is not decoration.
7288
+ The rail clips (`overflow: hidden`, which cuts at the PADDING box) and the list has
7289
+ none of its own, so with no padding here the first tick's top edge sits exactly on the
7290
+ clip line. A zone grows symmetrically — half of `hit − thickness` above and below the
7291
+ line — so the first tick would lose its upper half and the last tick its lower half,
7292
+ to paint AND to hit-testing: 13px of the 24px, on the two ticks a reader aims at most
7293
+ ("jump to the first message", "jump to the latest"). Padding by exactly that half puts
7294
+ the clip line outside every zone instead of through the two end ones. `box-sizing`
7295
+ keeps `max-height` measuring the same box it measured before. */
7230
7296
  aparte-scroll-rail {
7231
7297
  position: absolute;
7232
7298
  inset-inline-end: var(--aparte-space-2);
@@ -7234,7 +7300,9 @@ aparte-scroll-rail {
7234
7300
  transform: translateY(-50%);
7235
7301
  z-index: var(--aparte-z-floating);
7236
7302
  display: block;
7303
+ box-sizing: border-box;
7237
7304
  width: var(--aparte-scroll-rail-width);
7305
+ padding-block: calc((var(--aparte-scroll-rail-hit-size) - var(--aparte-scroll-rail-tick-thickness)) / 2);
7238
7306
  max-height: calc(100% - 2 * var(--aparte-space-8));
7239
7307
  overflow: hidden;
7240
7308
  }
@@ -7254,7 +7322,7 @@ aparte-scroll-rail[data-empty] {
7254
7322
  gap: var(--aparte-scroll-rail-gap);
7255
7323
  }
7256
7324
 
7257
- /* A tick is a real button, drawn as a line. The hit area is the gap around it, drawn
7325
+ /* A tick is a real button, drawn as a line. The hit area is the zone around it, drawn
7258
7326
  by the pseudo-element so the line itself can stay two pixels. */
7259
7327
  .aparte-scroll-rail__tick {
7260
7328
  appearance: none;
@@ -7275,10 +7343,23 @@ aparte-scroll-rail[data-empty] {
7275
7343
  background var(--aparte-duration-fast) var(--aparte-ease);
7276
7344
  }
7277
7345
 
7346
+ /* The pressable zone: 24px square around a 14×2 line (WCAG 2.5.8). It used to be 22×10
7347
+ — half the 8px gap in the block axis, one spacing step in the inline one — and no
7348
+ spacing exemption covered it, because at a 10px pitch a neighbour's 24px circle
7349
+ overlaps this one's.
7350
+ Block: grow symmetrically to the hit size. The gap is `hit − thickness`, so the pitch
7351
+ is exactly the hit size and two zones meet without overlapping — and the rail pads its
7352
+ block axis by the same half-growth, so the clip does not eat the first and last zone.
7353
+ Inline: grow INWARD only. The rail clips (`overflow: hidden`) and ticks are
7354
+ end-aligned, so a centred zone would hang past the end edge and come back cut to
7355
+ 19px; anchored to the tick's end edge it stays whole, and the rail's width carries
7356
+ the hit size as its floor so the column has room for it. */
7278
7357
  .aparte-scroll-rail__tick::before {
7279
7358
  content: "";
7280
7359
  position: absolute;
7281
- inset: calc(-1 * var(--aparte-scroll-rail-gap) / 2) calc(-1 * var(--aparte-space-2));
7360
+ inset-block: calc((var(--aparte-scroll-rail-tick-thickness) - var(--aparte-scroll-rail-hit-size)) / 2);
7361
+ inset-inline-end: 0;
7362
+ inset-inline-start: calc(var(--aparte-scroll-rail-tick-size) - var(--aparte-scroll-rail-hit-size));
7282
7363
  }
7283
7364
 
7284
7365
  .aparte-scroll-rail__tick:hover {
@@ -8240,9 +8321,16 @@ aparte-split[disabled] > .aparte-split__handle:hover {
8240
8321
  opacity: 1;
8241
8322
  }
8242
8323
 
8243
- /* Nor the ✕ on a pending attachment, which is the only way to remove one. */
8324
+ /* Nor the ✕ on a pending attachment, which is the only way to remove one — and
8325
+ seeing it is no use at 18px, under the 24 of WCAG 2.2 SC 2.5.8. 24 is not a new
8326
+ number: it is the box `aparte-btn--sm` already draws, so this is the component
8327
+ giving the recipe back its own size on touch rather than out-specifying it.
8328
+ Not `--aparte-touch-target-size`: 44px inside a 72px tile would swallow the
8329
+ picture, and full parity with the neighbouring controls needs the tile to grow
8330
+ with it — a second decision, not this floor. */
8244
8331
  .aparte-thumb__remove {
8245
8332
  opacity: 1;
8333
+ --aparte-thumb-remove-size: var(--aparte-btn-size-sm);
8246
8334
  }
8247
8335
 
8248
8336
  /* Hover cannot reveal the row's ⋯ on touch — keep it visible and tappable. */