motion-ui-skill 2.0.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.
- package/LICENSE +22 -0
- package/README.md +637 -0
- package/SKILL.md +529 -0
- package/agents/openai.yaml +8 -0
- package/bin/index.mjs +242 -0
- package/bin/installer.mjs +457 -0
- package/bin/updater.mjs +207 -0
- package/catalog.json +8120 -0
- package/components/agents/agent-activity/activity-row.tsx +242 -0
- package/components/agents/agent-activity/index.tsx +287 -0
- package/components/agents/agent-activity/types.ts +106 -0
- package/components/agents/agent-code.tsx +171 -0
- package/components/agents/agent-disclosure.tsx +55 -0
- package/components/agents/ai-sidebar.tsx +981 -0
- package/components/agents/approval-card/index.tsx +467 -0
- package/components/agents/approval-card/types.ts +56 -0
- package/components/agents/chat-app.tsx +97 -0
- package/components/agents/citations.tsx +288 -0
- package/components/agents/code-block.tsx +202 -0
- package/components/agents/file-diff.tsx +288 -0
- package/components/agents/image-generation.tsx +388 -0
- package/components/agents/loading-states/agent-progress.tsx +105 -0
- package/components/agents/loading-states/index.ts +13 -0
- package/components/agents/loading-states/reasoning-text.tsx +224 -0
- package/components/agents/loading-states/thinking-shimmer.tsx +27 -0
- package/components/agents/message-bubble.tsx +369 -0
- package/components/agents/message-context.tsx +9 -0
- package/components/agents/message-scroller.tsx +487 -0
- package/components/agents/message.tsx +272 -0
- package/components/agents/prompt-input.tsx +327 -0
- package/components/agents/streaming-response.tsx +263 -0
- package/components/agents/todo-list.tsx +387 -0
- package/components/agents/tool-approval.tsx +276 -0
- package/components/agents/tool-result.tsx +352 -0
- package/components/app/icons.tsx +19 -0
- package/components/audio/music-player-card.tsx +146 -0
- package/components/audio/music-playlist-card.tsx +172 -0
- package/components/audio/now-playing-bar.tsx +116 -0
- package/components/background-gradient/arc-bands-background.tsx +54 -0
- package/components/background-gradient/aurora-background.tsx +49 -0
- package/components/background-gradient/cherry-petal-background.tsx +50 -0
- package/components/background-gradient/coral-glow-background.tsx +47 -0
- package/components/background-gradient/dark-arc-bands-background.tsx +52 -0
- package/components/background-gradient/dark-aurora-background.tsx +52 -0
- package/components/background-gradient/dark-carbon-spotlight-background.tsx +47 -0
- package/components/background-gradient/dark-ember-background.tsx +47 -0
- package/components/background-gradient/dark-ink-field-background.tsx +46 -0
- package/components/background-gradient/dark-midnight-mesh-background.tsx +61 -0
- package/components/background-gradient/dark-rose-noir-background.tsx +52 -0
- package/components/background-gradient/dark-teal-depth-background.tsx +52 -0
- package/components/background-gradient/frost-mesh-background.tsx +56 -0
- package/components/background-gradient/halftone-pop-background.tsx +52 -0
- package/components/background-gradient/honey-ember-background.tsx +47 -0
- package/components/background-gradient/ink-wash-background.tsx +49 -0
- package/components/background-gradient/linen-weave-background.tsx +47 -0
- package/components/background-gradient/mint-lagoon-background.tsx +47 -0
- package/components/background-gradient/paper-fold-background.tsx +52 -0
- package/components/background-gradient/rain-prism-background.tsx +52 -0
- package/components/background-gradient/sand-drift-background.tsx +47 -0
- package/components/background-gradient/soft-spotlight-background.tsx +47 -0
- package/components/background-gradient/sunrise-horizon-background.tsx +51 -0
- package/components/background-gradient/sunset-bloom-background.tsx +47 -0
- package/components/background-gradient/terrazzo-fragment-background.tsx +53 -0
- package/components/background-gradient/tropical-tide-background.tsx +47 -0
- package/components/background-pattern/diagonal-box-pattern.tsx +29 -0
- package/components/background-pattern/dot-grid-pattern.tsx +29 -0
- package/components/background-pattern/fine-grain-pattern.tsx +39 -0
- package/components/background-pattern/graph-paper-pattern.tsx +29 -0
- package/components/background-pattern/line-grid-pattern.tsx +39 -0
- package/components/background-pattern/reverse-diagonal-box-pattern.tsx +30 -0
- package/components/buttons/add-to-cart-button.tsx +190 -0
- package/components/buttons/bookmark-save-button.tsx +105 -0
- package/components/buttons/cinder-latch-button.tsx +80 -0
- package/components/buttons/copy-button.tsx +155 -0
- package/components/buttons/depth-outline-button.tsx +72 -0
- package/components/buttons/dimple-switch.tsx +114 -0
- package/components/buttons/download-button.tsx +186 -0
- package/components/buttons/follow-button.tsx +98 -0
- package/components/buttons/hold-to-delete-button.tsx +205 -0
- package/components/buttons/inset-button.tsx +82 -0
- package/components/buttons/like-button.tsx +135 -0
- package/components/buttons/linen-tab-button.tsx +92 -0
- package/components/buttons/prism-depth-button.tsx +90 -0
- package/components/buttons/quantity-stepper-button.tsx +104 -0
- package/components/buttons/segmented-toggle-button.tsx +134 -0
- package/components/buttons/sheen-pill-button.tsx +127 -0
- package/components/buttons/slate-chip-switch.tsx +94 -0
- package/components/buttons/slide-to-confirm-button.tsx +163 -0
- package/components/buttons/soft-pill-button.tsx +86 -0
- package/components/buttons/soft-ui-button.tsx +59 -0
- package/components/buttons/three-d-button.tsx +99 -0
- package/components/buttons/three-d-icon-button.tsx +88 -0
- package/components/calendar/booking-slot-calendar.tsx +315 -0
- package/components/calendar/date-range-picker-card.tsx +216 -0
- package/components/calendar/event-countdown-card.tsx +105 -0
- package/components/calendar/month-picker-calendar.tsx +183 -0
- package/components/calendar/week-strip-calendar.tsx +182 -0
- package/components/contact/copy-email.tsx +94 -0
- package/components/docks/Icons.tsx +222 -0
- package/components/docks/app-dock.tsx +111 -0
- package/components/docks/editor-tool-dock.tsx +113 -0
- package/components/docks/mac-dock.tsx +144 -0
- package/components/docks/presence-dock.tsx +178 -0
- package/components/docks/spotlight-bar.tsx +188 -0
- package/components/dropdowns/file-menu-dropdown.tsx +192 -0
- package/components/dropdowns/filter-sort-dropdown.tsx +189 -0
- package/components/dropdowns/kebab-actions-dropdown.tsx +195 -0
- package/components/dropdowns/notification-dropdown.tsx +235 -0
- package/components/dropdowns/share-menu-dropdown.tsx +197 -0
- package/components/dropdowns/user-menu-dropdown.tsx +251 -0
- package/components/dropdowns/workspace-switcher-dropdown.tsx +248 -0
- package/components/event/event-ticket-card.tsx +116 -0
- package/components/files/ink-stamp-document-card.tsx +88 -0
- package/components/files/stacked-folder-card.tsx +176 -0
- package/components/folder/folder-tab-card.tsx +123 -0
- package/components/forms/contact-form.tsx +417 -0
- package/components/forms/forgot-password-form.tsx +264 -0
- package/components/forms/login-form.tsx +342 -0
- package/components/forms/newsletter-form.tsx +212 -0
- package/components/forms/signup-form.tsx +487 -0
- package/components/frames/circle-cut-frame.tsx +198 -0
- package/components/frames/hard-shadow-polaroid-frame.tsx +158 -0
- package/components/frames/sticky-note-polaroid-frame.tsx +230 -0
- package/components/frames/transform-box-frame.tsx +287 -0
- package/components/frames/transform-plus-frame.tsx +308 -0
- package/components/gallery/film-strip-card.tsx +103 -0
- package/components/gallery/gallery-grid-card.tsx +106 -0
- package/components/gallery/gallery-wall-card.tsx +71 -0
- package/components/gallery/glass-overlay-image-card.tsx +156 -0
- package/components/gallery/museum-placard-card.tsx +105 -0
- package/components/gallery/photo-album-card.tsx +114 -0
- package/components/gallery/photo-contact-sheet-card.tsx +71 -0
- package/components/gallery/polaroid-image-card.tsx +81 -0
- package/components/gallery/slow-living-polaroid-card.tsx +88 -0
- package/components/gallery/stamp-postcard-card.tsx +78 -0
- package/components/inputs/checkbox-field-input.tsx +148 -0
- package/components/inputs/combobox-field-input.tsx +466 -0
- package/components/inputs/date-field-input.tsx +472 -0
- package/components/inputs/file-upload-field-input.tsx +578 -0
- package/components/inputs/floating-label-field-input.tsx +206 -0
- package/components/inputs/input-group-field.tsx +132 -0
- package/components/inputs/password-field-input.tsx +125 -0
- package/components/inputs/phone-field-input.tsx +157 -0
- package/components/inputs/radio-group-field-input.tsx +220 -0
- package/components/inputs/search-input.tsx +163 -0
- package/components/inputs/select-field-input.tsx +416 -0
- package/components/inputs/switch-field-input.tsx +179 -0
- package/components/inputs/text-field-input.tsx +89 -0
- package/components/inputs/textarea-field-input.tsx +152 -0
- package/components/loaders/apple-hello-loader.tsx +86 -0
- package/components/loaders/spin-loader.tsx +51 -0
- package/components/loaders/text-loader.tsx +148 -0
- package/components/mockups/apple-ipod-mockup-card.tsx +155 -0
- package/components/mockups/apple-watch-mockup-card.tsx +278 -0
- package/components/mockups/browser-mockup-card.tsx +109 -0
- package/components/mockups/laptop-mockup-card.tsx +118 -0
- package/components/mockups/phone-mockup-card.tsx +210 -0
- package/components/mockups/tablet-mockup-card.tsx +212 -0
- package/components/motion/action-sheet.tsx +218 -0
- package/components/motion/action-swap-roll.tsx +32 -0
- package/components/motion/action-swap.tsx +340 -0
- package/components/motion/adaptive-stepper.tsx +442 -0
- package/components/motion/animated-badge.tsx +213 -0
- package/components/motion/animated-number.tsx +50 -0
- package/components/motion/animated-sidebar.tsx +1174 -0
- package/components/motion/animated-toast-stack.tsx +503 -0
- package/components/motion/aspect-ratio.tsx +55 -0
- package/components/motion/attachment-upload.tsx +1080 -0
- package/components/motion/audio-player.tsx +266 -0
- package/components/motion/autocomplete.tsx +183 -0
- package/components/motion/availability-scheduler/copy-menu.tsx +119 -0
- package/components/motion/availability-scheduler/day-row.tsx +231 -0
- package/components/motion/availability-scheduler/icon-button.tsx +46 -0
- package/components/motion/availability-scheduler/index.tsx +154 -0
- package/components/motion/availability-scheduler/time-select.tsx +49 -0
- package/components/motion/availability-scheduler/types.ts +189 -0
- package/components/motion/avatar-group.tsx +154 -0
- package/components/motion/bloom-menu.tsx +216 -0
- package/components/motion/bottom-sheet.tsx +249 -0
- package/components/motion/bounce-sidebar.tsx +266 -0
- package/components/motion/bouncy-accordion.tsx +325 -0
- package/components/motion/breadcrumb.tsx +122 -0
- package/components/motion/button/base.tsx +189 -0
- package/components/motion/button/index.tsx +13 -0
- package/components/motion/button/magnetic.tsx +25 -0
- package/components/motion/button/metallic.tsx +100 -0
- package/components/motion/button/stateful.tsx +237 -0
- package/components/motion/card-folder.tsx +345 -0
- package/components/motion/card-resize.tsx +229 -0
- package/components/motion/cascader.tsx +279 -0
- package/components/motion/center-morph-modal.tsx +366 -0
- package/components/motion/checkbox.tsx +123 -0
- package/components/motion/chip.tsx +121 -0
- package/components/motion/chromatic-text-reveal.tsx +203 -0
- package/components/motion/collapsible.tsx +108 -0
- package/components/motion/color-picker.tsx +206 -0
- package/components/motion/combobox/content.tsx +163 -0
- package/components/motion/combobox/context.tsx +359 -0
- package/components/motion/combobox/list.tsx +222 -0
- package/components/motion/combobox/trigger.tsx +191 -0
- package/components/motion/combobox/use-active-option.ts +130 -0
- package/components/motion/combobox.tsx +33 -0
- package/components/motion/command-palette.tsx +381 -0
- package/components/motion/confetti.tsx +128 -0
- package/components/motion/context-menu.tsx +790 -0
- package/components/motion/copy-button.tsx +103 -0
- package/components/motion/cylinder-carousel.tsx +459 -0
- package/components/motion/date-range-picker.tsx +218 -0
- package/components/motion/digit-swap.tsx +125 -0
- package/components/motion/dock.tsx +113 -0
- package/components/motion/drawer.tsx +108 -0
- package/components/motion/dynamic-island.tsx +221 -0
- package/components/motion/error-shake.tsx +66 -0
- package/components/motion/event-calendar.tsx +224 -0
- package/components/motion/expandable-action-bar.tsx +412 -0
- package/components/motion/expandable-control.tsx +254 -0
- package/components/motion/expandable-tabs.tsx +444 -0
- package/components/motion/expanding-arrow-button.tsx +171 -0
- package/components/motion/feedback-widget.tsx +473 -0
- package/components/motion/file-tree.tsx +429 -0
- package/components/motion/file-upload.tsx +606 -0
- package/components/motion/filter-builder.tsx +283 -0
- package/components/motion/frame.tsx +116 -0
- package/components/motion/gantt.tsx +156 -0
- package/components/motion/glow-button.tsx +101 -0
- package/components/motion/haptic-pressable.tsx +107 -0
- package/components/motion/hold-action-button.tsx +279 -0
- package/components/motion/hover-card.tsx +118 -0
- package/components/motion/icon-stack.tsx +138 -0
- package/components/motion/infinite-masonry.tsx +349 -0
- package/components/motion/input.tsx +242 -0
- package/components/motion/kanban.tsx +255 -0
- package/components/motion/kbd.tsx +127 -0
- package/components/motion/knockout-bracket.tsx +909 -0
- package/components/motion/knockout-wheel.tsx +1112 -0
- package/components/motion/liquid.tsx +351 -0
- package/components/motion/loader.tsx +616 -0
- package/components/motion/magnet-dock.tsx +117 -0
- package/components/motion/magnetic.tsx +50 -0
- package/components/motion/marquee.tsx +66 -0
- package/components/motion/metric-card.tsx +116 -0
- package/components/motion/morphing-modal.tsx +160 -0
- package/components/motion/morphing-search.tsx +643 -0
- package/components/motion/morphing-tabs.tsx +940 -0
- package/components/motion/multi-select/content.tsx +164 -0
- package/components/motion/multi-select/context.tsx +381 -0
- package/components/motion/multi-select/index.tsx +33 -0
- package/components/motion/multi-select/list.tsx +239 -0
- package/components/motion/multi-select/trigger.tsx +262 -0
- package/components/motion/not-found/glitch.tsx +107 -0
- package/components/motion/not-found/index.tsx +11 -0
- package/components/motion/not-found/magnetic.tsx +57 -0
- package/components/motion/not-found/shared.tsx +96 -0
- package/components/motion/not-found/spotlight.tsx +86 -0
- package/components/motion/not-found/stacked.tsx +77 -0
- package/components/motion/not-found/terminal.tsx +83 -0
- package/components/motion/notification-stack.tsx +380 -0
- package/components/motion/number-field.tsx +178 -0
- package/components/motion/number-ticker.tsx +185 -0
- package/components/motion/otp-input.tsx +392 -0
- package/components/motion/overflow-actions.tsx +375 -0
- package/components/motion/page-transition.tsx +92 -0
- package/components/motion/pagination.tsx +163 -0
- package/components/motion/parallax.tsx +64 -0
- package/components/motion/password-input.tsx +165 -0
- package/components/motion/phone-input.tsx +181 -0
- package/components/motion/popover-morph.tsx +317 -0
- package/components/motion/popover-position.ts +86 -0
- package/components/motion/popover.tsx +694 -0
- package/components/motion/prediction-market.tsx +682 -0
- package/components/motion/preview-rail.tsx +390 -0
- package/components/motion/progress-ring.tsx +115 -0
- package/components/motion/project-folder.tsx +418 -0
- package/components/motion/pull-to-refresh.tsx +522 -0
- package/components/motion/radio.tsx +148 -0
- package/components/motion/range-slider.tsx +93 -0
- package/components/motion/rating.tsx +171 -0
- package/components/motion/reorder-grid.tsx +123 -0
- package/components/motion/resizable-panel.tsx +160 -0
- package/components/motion/scroll-progress.tsx +127 -0
- package/components/motion/scroll-reveal.tsx +60 -0
- package/components/motion/scroll-to.tsx +48 -0
- package/components/motion/segmented-control.tsx +130 -0
- package/components/motion/segmented-progress.tsx +128 -0
- package/components/motion/select.tsx +440 -0
- package/components/motion/shader-background.tsx +134 -0
- package/components/motion/shared-layout-bg.tsx +151 -0
- package/components/motion/signup-form.tsx +467 -0
- package/components/motion/slide-action-button.tsx +203 -0
- package/components/motion/smooth-scroll.tsx +264 -0
- package/components/motion/sortable-list.tsx +98 -0
- package/components/motion/speed-dial.tsx +144 -0
- package/components/motion/split-text.tsx +115 -0
- package/components/motion/spotlight-card.tsx +89 -0
- package/components/motion/stepper.tsx +127 -0
- package/components/motion/success-check.tsx +142 -0
- package/components/motion/swap/constants.ts +2 -0
- package/components/motion/swap/controls.tsx +201 -0
- package/components/motion/swap/data.ts +186 -0
- package/components/motion/swap/field.tsx +115 -0
- package/components/motion/swap/quote-row.tsx +46 -0
- package/components/motion/swap/token-badges.tsx +83 -0
- package/components/motion/swap/token-picker.tsx +219 -0
- package/components/motion/swap/types.ts +20 -0
- package/components/motion/swap/utils.ts +28 -0
- package/components/motion/swap.tsx +182 -0
- package/components/motion/swipeable-list.tsx +507 -0
- package/components/motion/switch.tsx +96 -0
- package/components/motion/table/editable-cell.tsx +22 -0
- package/components/motion/table/index.tsx +431 -0
- package/components/motion/table/row-handle.tsx +81 -0
- package/components/motion/table/skeleton-rows.tsx +39 -0
- package/components/motion/table/table-header.tsx +344 -0
- package/components/motion/table/table-menu.tsx +129 -0
- package/components/motion/table/types.ts +86 -0
- package/components/motion/table/use-column-reorder.ts +105 -0
- package/components/motion/table/use-column-resize.ts +81 -0
- package/components/motion/table/use-column-sort.ts +64 -0
- package/components/motion/table/use-row-selection.ts +59 -0
- package/components/motion/table/utils.ts +33 -0
- package/components/motion/tabs.tsx +207 -0
- package/components/motion/text-cascade.tsx +22 -0
- package/components/motion/text-reveal.tsx +143 -0
- package/components/motion/text-scramble.tsx +84 -0
- package/components/motion/text-shimmer.tsx +34 -0
- package/components/motion/text-swap.tsx +56 -0
- package/components/motion/theme-toggle.tsx +210 -0
- package/components/motion/tilt-card.tsx +68 -0
- package/components/motion/timeline.tsx +236 -0
- package/components/motion/toggle-group.tsx +128 -0
- package/components/motion/tooltip.tsx +297 -0
- package/components/motion/tree-view.tsx +200 -0
- package/components/motion/wallet-card/account-avatar.tsx +21 -0
- package/components/motion/wallet-card/account-switcher.tsx +199 -0
- package/components/motion/wallet-card/actions.tsx +59 -0
- package/components/motion/wallet-card/balance-delta.tsx +69 -0
- package/components/motion/wallet-card/constants.ts +18 -0
- package/components/motion/wallet-card/copy-button.tsx +58 -0
- package/components/motion/wallet-card/index.tsx +151 -0
- package/components/motion/wallet-card/search-bar.tsx +198 -0
- package/components/motion/wallet-card/types.ts +34 -0
- package/components/motion/wallet-card/utils.ts +9 -0
- package/components/motion/wheel-picker.tsx +610 -0
- package/components/notifications/apple-notification-banner.tsx +155 -0
- package/components/notifications/calendar-reminder-notification-banner.tsx +138 -0
- package/components/notifications/chat-bubble-notification-banner.tsx +136 -0
- package/components/notifications/delivery-notification-banner.tsx +126 -0
- package/components/notifications/deploy-notification-banner.tsx +140 -0
- package/components/notifications/email-notification-banner.tsx +148 -0
- package/components/notifications/incoming-call-notification-banner.tsx +144 -0
- package/components/notifications/payment-notification-banner.tsx +137 -0
- package/components/notifications/system-alert-banner.tsx +137 -0
- package/components/notifications/toast-notification-banner.tsx +130 -0
- package/components/others/progress-ring-card.tsx +217 -0
- package/components/others/stacked-cards-effect.tsx +62 -0
- package/components/others/terminal-log-card.tsx +107 -0
- package/components/others/thermal-receipt-card.tsx +132 -0
- package/components/others/weather-snapshot-card.tsx +124 -0
- package/components/otp/otp-boxed-input.tsx +482 -0
- package/components/otp/otp-underline-input.tsx +356 -0
- package/components/previews/agents/agent-activity.preview.tsx +136 -0
- package/components/previews/agents/ai-sidebar.preview.tsx +236 -0
- package/components/previews/agents/chat-app-usage.tsx +689 -0
- package/components/previews/agents/chat-app.preview.tsx +130 -0
- package/components/previews/agents/chat-preview.tsx +285 -0
- package/components/previews/agents/citations.preview.tsx +78 -0
- package/components/previews/agents/code-block.preview.tsx +67 -0
- package/components/previews/agents/file-diff.preview.tsx +64 -0
- package/components/previews/agents/image-generation.preview.tsx +117 -0
- package/components/previews/agents/message-bubble.preview.tsx +12 -0
- package/components/previews/agents/message-scroller.preview.tsx +70 -0
- package/components/previews/agents/message.preview.tsx +14 -0
- package/components/previews/agents/prompt-input.preview.tsx +145 -0
- package/components/previews/agents/streaming-response.preview.tsx +156 -0
- package/components/previews/agents/todo-list.preview.tsx +73 -0
- package/components/previews/agents/tool-approval.preview.tsx +80 -0
- package/components/previews/agents/use-tool-result-demo.ts +28 -0
- package/components/previews/blocks/availability-scheduler.preview.tsx +11 -0
- package/components/previews/blocks/bloom-menu.preview.tsx +11 -0
- package/components/previews/blocks/card-folder.preview.tsx +105 -0
- package/components/previews/blocks/command-palette.preview.tsx +40 -0
- package/components/previews/blocks/dynamic-island.preview.tsx +150 -0
- package/components/previews/blocks/expandable-action-bar.preview.tsx +136 -0
- package/components/previews/blocks/expandable-tabs.preview.tsx +136 -0
- package/components/previews/blocks/feedback-widget.preview.tsx +34 -0
- package/components/previews/blocks/file-upload.preview.tsx +175 -0
- package/components/previews/blocks/infinite-masonry.preview.tsx +145 -0
- package/components/previews/blocks/knockout-bracket.preview.tsx +43 -0
- package/components/previews/blocks/morphing-search.preview.tsx +59 -0
- package/components/previews/blocks/morphing-tabs.preview.tsx +91 -0
- package/components/previews/blocks/notification-stack.preview.tsx +39 -0
- package/components/previews/blocks/otp-input.preview.tsx +29 -0
- package/components/previews/blocks/overflow-actions.preview.tsx +51 -0
- package/components/previews/blocks/prediction-market.preview.tsx +41 -0
- package/components/previews/blocks/project-folder.preview.tsx +41 -0
- package/components/previews/blocks/signup-form.preview.tsx +36 -0
- package/components/previews/blocks/swap.preview.tsx +11 -0
- package/components/previews/blocks/swipeable-list.preview.tsx +149 -0
- package/components/previews/blocks/wallet-card.preview.tsx +36 -0
- package/components/previews/motion/action-swap.preview.tsx +82 -0
- package/components/previews/motion/adaptive-stepper.preview.tsx +20 -0
- package/components/previews/motion/animated-badge.preview.tsx +43 -0
- package/components/previews/motion/animated-sidebar.preview.tsx +269 -0
- package/components/previews/motion/animated-toast-stack.preview.tsx +151 -0
- package/components/previews/motion/bottom-sheet.preview.tsx +35 -0
- package/components/previews/motion/bounce-sidebar.preview.tsx +30 -0
- package/components/previews/motion/bouncy-accordion.preview.tsx +66 -0
- package/components/previews/motion/center-morph-modal.preview.tsx +75 -0
- package/components/previews/motion/checkbox.preview.tsx +26 -0
- package/components/previews/motion/combobox.preview.tsx +119 -0
- package/components/previews/motion/context-menu.preview.tsx +144 -0
- package/components/previews/motion/cylinder-carousel.preview.tsx +96 -0
- package/components/previews/motion/dock.preview.tsx +46 -0
- package/components/previews/motion/drawer.preview.tsx +46 -0
- package/components/previews/motion/expandable-control.preview.tsx +27 -0
- package/components/previews/motion/expanding-arrow-button.preview.tsx +11 -0
- package/components/previews/motion/file-tree.preview.tsx +67 -0
- package/components/previews/motion/input.preview.tsx +55 -0
- package/components/previews/motion/loader.preview.tsx +36 -0
- package/components/previews/motion/marquee.preview.tsx +22 -0
- package/components/previews/motion/morphing-modal.preview.tsx +184 -0
- package/components/previews/motion/multi-select.preview.tsx +73 -0
- package/components/previews/motion/number.preview.tsx +61 -0
- package/components/previews/motion/popover.preview.tsx +53 -0
- package/components/previews/motion/preview-rail.preview.tsx +108 -0
- package/components/previews/motion/pull-to-refresh.preview.tsx +117 -0
- package/components/previews/motion/radio.preview.tsx +17 -0
- package/components/previews/motion/range-slider.preview.tsx +19 -0
- package/components/previews/motion/select.preview.tsx +29 -0
- package/components/previews/motion/shader-background.preview.tsx +308 -0
- package/components/previews/motion/shared-layout-bg.preview.tsx +33 -0
- package/components/previews/motion/switch.preview.tsx +15 -0
- package/components/previews/motion/table.preview.tsx +140 -0
- package/components/previews/motion/tabs.preview.tsx +49 -0
- package/components/previews/motion/text-animation.preview.tsx +63 -0
- package/components/previews/motion/theme-toggle.preview.tsx +28 -0
- package/components/previews/motion/tilt-card.preview.tsx +15 -0
- package/components/previews/motion/tooltip.preview.tsx +34 -0
- package/components/previews/motion/wheel-picker.preview.tsx +91 -0
- package/components/pricing/retail-price-tag-card.tsx +104 -0
- package/components/pricing/toggle-pricing-cards.tsx +209 -0
- package/components/profile/contact-profile-card.tsx +100 -0
- package/components/profile/editorial-staff-profile-card.tsx +79 -0
- package/components/profile/team-member-profile-grid.tsx +159 -0
- package/components/profile/user-profile-card.tsx +125 -0
- package/components/resources/resource-links-panel.tsx +176 -0
- package/components/socials/discord-chat-card.tsx +157 -0
- package/components/socials/facebook-post-card.tsx +198 -0
- package/components/socials/github-contribution.tsx +716 -0
- package/components/socials/github-repo-card.tsx +111 -0
- package/components/socials/instagram-post-card.tsx +204 -0
- package/components/socials/linked-in-post-card.tsx +213 -0
- package/components/socials/linked-in-profile-card.tsx +173 -0
- package/components/socials/threads-post-card.tsx +162 -0
- package/components/socials/twitter-post-card.tsx +184 -0
- package/components/socials/twitter-profile-card.tsx +180 -0
- package/components/system/loaders/index.ts +1 -0
- package/components/system/loaders/page-loader-overlay.tsx +16 -0
- package/components/table/customers-table.tsx +146 -0
- package/components/table/orders-table.tsx +195 -0
- package/components/table/recent-transactions-table.tsx +125 -0
- package/components/table/tasks-table.tsx +163 -0
- package/components/table/team-members-table.tsx +155 -0
- package/components/table/users-select-table.tsx +184 -0
- package/components/text/cafe-menu-board-card.tsx +101 -0
- package/components/text/cinema-ticket-card.tsx +99 -0
- package/components/text/daily-motivation-card.tsx +80 -0
- package/components/text/denim-product-editorial-card.tsx +101 -0
- package/components/text/drop-cap-editorial-card.tsx +54 -0
- package/components/text/editorial-quote-card.tsx +98 -0
- package/components/text/journal-writing-card.tsx +131 -0
- package/components/text/keyboard-shortcuts-card.tsx +92 -0
- package/components/text/magazine-cover-card.tsx +109 -0
- package/components/text/notepad-card.tsx +95 -0
- package/components/travel/flight-boarding-card.tsx +177 -0
- package/components/travel/travel-postcard-card.tsx +96 -0
- package/components/underlines/annotated-text-showcase.tsx +37 -0
- package/components/underlines/annotated-text.tsx +471 -0
- package/components/users/credit-card-glass.tsx +113 -0
- package/components/users/team-member-card.tsx +50 -0
- package/components/users/testimonial-card.tsx +239 -0
- package/components/wallet/wallet-pass-card.tsx +220 -0
- package/components/widgets/analog-clock-widget.tsx +203 -0
- package/components/widgets/audio-recorder-widget.tsx +87 -0
- package/components/widgets/battery-face-widget.tsx +152 -0
- package/components/widgets/blob-profile.tsx +64 -0
- package/components/widgets/bluetooth-face-widget.tsx +125 -0
- package/components/widgets/compass-widget.tsx +296 -0
- package/components/widgets/daily-activity-calendar-widget.tsx +133 -0
- package/components/widgets/dnd-face-widget.tsx +129 -0
- package/components/widgets/electric-scooter-widget.tsx +88 -0
- package/components/widgets/flight-arrival-widget.tsx +76 -0
- package/components/widgets/focus-breath-widget.tsx +71 -0
- package/components/widgets/heart-rate-widget.tsx +93 -0
- package/components/widgets/hydration-widget.tsx +109 -0
- package/components/widgets/ios-calendar-widget.tsx +87 -0
- package/components/widgets/ios-digital-clock-widget.tsx +192 -0
- package/components/widgets/ios-earbuds-widget.tsx +120 -0
- package/components/widgets/ios-map-location-widget.tsx +133 -0
- package/components/widgets/minimal-agenda-widget.tsx +123 -0
- package/components/widgets/pomodoro-widget.tsx +155 -0
- package/components/widgets/recorder-face-widget.tsx +146 -0
- package/components/widgets/ride-pickup-widget.tsx +72 -0
- package/components/widgets/sleep-score-widget.tsx +107 -0
- package/components/widgets/step-count-widget.tsx +53 -0
- package/components/widgets/stopwatch-widget.tsx +176 -0
- package/components/widgets/torch-face-widget.tsx +128 -0
- package/components/widgets/voice-assistant-widget.tsx +103 -0
- package/components/widgets/wifi-toggle-widget.tsx +108 -0
- package/lib/cn.ts +1 -0
- package/lib/ease.ts +108 -0
- package/lib/favicon.ts +8 -0
- package/lib/hooks/use-dismiss.ts +120 -0
- package/lib/hooks/use-favicon.ts +39 -0
- package/lib/hooks/use-haptic.ts +44 -0
- package/lib/hooks/use-hover-capable.ts +23 -0
- package/lib/hooks/use-hover-gesture.ts +61 -0
- package/lib/hooks/use-in-view-animation.ts +35 -0
- package/lib/hooks/use-measure.ts +39 -0
- package/lib/hooks/use-on-open.ts +24 -0
- package/lib/hooks/use-row-cursor.ts +86 -0
- package/lib/hooks/use-slider.ts +176 -0
- package/lib/hooks/use-tap-gesture.ts +66 -0
- package/lib/hooks/use-touch-capable.ts +31 -0
- package/lib/presence-gate.tsx +52 -0
- package/lib/styles.ts +124 -0
- package/lib/text-shimmer.ts +21 -0
- package/lib/tick-sound.ts +151 -0
- package/lib/touch.ts +117 -0
- package/lib/utils.ts +6 -0
- package/package.json +49 -0
- package/references/agents/agent-activity.md +747 -0
- package/references/agents/ai-sidebar.md +293 -0
- package/references/agents/approval-card.md +233 -0
- package/references/agents/chat-app.md +184 -0
- package/references/agents/citations.md +159 -0
- package/references/agents/code-block.md +122 -0
- package/references/agents/file-diff.md +119 -0
- package/references/agents/image-generation.md +173 -0
- package/references/agents/loading-states.md +186 -0
- package/references/agents/message-bubble.md +274 -0
- package/references/agents/message-scroller.md +128 -0
- package/references/agents/message.md +162 -0
- package/references/agents/prompt-input.md +204 -0
- package/references/agents/streaming-response.md +216 -0
- package/references/agents/todo-list.md +123 -0
- package/references/agents/tool-approval.md +143 -0
- package/references/agents/tool-result.md +199 -0
- package/references/blocks/availability-scheduler.md +59 -0
- package/references/blocks/bloom-menu.md +56 -0
- package/references/blocks/card-folder.md +165 -0
- package/references/blocks/command-palette.md +89 -0
- package/references/blocks/dynamic-island.md +203 -0
- package/references/blocks/expandable-action-bar.md +191 -0
- package/references/blocks/expandable-tabs.md +184 -0
- package/references/blocks/feedback-widget.md +82 -0
- package/references/blocks/file-upload.md +406 -0
- package/references/blocks/infinite-masonry.md +211 -0
- package/references/blocks/knockout-bracket.md +161 -0
- package/references/blocks/morphing-search.md +113 -0
- package/references/blocks/morphing-tabs.md +143 -0
- package/references/blocks/not-found.md +228 -0
- package/references/blocks/notification-stack.md +92 -0
- package/references/blocks/otp-input.md +85 -0
- package/references/blocks/overflow-actions.md +105 -0
- package/references/blocks/prediction-market.md +97 -0
- package/references/blocks/project-folder.md +99 -0
- package/references/blocks/signup-form.md +92 -0
- package/references/blocks/swap.md +58 -0
- package/references/blocks/swipeable-list.md +202 -0
- package/references/blocks/wallet-card.md +97 -0
- package/references/codex-install.md +59 -0
- package/references/craft/3d-button.md +51 -0
- package/references/craft/3d-icon-button.md +51 -0
- package/references/craft/add-to-cart-button.md +52 -0
- package/references/craft/analog-clock-minimal.md +51 -0
- package/references/craft/analog-clock-numeric.md +51 -0
- package/references/craft/analog-clock-roman.md +51 -0
- package/references/craft/annotated-text.md +51 -0
- package/references/craft/app-dock.md +52 -0
- package/references/craft/apple-hello-loader.md +51 -0
- package/references/craft/apple-notification.md +53 -0
- package/references/craft/apple-watch.md +51 -0
- package/references/craft/arc-bands-background.md +51 -0
- package/references/craft/audio-recorder.md +52 -0
- package/references/craft/aurora-background.md +51 -0
- package/references/craft/battery-face.md +52 -0
- package/references/craft/blob-profile.md +53 -0
- package/references/craft/bluetooth-face.md +52 -0
- package/references/craft/booking-slot-calendar.md +52 -0
- package/references/craft/bookmark-save-button.md +52 -0
- package/references/craft/browser.md +51 -0
- package/references/craft/cafe-menu-board.md +52 -0
- package/references/craft/calendar-reminder-notification.md +52 -0
- package/references/craft/chat-bubble-notification.md +53 -0
- package/references/craft/checkbox-field-input.md +52 -0
- package/references/craft/cherry-petal-background.md +51 -0
- package/references/craft/cinder-latch-button.md +51 -0
- package/references/craft/cinema-ticket.md +52 -0
- package/references/craft/circle-cut-frame.md +51 -0
- package/references/craft/combobox-field-input.md +52 -0
- package/references/craft/compass.md +51 -0
- package/references/craft/contact-form.md +52 -0
- package/references/craft/contact-profile.md +52 -0
- package/references/craft/copy-email.md +53 -0
- package/references/craft/coral-glow-background.md +51 -0
- package/references/craft/craft-copy-button.md +52 -0
- package/references/craft/craft-signup-form.md +53 -0
- package/references/craft/credit-card-glass.md +51 -0
- package/references/craft/customers-table.md +52 -0
- package/references/craft/daily-activity-calendar.md +52 -0
- package/references/craft/daily-motivation.md +53 -0
- package/references/craft/dark-arc-bands-background.md +51 -0
- package/references/craft/dark-aurora-background.md +51 -0
- package/references/craft/dark-carbon-spotlight-background.md +51 -0
- package/references/craft/dark-ember-background.md +51 -0
- package/references/craft/dark-ink-field-background.md +51 -0
- package/references/craft/dark-midnight-mesh-background.md +51 -0
- package/references/craft/dark-rose-noir-background.md +51 -0
- package/references/craft/dark-teal-depth-background.md +51 -0
- package/references/craft/date-field-input.md +52 -0
- package/references/craft/date-range-picker-card.md +52 -0
- package/references/craft/delivery-notification.md +52 -0
- package/references/craft/denim-product-editorial.md +52 -0
- package/references/craft/deploy-notification.md +52 -0
- package/references/craft/depth-outline-button.md +51 -0
- package/references/craft/diagonal-box-pattern.md +51 -0
- package/references/craft/dimple-switch.md +51 -0
- package/references/craft/discord-chat.md +53 -0
- package/references/craft/dnd-face.md +52 -0
- package/references/craft/dot-grid-pattern.md +51 -0
- package/references/craft/download-button.md +52 -0
- package/references/craft/drop-cap-editorial.md +52 -0
- package/references/craft/editor-tool-dock.md +52 -0
- package/references/craft/editorial-quote.md +52 -0
- package/references/craft/editorial-staff-profile.md +51 -0
- package/references/craft/electric-scooter.md +53 -0
- package/references/craft/email-notification.md +53 -0
- package/references/craft/event-countdown-card.md +51 -0
- package/references/craft/event-ticket.md +53 -0
- package/references/craft/facebook-post.md +53 -0
- package/references/craft/file-menu.md +52 -0
- package/references/craft/file-upload-field-input.md +52 -0
- package/references/craft/film-strip.md +52 -0
- package/references/craft/filter-sort.md +52 -0
- package/references/craft/fine-grain-pattern.md +51 -0
- package/references/craft/flight-arrival.md +52 -0
- package/references/craft/flight-boarding.md +52 -0
- package/references/craft/floating-label-field-input.md +51 -0
- package/references/craft/focus-breath.md +51 -0
- package/references/craft/folder-tab-card.md +52 -0
- package/references/craft/follow-button.md +52 -0
- package/references/craft/forgot-password-form.md +53 -0
- package/references/craft/frost-mesh-background.md +51 -0
- package/references/craft/gallery-grid.md +53 -0
- package/references/craft/gallery-wall.md +52 -0
- package/references/craft/github-contribution.md +52 -0
- package/references/craft/github-repo.md +52 -0
- package/references/craft/glass-overlay-image.md +53 -0
- package/references/craft/graph-paper-pattern.md +51 -0
- package/references/craft/halftone-pop-background.md +51 -0
- package/references/craft/hard-shadow-polaroid-frame.md +51 -0
- package/references/craft/heart-rate.md +52 -0
- package/references/craft/hold-to-delete-button.md +52 -0
- package/references/craft/honey-ember-background.md +51 -0
- package/references/craft/hydration.md +51 -0
- package/references/craft/incoming-call-notification.md +53 -0
- package/references/craft/ink-stamp-document.md +52 -0
- package/references/craft/ink-wash-background.md +51 -0
- package/references/craft/input-group-field.md +51 -0
- package/references/craft/inset-button.md +51 -0
- package/references/craft/instagram-post.md +53 -0
- package/references/craft/ios-calendar-widget.md +51 -0
- package/references/craft/ios-digital-clock.md +51 -0
- package/references/craft/ios-earbuds.md +51 -0
- package/references/craft/ios-map-location.md +51 -0
- package/references/craft/ipad.md +51 -0
- package/references/craft/ipod.md +51 -0
- package/references/craft/journal-writing.md +52 -0
- package/references/craft/kebab-actions.md +52 -0
- package/references/craft/keyboard-shortcuts.md +52 -0
- package/references/craft/laptop.md +51 -0
- package/references/craft/like-button.md +52 -0
- package/references/craft/line-grid-pattern.md +51 -0
- package/references/craft/linen-tab-button.md +51 -0
- package/references/craft/linen-weave-background.md +51 -0
- package/references/craft/linked-in-post.md +53 -0
- package/references/craft/linked-in-profile.md +52 -0
- package/references/craft/login-form.md +53 -0
- package/references/craft/mac-dock.md +52 -0
- package/references/craft/magazine-cover.md +53 -0
- package/references/craft/minimal-agenda.md +52 -0
- package/references/craft/mint-lagoon-background.md +51 -0
- package/references/craft/month-picker-calendar.md +52 -0
- package/references/craft/museum-placard.md +51 -0
- package/references/craft/music-player.md +53 -0
- package/references/craft/music-playlist.md +53 -0
- package/references/craft/newsletter-form.md +52 -0
- package/references/craft/notepad.md +52 -0
- package/references/craft/notification.md +52 -0
- package/references/craft/now-playing-bar.md +53 -0
- package/references/craft/orders-table.md +52 -0
- package/references/craft/otp-boxed-input.md +52 -0
- package/references/craft/otp-underline-input.md +51 -0
- package/references/craft/page-loader-overlay.md +46 -0
- package/references/craft/paper-fold-background.md +51 -0
- package/references/craft/password-field-input.md +52 -0
- package/references/craft/payment-notification.md +52 -0
- package/references/craft/phone-field-input.md +51 -0
- package/references/craft/phone.md +51 -0
- package/references/craft/photo-album.md +52 -0
- package/references/craft/photo-contact-sheet.md +52 -0
- package/references/craft/polaroid-image.md +52 -0
- package/references/craft/pomodoro.md +52 -0
- package/references/craft/presence-dock.md +51 -0
- package/references/craft/prism-depth-button.md +51 -0
- package/references/craft/progress-ring-card.md +51 -0
- package/references/craft/quantity-stepper-button.md +52 -0
- package/references/craft/radio-group-field-input.md +51 -0
- package/references/craft/rain-prism-background.md +51 -0
- package/references/craft/recent-transactions-table.md +52 -0
- package/references/craft/recorder-face.md +52 -0
- package/references/craft/resource-links-panel.md +52 -0
- package/references/craft/retail-price-tag.md +51 -0
- package/references/craft/reverse-diagonal-box-pattern.md +51 -0
- package/references/craft/ride-pickup.md +53 -0
- package/references/craft/sand-drift-background.md +51 -0
- package/references/craft/search-input.md +52 -0
- package/references/craft/segmented-toggle-button.md +51 -0
- package/references/craft/select-field-input.md +52 -0
- package/references/craft/share-menu.md +52 -0
- package/references/craft/sheen-pill-button.md +51 -0
- package/references/craft/slate-chip-switch.md +51 -0
- package/references/craft/sleep-score.md +52 -0
- package/references/craft/slide-to-confirm-button.md +52 -0
- package/references/craft/slow-living-polaroid.md +52 -0
- package/references/craft/soft-pill-button.md +51 -0
- package/references/craft/soft-spotlight-background.md +51 -0
- package/references/craft/soft-ui-button.md +51 -0
- package/references/craft/spin-loader.md +52 -0
- package/references/craft/spotlight-bar.md +52 -0
- package/references/craft/stacked-cards-effect.md +52 -0
- package/references/craft/stacked-folder-card.md +52 -0
- package/references/craft/stamp-postcard.md +52 -0
- package/references/craft/step-count.md +51 -0
- package/references/craft/sticky-note-polaroid-frame.md +51 -0
- package/references/craft/stopwatch.md +52 -0
- package/references/craft/sunrise-horizon-background.md +51 -0
- package/references/craft/sunset-bloom-background.md +51 -0
- package/references/craft/switch-field-input.md +51 -0
- package/references/craft/system-alert.md +52 -0
- package/references/craft/tasks-table.md +53 -0
- package/references/craft/team-member-profile-grid.md +52 -0
- package/references/craft/team-member.md +53 -0
- package/references/craft/team-members-table.md +53 -0
- package/references/craft/terminal-log.md +52 -0
- package/references/craft/terrazzo-fragment-background.md +51 -0
- package/references/craft/testimonial.md +53 -0
- package/references/craft/text-field-input.md +51 -0
- package/references/craft/text-loader.md +51 -0
- package/references/craft/textarea-field-input.md +51 -0
- package/references/craft/thermal-receipt.md +52 -0
- package/references/craft/threads-post.md +53 -0
- package/references/craft/toast-notification.md +52 -0
- package/references/craft/toggle-pricing-cards.md +52 -0
- package/references/craft/torch-face.md +52 -0
- package/references/craft/transform-box-frame.md +51 -0
- package/references/craft/transform-plus-frame.md +51 -0
- package/references/craft/travel-postcard.md +53 -0
- package/references/craft/tropical-tide-background.md +51 -0
- package/references/craft/twitter-post.md +53 -0
- package/references/craft/twitter-profile.md +53 -0
- package/references/craft/user-menu.md +53 -0
- package/references/craft/user-profile.md +53 -0
- package/references/craft/users-select-table.md +53 -0
- package/references/craft/voice-assistant.md +52 -0
- package/references/craft/wallet-pass.md +52 -0
- package/references/craft/weather-snapshot.md +52 -0
- package/references/craft/week-strip-calendar.md +52 -0
- package/references/craft/wifi-toggle.md +52 -0
- package/references/craft/workspace-switcher.md +52 -0
- package/references/guides/baseline-ui-craft.md +61 -0
- package/references/guides/craft-surface-design-system.md +120 -0
- package/references/guides/design-system-checklist.md +98 -0
- package/references/guides/enterprise-dashboard-patterns.md +53 -0
- package/references/guides/interaction-transitions.md +71 -0
- package/references/guides/motion-engineering.md +137 -0
- package/references/guides/motion-patterns.md +106 -0
- package/references/guides/motion-performance.md +66 -0
- package/references/guides/style-presets.md +115 -0
- package/references/guides/universal-mobile-patterns.md +72 -0
- package/references/motion/action-sheet.md +98 -0
- package/references/motion/action-swap.md +339 -0
- package/references/motion/adaptive-stepper.md +95 -0
- package/references/motion/animated-badge.md +92 -0
- package/references/motion/animated-sidebar.md +446 -0
- package/references/motion/animated-toast-stack.md +214 -0
- package/references/motion/aspect-ratio.md +59 -0
- package/references/motion/audio-player.md +59 -0
- package/references/motion/autocomplete.md +69 -0
- package/references/motion/avatar-group.md +93 -0
- package/references/motion/bottom-sheet.md +85 -0
- package/references/motion/bounce-sidebar.md +80 -0
- package/references/motion/bouncy-accordion.md +115 -0
- package/references/motion/breadcrumb.md +62 -0
- package/references/motion/button.md +271 -0
- package/references/motion/card-resize.md +60 -0
- package/references/motion/cascader.md +99 -0
- package/references/motion/center-morph-modal.md +133 -0
- package/references/motion/checkbox.md +76 -0
- package/references/motion/chip.md +81 -0
- package/references/motion/collapsible.md +73 -0
- package/references/motion/color-picker.md +57 -0
- package/references/motion/combobox.md +247 -0
- package/references/motion/confetti.md +65 -0
- package/references/motion/context-menu.md +269 -0
- package/references/motion/copy-button.md +59 -0
- package/references/motion/cylinder-carousel.md +151 -0
- package/references/motion/date-range-picker.md +68 -0
- package/references/motion/dock.md +105 -0
- package/references/motion/drawer.md +94 -0
- package/references/motion/error-shake.md +85 -0
- package/references/motion/event-calendar.md +81 -0
- package/references/motion/expandable-control.md +89 -0
- package/references/motion/expanding-arrow-button.md +178 -0
- package/references/motion/file-tree.md +139 -0
- package/references/motion/filter-builder.md +106 -0
- package/references/motion/frame.md +61 -0
- package/references/motion/gantt.md +61 -0
- package/references/motion/glow-button.md +59 -0
- package/references/motion/haptic-pressable.md +64 -0
- package/references/motion/hover-card.md +86 -0
- package/references/motion/icon-stack.md +64 -0
- package/references/motion/input.md +108 -0
- package/references/motion/kanban.md +111 -0
- package/references/motion/kbd.md +66 -0
- package/references/motion/loader.md +82 -0
- package/references/motion/magnet-dock.md +64 -0
- package/references/motion/marquee.md +68 -0
- package/references/motion/metric-card.md +81 -0
- package/references/motion/morphing-modal.md +230 -0
- package/references/motion/multi-select.md +202 -0
- package/references/motion/number-field.md +70 -0
- package/references/motion/number.md +198 -0
- package/references/motion/page-transition.md +78 -0
- package/references/motion/pagination.md +64 -0
- package/references/motion/password-input.md +77 -0
- package/references/motion/phone-input.md +63 -0
- package/references/motion/popover.md +209 -0
- package/references/motion/preview-rail.md +165 -0
- package/references/motion/progress-ring.md +73 -0
- package/references/motion/pull-to-refresh.md +172 -0
- package/references/motion/radio.md +73 -0
- package/references/motion/range-slider.md +290 -0
- package/references/motion/rating.md +69 -0
- package/references/motion/reorder-grid.md +69 -0
- package/references/motion/resizable-panel.md +75 -0
- package/references/motion/scroll-animation.md +397 -0
- package/references/motion/segmented-control.md +68 -0
- package/references/motion/segmented-progress.md +70 -0
- package/references/motion/select.md +196 -0
- package/references/motion/shader-background.md +351 -0
- package/references/motion/shared-layout-bg.md +80 -0
- package/references/motion/sortable-list.md +62 -0
- package/references/motion/speed-dial.md +63 -0
- package/references/motion/split-text.md +62 -0
- package/references/motion/spotlight-card.md +67 -0
- package/references/motion/stepper.md +83 -0
- package/references/motion/success-check.md +67 -0
- package/references/motion/switch.md +62 -0
- package/references/motion/table.md +519 -0
- package/references/motion/tabs.md +116 -0
- package/references/motion/text-animation.md +285 -0
- package/references/motion/text-swap.md +64 -0
- package/references/motion/theme-toggle.md +82 -0
- package/references/motion/tilt-card.md +59 -0
- package/references/motion/timeline.md +85 -0
- package/references/motion/toggle-group.md +70 -0
- package/references/motion/tooltip.md +82 -0
- package/references/motion/tree-view.md +87 -0
- package/references/motion/wheel-picker.md +142 -0
- package/scripts/build_docs.py +557 -0
- package/scripts/install-component.py +232 -0
- package/scripts/test_release.py +119 -0
- package/scripts/updater.mjs +9 -0
- package/scripts/verify_skill.py +84 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Shield } from "lucide-react";
|
|
4
|
+
import { motion, useInView, useReducedMotion } from "motion/react";
|
|
5
|
+
import {
|
|
6
|
+
type KeyboardEvent,
|
|
7
|
+
memo,
|
|
8
|
+
useCallback,
|
|
9
|
+
useId,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { Tooltip } from "@/components/motion/tooltip";
|
|
15
|
+
import { SPRING_PANEL } from "@/lib/ease";
|
|
16
|
+
import { useDismiss } from "@/lib/hooks/use-dismiss";
|
|
17
|
+
import { useHoverGesture } from "@/lib/hooks/use-hover-gesture";
|
|
18
|
+
import { useTapGesture } from "@/lib/hooks/use-tap-gesture";
|
|
19
|
+
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
|
|
20
|
+
import { cn } from "@/lib/utils";
|
|
21
|
+
|
|
22
|
+
export type Team = {
|
|
23
|
+
name: string;
|
|
24
|
+
/**
|
|
25
|
+
* Any square image URL — club crest, org mark, player photo. Wins over `code`.
|
|
26
|
+
* Drawn as-is on the node's card-colored disc, so a transparent-background
|
|
27
|
+
* mark inked for one theme disappears in the other: ship artwork that reads on
|
|
28
|
+
* both, or pick the URL yourself from your theme state.
|
|
29
|
+
*/
|
|
30
|
+
logo?: string;
|
|
31
|
+
/** ISO 3166-1 alpha-2 code, loaded from flagcdn.com (England is gb-eng). Used when `logo` is absent. */
|
|
32
|
+
code?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type MatchSide = {
|
|
36
|
+
team: Team | null;
|
|
37
|
+
score: number | null;
|
|
38
|
+
/** Present on both sides to render shootout scores — 1 (3). */
|
|
39
|
+
penalties?: number | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Structurally compatible with the knockout bracket's Match, minus the fields
|
|
43
|
+
* the wheel never draws (date, time, status). */
|
|
44
|
+
export type Match = {
|
|
45
|
+
id: string;
|
|
46
|
+
home: MatchSide;
|
|
47
|
+
away: MatchSide;
|
|
48
|
+
winner?: "home" | "away";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type Round = {
|
|
52
|
+
/** Read out with the match in tooltips and the screen-reader list. */
|
|
53
|
+
name: string;
|
|
54
|
+
matches: Match[];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export interface KnockoutWheelProps {
|
|
58
|
+
/**
|
|
59
|
+
* The whole draw, ordered widest round first — the same array the knockout
|
|
60
|
+
* bracket takes. Any single-elimination tournament fits: each round holds half
|
|
61
|
+
* the matches of the one before it (16 → 8 → 4 → 2 → 1) and `rounds[r].matches[k]`
|
|
62
|
+
* is fed by matches `2k` and `2k + 1` of the round before it. Two rounds are
|
|
63
|
+
* enough; the wheel grows a ring per round and sizes itself to the rim.
|
|
64
|
+
*/
|
|
65
|
+
rounds: Round[];
|
|
66
|
+
/**
|
|
67
|
+
* Index of the outermost round to draw. Earlier rounds are dropped and the
|
|
68
|
+
* kept round's own teams become the rim, so `1` on a 32-team draw opens at the
|
|
69
|
+
* Round of 16. Defaults to 0 (the whole tree); clamped to the valid range.
|
|
70
|
+
*/
|
|
71
|
+
initialRound?: number;
|
|
72
|
+
className?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const SIZE = 760;
|
|
76
|
+
const CENTER = SIZE / 2;
|
|
77
|
+
// Solid trophy glyph drawn in a 24-unit box.
|
|
78
|
+
const TROPHY_SIZE = 24;
|
|
79
|
+
const TROPHY_PATH =
|
|
80
|
+
"M5 1h12v3h3a2 2 0 0 1 2 2c0 3.3-2.2 5.6-5.2 6A6 6 0 0 1 12 15a6 6 0 0 1-4.8-2.9C4.2 11.6 2 9.3 2 6a2 2 0 0 1 2-2h1V1Zm0 5H4c0 1.9 1.1 3.2 2.6 3.7A12 12 0 0 1 5 6Zm14 0h-1a12 12 0 0 1-1.6 3.7C17.9 9.2 19 7.9 19 6ZM9 16.5h6V19h2.5v2h-11v-2H9v-2.5Z";
|
|
81
|
+
// Clears the hub's top edge. The hub's two feeders sit on the horizontal, so
|
|
82
|
+
// the space directly above it is always free.
|
|
83
|
+
const TROPHY_GAP = 6;
|
|
84
|
+
// Outermost ring. The remaining 62px of the box absorbs the largest node plus
|
|
85
|
+
// its ring stroke, so nothing clips at the viewBox edge.
|
|
86
|
+
const OUTER_R = 318;
|
|
87
|
+
const HUB_R = 34;
|
|
88
|
+
// Nodes grow outward so the crowded outer ring still reads at small sizes.
|
|
89
|
+
const NODE_MIN = 14.2;
|
|
90
|
+
const NODE_STEP = 2.2;
|
|
91
|
+
// Initials floor, in viewBox units. The stage never goes below 32rem against a
|
|
92
|
+
// 760-unit box (scale ~0.674), so 15 units is ~10px on screen — under that, two
|
|
93
|
+
// letters are a smudge. `node.r * 0.8` alone puts the inner ring at 7.6px.
|
|
94
|
+
const INITIALS_MIN = 15;
|
|
95
|
+
// Siblings pull slightly toward their parent, opening a lane between subtrees.
|
|
96
|
+
const SIBLING_GAP = 0.9;
|
|
97
|
+
// Puts the hub's two feeders on the horizontal, where there's room for them.
|
|
98
|
+
const HUB_ANGLE = 90;
|
|
99
|
+
|
|
100
|
+
// Module scope so the memoized marks keep a stable transition identity.
|
|
101
|
+
const DIM_TRANSITION = { duration: 0.18 } as const;
|
|
102
|
+
const NO_TRANSITION = { duration: 0 } as const;
|
|
103
|
+
|
|
104
|
+
// Math.sin/cos are implementation-defined down in the last digits, so the SSR
|
|
105
|
+
// engine and the browser disagree and React reports a hydration mismatch on
|
|
106
|
+
// every coordinate. Quantizing well below sub-pixel makes both agree exactly.
|
|
107
|
+
const quantize = (n: number) => Math.round(n * 1e3) / 1e3;
|
|
108
|
+
|
|
109
|
+
const polar = (radius: number, deg: number) => {
|
|
110
|
+
const rad = (deg * Math.PI) / 180;
|
|
111
|
+
return {
|
|
112
|
+
x: quantize(CENTER + radius * Math.cos(rad)),
|
|
113
|
+
y: quantize(CENTER + radius * Math.sin(rad)),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const point = (radius: number, deg: number) => {
|
|
118
|
+
const { x, y } = polar(radius, deg);
|
|
119
|
+
return `${x.toFixed(2)} ${y.toFixed(2)}`;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Fixed precision so the server and client render byte-identical style strings.
|
|
123
|
+
// Raw floats serialize differently across the two and trip a hydration mismatch.
|
|
124
|
+
const pct = (value: number) => `${((value / SIZE) * 100).toFixed(4)}%`;
|
|
125
|
+
|
|
126
|
+
type WheelNode = {
|
|
127
|
+
id: string;
|
|
128
|
+
parentId: string | null;
|
|
129
|
+
depth: number;
|
|
130
|
+
/** Position around the wheel, in degrees. Orders arrow-key navigation. */
|
|
131
|
+
angle: number;
|
|
132
|
+
x: number;
|
|
133
|
+
y: number;
|
|
134
|
+
r: number;
|
|
135
|
+
team: Team | null;
|
|
136
|
+
label: string;
|
|
137
|
+
/** Round the node's match belongs to; null on the rim, which holds teams. */
|
|
138
|
+
round: string | null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type WheelLink = {
|
|
142
|
+
id: string;
|
|
143
|
+
d: string;
|
|
144
|
+
depth: number;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Names and round labels are single ideas, so they wrap as a unit. Without this
|
|
148
|
+
// "Round of 16" strands a lone "16" on the next line.
|
|
149
|
+
const keepTogether = (text: string) => text.replace(/ /g, " ");
|
|
150
|
+
|
|
151
|
+
const teamName = (side: MatchSide) => keepTogether(side.team?.name ?? "TBD");
|
|
152
|
+
|
|
153
|
+
/** A `logo` is used as given; a country `code` loads a flag from flagcdn.com. */
|
|
154
|
+
const crestSrc = (team: Team) =>
|
|
155
|
+
team.logo ?? (team.code ? `https://flagcdn.com/w80/${team.code}.png` : null);
|
|
156
|
+
|
|
157
|
+
/** Two-letter stand-in when a team has no artwork — "Real Madrid" → RM.
|
|
158
|
+
* Spread, not `word[0]`: an emoji or astral first character is a surrogate pair
|
|
159
|
+
* and indexing it renders a replacement glyph. */
|
|
160
|
+
const initials = (name: string) =>
|
|
161
|
+
name
|
|
162
|
+
.split(/\s+/)
|
|
163
|
+
.slice(0, 2)
|
|
164
|
+
.map((word) => [...word][0])
|
|
165
|
+
.join("")
|
|
166
|
+
.toUpperCase();
|
|
167
|
+
|
|
168
|
+
/** Teams · score, in the order they were played. The round is prepended by the
|
|
169
|
+
* caller that has it, so the round list can reuse this without repeating it. */
|
|
170
|
+
function matchLabel(match: Match) {
|
|
171
|
+
const teams = `${teamName(match.home)} v ${teamName(match.away)}`;
|
|
172
|
+
if (match.home.score == null || match.away.score == null) return teams;
|
|
173
|
+
const pens =
|
|
174
|
+
match.home.penalties != null && match.away.penalties != null
|
|
175
|
+
? ` (${match.home.penalties}–${match.away.penalties} pens)`
|
|
176
|
+
: "";
|
|
177
|
+
return `${teams} · ${match.home.score}–${match.away.score}${pens}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Walks the match tree from the final outward, laying every node on a ring and
|
|
181
|
+
* splitting each parent's wedge between its two feeders. */
|
|
182
|
+
function buildWheel(rounds: Round[]) {
|
|
183
|
+
const nodes: WheelNode[] = [];
|
|
184
|
+
const links: WheelLink[] = [];
|
|
185
|
+
const layers = rounds.length;
|
|
186
|
+
const ringR = (depth: number) => (depth / layers) * OUTER_R;
|
|
187
|
+
const nodeR = (depth: number) => NODE_MIN + (depth - 1) * NODE_STEP;
|
|
188
|
+
|
|
189
|
+
// An empty or malformed catalog renders nothing rather than throwing on the
|
|
190
|
+
// way to the hub.
|
|
191
|
+
const final = rounds[layers - 1]?.matches[0];
|
|
192
|
+
if (!final) return { nodes, links, champion: null };
|
|
193
|
+
|
|
194
|
+
const champion = final.winner ? final[final.winner].team : null;
|
|
195
|
+
nodes.push({
|
|
196
|
+
id: final.id,
|
|
197
|
+
parentId: null,
|
|
198
|
+
depth: 0,
|
|
199
|
+
angle: HUB_ANGLE,
|
|
200
|
+
x: CENTER,
|
|
201
|
+
y: CENTER,
|
|
202
|
+
r: HUB_R,
|
|
203
|
+
team: champion,
|
|
204
|
+
label: matchLabel(final),
|
|
205
|
+
round: rounds[layers - 1].name,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// `roundIndex` is the round `match` belongs to; its two feeders live one round
|
|
209
|
+
// out, or, past the first round, are the two teams that played it.
|
|
210
|
+
const walk = (
|
|
211
|
+
match: Match,
|
|
212
|
+
roundIndex: number,
|
|
213
|
+
index: number,
|
|
214
|
+
parent: WheelNode,
|
|
215
|
+
angle: number,
|
|
216
|
+
wedge: number,
|
|
217
|
+
) => {
|
|
218
|
+
const depth = parent.depth + 1;
|
|
219
|
+
const radius = ringR(depth);
|
|
220
|
+
const r = nodeR(depth);
|
|
221
|
+
// The hub's two feeders sit opposite each other, so they get plain radial
|
|
222
|
+
// lines; an arc between them would be a half circle.
|
|
223
|
+
const offset = (wedge / 4) * (parent.depth === 0 ? 1 : SIBLING_GAP);
|
|
224
|
+
const angles = [angle - offset, angle + offset];
|
|
225
|
+
const sides = ["home", "away"] as const;
|
|
226
|
+
|
|
227
|
+
const children = angles.map((childAngle, side) => {
|
|
228
|
+
const { x, y } = polar(radius, childAngle);
|
|
229
|
+
const feeder =
|
|
230
|
+
roundIndex > 0
|
|
231
|
+
? rounds[roundIndex - 1].matches[2 * index + side]
|
|
232
|
+
: undefined;
|
|
233
|
+
const node: WheelNode = feeder
|
|
234
|
+
? {
|
|
235
|
+
id: feeder.id,
|
|
236
|
+
parentId: parent.id,
|
|
237
|
+
depth,
|
|
238
|
+
angle: childAngle,
|
|
239
|
+
x,
|
|
240
|
+
y,
|
|
241
|
+
r,
|
|
242
|
+
team: feeder.winner ? feeder[feeder.winner].team : null,
|
|
243
|
+
label: matchLabel(feeder),
|
|
244
|
+
round: rounds[roundIndex - 1].name,
|
|
245
|
+
}
|
|
246
|
+
: {
|
|
247
|
+
id: `${match.id}-${sides[side]}`,
|
|
248
|
+
parentId: parent.id,
|
|
249
|
+
depth,
|
|
250
|
+
angle: childAngle,
|
|
251
|
+
x,
|
|
252
|
+
y,
|
|
253
|
+
r,
|
|
254
|
+
team: match[sides[side]].team,
|
|
255
|
+
label: match[sides[side]].team?.name ?? "TBD",
|
|
256
|
+
round: null,
|
|
257
|
+
};
|
|
258
|
+
nodes.push(node);
|
|
259
|
+
return { node, angle: childAngle, feeder };
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (parent.depth === 0) {
|
|
263
|
+
for (const child of children) {
|
|
264
|
+
links.push({
|
|
265
|
+
id: `${parent.id}-${child.node.id}`,
|
|
266
|
+
d: `M ${point(radius, child.angle)} L ${CENTER} ${CENTER}`,
|
|
267
|
+
depth,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
const midR = (ringR(parent.depth) + radius) / 2;
|
|
272
|
+
links.push({
|
|
273
|
+
id: `${parent.id}-arc`,
|
|
274
|
+
d: `M ${point(radius, angles[0])} L ${point(midR, angles[0])} A ${midR} ${midR} 0 0 1 ${point(midR, angles[1])} L ${point(radius, angles[1])}`,
|
|
275
|
+
depth,
|
|
276
|
+
});
|
|
277
|
+
links.push({
|
|
278
|
+
id: `${parent.id}-stem`,
|
|
279
|
+
d: `M ${point(midR, angle)} L ${point(ringR(parent.depth), angle)}`,
|
|
280
|
+
depth,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const [side, child] of children.entries()) {
|
|
285
|
+
if (child.feeder) {
|
|
286
|
+
walk(
|
|
287
|
+
child.feeder,
|
|
288
|
+
roundIndex - 1,
|
|
289
|
+
2 * index + side,
|
|
290
|
+
child.node,
|
|
291
|
+
child.angle,
|
|
292
|
+
wedge / 2,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
walk(final, layers - 1, 0, nodes[0], HUB_ANGLE, 360);
|
|
299
|
+
return { nodes, links, champion };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Match ids from the hub down to the champion's first-round win. */
|
|
303
|
+
function championPath(rounds: Round[]) {
|
|
304
|
+
const path = new Set<string>();
|
|
305
|
+
let index = 0;
|
|
306
|
+
for (let r = rounds.length - 1; r >= 0; r--) {
|
|
307
|
+
const match = rounds[r].matches[index];
|
|
308
|
+
if (!match?.winner) return path;
|
|
309
|
+
path.add(match.id);
|
|
310
|
+
if (r === 0) path.add(`${match.id}-${match.winner}`);
|
|
311
|
+
index = 2 * index + (match.winner === "home" ? 0 : 1);
|
|
312
|
+
}
|
|
313
|
+
return path;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function TeamMark({
|
|
317
|
+
node,
|
|
318
|
+
clipId,
|
|
319
|
+
lit,
|
|
320
|
+
dimmed,
|
|
321
|
+
loadFlag,
|
|
322
|
+
transition,
|
|
323
|
+
}: {
|
|
324
|
+
node: WheelNode;
|
|
325
|
+
clipId: string;
|
|
326
|
+
lit: boolean;
|
|
327
|
+
dimmed: boolean;
|
|
328
|
+
loadFlag: boolean;
|
|
329
|
+
transition: object;
|
|
330
|
+
}) {
|
|
331
|
+
// The failed URL, not a boolean: a corrected logo on the same node should be
|
|
332
|
+
// tried again rather than stay initials for the life of the wheel.
|
|
333
|
+
const [failedSrc, setFailedSrc] = useState<string | null>(null);
|
|
334
|
+
const resolved = node.team ? crestSrc(node.team) : null;
|
|
335
|
+
const src = resolved === failedSrc ? null : resolved;
|
|
336
|
+
const showFlag = src != null && loadFlag;
|
|
337
|
+
// A square logo is fitted whole; a 4:3 flag is cropped to fill the disc.
|
|
338
|
+
const box = node.team?.logo
|
|
339
|
+
? { w: node.r * 1.44, h: node.r * 1.44, fit: "xMidYMid meet" }
|
|
340
|
+
: { w: node.r * 2.68, h: node.r * 2, fit: "xMidYMid slice" };
|
|
341
|
+
// Dimming rides on the mark itself rather than a scrim tinted with the page
|
|
342
|
+
// background, so the wheel recedes correctly on any surface it's dropped on.
|
|
343
|
+
const fade = { opacity: dimmed ? 0.38 : 1 };
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<>
|
|
347
|
+
{/* Stays opaque at every state: links are routed underneath and would
|
|
348
|
+
otherwise read straight through the flag. */}
|
|
349
|
+
<circle cx={node.x} cy={node.y} r={node.r} className="fill-card" />
|
|
350
|
+
{showFlag && node.team ? (
|
|
351
|
+
<>
|
|
352
|
+
<clipPath id={clipId}>
|
|
353
|
+
<circle cx={node.x} cy={node.y} r={node.r} />
|
|
354
|
+
</clipPath>
|
|
355
|
+
{/* Plain <image> — flags from flagcdn.com, logos from wherever you host
|
|
356
|
+
them. A 4:3 flag is cropped to fill the disc; a logo is fitted whole
|
|
357
|
+
inside it, since a crest cropped to a circle loses its shape. */}
|
|
358
|
+
<motion.image
|
|
359
|
+
href={src}
|
|
360
|
+
x={node.x - box.w / 2}
|
|
361
|
+
y={node.y - box.h / 2}
|
|
362
|
+
width={box.w}
|
|
363
|
+
height={box.h}
|
|
364
|
+
clipPath={`url(#${clipId})`}
|
|
365
|
+
preserveAspectRatio={box.fit}
|
|
366
|
+
initial={false}
|
|
367
|
+
animate={fade}
|
|
368
|
+
transition={transition}
|
|
369
|
+
onError={() => setFailedSrc(src)}
|
|
370
|
+
/>
|
|
371
|
+
</>
|
|
372
|
+
) : node.team ? (
|
|
373
|
+
// No artwork on this team — initials keep the ring readable.
|
|
374
|
+
<motion.text
|
|
375
|
+
x={node.x}
|
|
376
|
+
y={node.y}
|
|
377
|
+
textAnchor="middle"
|
|
378
|
+
dominantBaseline="central"
|
|
379
|
+
fontSize={Math.max(node.r * 0.8, INITIALS_MIN)}
|
|
380
|
+
initial={false}
|
|
381
|
+
animate={fade}
|
|
382
|
+
transition={transition}
|
|
383
|
+
className="fill-muted-foreground font-semibold"
|
|
384
|
+
>
|
|
385
|
+
{initials(node.team.name)}
|
|
386
|
+
</motion.text>
|
|
387
|
+
) : (
|
|
388
|
+
// Same shield the knockout bracket uses for a TBD slot, so an
|
|
389
|
+
// undecided place reads identically across both fixture styles.
|
|
390
|
+
<motion.g initial={false} animate={fade} transition={transition}>
|
|
391
|
+
<Shield
|
|
392
|
+
x={node.x - node.r * 0.7}
|
|
393
|
+
y={node.y - node.r * 0.7}
|
|
394
|
+
width={node.r * 1.4}
|
|
395
|
+
height={node.r * 1.4}
|
|
396
|
+
className="fill-current text-muted-foreground/50"
|
|
397
|
+
/>
|
|
398
|
+
</motion.g>
|
|
399
|
+
)}
|
|
400
|
+
<circle
|
|
401
|
+
cx={node.x}
|
|
402
|
+
cy={node.y}
|
|
403
|
+
r={node.r}
|
|
404
|
+
fill="none"
|
|
405
|
+
strokeWidth={lit ? 2 : 1}
|
|
406
|
+
className={lit ? "stroke-foreground" : "stroke-border"}
|
|
407
|
+
/>
|
|
408
|
+
</>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Memoized so pointing at one flag re-renders two marks, not all 63. */
|
|
413
|
+
const WheelMark = memo(function WheelMark({
|
|
414
|
+
node,
|
|
415
|
+
isLit,
|
|
416
|
+
dimmed,
|
|
417
|
+
loadFlag,
|
|
418
|
+
reduce,
|
|
419
|
+
enter,
|
|
420
|
+
showTrophy,
|
|
421
|
+
clipId,
|
|
422
|
+
}: {
|
|
423
|
+
node: WheelNode;
|
|
424
|
+
isLit: boolean;
|
|
425
|
+
dimmed: boolean;
|
|
426
|
+
loadFlag: boolean;
|
|
427
|
+
reduce: boolean;
|
|
428
|
+
enter: object;
|
|
429
|
+
showTrophy: boolean;
|
|
430
|
+
clipId: string;
|
|
431
|
+
}) {
|
|
432
|
+
return (
|
|
433
|
+
<motion.g
|
|
434
|
+
initial={reduce ? false : { opacity: 0, scale: 0.6 }}
|
|
435
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
436
|
+
transition={enter}
|
|
437
|
+
style={{ transformOrigin: `${node.x}px ${node.y}px` }}
|
|
438
|
+
>
|
|
439
|
+
{showTrophy && (
|
|
440
|
+
<g
|
|
441
|
+
transform={`translate(${CENTER - TROPHY_SIZE / 2}, ${CENTER - HUB_R - TROPHY_GAP - TROPHY_SIZE})`}
|
|
442
|
+
>
|
|
443
|
+
<path d={TROPHY_PATH} className="fill-warning" />
|
|
444
|
+
</g>
|
|
445
|
+
)}
|
|
446
|
+
<TeamMark
|
|
447
|
+
node={node}
|
|
448
|
+
clipId={clipId}
|
|
449
|
+
lit={isLit}
|
|
450
|
+
dimmed={dimmed}
|
|
451
|
+
loadFlag={loadFlag}
|
|
452
|
+
transition={reduce ? NO_TRANSITION : DIM_TRANSITION}
|
|
453
|
+
/>
|
|
454
|
+
</motion.g>
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/** Invisible hit area over one flag: hover, tap, focus and arrow keys. */
|
|
459
|
+
const WheelAnchor = memo(function WheelAnchor({
|
|
460
|
+
node,
|
|
461
|
+
caption,
|
|
462
|
+
isTabStop,
|
|
463
|
+
isPinned,
|
|
464
|
+
canHover,
|
|
465
|
+
uid,
|
|
466
|
+
onHover,
|
|
467
|
+
onFocusNode,
|
|
468
|
+
onToggle,
|
|
469
|
+
onKey,
|
|
470
|
+
}: {
|
|
471
|
+
node: WheelNode;
|
|
472
|
+
caption: string;
|
|
473
|
+
isTabStop: boolean;
|
|
474
|
+
isPinned: boolean;
|
|
475
|
+
canHover: boolean;
|
|
476
|
+
uid: string;
|
|
477
|
+
onHover: (id: string | null) => void;
|
|
478
|
+
onFocusNode: (id: string | null) => void;
|
|
479
|
+
onToggle: (id: string) => void;
|
|
480
|
+
onKey: (node: WheelNode, key: string) => void;
|
|
481
|
+
}) {
|
|
482
|
+
const size = pct(node.r * 2);
|
|
483
|
+
// Capture-phase focus props, so a Tooltip cloning the child cannot overwrite
|
|
484
|
+
// them. Both interaction paths are always attached: iPadOS answers the hover
|
|
485
|
+
// query with true for a finger, so hanging the tap path off "cannot hover"
|
|
486
|
+
// left it unreachable on the very device it was written for. The event says
|
|
487
|
+
// which input arrived.
|
|
488
|
+
const tap = useTapGesture<boolean>();
|
|
489
|
+
const hover = useHoverGesture();
|
|
490
|
+
const trigger = (
|
|
491
|
+
<button
|
|
492
|
+
type="button"
|
|
493
|
+
id={`${uid}-${node.id}`}
|
|
494
|
+
tabIndex={isTabStop ? 0 : -1}
|
|
495
|
+
aria-label={caption}
|
|
496
|
+
onKeyDown={(event: KeyboardEvent) => {
|
|
497
|
+
// A key press starts a keyboard activation, which never had a pointer
|
|
498
|
+
// behind it: a gesture the platform took away must not be read as the
|
|
499
|
+
// tap behind the click this press synthesizes.
|
|
500
|
+
tap.drop();
|
|
501
|
+
if (!event.key.startsWith("Arrow")) return;
|
|
502
|
+
// Arrows drive the wheel here, so they must not also scroll the page.
|
|
503
|
+
event.preventDefault();
|
|
504
|
+
onKey(node, event.key);
|
|
505
|
+
}}
|
|
506
|
+
onFocusCapture={() => onFocusNode(node.id)}
|
|
507
|
+
onBlurCapture={() => onFocusNode(null)}
|
|
508
|
+
onPointerEnter={(event) => {
|
|
509
|
+
if (hover.enter(event)) onHover(node.id);
|
|
510
|
+
}}
|
|
511
|
+
onPointerLeave={(event) => {
|
|
512
|
+
if (hover.leave(event)) onHover(null);
|
|
513
|
+
}}
|
|
514
|
+
onPointerDown={(event) => {
|
|
515
|
+
tap.start(event, isPinned);
|
|
516
|
+
}}
|
|
517
|
+
onPointerCancel={tap.drop}
|
|
518
|
+
// Click, not pointerdown: a tap focuses the button first, and unpinning
|
|
519
|
+
// has to also drop that focus or the flag stays lit.
|
|
520
|
+
onClick={(event) => {
|
|
521
|
+
const gesture = tap.take();
|
|
522
|
+
// A hovering pointer lit the flag on its way in and puts it out on the
|
|
523
|
+
// way past; only a gesture without a hover pins one.
|
|
524
|
+
if (!gesture || gesture.pointerType === "mouse") return;
|
|
525
|
+
onToggle(node.id);
|
|
526
|
+
if (gesture.state) event.currentTarget.blur();
|
|
527
|
+
}}
|
|
528
|
+
// ring-foreground, not the ring token: --ring is a 10% white hairline
|
|
529
|
+
// that disappears over a flag. Focus has to be obvious.
|
|
530
|
+
className="block h-full w-full rounded-full outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
531
|
+
/>
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<div
|
|
536
|
+
className="absolute"
|
|
537
|
+
style={{
|
|
538
|
+
left: pct(node.x - node.r),
|
|
539
|
+
top: pct(node.y - node.r),
|
|
540
|
+
width: size,
|
|
541
|
+
height: size,
|
|
542
|
+
}}
|
|
543
|
+
>
|
|
544
|
+
{/* Touch never opens a Tooltip, so those devices skip mounting one per
|
|
545
|
+
flag and read the tapped label instead. */}
|
|
546
|
+
{canHover ? (
|
|
547
|
+
<Tooltip
|
|
548
|
+
content={caption}
|
|
549
|
+
side="top"
|
|
550
|
+
wrapperClassName="block h-full w-full"
|
|
551
|
+
className="max-w-[20rem] whitespace-normal text-balance break-words text-center"
|
|
552
|
+
>
|
|
553
|
+
{trigger}
|
|
554
|
+
</Tooltip>
|
|
555
|
+
) : (
|
|
556
|
+
trigger
|
|
557
|
+
)}
|
|
558
|
+
</div>
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
export function KnockoutWheel({
|
|
563
|
+
rounds,
|
|
564
|
+
initialRound = 0,
|
|
565
|
+
className,
|
|
566
|
+
}: KnockoutWheelProps) {
|
|
567
|
+
const reduce = useReducedMotion();
|
|
568
|
+
const canHover = useHoverCapable();
|
|
569
|
+
const uid = useId().replace(/:/g, "");
|
|
570
|
+
const ref = useRef<SVGSVGElement>(null);
|
|
571
|
+
const stageRef = useRef<HTMLDivElement>(null);
|
|
572
|
+
// 63 flags for a 32-team draw, and SVG <image> has no lazy attribute — so the
|
|
573
|
+
// requests wait until the wheel is nearly on screen.
|
|
574
|
+
const inView = useInView(ref, { once: true, margin: "300px" });
|
|
575
|
+
// Hover, tap and focus are tracked apart. Sharing one slot let a stray mouse
|
|
576
|
+
// move clear the isolation while a node still held focus, and a tap has to
|
|
577
|
+
// outlive the pointerleave that a finger fires the moment it lifts.
|
|
578
|
+
const [hovered, setHovered] = useState<string | null>(null);
|
|
579
|
+
const [pinned, setPinned] = useState<string | null>(null);
|
|
580
|
+
const [focused, setFocused] = useState<string | null>(null);
|
|
581
|
+
const active = hovered ?? pinned ?? focused;
|
|
582
|
+
|
|
583
|
+
// Everything downstream reads the trimmed catalog, so the kept round's own
|
|
584
|
+
// teams become the rim and the sr-only list matches what's drawn.
|
|
585
|
+
const visible = useMemo(() => {
|
|
586
|
+
const from = Math.min(Math.max(initialRound, 0), Math.max(rounds.length - 1, 0));
|
|
587
|
+
return from > 0 ? rounds.slice(from) : rounds;
|
|
588
|
+
}, [rounds, initialRound]);
|
|
589
|
+
|
|
590
|
+
const { nodes, links, champion } = useMemo(
|
|
591
|
+
() => buildWheel(visible),
|
|
592
|
+
[visible],
|
|
593
|
+
);
|
|
594
|
+
const winners = useMemo(() => championPath(visible), [visible]);
|
|
595
|
+
const activeNode = useMemo(
|
|
596
|
+
() => nodes.find((node) => node.id === active),
|
|
597
|
+
[active, nodes],
|
|
598
|
+
);
|
|
599
|
+
// Pointing at one flag isolates that flag. Only at rest does the wheel fall
|
|
600
|
+
// back to lighting the champion's whole run.
|
|
601
|
+
const lit = useMemo(
|
|
602
|
+
() => (activeNode ? new Set([activeNode.id]) : winners),
|
|
603
|
+
[activeNode, winners],
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// Stable identity, or every mark re-renders on each pointer move.
|
|
607
|
+
const enter = useMemo(
|
|
608
|
+
() =>
|
|
609
|
+
reduce
|
|
610
|
+
? { duration: 0, opacity: { duration: 0 } }
|
|
611
|
+
: { ...SPRING_PANEL, opacity: { duration: 0.24 } },
|
|
612
|
+
[reduce],
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// One transition object per ring, cached, so the ring-by-ring entrance delay
|
|
616
|
+
// survives memoization instead of allocating 63 objects per render.
|
|
617
|
+
const enterFor = useMemo(() => {
|
|
618
|
+
const cache = new Map<number, object>();
|
|
619
|
+
return (depth: number) => {
|
|
620
|
+
const hit = cache.get(depth);
|
|
621
|
+
if (hit) return hit;
|
|
622
|
+
const value = { ...enter, delay: reduce ? 0 : depth * 0.06 };
|
|
623
|
+
cache.set(depth, value);
|
|
624
|
+
return value;
|
|
625
|
+
};
|
|
626
|
+
}, [enter, reduce]);
|
|
627
|
+
|
|
628
|
+
// Tooltip is hover-only by design, so touch gets the same label anchored to
|
|
629
|
+
// the tapped flag. It flips to the far side near the rim so it stays on stage.
|
|
630
|
+
const tapped = canHover ? undefined : activeNode;
|
|
631
|
+
const tapAbove = tapped ? tapped.y > CENTER : false;
|
|
632
|
+
|
|
633
|
+
// Arrow keys follow the geometry: up walks toward the hub, down walks out to
|
|
634
|
+
// a feeder, left/right go round the ring.
|
|
635
|
+
const { ring, firstChild } = useMemo(() => {
|
|
636
|
+
const byDepth = new Map<number, WheelNode[]>();
|
|
637
|
+
const child = new Map<string, WheelNode>();
|
|
638
|
+
for (const node of nodes) {
|
|
639
|
+
const peers = byDepth.get(node.depth) ?? [];
|
|
640
|
+
peers.push(node);
|
|
641
|
+
byDepth.set(node.depth, peers);
|
|
642
|
+
if (node.parentId && !child.has(node.parentId)) {
|
|
643
|
+
child.set(node.parentId, node);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
for (const peers of byDepth.values()) {
|
|
647
|
+
peers.sort((a, b) => a.angle - b.angle);
|
|
648
|
+
}
|
|
649
|
+
return { ring: byDepth, firstChild: child };
|
|
650
|
+
}, [nodes]);
|
|
651
|
+
|
|
652
|
+
const tabStop = activeNode?.id ?? nodes[0]?.id;
|
|
653
|
+
|
|
654
|
+
// Stable callbacks so the memoized anchors don't re-render on every hover.
|
|
655
|
+
const onKey = useCallback(
|
|
656
|
+
(node: WheelNode, key: string) => {
|
|
657
|
+
if (!key.startsWith("Arrow")) return;
|
|
658
|
+
const target =
|
|
659
|
+
key === "ArrowUp"
|
|
660
|
+
? nodes.find((peer) => peer.id === node.parentId)
|
|
661
|
+
: key === "ArrowDown"
|
|
662
|
+
? firstChild.get(node.id)
|
|
663
|
+
: (() => {
|
|
664
|
+
const peers = ring.get(node.depth) ?? [];
|
|
665
|
+
if (peers.length < 2) return undefined;
|
|
666
|
+
const at = peers.indexOf(node);
|
|
667
|
+
const next = key === "ArrowRight" ? at + 1 : at - 1;
|
|
668
|
+
return peers[(next + peers.length) % peers.length];
|
|
669
|
+
})();
|
|
670
|
+
if (!target) return;
|
|
671
|
+
// Focus moves; the focus handler lights the node it lands on.
|
|
672
|
+
document.getElementById(`${uid}-${target.id}`)?.focus();
|
|
673
|
+
},
|
|
674
|
+
[nodes, ring, firstChild, uid],
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
const onToggle = useCallback(
|
|
678
|
+
(id: string) => setPinned((current) => (current === id ? null : id)),
|
|
679
|
+
[],
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// A tap on another flag hands the isolation over rather than ending it.
|
|
683
|
+
const onFlag = useCallback(
|
|
684
|
+
(target: Element) =>
|
|
685
|
+
Boolean(stageRef.current?.contains(target) && target.closest("button")),
|
|
686
|
+
[],
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
// A finger never leaves the flag it lit, so the isolation would hold for good
|
|
690
|
+
// — and bare stage reports no pointer event of its own to end it. The next
|
|
691
|
+
// pointerdown that isn't on a flag stands in for the pointer leaving; it is
|
|
692
|
+
// consumed, since a wheel spanning the viewport makes tapping past it the
|
|
693
|
+
// natural way out and there is no reason for that tap to do anything else.
|
|
694
|
+
// Only a pinned flag arms this: a mouse ends its own hover, and the browser
|
|
695
|
+
// drops focus on its own.
|
|
696
|
+
const unpin = useCallback(() => {
|
|
697
|
+
setPinned(null);
|
|
698
|
+
const focus = document.activeElement;
|
|
699
|
+
if (focus instanceof HTMLElement && stageRef.current?.contains(focus)) {
|
|
700
|
+
focus.blur();
|
|
701
|
+
}
|
|
702
|
+
}, []);
|
|
703
|
+
|
|
704
|
+
useDismiss(pinned !== null, unpin, null, {
|
|
705
|
+
behavior: "consume",
|
|
706
|
+
ignore: onFlag,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Round · teams · score for a decided match; a rim node is just a team. A slot
|
|
710
|
+
// whose team isn't known yet reads TBD, matching the shield drawn in its place.
|
|
711
|
+
const captions = useMemo(
|
|
712
|
+
() =>
|
|
713
|
+
new Map(
|
|
714
|
+
nodes.map((node) => [
|
|
715
|
+
node.id,
|
|
716
|
+
node.team == null
|
|
717
|
+
? "TBD"
|
|
718
|
+
: node.round
|
|
719
|
+
? `${keepTogether(node.round)} · ${node.label}`
|
|
720
|
+
: node.team.name,
|
|
721
|
+
]),
|
|
722
|
+
),
|
|
723
|
+
[nodes],
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
return (
|
|
727
|
+
<div
|
|
728
|
+
className={cn(
|
|
729
|
+
"w-full max-w-full overflow-x-auto overscroll-x-contain",
|
|
730
|
+
className,
|
|
731
|
+
)}
|
|
732
|
+
>
|
|
733
|
+
{/* Below the min width the rim's marks collapse too small to tell apart or
|
|
734
|
+
tap, so the wheel holds its size and pans instead. The floor is fixed,
|
|
735
|
+
not rim-derived: node radius grows with depth, so a shallower draw has
|
|
736
|
+
*smaller* marks and needs the width more, not less. */}
|
|
737
|
+
<div
|
|
738
|
+
ref={stageRef}
|
|
739
|
+
className="relative mx-auto w-full min-w-[32rem] max-w-[34rem]"
|
|
740
|
+
>
|
|
741
|
+
<svg
|
|
742
|
+
ref={ref}
|
|
743
|
+
viewBox={`0 0 ${SIZE} ${SIZE}`}
|
|
744
|
+
role="img"
|
|
745
|
+
aria-label={`Tournament wheel${champion ? `, won by ${champion.name}` : ""}`}
|
|
746
|
+
className="h-auto w-full touch-manipulation"
|
|
747
|
+
>
|
|
748
|
+
<defs>
|
|
749
|
+
{/* Warm halo marking the champion. Kept faint: --warning is saturated
|
|
750
|
+
enough that anything stronger drowns the connectors under it. */}
|
|
751
|
+
<radialGradient id={`${uid}-glow`}>
|
|
752
|
+
<stop offset="0%" stopColor="var(--color-warning)" stopOpacity="0.14" />
|
|
753
|
+
<stop offset="45%" stopColor="var(--color-warning)" stopOpacity="0.04" />
|
|
754
|
+
<stop offset="100%" stopColor="var(--color-warning)" stopOpacity="0" />
|
|
755
|
+
</radialGradient>
|
|
756
|
+
</defs>
|
|
757
|
+
|
|
758
|
+
{champion && (
|
|
759
|
+
<circle
|
|
760
|
+
cx={CENTER}
|
|
761
|
+
cy={CENTER}
|
|
762
|
+
r={OUTER_R * 0.62}
|
|
763
|
+
fill={`url(#${uid}-glow)`}
|
|
764
|
+
/>
|
|
765
|
+
)}
|
|
766
|
+
|
|
767
|
+
{/* role="img" on the svg prunes descendants from the accessibility tree,
|
|
768
|
+
so the structure needs no aria-hidden of its own. */}
|
|
769
|
+
<g>
|
|
770
|
+
{links.map((link) => (
|
|
771
|
+
<motion.path
|
|
772
|
+
key={link.id}
|
|
773
|
+
d={link.d}
|
|
774
|
+
fill="none"
|
|
775
|
+
initial={reduce ? false : { opacity: 0 }}
|
|
776
|
+
animate={{ opacity: 1 }}
|
|
777
|
+
transition={{ duration: 0.3, delay: link.depth * 0.06 }}
|
|
778
|
+
className="stroke-border-strong"
|
|
779
|
+
/>
|
|
780
|
+
))}
|
|
781
|
+
</g>
|
|
782
|
+
|
|
783
|
+
{nodes.map((node) => (
|
|
784
|
+
<WheelMark
|
|
785
|
+
key={node.id}
|
|
786
|
+
node={node}
|
|
787
|
+
isLit={lit.has(node.id)}
|
|
788
|
+
dimmed={lit.size > 0 && !lit.has(node.id)}
|
|
789
|
+
loadFlag={inView}
|
|
790
|
+
reduce={Boolean(reduce)}
|
|
791
|
+
enter={enterFor(node.depth)}
|
|
792
|
+
showTrophy={node.depth === 0 && champion != null}
|
|
793
|
+
clipId={`${uid}-clip-${node.id}`}
|
|
794
|
+
/>
|
|
795
|
+
))}
|
|
796
|
+
</svg>
|
|
797
|
+
|
|
798
|
+
{/* An SVG <g> can't anchor a Tooltip, so each flag gets an invisible
|
|
799
|
+
HTML hit area laid over it in percentage units, which track the
|
|
800
|
+
wheel as it scales. */}
|
|
801
|
+
{nodes.map((node) => (
|
|
802
|
+
<WheelAnchor
|
|
803
|
+
key={node.id}
|
|
804
|
+
node={node}
|
|
805
|
+
caption={captions.get(node.id) ?? ""}
|
|
806
|
+
isTabStop={node.id === tabStop}
|
|
807
|
+
isPinned={node.id === pinned}
|
|
808
|
+
canHover={canHover}
|
|
809
|
+
uid={uid}
|
|
810
|
+
onHover={setHovered}
|
|
811
|
+
onFocusNode={setFocused}
|
|
812
|
+
onToggle={onToggle}
|
|
813
|
+
onKey={onKey}
|
|
814
|
+
/>
|
|
815
|
+
))}
|
|
816
|
+
|
|
817
|
+
{tapped && (
|
|
818
|
+
<motion.p
|
|
819
|
+
initial={reduce ? false : { opacity: 0, scale: 0.94 }}
|
|
820
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
821
|
+
transition={reduce ? NO_TRANSITION : DIM_TRANSITION}
|
|
822
|
+
style={{
|
|
823
|
+
left: pct(tapped.x),
|
|
824
|
+
top: pct(tapped.y + (tapAbove ? -tapped.r : tapped.r)),
|
|
825
|
+
}}
|
|
826
|
+
className={cn(
|
|
827
|
+
"pointer-events-none absolute z-10 w-max max-w-[min(18rem,80vw)] -translate-x-1/2 text-balance break-words rounded-lg border border-border bg-background px-2.5 py-1 text-center text-xs font-medium text-foreground shadow-lg",
|
|
828
|
+
tapAbove ? "-translate-y-[calc(100%+8px)]" : "translate-y-2",
|
|
829
|
+
)}
|
|
830
|
+
>
|
|
831
|
+
{captions.get(tapped.id)}
|
|
832
|
+
</motion.p>
|
|
833
|
+
)}
|
|
834
|
+
</div>
|
|
835
|
+
|
|
836
|
+
{/* role="img" prunes the svg's descendants, so every result is restated
|
|
837
|
+
here for screen readers. */}
|
|
838
|
+
{/* Named lists, not <section aria-label> — a named section is a landmark,
|
|
839
|
+
and five of those would crowd real page landmarks. */}
|
|
840
|
+
<div className="sr-only">
|
|
841
|
+
{visible
|
|
842
|
+
.slice()
|
|
843
|
+
.reverse()
|
|
844
|
+
.map((round) => (
|
|
845
|
+
<ul key={round.name} aria-label={round.name}>
|
|
846
|
+
{round.matches.map((match) => (
|
|
847
|
+
<li key={match.id}>{matchLabel(match)}</li>
|
|
848
|
+
))}
|
|
849
|
+
</ul>
|
|
850
|
+
))}
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ── Sample data ──────────────────────────────────────────────────────────────
|
|
857
|
+
// A finished 32-team cup, here to demo the shape. Swap it for your own
|
|
858
|
+
// tournament. Rounds run widest first and each holds half as many matches as the
|
|
859
|
+
// one before it (16 → 8 → 4 → 2 → 1); `matches[k]` of a round is fed by matches
|
|
860
|
+
// `2k` and `2k + 1` of the round before it, which is what pairs the branches.
|
|
861
|
+
// Any draw works: pass fewer rounds for a smaller cup, give teams a `logo`
|
|
862
|
+
// instead of a country `code`, or neither for initials. The knockout bracket
|
|
863
|
+
// takes the same array, so one dataset feeds both fixture styles.
|
|
864
|
+
|
|
865
|
+
export const TEAMS = {
|
|
866
|
+
spain: { name: "Spain", code: "es" },
|
|
867
|
+
japan: { name: "Japan", code: "jp" },
|
|
868
|
+
netherlands: { name: "Netherlands", code: "nl" },
|
|
869
|
+
portugal: { name: "Portugal", code: "pt" },
|
|
870
|
+
england: { name: "England", code: "gb-eng" },
|
|
871
|
+
uruguay: { name: "Uruguay", code: "uy" },
|
|
872
|
+
croatia: { name: "Croatia", code: "hr" },
|
|
873
|
+
brazil: { name: "Brazil", code: "br" },
|
|
874
|
+
france: { name: "France", code: "fr" },
|
|
875
|
+
morocco: { name: "Morocco", code: "ma" },
|
|
876
|
+
belgium: { name: "Belgium", code: "be" },
|
|
877
|
+
italy: { name: "Italy", code: "it" },
|
|
878
|
+
argentina: { name: "Argentina", code: "ar" },
|
|
879
|
+
mexico: { name: "Mexico", code: "mx" },
|
|
880
|
+
germany: { name: "Germany", code: "de" },
|
|
881
|
+
norway: { name: "Norway", code: "no" },
|
|
882
|
+
costaRica: { name: "Costa Rica", code: "cr" },
|
|
883
|
+
serbia: { name: "Serbia", code: "rs" },
|
|
884
|
+
ecuador: { name: "Ecuador", code: "ec" },
|
|
885
|
+
ghana: { name: "Ghana", code: "gh" },
|
|
886
|
+
wales: { name: "Wales", code: "gb-wls" },
|
|
887
|
+
canada: { name: "Canada", code: "ca" },
|
|
888
|
+
denmark: { name: "Denmark", code: "dk" },
|
|
889
|
+
cameroon: { name: "Cameroon", code: "cm" },
|
|
890
|
+
poland: { name: "Poland", code: "pl" },
|
|
891
|
+
senegal: { name: "Senegal", code: "sn" },
|
|
892
|
+
tunisia: { name: "Tunisia", code: "tn" },
|
|
893
|
+
switzerland: { name: "Switzerland", code: "ch" },
|
|
894
|
+
peru: { name: "Peru", code: "pe" },
|
|
895
|
+
qatar: { name: "Qatar", code: "qa" },
|
|
896
|
+
sweden: { name: "Sweden", code: "se" },
|
|
897
|
+
austria: { name: "Austria", code: "at" },
|
|
898
|
+
} satisfies Record<string, Team>;
|
|
899
|
+
|
|
900
|
+
export const ROUNDS: Round[] = [
|
|
901
|
+
{
|
|
902
|
+
name: "Round of 32",
|
|
903
|
+
matches: [
|
|
904
|
+
{
|
|
905
|
+
id: "w-r32-1",
|
|
906
|
+
home: { team: TEAMS.spain, score: 3 },
|
|
907
|
+
away: { team: TEAMS.costaRica, score: 0 },
|
|
908
|
+
winner: "home",
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
id: "w-r32-2",
|
|
912
|
+
home: { team: TEAMS.japan, score: 2 },
|
|
913
|
+
away: { team: TEAMS.serbia, score: 1 },
|
|
914
|
+
winner: "home",
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
id: "w-r32-3",
|
|
918
|
+
home: { team: TEAMS.netherlands, score: 2 },
|
|
919
|
+
away: { team: TEAMS.ecuador, score: 0 },
|
|
920
|
+
winner: "home",
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
id: "w-r32-4",
|
|
924
|
+
home: { team: TEAMS.ghana, score: 2 },
|
|
925
|
+
away: { team: TEAMS.portugal, score: 3 },
|
|
926
|
+
winner: "away",
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
id: "w-r32-5",
|
|
930
|
+
home: { team: TEAMS.england, score: 4 },
|
|
931
|
+
away: { team: TEAMS.wales, score: 0 },
|
|
932
|
+
winner: "home",
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
id: "w-r32-6",
|
|
936
|
+
home: { team: TEAMS.canada, score: 0 },
|
|
937
|
+
away: { team: TEAMS.uruguay, score: 2 },
|
|
938
|
+
winner: "away",
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
id: "w-r32-7",
|
|
942
|
+
home: { team: TEAMS.croatia, score: 1 },
|
|
943
|
+
away: { team: TEAMS.denmark, score: 0 },
|
|
944
|
+
winner: "home",
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
id: "w-r32-8",
|
|
948
|
+
home: { team: TEAMS.brazil, score: 3 },
|
|
949
|
+
away: { team: TEAMS.cameroon, score: 1 },
|
|
950
|
+
winner: "home",
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
id: "w-r32-9",
|
|
954
|
+
home: { team: TEAMS.france, score: 2 },
|
|
955
|
+
away: { team: TEAMS.poland, score: 1 },
|
|
956
|
+
winner: "home",
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
id: "w-r32-10",
|
|
960
|
+
home: { team: TEAMS.senegal, score: 0 },
|
|
961
|
+
away: { team: TEAMS.morocco, score: 1 },
|
|
962
|
+
winner: "away",
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
id: "w-r32-11",
|
|
966
|
+
home: { team: TEAMS.belgium, score: 2 },
|
|
967
|
+
away: { team: TEAMS.tunisia, score: 0 },
|
|
968
|
+
winner: "home",
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
id: "w-r32-12",
|
|
972
|
+
home: { team: TEAMS.switzerland, score: 1 },
|
|
973
|
+
away: { team: TEAMS.italy, score: 3 },
|
|
974
|
+
winner: "away",
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
id: "w-r32-13",
|
|
978
|
+
home: { team: TEAMS.argentina, score: 2 },
|
|
979
|
+
away: { team: TEAMS.peru, score: 0 },
|
|
980
|
+
winner: "home",
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
id: "w-r32-14",
|
|
984
|
+
home: { team: TEAMS.qatar, score: 0 },
|
|
985
|
+
away: { team: TEAMS.mexico, score: 1 },
|
|
986
|
+
winner: "away",
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
id: "w-r32-15",
|
|
990
|
+
home: { team: TEAMS.germany, score: 4 },
|
|
991
|
+
away: { team: TEAMS.sweden, score: 2 },
|
|
992
|
+
winner: "home",
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
id: "w-r32-16",
|
|
996
|
+
home: { team: TEAMS.austria, score: 1 },
|
|
997
|
+
away: { team: TEAMS.norway, score: 2 },
|
|
998
|
+
winner: "away",
|
|
999
|
+
},
|
|
1000
|
+
],
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
name: "Round of 16",
|
|
1004
|
+
matches: [
|
|
1005
|
+
{
|
|
1006
|
+
id: "w-r16-1",
|
|
1007
|
+
home: { team: TEAMS.spain, score: 2 },
|
|
1008
|
+
away: { team: TEAMS.japan, score: 0 },
|
|
1009
|
+
winner: "home",
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
id: "w-r16-2",
|
|
1013
|
+
home: { team: TEAMS.netherlands, score: 1 },
|
|
1014
|
+
away: { team: TEAMS.portugal, score: 3 },
|
|
1015
|
+
winner: "away",
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
id: "w-r16-3",
|
|
1019
|
+
home: { team: TEAMS.england, score: 2 },
|
|
1020
|
+
away: { team: TEAMS.uruguay, score: 1 },
|
|
1021
|
+
winner: "home",
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
id: "w-r16-4",
|
|
1025
|
+
home: { team: TEAMS.croatia, score: 0 },
|
|
1026
|
+
away: { team: TEAMS.brazil, score: 1 },
|
|
1027
|
+
winner: "away",
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
id: "w-r16-5",
|
|
1031
|
+
home: { team: TEAMS.france, score: 3 },
|
|
1032
|
+
away: { team: TEAMS.morocco, score: 1 },
|
|
1033
|
+
winner: "home",
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
id: "w-r16-6",
|
|
1037
|
+
home: { team: TEAMS.belgium, score: 1 },
|
|
1038
|
+
away: { team: TEAMS.italy, score: 2 },
|
|
1039
|
+
winner: "away",
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
id: "w-r16-7",
|
|
1043
|
+
home: { team: TEAMS.argentina, score: 2 },
|
|
1044
|
+
away: { team: TEAMS.mexico, score: 0 },
|
|
1045
|
+
winner: "home",
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
id: "w-r16-8",
|
|
1049
|
+
home: { team: TEAMS.germany, score: 1, penalties: 4 },
|
|
1050
|
+
away: { team: TEAMS.norway, score: 1, penalties: 2 },
|
|
1051
|
+
winner: "home",
|
|
1052
|
+
},
|
|
1053
|
+
],
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
name: "Quarter-finals",
|
|
1057
|
+
matches: [
|
|
1058
|
+
{
|
|
1059
|
+
id: "w-qf-1",
|
|
1060
|
+
home: { team: TEAMS.spain, score: 1 },
|
|
1061
|
+
away: { team: TEAMS.portugal, score: 0 },
|
|
1062
|
+
winner: "home",
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
id: "w-qf-2",
|
|
1066
|
+
home: { team: TEAMS.england, score: 2 },
|
|
1067
|
+
away: { team: TEAMS.brazil, score: 3 },
|
|
1068
|
+
winner: "away",
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
id: "w-qf-3",
|
|
1072
|
+
home: { team: TEAMS.france, score: 2 },
|
|
1073
|
+
away: { team: TEAMS.italy, score: 1 },
|
|
1074
|
+
winner: "home",
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
id: "w-qf-4",
|
|
1078
|
+
home: { team: TEAMS.argentina, score: 3 },
|
|
1079
|
+
away: { team: TEAMS.germany, score: 1 },
|
|
1080
|
+
winner: "home",
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
name: "Semi-finals",
|
|
1086
|
+
matches: [
|
|
1087
|
+
{
|
|
1088
|
+
id: "w-sf-1",
|
|
1089
|
+
home: { team: TEAMS.spain, score: 2 },
|
|
1090
|
+
away: { team: TEAMS.brazil, score: 1 },
|
|
1091
|
+
winner: "home",
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
id: "w-sf-2",
|
|
1095
|
+
home: { team: TEAMS.france, score: 0, penalties: 3 },
|
|
1096
|
+
away: { team: TEAMS.argentina, score: 0, penalties: 4 },
|
|
1097
|
+
winner: "away",
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
name: "Final",
|
|
1103
|
+
matches: [
|
|
1104
|
+
{
|
|
1105
|
+
id: "w-f-1",
|
|
1106
|
+
home: { team: TEAMS.spain, score: 2 },
|
|
1107
|
+
away: { team: TEAMS.argentina, score: 1 },
|
|
1108
|
+
winner: "home",
|
|
1109
|
+
},
|
|
1110
|
+
],
|
|
1111
|
+
},
|
|
1112
|
+
];
|