You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/audit/components/carousel.md

61 lines
6.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Audit: carousel
audit-version: 1
audited-at: 2026-06-26
scope: (SCOPE-DRIFT → SYS-1)
method: adversarially-verified workflow (analyze → refute); HIGH lead-verified by direct read of the cited code. B5 ground-truth: the A31 O(N²) isSelected/isExpanded (.includes from a per-item $derived) is confirmed across listbox/grid-list/tree-view/tree-grid/tag-group (SYS-7); rovingTargetEl is correctly LIFTED everywhere (not A31); virtual-* use SvelteMap (A33-clean).
provider: G:\dev\svelte\vicen\src\uix\soma\components\carousel\carousel-provider.svelte.ts
reactivity (A31/A33/A35): "CLEAN: Per-item isActive derivations read global state directly (opts.value.current), not via method calls—O(1) per item, not O(N²). No $state(Map/Set) usage (A33 clean). ItemGroup effect at line 529–540 is component-level drag tracking, not per-item—no A35 loop risk. Ref registrations via onRefChange (lines 487, 522), not $effect—A30 clean. ResizeObserver and autoplay timer both disposed via eff
## Summary
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 1.
## Findings
### MEDIUM: SYS-1 (Scope-drift) — carousel-001 <!-- id: carousel-001 -->
- dimension: A
- rule: SYS-1 (Scope-drift)
- location: src/uix/morfo/components/carousel.ts:7
- evidence: Morfo declares scope: ['soma', 'sema'] but eidos directory exists at src/uix/eidos/components/carousel/ with full recipe and visual components
- impact: Eidos concerns (visual/size/variant/color/block attrs) are implemented but not declared in morfo scope, creating scope ambiguity
- proposed-fix: Add 'eidos' to morfo scope: scope: ['soma', 'sema', 'eidos']
- verify: [confirmed] Confirmed at src/uix/morfo/components/carousel.ts:7 — `scope: ['soma', 'sema'],` with NO 'eidos', while a full eidos implementation exists: src/uix/eidos/components/carousel/carousel.css is 10912 bytes plus types.ts, context, and 8 visual .svelte wrappers (carousel-item.svelte, carousel-viewport.svelte, etc.). This is exactly the SYS-1 scope-drift pattern: eidos materializes visual/size/variant attrs the morfo scope doesn't declare. Severity MEDIUM is correct (matches baseline). Note this is broadly systemic on this branch — calendar.ts:7, color-picker.ts:7, combobox.ts:7, command.ts:7, dialog.ts:18 and others all declare ['soma','sema'] yet ship eidos dirs — so the fix should likely be a sweep, but the carousel finding stands on its own evidence.
- fix-status: fixed (212624e0)
### MEDIUM: SYS-3 (jsdom-only test of interaction-heavy path) — carousel-102 <!-- id: carousel-102 -->
- dimension: F
- rule: SYS-3 (jsdom-only test of interaction-heavy path)
- location: src/uix/soma/components/carousel/carousel-provider.svelte.test.ts:1
- evidence: `// @vitest-environment jsdom`. The suite covers trigger/keyboard nav, autoplay timer + pause states, viewport-geometry props and timer disposal (line 370), but the DRAG path is entirely untested: finishDrag() (provider lines 389-415) computes fraction/velocity/fastSwipe + RTL-mirrored direction, and the ItemGroup drag $effect (lines 529-540) writes dragOffset from gesture.offsetX/Y. None of fraction-threshold, fastSwipe (velocity>0.5), or the `isRtlHorizontal` mirror (line 411-412) is exercised.
- impact: Swipe-to-advance (the carousel's primary touch interaction) and its RTL direction-flip have zero regression coverage; a sign or threshold regression in finishDrag would ship silently.
- proposed-fix: Add a unit test that drives the gesture via the provider's finishDrag()/dragOffset surface with synthetic offset+velocity across LTR/RTL and vertical axes; assert the resulting next()/prev() and dragOffset reset.
- verify: [verifier-added] added by adversarial verify pass
- fix-status: open
### LOW: A10 (getItems must filter nested via .closest === root) — carousel-101 <!-- id: carousel-101 -->
- dimension: B
- rule: A10 (getItems must filter nested via .closest === root)
- location: src/uix/soma/components/carousel/carousel-provider.svelte.ts:169-173
- evidence: getItems() returns `Array.from(group.querySelectorAll<HTMLElement>(`[${attrs.item}]`))` with no `.closest('[data-carousel]') === group` filter. A carousel nested inside another carousel's slide would have its inner `[data-carousel-item]` elements counted by the OUTER provider's querySelectorAll, inflating `slideCount` (line 175) and corrupting the outer carousel's clamp/loop math (commit() uses slideCount at line 266).
- impact: Only manifests when carousels are nested (rare). When it happens, the outer carousel mis-counts slides → wrong lastSnap clamp, wrong canGoNext, broken End-key target. No crash, no leak.
- repro: Render a Carousel whose slide contains another Carousel; observe outer provider.slideCount includes inner slides.
- proposed-fix: Filter to direct items: `Array.from(group.querySelectorAll('[data-carousel-item]')).filter(el => el.closest('[data-carousel-item-group]') === group)` — mirrors the A10 pattern used elsewhere.
- verify: [verifier-added] added by adversarial verify pass
- fix-status: open
## No-findings dimensions
B-A31, B-A33, B-A35, B-A30, B-A6, B-A12, B-A14, B-A18, B-A10, C, D-soma-imports-eidos, E-tokens, E-bis-theming, F-test-structure, G-redundancy
## Theming facts (E-bis)
- magic z-index: none
- magic literals: none
- undeclared parts: none
- roles clean: true · variants clean: true
## Tests (F)
- exists: true · env: jsdom
- covers: Navigation by trigger (next/prev), keyboard (Arrow keys, Home, End), and indicator click; Loop wrapping math (lastSnap calculation, modulo wrapping for loop mode, clamping for non-loop); Disabled state guard (keyboard/click/programmatic blocked); Autoplay lifecycle via uix.timers.interval with proper disposal on effect cleanup; Pause/resume on hover/focus state transitions; Viewport/ItemGroup/Item/Indicator props projection and data-state/data-index attrs; User interaction stops autoplay when stopOnInteraction=true; Indicator multi-click and aria-selected toggle
- untested: Virtualization edge cases (very large slide count with multiple slidesPerView); RTL keyboard navigation and transform direction mirroring with dragging; Drag gesture offset tracking and finishDrag snap-point logic; ResizeObserver responsiveness to viewport resize/orientation flip; Vertical orientation layout and indicator group flex direction
## Style observations (non-blocking)
- CSS recipe uses canonical 9-role color system with -solid/-element variants (primary, secondary, affirm, risk, threat, fulfill, loss, neutral). All spacing via --space-* and radius via --radius-*. Opacity uses --opacity-disabled. Z-index: 1 is local-relative within carousel subtree (triggers), acceptable per audit rules. Indicator size sizes with var(--_carousel-indicator-size) cascaded from --space-* + explicit px multiples in data-size breakpoints.
- Vertical orientation layout correctly flips root flex-direction to row with viewport taking flex: 1 1 auto and indicator-group as narrow flex: 0 0 auto column on right, matching common pattern in Mantine/ark-ui.

Powered by TurnKey Linux.