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

6.9 KiB

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

  • 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)
  • 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
  • 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.