@mova-ui/react 0.1.0 → 0.4.1
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/README.md +70 -21
- package/bin/mova.mjs +38 -12
- package/dist/catalog/components/AnimatedButton.d.ts +9 -0
- package/dist/catalog/components/AnimatedButton.js +346 -0
- package/dist/catalog/components/IconSwap.d.ts +6 -0
- package/dist/catalog/components/IconSwap.js +9 -0
- package/dist/catalog/components/InViewRender.d.ts +4 -0
- package/dist/catalog/components/InViewRender.js +14 -0
- package/dist/catalog/components/PageTransitionCard.d.ts +9 -0
- package/dist/catalog/components/PageTransitionCard.js +110 -0
- package/dist/catalog/components/PageTransitionModal.d.ts +10 -0
- package/dist/catalog/components/PageTransitionModal.js +180 -0
- package/dist/catalog/components/PageTransitionOverlay.d.ts +9 -0
- package/dist/catalog/components/PageTransitionOverlay.js +7 -0
- package/dist/catalog/components/cards/CardArc5.d.ts +14 -0
- package/dist/catalog/components/cards/CardArc5.js +76 -0
- package/dist/catalog/components/cards/CardArc7.d.ts +14 -0
- package/dist/catalog/components/cards/CardArc7.js +77 -0
- package/dist/catalog/components/cards/CardCarousel.d.ts +12 -0
- package/dist/catalog/components/cards/CardCarousel.js +139 -0
- package/dist/catalog/components/cards/CardCascadeStagger.d.ts +9 -0
- package/dist/catalog/components/cards/CardCascadeStagger.js +64 -0
- package/dist/catalog/components/cards/CardCornerFan.d.ts +12 -0
- package/dist/catalog/components/cards/CardCornerFan.js +66 -0
- package/dist/catalog/components/cards/CardCoverFlow.d.ts +12 -0
- package/dist/catalog/components/cards/CardCoverFlow.js +115 -0
- package/dist/catalog/components/cards/CardLinearSpread.d.ts +12 -0
- package/dist/catalog/components/cards/CardLinearSpread.js +62 -0
- package/dist/catalog/components/cards/CardLongArc5.d.ts +14 -0
- package/dist/catalog/components/cards/CardLongArc5.js +76 -0
- package/dist/catalog/components/cards/CardScatterSpread.d.ts +9 -0
- package/dist/catalog/components/cards/CardScatterSpread.js +69 -0
- package/dist/catalog/components/cards/CardStampArc.d.ts +14 -0
- package/dist/catalog/components/cards/CardStampArc.js +101 -0
- package/dist/catalog/components/cards/CardTimeMachine.d.ts +8 -0
- package/dist/catalog/components/cards/CardTimeMachine.js +182 -0
- package/dist/catalog/components/cards/CardWheelFan.d.ts +9 -0
- package/dist/catalog/components/cards/CardWheelFan.js +68 -0
- package/dist/catalog/components/cards/FocusBlur.d.ts +14 -0
- package/dist/catalog/components/cards/FocusBlur.js +7 -0
- package/dist/catalog/components/css-animations/Dock.d.ts +6 -0
- package/dist/catalog/components/css-animations/Dock.js +93 -0
- package/dist/catalog/components/css-animations/FocusBlur.d.ts +4 -0
- package/dist/catalog/components/css-animations/FocusBlur.js +35 -0
- package/dist/catalog/components/css-animations/FrequencySelector.d.ts +6 -0
- package/dist/catalog/components/css-animations/FrequencySelector.js +100 -0
- package/dist/catalog/components/css-animations/MediaBetweenText.d.ts +34 -0
- package/dist/catalog/components/css-animations/MediaBetweenText.js +125 -0
- package/dist/catalog/components/css-animations/ScrambleHover.d.ts +16 -0
- package/dist/catalog/components/css-animations/ScrambleHover.js +101 -0
- package/dist/catalog/components/css-animations/TabBar.d.ts +6 -0
- package/dist/catalog/components/css-animations/TabBar.js +57 -0
- package/dist/catalog/components/css-animations/whimsical/AppleLoaders.d.ts +9 -0
- package/dist/catalog/components/css-animations/whimsical/AppleLoaders.js +57 -0
- package/dist/catalog/components/css-animations/whimsical/Area1.d.ts +7 -0
- package/dist/catalog/components/css-animations/whimsical/Area1.js +56 -0
- package/dist/catalog/components/css-animations/whimsical/Area6.d.ts +7 -0
- package/dist/catalog/components/css-animations/whimsical/Area6.js +53 -0
- package/dist/catalog/components/css-animations/whimsical/Bookmark.d.ts +7 -0
- package/dist/catalog/components/css-animations/whimsical/Bookmark.js +183 -0
- package/dist/catalog/components/css-animations/whimsical/ConceptTrios1.d.ts +37 -0
- package/dist/catalog/components/css-animations/whimsical/ConceptTrios1.js +253 -0
- package/dist/catalog/components/css-animations/whimsical/ConceptTrios2.d.ts +37 -0
- package/dist/catalog/components/css-animations/whimsical/ConceptTrios2.js +237 -0
- package/dist/catalog/components/css-animations/whimsical/ConceptTrios3.d.ts +73 -0
- package/dist/catalog/components/css-animations/whimsical/ConceptTrios3.js +441 -0
- package/dist/catalog/components/css-animations/whimsical/Curtain.d.ts +7 -0
- package/dist/catalog/components/css-animations/whimsical/Curtain.js +155 -0
- package/dist/catalog/components/css-animations/whimsical/Kakikaki.d.ts +7 -0
- package/dist/catalog/components/css-animations/whimsical/Kakikaki.js +67 -0
- package/dist/catalog/components/css-animations/whimsical/KineticTensionCapsule.d.ts +7 -0
- package/dist/catalog/components/css-animations/whimsical/KineticTensionCapsule.js +82 -0
- package/dist/catalog/components/css-animations/whimsical/MatrixGridLoader.d.ts +5 -0
- package/dist/catalog/components/css-animations/whimsical/MatrixGridLoader.js +88 -0
- package/dist/catalog/components/css-animations/whimsical/RedesignedPhysicsTrios.d.ts +103 -0
- package/dist/catalog/components/css-animations/whimsical/RedesignedPhysicsTrios.js +607 -0
- package/dist/catalog/components/css-animations/whimsical/Tissue.d.ts +7 -0
- package/dist/catalog/components/css-animations/whimsical/Tissue.js +111 -0
- package/dist/catalog/components/css-animations/whimsical/Tsumiki.d.ts +8 -0
- package/dist/catalog/components/css-animations/whimsical/Tsumiki.js +83 -0
- package/dist/catalog/components/css-animations/whimsical/WhimsicalVariations.d.ts +37 -0
- package/dist/catalog/components/css-animations/whimsical/WhimsicalVariations.js +325 -0
- package/dist/catalog/components/css-animations/whimsical/YuiPhysicsExperiments.d.ts +37 -0
- package/dist/catalog/components/css-animations/whimsical/YuiPhysicsExperiments.js +247 -0
- package/dist/catalog/components/css-animations/yui-components/RedesignedUiTrios.d.ts +19 -0
- package/dist/catalog/components/css-animations/yui-components/RedesignedUiTrios.js +174 -0
- package/dist/catalog/components/css-animations/yui-components/UiKitTrios.d.ts +19 -0
- package/dist/catalog/components/css-animations/yui-components/UiKitTrios.js +147 -0
- package/dist/catalog/components/css-animations/yui-components/YuiUiKit1.d.ts +29 -0
- package/dist/catalog/components/css-animations/yui-components/YuiUiKit1.js +317 -0
- package/dist/catalog/components/css-animations/yui-components/YuiUiKit2.d.ts +34 -0
- package/dist/catalog/components/css-animations/yui-components/YuiUiKit2.js +356 -0
- package/dist/catalog/components/dither-charts/ActivityHeatmap.d.ts +5 -0
- package/dist/catalog/components/dither-charts/ActivityHeatmap.js +8 -0
- package/dist/catalog/components/dither-charts/ChartCard.d.ts +2 -0
- package/dist/catalog/components/dither-charts/ChartCard.js +8 -0
- package/dist/catalog/components/dither-charts/DeviceUsageChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/DeviceUsageChart.js +8 -0
- package/dist/catalog/components/dither-charts/DitherBarChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/DitherBarChart.js +148 -0
- package/dist/catalog/components/dither-charts/DitherBook.d.ts +23 -0
- package/dist/catalog/components/dither-charts/DitherBook.js +352 -0
- package/dist/catalog/components/dither-charts/DitherChartsGrid.d.ts +9 -0
- package/dist/catalog/components/dither-charts/DitherChartsGrid.js +108 -0
- package/dist/catalog/components/dither-charts/DitherDonutChart.d.ts +7 -0
- package/dist/catalog/components/dither-charts/DitherDonutChart.js +8 -0
- package/dist/catalog/components/dither-charts/DitherFunnelChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/DitherFunnelChart.js +8 -0
- package/dist/catalog/components/dither-charts/DitherGrowthChart.d.ts +7 -0
- package/dist/catalog/components/dither-charts/DitherGrowthChart.js +8 -0
- package/dist/catalog/components/dither-charts/DitherHeatmapGrid.d.ts +5 -0
- package/dist/catalog/components/dither-charts/DitherHeatmapGrid.js +115 -0
- package/dist/catalog/components/dither-charts/DitherRadialChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/DitherRadialChart.js +114 -0
- package/dist/catalog/components/dither-charts/DitherScatterChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/DitherScatterChart.js +151 -0
- package/dist/catalog/components/dither-charts/DitherSparklineMatrix.d.ts +5 -0
- package/dist/catalog/components/dither-charts/DitherSparklineMatrix.js +89 -0
- package/dist/catalog/components/dither-charts/DitherStackedChart.d.ts +7 -0
- package/dist/catalog/components/dither-charts/DitherStackedChart.js +8 -0
- package/dist/catalog/components/dither-charts/MembersGrowthChart.d.ts +2 -0
- package/dist/catalog/components/dither-charts/MembersGrowthChart.js +8 -0
- package/dist/catalog/components/dither-charts/PaymentsChart.d.ts +2 -0
- package/dist/catalog/components/dither-charts/PaymentsChart.js +8 -0
- package/dist/catalog/components/dither-charts/RevenueLineChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/RevenueLineChart.js +8 -0
- package/dist/catalog/components/dither-charts/ServerGauge.d.ts +5 -0
- package/dist/catalog/components/dither-charts/ServerGauge.js +8 -0
- package/dist/catalog/components/dither-charts/SimpleCompExtracted.d.ts +6 -0
- package/dist/catalog/components/dither-charts/SimpleCompExtracted.js +423 -0
- package/dist/catalog/components/dither-charts/StorageUsageChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/StorageUsageChart.js +8 -0
- package/dist/catalog/components/dither-charts/TrafficBubble.d.ts +5 -0
- package/dist/catalog/components/dither-charts/TrafficBubble.js +8 -0
- package/dist/catalog/components/dither-charts/UptimeChart.d.ts +5 -0
- package/dist/catalog/components/dither-charts/UptimeChart.js +8 -0
- package/dist/catalog/components/dither-charts/lib/ditherPatterns.d.ts +7 -0
- package/dist/catalog/components/dither-charts/lib/ditherPatterns.js +107 -0
- package/dist/catalog/components/dither-charts/lib/recharts-legend.d.ts +8 -0
- package/dist/catalog/components/dither-charts/lib/recharts-legend.js +40 -0
- package/dist/catalog/components/dither-charts/lib/recharts-tooltip.d.ts +10 -0
- package/dist/catalog/components/dither-charts/lib/recharts-tooltip.js +7 -0
- package/dist/catalog/components/dither-charts/lib/utils.d.ts +3 -0
- package/dist/catalog/components/dither-charts/lib/utils.js +7 -0
- package/dist/catalog/components/forms/AnimatedFormElement.d.ts +8 -0
- package/dist/catalog/components/forms/AnimatedFormElement.js +412 -0
- package/dist/catalog/components/metrics/AnimatedMetricCard.d.ts +8 -0
- package/dist/catalog/components/metrics/AnimatedMetricCard.js +1049 -0
- package/dist/catalog/components/metrics/Budget.d.ts +1 -0
- package/dist/catalog/components/metrics/Budget.js +35 -0
- package/dist/catalog/components/metrics/CacheStatsCard.d.ts +1 -0
- package/dist/catalog/components/metrics/CacheStatsCard.js +88 -0
- package/dist/catalog/components/metrics/CourseProgressCard.d.ts +1 -0
- package/dist/catalog/components/metrics/CourseProgressCard.js +86 -0
- package/dist/catalog/components/metrics/CreditScoreCards.d.ts +1 -0
- package/dist/catalog/components/metrics/CreditScoreCards.js +139 -0
- package/dist/catalog/components/metrics/FeedbackCard.d.ts +1 -0
- package/dist/catalog/components/metrics/FeedbackCard.js +59 -0
- package/dist/catalog/components/metrics/FinanceDashboard.d.ts +1 -0
- package/dist/catalog/components/metrics/FinanceDashboard.js +164 -0
- package/dist/catalog/components/metrics/Growth.d.ts +1 -0
- package/dist/catalog/components/metrics/Growth.js +40 -0
- package/dist/catalog/components/metrics/GrowthCalendar.d.ts +1 -0
- package/dist/catalog/components/metrics/GrowthCalendar.js +87 -0
- package/dist/catalog/components/metrics/HealthCards.d.ts +1 -0
- package/dist/catalog/components/metrics/HealthCards.js +244 -0
- package/dist/catalog/components/metrics/MarketingCards.d.ts +1 -0
- package/dist/catalog/components/metrics/MarketingCards.js +70 -0
- package/dist/catalog/components/metrics/NetworkTelemetry.d.ts +1 -0
- package/dist/catalog/components/metrics/NetworkTelemetry.js +198 -0
- package/dist/catalog/components/metrics/NoiseCards.d.ts +1 -0
- package/dist/catalog/components/metrics/NoiseCards.js +61 -0
- package/dist/catalog/components/metrics/OverviewChart.d.ts +1 -0
- package/dist/catalog/components/metrics/OverviewChart.js +69 -0
- package/dist/catalog/components/metrics/ProgressIndicator.d.ts +1 -0
- package/dist/catalog/components/metrics/ProgressIndicator.js +35 -0
- package/dist/catalog/components/metrics/PromptsCard.d.ts +1 -0
- package/dist/catalog/components/metrics/PromptsCard.js +29 -0
- package/dist/catalog/components/metrics/RealTimeAlerts.d.ts +1 -0
- package/dist/catalog/components/metrics/RealTimeAlerts.js +64 -0
- package/dist/catalog/components/metrics/RunningStatsCard.d.ts +1 -0
- package/dist/catalog/components/metrics/RunningStatsCard.js +41 -0
- package/dist/catalog/components/metrics/SalesDashboard.d.ts +1 -0
- package/dist/catalog/components/metrics/SalesDashboard.js +156 -0
- package/dist/catalog/components/metrics/SalesOverview.d.ts +1 -0
- package/dist/catalog/components/metrics/SalesOverview.js +71 -0
- package/dist/catalog/components/metrics/SavingsCards.d.ts +1 -0
- package/dist/catalog/components/metrics/SavingsCards.js +118 -0
- package/dist/catalog/components/metrics/ServerPerformance.d.ts +1 -0
- package/dist/catalog/components/metrics/ServerPerformance.js +26 -0
- package/dist/catalog/components/metrics/StatusCards.d.ts +1 -0
- package/dist/catalog/components/metrics/StatusCards.js +165 -0
- package/dist/catalog/components/metrics/StockChartCard.d.ts +1 -0
- package/dist/catalog/components/metrics/StockChartCard.js +122 -0
- package/dist/catalog/components/metrics/SystemMetricsCard.d.ts +1 -0
- package/dist/catalog/components/metrics/SystemMetricsCard.js +129 -0
- package/dist/catalog/components/metrics/TimerCard.d.ts +1 -0
- package/dist/catalog/components/metrics/TimerCard.js +35 -0
- package/dist/catalog/components/metrics/UserMetrics.d.ts +1 -0
- package/dist/catalog/components/metrics/UserMetrics.js +128 -0
- package/dist/catalog/components/metrics/UsersChartCard.d.ts +1 -0
- package/dist/catalog/components/metrics/UsersChartCard.js +111 -0
- package/dist/catalog/components/metrics/VisitorsChartCard.d.ts +1 -0
- package/dist/catalog/components/metrics/VisitorsChartCard.js +178 -0
- package/dist/catalog/components/mono-charts/GitHubActivity.d.ts +30 -0
- package/dist/catalog/components/mono-charts/GitHubActivity.js +547 -0
- package/dist/catalog/components/mono-charts/MonoActivityHeatmap.d.ts +14 -0
- package/dist/catalog/components/mono-charts/MonoActivityHeatmap.js +170 -0
- package/dist/catalog/components/mono-charts/MonoRoundedAreaChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedAreaChart.js +107 -0
- package/dist/catalog/components/mono-charts/MonoRoundedBarChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedBarChart.js +121 -0
- package/dist/catalog/components/mono-charts/MonoRoundedBubbleChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedBubbleChart.js +67 -0
- package/dist/catalog/components/mono-charts/MonoRoundedBulletChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedBulletChart.js +64 -0
- package/dist/catalog/components/mono-charts/MonoRoundedCandlestickChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedCandlestickChart.js +76 -0
- package/dist/catalog/components/mono-charts/MonoRoundedComposedChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedComposedChart.js +117 -0
- package/dist/catalog/components/mono-charts/MonoRoundedDonutChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedDonutChart.js +97 -0
- package/dist/catalog/components/mono-charts/MonoRoundedFunnelChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedFunnelChart.js +65 -0
- package/dist/catalog/components/mono-charts/MonoRoundedGaugeArc.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedGaugeArc.js +64 -0
- package/dist/catalog/components/mono-charts/MonoRoundedHeatmapChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedHeatmapChart.js +56 -0
- package/dist/catalog/components/mono-charts/MonoRoundedKpiCardChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedKpiCardChart.js +61 -0
- package/dist/catalog/components/mono-charts/MonoRoundedLineChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedLineChart.js +142 -0
- package/dist/catalog/components/mono-charts/MonoRoundedMeterChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedMeterChart.js +68 -0
- package/dist/catalog/components/mono-charts/MonoRoundedPolarChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedPolarChart.js +67 -0
- package/dist/catalog/components/mono-charts/MonoRoundedPyramidChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedPyramidChart.js +52 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRadarChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRadarChart.js +70 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRadialBarGroup.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRadialBarGroup.js +69 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRadialGaugeChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRadialGaugeChart.js +68 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRangeChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedRangeChart.js +77 -0
- package/dist/catalog/components/mono-charts/MonoRoundedSankeyChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedSankeyChart.js +66 -0
- package/dist/catalog/components/mono-charts/MonoRoundedScatterChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedScatterChart.js +82 -0
- package/dist/catalog/components/mono-charts/MonoRoundedSparklineChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedSparklineChart.js +73 -0
- package/dist/catalog/components/mono-charts/MonoRoundedStackedBarChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedStackedBarChart.js +63 -0
- package/dist/catalog/components/mono-charts/MonoRoundedStepChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedStepChart.js +72 -0
- package/dist/catalog/components/mono-charts/MonoRoundedStreamChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedStreamChart.js +98 -0
- package/dist/catalog/components/mono-charts/MonoRoundedTreemapChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedTreemapChart.js +56 -0
- package/dist/catalog/components/mono-charts/MonoRoundedWaterfallChart.d.ts +7 -0
- package/dist/catalog/components/mono-charts/MonoRoundedWaterfallChart.js +67 -0
- package/dist/catalog/components/simple-comp/ActivityHeatmap.d.ts +5 -0
- package/dist/catalog/components/simple-comp/ActivityHeatmap.js +7 -0
- package/dist/catalog/components/simple-comp/ChartCard.d.ts +2 -0
- package/dist/catalog/components/simple-comp/ChartCard.js +8 -0
- package/dist/catalog/components/simple-comp/DeviceUsageChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/DeviceUsageChart.js +7 -0
- package/dist/catalog/components/simple-comp/DitherBarChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/DitherBarChart.js +148 -0
- package/dist/catalog/components/simple-comp/DitherBook.d.ts +23 -0
- package/dist/catalog/components/simple-comp/DitherBook.js +352 -0
- package/dist/catalog/components/simple-comp/DitherChartsGrid.d.ts +9 -0
- package/dist/catalog/components/simple-comp/DitherChartsGrid.js +107 -0
- package/dist/catalog/components/simple-comp/DitherDonutChart.d.ts +7 -0
- package/dist/catalog/components/simple-comp/DitherDonutChart.js +7 -0
- package/dist/catalog/components/simple-comp/DitherFunnelChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/DitherFunnelChart.js +7 -0
- package/dist/catalog/components/simple-comp/DitherGrowthChart.d.ts +7 -0
- package/dist/catalog/components/simple-comp/DitherGrowthChart.js +7 -0
- package/dist/catalog/components/simple-comp/DitherHeatmapGrid.d.ts +5 -0
- package/dist/catalog/components/simple-comp/DitherHeatmapGrid.js +115 -0
- package/dist/catalog/components/simple-comp/DitherRadialChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/DitherRadialChart.js +114 -0
- package/dist/catalog/components/simple-comp/DitherScatterChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/DitherScatterChart.js +151 -0
- package/dist/catalog/components/simple-comp/DitherSparklineMatrix.d.ts +5 -0
- package/dist/catalog/components/simple-comp/DitherSparklineMatrix.js +89 -0
- package/dist/catalog/components/simple-comp/DitherStackedChart.d.ts +7 -0
- package/dist/catalog/components/simple-comp/DitherStackedChart.js +7 -0
- package/dist/catalog/components/simple-comp/MembersGrowthChart.d.ts +2 -0
- package/dist/catalog/components/simple-comp/MembersGrowthChart.js +8 -0
- package/dist/catalog/components/simple-comp/PaymentsChart.d.ts +2 -0
- package/dist/catalog/components/simple-comp/PaymentsChart.js +8 -0
- package/dist/catalog/components/simple-comp/RevenueLineChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/RevenueLineChart.js +7 -0
- package/dist/catalog/components/simple-comp/ServerGauge.d.ts +5 -0
- package/dist/catalog/components/simple-comp/ServerGauge.js +7 -0
- package/dist/catalog/components/simple-comp/SimpleCompExtracted.d.ts +6 -0
- package/dist/catalog/components/simple-comp/SimpleCompExtracted.js +423 -0
- package/dist/catalog/components/simple-comp/StorageUsageChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/StorageUsageChart.js +7 -0
- package/dist/catalog/components/simple-comp/TrafficBubble.d.ts +5 -0
- package/dist/catalog/components/simple-comp/TrafficBubble.js +7 -0
- package/dist/catalog/components/simple-comp/UptimeChart.d.ts +5 -0
- package/dist/catalog/components/simple-comp/UptimeChart.js +7 -0
- package/dist/catalog/components/simple-comp/lib/utils.d.ts +3 -0
- package/dist/catalog/components/simple-comp/lib/utils.js +7 -0
- package/dist/catalog/components/text/AnimatedText.d.ts +8 -0
- package/dist/catalog/components/text/AnimatedText.js +554 -0
- package/dist/catalog/components/toggles/AnimatedToggle.d.ts +8 -0
- package/dist/catalog/components/toggles/AnimatedToggle.js +243 -0
- package/dist/catalog/data/buttons.d.ts +14 -0
- package/dist/catalog/data/buttons.js +94 -0
- package/dist/catalog/data/ditherCharts.d.ts +12 -0
- package/dist/catalog/data/ditherCharts.js +11 -0
- package/dist/catalog/data/formElements.d.ts +9 -0
- package/dist/catalog/data/formElements.js +117 -0
- package/dist/catalog/data/installCommand.d.ts +3 -0
- package/dist/catalog/data/installCommand.js +10 -0
- package/dist/catalog/data/metrics.d.ts +10 -0
- package/dist/catalog/data/metrics.js +136 -0
- package/dist/catalog/data/supportedRegistryIds.d.ts +1 -0
- package/dist/catalog/data/supportedRegistryIds.js +7 -0
- package/dist/catalog/data/textAnimations.d.ts +10 -0
- package/dist/catalog/data/textAnimations.js +65 -0
- package/dist/catalog/data/toggles.d.ts +9 -0
- package/dist/catalog/data/toggles.js +96 -0
- package/dist/catalog/data/transitions.d.ts +11 -0
- package/dist/catalog/data/transitions.js +198 -0
- package/dist/catalog/hooks/useCanvasSetup.d.ts +25 -0
- package/dist/catalog/hooks/useCanvasSetup.js +7 -0
- package/dist/catalog/hooks/useIsMobile.d.ts +5 -0
- package/dist/catalog/hooks/useIsMobile.js +7 -0
- package/dist/catalog/hooks/useLoopFlg.d.ts +1 -0
- package/dist/catalog/hooks/useLoopFlg.js +7 -0
- package/dist/catalog/hooks/useWebHaptics.d.ts +4 -0
- package/dist/catalog/hooks/useWebHaptics.js +7 -0
- package/dist/catalog/utils/cn.d.ts +1 -0
- package/dist/catalog/utils/cn.js +7 -0
- package/dist/catalog-chunks/chunk-3QX6CASP.js +93 -0
- package/dist/catalog-chunks/chunk-3TYA5TCG.js +34 -0
- package/dist/catalog-chunks/chunk-4M7HZE4Z.js +107 -0
- package/dist/catalog-chunks/chunk-62CFYJEA.js +141 -0
- package/dist/catalog-chunks/chunk-7GU353UA.js +261 -0
- package/dist/catalog-chunks/chunk-7XF4PVBJ.js +85 -0
- package/dist/catalog-chunks/chunk-AJLUWL56.js +99 -0
- package/dist/catalog-chunks/chunk-B64DVBSK.js +105 -0
- package/dist/catalog-chunks/chunk-BSR45QIS.js +349 -0
- package/dist/catalog-chunks/chunk-E2GLS5WF.js +325 -0
- package/dist/catalog-chunks/chunk-EC4R54F3.js +35 -0
- package/dist/catalog-chunks/chunk-F47LIDB6.js +19 -0
- package/dist/catalog-chunks/chunk-FXF53HPE.js +476 -0
- package/dist/catalog-chunks/chunk-GDCQNKP7.js +45 -0
- package/dist/catalog-chunks/chunk-I4BVACNZ.js +349 -0
- package/dist/catalog-chunks/chunk-I5MYV6VF.js +248 -0
- package/dist/catalog-chunks/chunk-JSEX6PVP.js +84 -0
- package/dist/catalog-chunks/chunk-KN7YEZTH.js +294 -0
- package/dist/catalog-chunks/chunk-KRXJK3FG.js +19 -0
- package/dist/catalog-chunks/chunk-KTP5OAIN.js +17 -0
- package/dist/catalog-chunks/chunk-KYYXENHF.js +105 -0
- package/dist/catalog-chunks/chunk-M2O2SNWE.js +132 -0
- package/dist/catalog-chunks/chunk-NITEDAMY.js +325 -0
- package/dist/catalog-chunks/chunk-OTFAZXKB.js +8 -0
- package/dist/catalog-chunks/chunk-OVOLUMYF.js +452 -0
- package/dist/catalog-chunks/chunk-OWYADZUR.js +156 -0
- package/dist/catalog-chunks/chunk-PD47DK6W.js +100 -0
- package/dist/catalog-chunks/chunk-QITBAUBH.js +101 -0
- package/dist/catalog-chunks/chunk-QXWREUPI.js +56 -0
- package/dist/catalog-chunks/chunk-RKATUMDL.js +244 -0
- package/dist/catalog-chunks/chunk-S7UYABQK.js +99 -0
- package/dist/catalog-chunks/chunk-SIL53FHG.js +169 -0
- package/dist/catalog-chunks/chunk-SWPGAN45.js +28 -0
- package/dist/catalog-chunks/chunk-VCDBPAAS.js +85 -0
- package/dist/catalog-chunks/chunk-VMGI6CMO.js +15 -0
- package/dist/catalog-chunks/chunk-W7ZMAKCC.js +53 -0
- package/dist/catalog-chunks/chunk-XDDKOCZ7.js +89 -0
- package/dist/catalog-chunks/chunk-YGLQ5K3F.js +452 -0
- package/dist/catalog-chunks/chunk-YHU4BQDL.js +292 -0
- package/dist/catalog-chunks/chunk-ZCRXHCGS.js +108 -0
- package/dist/catalog-chunks/chunk-ZUQZVT7M.js +87 -0
- package/dist/catalog-chunks/chunk-ZXDZMNRZ.js +267 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +6237 -879
- package/dist/styles.css +1 -1
- package/dist/ui/ai/ai-approval-card.d.ts +28 -0
- package/dist/ui/ai/ai-code-block.d.ts +33 -0
- package/dist/ui/ai/ai-context-meter.d.ts +14 -0
- package/dist/ui/ai/ai-fine-tune-card.d.ts +43 -0
- package/dist/ui/ai/ai-prompt-bar.d.ts +25 -0
- package/dist/ui/ai/ai-sidebar-nav.d.ts +26 -0
- package/dist/ui/ai/ai-sources.d.ts +16 -0
- package/dist/ui/ai/ai-suggestions.d.ts +12 -0
- package/dist/ui/ai/ai-task-list.d.ts +15 -0
- package/dist/ui/ai/ai-tool-call.d.ts +13 -0
- package/dist/ui/ai/rudder-issue-board.d.ts +21 -0
- package/dist/ui/animated-sidebar.d.ts +9 -0
- package/dist/ui/hover/inset-button.d.ts +16 -0
- package/dist/ui/loading/analyzing-image.d.ts +2 -0
- package/dist/ui/loading/border-trail-loading.d.ts +9 -0
- package/dist/ui/loading/wandering-eyes.d.ts +10 -0
- package/dist/ui/patterns/animated-tags.d.ts +14 -0
- package/dist/ui/patterns/expanding-icon-tabs.d.ts +8 -0
- package/dist/ui/patterns/gooey-popover.d.ts +24 -0
- package/package.json +79 -14
- package/registry/1.json +19 -0
- package/registry/10.json +19 -0
- package/registry/11.json +19 -0
- package/registry/12.json +19 -0
- package/registry/13.json +19 -0
- package/registry/14.json +19 -0
- package/registry/15.json +19 -0
- package/registry/16.json +19 -0
- package/registry/17.json +19 -0
- package/registry/18.json +19 -0
- package/registry/19.json +19 -0
- package/registry/2.json +19 -0
- package/registry/20.json +19 -0
- package/registry/21.json +19 -0
- package/registry/22.json +19 -0
- package/registry/23.json +19 -0
- package/registry/24.json +19 -0
- package/registry/25.json +19 -0
- package/registry/26.json +19 -0
- package/registry/27.json +19 -0
- package/registry/28.json +19 -0
- package/registry/29.json +19 -0
- package/registry/3.json +19 -0
- package/registry/30.json +19 -0
- package/registry/31.json +19 -0
- package/registry/32.json +19 -0
- package/registry/33.json +19 -0
- package/registry/34.json +19 -0
- package/registry/35.json +19 -0
- package/registry/36.json +16 -0
- package/registry/37.json +16 -0
- package/registry/4.json +19 -0
- package/registry/5.json +19 -0
- package/registry/6.json +19 -0
- package/registry/7.json +19 -0
- package/registry/8.json +19 -0
- package/registry/9.json +19 -0
- package/registry/LICENSE.third-party +542 -0
- package/registry/ab-segmented-tabs.json +18 -0
- package/registry/accordion-blind.json +15 -0
- package/registry/accordion-loader.json +9 -0
- package/registry/ai-approval-card.json +17 -0
- package/registry/ai-code-block.json +19 -0
- package/registry/ai-context-meter.json +19 -0
- package/registry/ai-fine-tune-card.json +19 -0
- package/registry/ai-prompt-bar.json +17 -0
- package/registry/ai-sidebar-nav.json +17 -0
- package/registry/ai-sources.json +20 -0
- package/registry/ai-suggestions.json +19 -0
- package/registry/ai-task-list.json +20 -0
- package/registry/ai-tool-call.json +20 -0
- package/registry/analyzing-image.json +17 -0
- package/registry/animated-sidebar.json +19 -0
- package/registry/animated-tags.json +19 -0
- package/registry/app-icon-load.json +9 -0
- package/registry/apple-breathe.json +9 -0
- package/registry/apple-equalizer.json +9 -0
- package/registry/apple-icon-morph.json +9 -0
- package/registry/apple-pulse-dots.json +9 -0
- package/registry/apple-scale-pulse.json +9 -0
- package/registry/apple-sound-wave.json +9 -0
- package/registry/apple-text-reveal.json +9 -0
- package/registry/apple-unlock.json +9 -0
- package/registry/arc-tracer.json +9 -0
- package/registry/bar-cascade.json +9 -0
- package/registry/bar-sweep.json +9 -0
- package/registry/blur-text.json +9 -0
- package/registry/blur-up-char.json +19 -0
- package/registry/blur-up-word.json +19 -0
- package/registry/bobbing-dots.json +9 -0
- package/registry/bookmark-corner-peel.json +15 -0
- package/registry/bookmark-save-pill.json +18 -0
- package/registry/bookmark-toggle.json +19 -0
- package/registry/border-trail-loading.json +17 -0
- package/registry/bounce-dots.json +9 -0
- package/registry/bounce-in-char.json +19 -0
- package/registry/bouncing-bars.json +9 -0
- package/registry/bouncing-dots.json +9 -0
- package/registry/bouncing-lines.json +9 -0
- package/registry/bouncing-square.json +9 -0
- package/registry/breathe-ring.json +9 -0
- package/registry/breathing-glow.json +9 -0
- package/registry/breathing-square.json +9 -0
- package/registry/btn-1.json +19 -0
- package/registry/btn-10.json +19 -0
- package/registry/btn-11.json +19 -0
- package/registry/btn-12.json +19 -0
- package/registry/btn-13.json +19 -0
- package/registry/btn-14.json +19 -0
- package/registry/btn-15.json +19 -0
- package/registry/btn-16.json +19 -0
- package/registry/btn-17.json +19 -0
- package/registry/btn-18.json +19 -0
- package/registry/btn-19.json +19 -0
- package/registry/btn-2.json +19 -0
- package/registry/btn-20.json +19 -0
- package/registry/btn-21.json +19 -0
- package/registry/btn-22.json +19 -0
- package/registry/btn-23.json +19 -0
- package/registry/btn-24.json +19 -0
- package/registry/btn-25.json +19 -0
- package/registry/btn-26.json +19 -0
- package/registry/btn-27.json +19 -0
- package/registry/btn-28.json +19 -0
- package/registry/btn-29.json +19 -0
- package/registry/btn-3.json +19 -0
- package/registry/btn-30.json +19 -0
- package/registry/btn-31.json +19 -0
- package/registry/btn-32.json +19 -0
- package/registry/btn-33.json +19 -0
- package/registry/btn-34.json +19 -0
- package/registry/btn-35.json +19 -0
- package/registry/btn-36.json +16 -0
- package/registry/btn-37.json +16 -0
- package/registry/btn-4.json +19 -0
- package/registry/btn-5.json +19 -0
- package/registry/btn-6.json +19 -0
- package/registry/btn-7.json +19 -0
- package/registry/btn-8.json +19 -0
- package/registry/btn-9.json +19 -0
- package/registry/button-slide-arrow.json +19 -0
- package/registry/cacheable-bandwidth-cost.json +19 -0
- package/registry/card-arc-5.json +18 -0
- package/registry/card-arc-7.json +18 -0
- package/registry/card-carousel-mono.json +19 -0
- package/registry/card-carousel.json +19 -0
- package/registry/card-cascade-stagger.json +18 -0
- package/registry/card-corner-fan.json +18 -0
- package/registry/card-cover-flow-mono.json +19 -0
- package/registry/card-cover-flow.json +19 -0
- package/registry/card-deck-cascade.json +15 -0
- package/registry/card-glance-preview.json +18 -0
- package/registry/card-hover.json +9 -0
- package/registry/card-linear-spread.json +18 -0
- package/registry/card-long-arc-5.json +18 -0
- package/registry/card-scatter-spread.json +18 -0
- package/registry/card-stack-peel.json +15 -0
- package/registry/card-stamp-arc.json +18 -0
- package/registry/card-time-machine-mono.json +18 -0
- package/registry/card-time-machine.json +18 -0
- package/registry/card-wheel-fan.json +18 -0
- package/registry/category-select.json +18 -0
- package/registry/character-stagger.json +9 -0
- package/registry/checkbox-draw.json +19 -0
- package/registry/checkmark-draw.json +19 -0
- package/registry/circle-toggle.json +19 -0
- package/registry/circuit-trace-draw.json +15 -0
- package/registry/circular-bars.json +9 -0
- package/registry/circular-create-button.json +16 -0
- package/registry/classic-spinner.json +9 -0
- package/registry/classic-toggle.json +9 -0
- package/registry/clock-spinner.json +9 -0
- package/registry/comet-spinner.json +9 -0
- package/registry/compass-needle-deflect.json +15 -0
- package/registry/concentric-pulse.json +9 -0
- package/registry/concentric-ring.json +9 -0
- package/registry/concentric-squares.json +9 -0
- package/registry/context-menu.json +18 -0
- package/registry/conveyor-loop.json +9 -0
- package/registry/credit-score-barcode-meter.json +19 -0
- package/registry/cross-spinner.json +9 -0
- package/registry/cube-flip-spring.json +9 -0
- package/registry/cursor-trail.json +9 -0
- package/registry/dash-ring.json +9 -0
- package/registry/dashed-spiral.json +9 -0
- package/registry/date-position-selector.json +18 -0
- package/registry/dia-text-reveal.json +19 -0
- package/registry/diamond-grid.json +9 -0
- package/registry/diamond-rotate-spring.json +9 -0
- package/registry/dislike-toggle.json +19 -0
- package/registry/dither-book.json +19 -0
- package/registry/domino-cascade.json +15 -0
- package/registry/dot-spinner.json +9 -0
- package/registry/dots-ring.json +9 -0
- package/registry/double-bounce-toggle.json +19 -0
- package/registry/double-ring.json +9 -0
- package/registry/download-icons.json +18 -0
- package/registry/drop-dot.json +9 -0
- package/registry/drop-in-char.json +19 -0
- package/registry/droplet-squish.json +15 -0
- package/registry/dual-arc.json +9 -0
- package/registry/dual-magnet-dipole.json +15 -0
- package/registry/dynamic-island.json +9 -0
- package/registry/elastic-bars.json +9 -0
- package/registry/elastic-square.json +9 -0
- package/registry/elastic-tag-snap.json +15 -0
- package/registry/error-shake.json +19 -0
- package/registry/expanding-cross.json +9 -0
- package/registry/expanding-icon-tabs.json +17 -0
- package/registry/face-id-scan.json +9 -0
- package/registry/fade-arc.json +9 -0
- package/registry/fade-dots.json +9 -0
- package/registry/fade-down.json +9 -0
- package/registry/fade-in-char.json +19 -0
- package/registry/fade-in-text.json +19 -0
- package/registry/fade-in-word.json +19 -0
- package/registry/fade-in.json +9 -0
- package/registry/fade-up.json +9 -0
- package/registry/file-upload-dropzone.json +19 -0
- package/registry/filter-tag-pill.json +18 -0
- package/registry/flip-square.json +9 -0
- package/registry/flip-x-char.json +19 -0
- package/registry/flip-y-char.json +19 -0
- package/registry/float-char.json +19 -0
- package/registry/float-word.json +19 -0
- package/registry/floating-diamonds.json +9 -0
- package/registry/floating-label-input.json +19 -0
- package/registry/fluid-bars.json +9 -0
- package/registry/fluid-diamond.json +9 -0
- package/registry/fluid-dot-orbit.json +9 -0
- package/registry/fluid-skeleton.json +9 -0
- package/registry/focus-blur.json +18 -0
- package/registry/form-submit-button.json +19 -0
- package/registry/french-doors-3d.json +18 -0
- package/registry/gear-tooth-step.json +15 -0
- package/registry/gears.json +9 -0
- package/registry/gelatin-wobble.json +15 -0
- package/registry/geometric-iris-shutter.json +15 -0
- package/registry/glassmorphic-card.json +9 -0
- package/registry/glow-button.json +9 -0
- package/registry/glow-text.json +19 -0
- package/registry/gooey-popover.json +18 -0
- package/registry/gradient-arc.json +9 -0
- package/registry/gradient-shimmer.json +18 -0
- package/registry/grid-dots.json +9 -0
- package/registry/haptic-ring.json +9 -0
- package/registry/heartbeat.json +9 -0
- package/registry/hex-lattice-draw.json +15 -0
- package/registry/hexagon-spinner.json +9 -0
- package/registry/hourglass.json +9 -0
- package/registry/hover-lift-char.json +19 -0
- package/registry/hover-lift-word.json +19 -0
- package/registry/hover-link-card.json +18 -0
- package/registry/hover-scale-char.json +19 -0
- package/registry/hover-scale-word.json +19 -0
- package/registry/inertia-skid-stop.json +15 -0
- package/registry/infinity-path.json +9 -0
- package/registry/input-focus-glow.json +19 -0
- package/registry/inset-action-button.json +16 -0
- package/registry/inset-button.json +17 -0
- package/registry/intersecting-rings.json +9 -0
- package/registry/ios-spinner.json +9 -0
- package/registry/kinetic-metronome-tick.json +15 -0
- package/registry/kinetic-tension-capsule.json +15 -0
- package/registry/light-dark-morph.json +18 -0
- package/registry/like-toggle.json +19 -0
- package/registry/line-spinner.json +9 -0
- package/registry/liquid-dots.json +9 -0
- package/registry/mac-terminal.json +9 -0
- package/registry/magnetic-button.json +9 -0
- package/registry/magnetic-disks.json +15 -0
- package/registry/magnetic-dots.json +9 -0
- package/registry/magnetic-slide-btn.json +18 -0
- package/registry/manifest.json +11258 -0
- package/registry/matrix-grid-loader.json +15 -0
- package/registry/minimal-triangle.json +9 -0
- package/registry/modular-tile-snap.json +15 -0
- package/registry/mono-activity-blue.json +18 -0
- package/registry/mono-activity-green.json +18 -0
- package/registry/mono-activity-purple.json +18 -0
- package/registry/mono-heatmap.json +19 -0
- package/registry/mono-rounded-area.json +18 -0
- package/registry/mono-rounded-bar.json +18 -0
- package/registry/mono-rounded-bubble.json +18 -0
- package/registry/mono-rounded-bullet.json +16 -0
- package/registry/mono-rounded-candlestick.json +16 -0
- package/registry/mono-rounded-composed.json +18 -0
- package/registry/mono-rounded-donut.json +18 -0
- package/registry/mono-rounded-funnel.json +18 -0
- package/registry/mono-rounded-gauge-arc.json +18 -0
- package/registry/mono-rounded-heatmap.json +16 -0
- package/registry/mono-rounded-kpi.json +18 -0
- package/registry/mono-rounded-line.json +18 -0
- package/registry/mono-rounded-meter.json +18 -0
- package/registry/mono-rounded-polar.json +18 -0
- package/registry/mono-rounded-pyramid.json +16 -0
- package/registry/mono-rounded-radar.json +18 -0
- package/registry/mono-rounded-radial-gauge.json +18 -0
- package/registry/mono-rounded-radial-group.json +18 -0
- package/registry/mono-rounded-range.json +18 -0
- package/registry/mono-rounded-sankey.json +16 -0
- package/registry/mono-rounded-scatter.json +18 -0
- package/registry/mono-rounded-sparkline.json +18 -0
- package/registry/mono-rounded-stacked-bar.json +18 -0
- package/registry/mono-rounded-step.json +18 -0
- package/registry/mono-rounded-stream.json +18 -0
- package/registry/mono-rounded-treemap.json +16 -0
- package/registry/mono-rounded-waterfall.json +18 -0
- package/registry/mono-stock.json +19 -0
- package/registry/morph-action-pill.json +18 -0
- package/registry/morph-dot-ring.json +9 -0
- package/registry/morph-loader.json +9 -0
- package/registry/morph-toggle.json +19 -0
- package/registry/morphing-bars.json +9 -0
- package/registry/morphing-infinity.json +9 -0
- package/registry/morphing-ring.json +9 -0
- package/registry/morphing-shape.json +9 -0
- package/registry/mouse-follow.json +9 -0
- package/registry/multi-select-chips.json +19 -0
- package/registry/multi-tab-bar.json +18 -0
- package/registry/neon-sign-draw.json +15 -0
- package/registry/nested-orbital-gimbal.json +15 -0
- package/registry/network-telemetry-matrix.json +19 -0
- package/registry/newtons-cradle.json +9 -0
- package/registry/noise-decibel-level.json +19 -0
- package/registry/obsidian-liquid-wave.json +16 -0
- package/registry/offset-rings.json +9 -0
- package/registry/orbiting-circles.json +9 -0
- package/registry/orbiting-dot.json +9 -0
- package/registry/origami-envelope-unfold.json +15 -0
- package/registry/origami-shape.json +9 -0
- package/registry/otp-input.json +19 -0
- package/registry/overview-bar-scrubber-card.json +19 -0
- package/registry/page-transition.json +11 -0
- package/registry/page-turn-curl.json +15 -0
- package/registry/password-toggle.json +19 -0
- package/registry/pendulum-bubble-level.json +15 -0
- package/registry/pendulum.json +9 -0
- package/registry/perspective-flip-stage.json +18 -0
- package/registry/perspective-flip.json +18 -0
- package/registry/perspective-layout-switcher.json +18 -0
- package/registry/physics-dock.json +17 -0
- package/registry/pill-tabs.json +19 -0
- package/registry/plus-minus-toggle.json +18 -0
- package/registry/presets.json +9 -0
- package/registry/prism-block-stack.json +15 -0
- package/registry/progress-indicator-piano.json +19 -0
- package/registry/progress-indicator.json +9 -0
- package/registry/progress-stepper.json +18 -0
- package/registry/pulsating-dots.json +9 -0
- package/registry/pulse-char.json +19 -0
- package/registry/pulse-dot.json +9 -0
- package/registry/pulse-dots.json +9 -0
- package/registry/pulse-square.json +9 -0
- package/registry/pulse-word.json +19 -0
- package/registry/pulse.json +9 -0
- package/registry/pumping-heart.json +9 -0
- package/registry/pyramid-block-build.json +15 -0
- package/registry/radar-sweep.json +9 -0
- package/registry/radial-iris-mask.json +18 -0
- package/registry/radial-spinner.json +15 -0
- package/registry/radio-scale.json +19 -0
- package/registry/range-slider.json +19 -0
- package/registry/receipt-tape-print.json +15 -0
- package/registry/rectangle-toggle.json +19 -0
- package/registry/repost-toggle.json +19 -0
- package/registry/reveal-text.json +19 -0
- package/registry/ribbon-banner-slide.json +15 -0
- package/registry/ring-sweep.json +9 -0
- package/registry/ripple-effect.json +9 -0
- package/registry/rise-up-word.json +19 -0
- package/registry/roller-blind-drop.json +15 -0
- package/registry/rolling-tumble.json +15 -0
- package/registry/rotate-in-char.json +19 -0
- package/registry/rotating-cross.json +9 -0
- package/registry/rotating-louvers.json +15 -0
- package/registry/rotating-triangle.json +9 -0
- package/registry/rudder-issue-board.json +24 -0
- package/registry/sales-analytics-dual-bars.json +19 -0
- package/registry/sales-overview-radial-dashboard.json +19 -0
- package/registry/sales-target-segmented-arc.json +19 -0
- package/registry/scale-in-char.json +19 -0
- package/registry/scale-in-text.json +19 -0
- package/registry/scale-in-word.json +19 -0
- package/registry/scale-in.json +9 -0
- package/registry/scramble-text.json +18 -0
- package/registry/scroll-canvas-unroll.json +15 -0
- package/registry/scroll-reveal.json +9 -0
- package/registry/search-expand.json +19 -0
- package/registry/segmented-arc-meter.json +18 -0
- package/registry/segmented-link-stretch.json +15 -0
- package/registry/segmented-step-bar.json +18 -0
- package/registry/segmented-stepper-dots.json +18 -0
- package/registry/select-dropdown.json +19 -0
- package/registry/server-performance-step-bars.json +19 -0
- package/registry/shape-shift-grid.json +9 -0
- package/registry/shimmer-line.json +9 -0
- package/registry/shimmer-text.json +19 -0
- package/registry/shutter-slide.json +15 -0
- package/registry/shutter-step-slices.json +15 -0
- package/registry/siri-wave.json +9 -0
- package/registry/skeleton-loader.json +9 -0
- package/registry/skeleton.json +9 -0
- package/registry/skew-x-char.json +19 -0
- package/registry/slide-down-char.json +19 -0
- package/registry/slide-down-word.json +19 -0
- package/registry/slide-left-char.json +19 -0
- package/registry/slide-left.json +9 -0
- package/registry/slide-right-char.json +19 -0
- package/registry/slide-right.json +9 -0
- package/registry/slide-up-char.json +19 -0
- package/registry/slide-up-text.json +19 -0
- package/registry/slide-up-word.json +19 -0
- package/registry/sliding-bars.json +9 -0
- package/registry/slinky-coil.json +15 -0
- package/registry/smart-card-dispenser.json +15 -0
- package/registry/smooth-dot-shift.json +9 -0
- package/registry/smooth-ring.json +9 -0
- package/registry/smooth-rounded-square.json +9 -0
- package/registry/solid-switch.json +19 -0
- package/registry/spatial-door-portal.json +18 -0
- package/registry/spinning-squares.json +9 -0
- package/registry/spiral-spinner.json +9 -0
- package/registry/split-gate-reveal.json +15 -0
- package/registry/spotlight.json +9 -0
- package/registry/spring-bars.json +9 -0
- package/registry/spring-dot-matrix.json +9 -0
- package/registry/spring-hexagon.json +9 -0
- package/registry/spring-ring-expand.json +9 -0
- package/registry/spring-text-pop.json +9 -0
- package/registry/spring-text.json +19 -0
- package/registry/square-accordion.json +9 -0
- package/registry/square-grid.json +9 -0
- package/registry/square-snake.json +9 -0
- package/registry/square-spinner.json +9 -0
- package/registry/squash-sphere.json +15 -0
- package/registry/stacked-bar-pulse.json +9 -0
- package/registry/stagger-text.json +19 -0
- package/registry/staggered-glass-curtain.json +18 -0
- package/registry/sticky-note-peel.json +15 -0
- package/registry/sticky-reveal.json +9 -0
- package/registry/stretch-x-char.json +19 -0
- package/registry/stretch-y-char.json +19 -0
- package/registry/stroke-waveform.json +15 -0
- package/registry/submenu-flyout.json +18 -0
- package/registry/success-check.json +19 -0
- package/registry/sudden-brake.json +15 -0
- package/registry/swapping-dots.json +9 -0
- package/registry/swing-word.json +19 -0
- package/registry/swirling-spinner.json +9 -0
- package/registry/symmetric-wave.json +9 -0
- package/registry/terminal-loader.json +9 -0
- package/registry/text-blink.json +9 -0
- package/registry/text-dots.json +9 -0
- package/registry/text-morph.json +9 -0
- package/registry/text-reveal.json +9 -0
- package/registry/text-shimmer-wave.json +9 -0
- package/registry/text-shimmer.json +9 -0
- package/registry/textarea-auto-grow.json +19 -0
- package/registry/theme-toggle.json +19 -0
- package/registry/tilt-card.json +9 -0
- package/registry/timer-preparation-segmented.json +19 -0
- package/registry/tracking-in-text.json +19 -0
- package/registry/tracking-out-text.json +19 -0
- package/registry/trailing-dots.json +9 -0
- package/registry/triple-dot-spinner.json +9 -0
- package/registry/twin-orbit.json +9 -0
- package/registry/typewriter-text.json +18 -0
- package/registry/typing-indicator.json +9 -0
- package/registry/typing.json +9 -0
- package/registry/use-mouse-position.json +10 -0
- package/registry/use-reduced-motion.json +9 -0
- package/registry/use-scroll-progress.json +9 -0
- package/registry/use-stagger.json +9 -0
- package/registry/use-web-haptics.json +10 -0
- package/registry/users-growth-pill-progress.json +19 -0
- package/registry/utils.json +10 -0
- package/registry/vertical-wheel-counter.json +18 -0
- package/registry/views-hourly-wave-chart.json +19 -0
- package/registry/wandering-cube.json +9 -0
- package/registry/wandering-eyes.json +15 -0
- package/registry/watch-spinner.json +9 -0
- package/registry/wave-dots.json +9 -0
- package/registry/wave-physics-loader.json +9 -0
- package/registry/wave-reveal.json +18 -0
- package/registry/waveform-loader.json +9 -0
- package/registry/word-reveal.json +9 -0
- package/registry/zig-zag-pulse.json +9 -0
- package/registry/zoom-in-text.json +19 -0
- package/registry/zoom-in.json +9 -0
- package/registry/zoom-out-text.json +19 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-approval-card",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-approval-card.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-approval-card.tsx",
|
|
7
|
+
"registryFile": "ai-approval-card.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AIApprovalCard",
|
|
10
|
+
"ApprovalLabels",
|
|
11
|
+
"ApprovalQuestion"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": [],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"previewMode": "client",
|
|
16
|
+
"content": "\"use client\";\n\"use client\";\n\n/** Adapted from Beautiful UI (https://github.com/slev12397/beautiful-ui).\n * Copyright (c) 2026 Shane Levine. MIT License; see LICENSE.third-party.\n * Original card geometry, interactions, and motion are preserved. */\nimport * as React from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * APPROVAL CARD (human-in-the-loop)\n * One question at a time. The stack slides vertically as you\n * move between questions (the card's height animates to fit),\n * the step counter rolls like an odometer, and the footer uses\n * pill actions — a quiet Skip and a dark Continue with a ⏎.\n * Single-choice answers auto-advance; multi-select waits.\n * ───────────────────────────────────────────────────────── */\n\nexport type ApprovalQuestion = {\n q: string;\n type: \"radio\" | \"check\";\n options: string[];\n};\n\nconst QUESTIONS: ApprovalQuestion[] = [\n {\n q: \"How many flavors should we launch?\",\n type: \"radio\",\n options: [\"Three (core line)\", \"Five (full case)\", \"Just one hero\"],\n },\n {\n q: \"Which mix-ins should we stock?\",\n type: \"check\",\n options: [\"Chocolate chips\", \"Waffle bits\", \"Sprinkles\"],\n },\n {\n q: \"Which market do we enter first?\",\n type: \"radio\",\n options: [\"Food trucks\", \"Grocery freezers\", \"Scoop shops\"],\n },\n];\n\nexport type ApprovalLabels = {\n skip: string;\n continue: string;\n send: string;\n customPlaceholder: string;\n sentMessage: string;\n};\n\nconst DEFAULT_LABELS: ApprovalLabels = {\n skip: \"Skip\",\n continue: \"Continue\",\n send: \"Send\",\n customPlaceholder: \"Something else…\",\n sentMessage: \"Answers sent\",\n};\n\nconst ROLL_MS = 400;\nconst SLIDE = \"360ms cubic-bezier(0.22, 1, 0.36, 1)\";\n\n/* odometer digits — each character that changes rolls up (or down) */\nfunction RollingDigits({ value }: { value: string }) {\n const prevRef = useRef(value);\n const [oldVal, setOldVal] = useState(value);\n const [newVal, setNewVal] = useState(value);\n const [rolling, setRolling] = useState(false);\n const [shifted, setShifted] = useState(false);\n const [dir, setDir] = useState<\"up\" | \"down\">(\"up\");\n\n useEffect(() => {\n if (prevRef.current === value) return;\n const from = prevRef.current;\n prevRef.current = value;\n const fromN = parseInt(from, 10);\n const toN = parseInt(value, 10);\n setDir(Number.isFinite(fromN) && Number.isFinite(toN) && toN < fromN ? \"down\" : \"up\");\n setOldVal(from);\n setNewVal(value);\n setRolling(true);\n setShifted(false);\n\n let raf2 = 0;\n const raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(() => setShifted(true));\n });\n const done = setTimeout(() => {\n setRolling(false);\n setOldVal(value);\n setShifted(false);\n }, ROLL_MS);\n\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n clearTimeout(done);\n };\n }, [value]);\n\n const chars = rolling ? newVal : oldVal;\n\n return (\n <>\n {Array.from({ length: chars.length }, (_, i) => {\n const o = oldVal[i] ?? \"\";\n const n = chars[i] ?? \"\";\n if (!rolling || o === n) {\n return <span key={`${i}-${n}`}>{n}</span>;\n }\n const top = dir === \"down\" ? n : o;\n const bottom = dir === \"down\" ? o : n;\n const restY = dir === \"down\" ? \"0\" : \"-1em\";\n const startY = dir === \"down\" ? \"-1em\" : \"0\";\n return (\n <span\n key={`${i}-${o}-${n}-${dir}`}\n style={{ display: \"inline-block\", position: \"relative\", overflow: \"hidden\", height: \"1em\", lineHeight: \"1em\", verticalAlign: \"-0.05em\" }}\n >\n <span\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n transition: \"transform 350ms cubic-bezier(0.4, 0, 0.2, 1)\",\n transform: `translateY(${shifted ? restY : startY})`,\n }}\n >\n <span style={{ height: \"1em\", lineHeight: \"1em\" }}>{top}</span>\n <span style={{ height: \"1em\", lineHeight: \"1em\" }}>{bottom}</span>\n </span>\n </span>\n );\n })}\n </>\n );\n}\n\nfunction Ico({ path, size = 14, sw = 2 }: { path: React.ReactNode; size?: number; sw?: number }) {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={sw} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n {path}\n </svg>\n );\n}\n\nfunction ApprovalCard({\n questions = QUESTIONS,\n labels,\n onSubmitted,\n onAnswerChange,\n resettable = true,\n}: {\n questions?: ApprovalQuestion[];\n labels?: Partial<ApprovalLabels>;\n onSubmitted?: (answers: Record<number, number[]>, customAnswers: Record<number, string>) => void;\n onAnswerChange?: (questionIndex: number, answer: number[]) => void;\n resettable?: boolean;\n variant?: string;\n} = {}) {\n const t = { ...DEFAULT_LABELS, ...labels };\n const [qi, setQi] = useState(0);\n const [answers, setAnswers] = useState<Record<number, number[]>>({});\n const [custom, setCustom] = useState<Record<number, string>>({});\n const answersRef = useRef(answers);\n answersRef.current = answers;\n const customRef = useRef(custom);\n customRef.current = custom;\n const [sent, setSent] = useState(false);\n const [open, setOpen] = useState(true);\n const reopenRef = useRef<HTMLButtonElement>(null);\n const sentStatusRef = useRef<HTMLDivElement>(null);\n const previousView = useRef({ open: true, sent: false });\n\n const advanceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const questionRefs = useRef<(HTMLDivElement | null)[]>([]);\n const measured = useRef(false);\n const [viewportH, setViewportH] = useState<number | undefined>(undefined);\n const [trackY, setTrackY] = useState(0);\n const [animate, setAnimate] = useState(false);\n // Until the first question is measured, render only the active one so the\n // initial (and SSR) height is Q1's height — not all questions stacked, which\n // would flash to full height and then shrink on mount.\n const [ready, setReady] = useState(false);\n\n const last = qi >= questions.length - 1;\n const selected = answers[qi] ?? [];\n const hasAnswer = selected.length > 0 || Boolean(custom[qi]?.trim());\n\n const sync = (withAnim: boolean) => {\n const item = questionRefs.current[qi];\n if (!item) return;\n const reduce = typeof window !== \"undefined\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n setViewportH(item.offsetHeight);\n setTrackY(item.offsetTop);\n setAnimate(withAnim && !reduce);\n };\n\n useLayoutEffect(() => {\n const withAnim = measured.current;\n measured.current = true;\n sync(withAnim);\n setReady(true);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [qi, answers, custom, open, sent]);\n\n useEffect(() => {\n const previous = previousView.current;\n previousView.current = { open, sent };\n if (!open) reopenRef.current?.focus({ preventScroll: true });\n else if (sent) sentStatusRef.current?.focus({ preventScroll: true });\n else if (!previous.open || previous.sent) questionRefs.current[qi]?.querySelector<HTMLButtonElement>(\"button\")?.focus({ preventScroll: true });\n }, [open, sent, qi]);\n\n useEffect(() => {\n const focused = document.activeElement;\n const previousQuestion = questionRefs.current.find((item, index) => index !== qi && item?.contains(focused));\n if (previousQuestion) questionRefs.current[qi]?.querySelector<HTMLButtonElement>(\"button\")?.focus({ preventScroll: true });\n }, [qi]);\n\n useEffect(() => {\n setQi((current) => Math.max(0, Math.min(current, questions.length - 1)));\n }, [questions.length]);\n\n useEffect(() => {\n const id = requestAnimationFrame(() => sync(measured.current));\n return () => cancelAnimationFrame(id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [qi]);\n\n useEffect(() => {\n const item = questionRefs.current[qi];\n if (!item || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => sync(false));\n observer.observe(item);\n return () => observer.disconnect();\n }, [qi, open, sent, questions]);\n\n useEffect(() => () => { if (advanceTimer.current) clearTimeout(advanceTimer.current); }, []);\n\n const goTo = (next: number) => {\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n setQi(Math.max(0, Math.min(Math.max(next, 0), questions.length - 1)));\n };\n\n const send = () => {\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n setSent(true);\n onSubmitted?.(answersRef.current, customRef.current);\n };\n\n const advance = () => {\n if (last) send();\n else goTo(qi + 1);\n };\n\n const toggle = (index: number) => {\n const type = questions[qi].type;\n const current = answersRef.current;\n const picked = current[qi] ?? [];\n const next = type === \"radio\" ? [index] : picked.includes(index) ? picked.filter((item) => item !== index) : [...picked, index];\n const updated = { ...current, [qi]: next };\n answersRef.current = updated;\n setAnswers(updated);\n onAnswerChange?.(qi, next);\n if (type === \"radio\") {\n setCustom((current) => ({ ...current, [qi]: \"\" }));\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n advanceTimer.current = setTimeout(() => {\n if (last) send();\n else setQi((current) => Math.min(questions.length - 1, current + 1));\n }, 480);\n }\n };\n\n const reset = () => {\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n setQi(0);\n setAnswers({});\n setCustom({});\n setSent(false);\n setOpen(true);\n measured.current = false;\n };\n\n if (questions.length === 0) return <div role=\"status\" className=\"text-[13px]\">No questions to approve.</div>;\n\n if (!open) {\n return (\n <button ref={reopenRef} type=\"button\" onClick={() => setOpen(true)} className=\"rounded-[8px] bg-[var(--surface)] px-3 py-2 text-[12.5px] font-medium text-[var(--ink)] shadow-[var(--shadow-btn)] transition-colors duration-150 hover:bg-[var(--hover)]\">\n Open approval\n </button>\n );\n }\n\n if (sent) {\n return (\n <div ref={sentStatusRef} tabIndex={-1} role=\"status\" className=\"flex w-full max-w-80 flex-wrap items-center gap-3\" style={{ animation: \"mova-ai-approval-card-pop-in 260ms cubic-bezier(0.23,1,0.32,1) both\" }}>\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--green-tint)] py-1 pr-2.5 pl-1 text-[12.5px] font-medium text-[var(--green)]\">\n <span className=\"flex size-4.5 items-center justify-center rounded-full bg-[var(--green)] text-white\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n </span>\n {t.sentMessage}\n </span>\n {resettable && (\n <button type=\"button\" onClick={reset} className=\"text-[12px] font-medium text-[var(--ink-3)] transition-colors duration-150 hover:text-[var(--ink)]\">\n Start over\n </button>\n )}\n </div>\n );\n }\n\n return (\n <div className=\"w-full max-w-80\">\n <div className=\"relative overflow-hidden rounded-[10px] bg-[var(--surface)] shadow-[var(--shadow-card)]\" style={{ animation: \"mova-ai-approval-card-fade-up 380ms cubic-bezier(0.23,1,0.32,1) both\" }}>\n <button\n type=\"button\"\n aria-label=\"Dismiss\"\n onClick={() => { if (advanceTimer.current) clearTimeout(advanceTimer.current); setOpen(false); }}\n className=\"primitive-icon-button absolute right-2.5 top-2.5 z-10 text-[var(--ink-3)] transition-colors duration-100 hover:bg-[var(--hover)] hover:text-[var(--ink)]\"\n >\n <Ico size={14} sw={2.2} path={<path d=\"M18 6L6 18M6 6l12 12\" />} />\n </button>\n <div className=\"primitive-card-pad\">\n {/* the question itself is the heading */}\n <div\n className=\"overflow-hidden\"\n style={{ height: viewportH, transition: animate ? `height ${SLIDE}` : undefined }}\n aria-live=\"polite\"\n >\n <div\n style={{\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: 26,\n transform: `translate3d(0, ${-trackY}px, 0)`,\n transition: animate ? `transform ${SLIDE}` : undefined,\n willChange: \"transform\",\n }}\n >\n {questions.map((question, qIdx) => {\n const active = qIdx === qi;\n // Before the first measure, mount only the active question so the\n // card opens at its real height instead of flashing to full height.\n if (!ready && !active) return null;\n const picked = answers[qIdx] ?? [];\n const questionStyle: CSSProperties = {\n opacity: active ? 1 : 0,\n transition: animate ? `opacity ${SLIDE}` : undefined,\n pointerEvents: active ? undefined : \"none\",\n };\n return (\n <div\n key={qIdx}\n ref={(el) => { questionRefs.current[qIdx] = el; }}\n aria-hidden={active ? undefined : true}\n style={questionStyle}\n >\n <div className=\"pr-7 text-[14px] font-medium text-[var(--ink)]\">{question.q}</div>\n <GlideMenu className=\"mt-2.5 flex flex-col gap-1\" highlightClassName=\"inset-x-0 rounded-[8px] bg-[var(--hover)]\">\n {question.options.map((option, i) => {\n const on = picked.includes(i);\n return (\n <button\n key={option}\n type=\"button\"\n data-menu-row\n aria-pressed={on}\n tabIndex={active ? 0 : -1}\n onClick={() => { if (active) toggle(i); }}\n className=\"relative z-10 flex items-center gap-1.5 rounded-[8px] pl-1 pr-2 py-1 text-left transition-colors duration-100\"\n >\n <span\n className={`flex size-4 shrink-0 items-center justify-center transition-colors duration-200\n ${question.type === \"radio\" ? \"rounded-full\" : \"rounded-[5px]\"}\n ${on ? \"bg-[var(--ink)] text-[var(--canvas)]\" : \"shadow-[inset_0_0_0_1.5px_var(--line-strong)] text-transparent\"}`}\n >\n {question.type === \"radio\" ? (\n <span className=\"size-1.5 rounded-full bg-[var(--canvas)] transition-transform duration-200\" style={{ transform: on ? \"scale(1)\" : \"scale(0)\" }} />\n ) : (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n )}\n </span>\n <span className={`text-[13px] leading-snug transition-colors duration-200 ${on ? \"text-[var(--ink)]\" : \"text-[var(--ink-2)]\"}`}>\n {option}\n </span>\n </button>\n );\n })}\n <label data-menu-row className=\"relative z-10 flex items-center gap-1.5 rounded-[8px] pl-1 pr-2 py-1 transition-colors duration-100\">\n <input\n value={custom[qIdx] ?? \"\"}\n tabIndex={active ? 0 : -1}\n onChange={(event) => {\n if (!active) return;\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n setCustom((current) => ({ ...current, [qIdx]: event.target.value }));\n if (question.type === \"radio\") setAnswers((current) => ({ ...current, [qIdx]: [] }));\n }}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" && hasAnswer) {\n event.preventDefault();\n advance();\n }\n }}\n placeholder={t.customPlaceholder}\n aria-label=\"Custom answer\"\n className=\"min-w-0 flex-1 bg-transparent pl-1.5 text-[13px] text-[var(--ink)] outline-none placeholder:text-[var(--ink-3)]\"\n />\n </label>\n </GlideMenu>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {/* footer — step nav (rolling counter) + pill actions */}\n <div className=\"primitive-card-footer flex items-center justify-between gap-3\">\n <div className=\"flex items-center gap-1 text-[var(--ink-3)]\">\n <button\n type=\"button\"\n aria-label=\"Previous question\"\n disabled={qi <= 0}\n onClick={() => goTo(qi - 1)}\n className=\"flex size-[18px] items-center justify-center rounded-[5px] transition-colors duration-100 enabled:hover:text-[var(--ink)] disabled:opacity-30\"\n >\n <Ico size={14} path={<path d=\"M18 15l-6-6-6 6\" />} />\n </button>\n <span className=\"inline-flex items-center text-[12px] font-medium tabular-nums text-[var(--ink-3)]\" style={{ letterSpacing: \"-0.1px\", lineHeight: 1 }}>\n <RollingDigits value={`${qi + 1} / ${questions.length}`} />\n </span>\n <button\n type=\"button\"\n aria-label=\"Next question\"\n disabled={last}\n onClick={() => goTo(qi + 1)}\n className=\"flex size-[18px] items-center justify-center rounded-[5px] transition-colors duration-100 enabled:hover:text-[var(--ink)] disabled:opacity-30\"\n >\n <Ico size={14} path={<path d=\"M6 9l6 6 6-6\" />} />\n </button>\n </div>\n\n <div className=\"-mr-0.5 flex items-center gap-1.5\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => { if (advanceTimer.current) clearTimeout(advanceTimer.current); if (last) setOpen(false); else goTo(qi + 1); }}>\n {t.skip}\n </Button>\n <Button variant=\"accent\" size=\"sm\" disabled={!hasAnswer} onClick={advance}>\n {last ? t.send : t.continue}\n </Button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\ntype GlideMenuProps = {\n children: ReactNode;\n className?: string;\n highlightClassName?: string;\n rowSelector?: string;\n};\n\n/** A single hover layer that glides between interactive menu rows. */\nfunction GlideMenu({\n children,\n className = \"\",\n highlightClassName = \"inset-x-0 rounded-[8px] bg-[var(--hover)]\",\n rowSelector = \"[data-menu-row]\",\n}: GlideMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<{ top: number; height: number } | null>(null);\n const [visible, setVisible] = useState(false);\n\n const moveTo = (target: EventTarget | null) => {\n const container = ref.current;\n if (!(target instanceof Element) || !container) return;\n const row = target.closest(rowSelector);\n if (!(row instanceof HTMLElement) || !container.contains(row)) return;\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n setBox({ top: rowRect.top - containerRect.top, height: rowRect.height });\n setVisible(true);\n };\n\n return (\n <div\n ref={ref}\n onMouseOver={(event) => moveTo(event.target)}\n onMouseLeave={() => setVisible(false)}\n onFocusCapture={(event) => moveTo(event.target)}\n onBlurCapture={(event) => {\n if (!ref.current?.contains(event.relatedTarget as Node | null)) setVisible(false);\n }}\n className={`group/glide-menu relative ${className}`}\n >\n <span\n aria-hidden\n className={`pointer-events-none absolute ${highlightClassName}`}\n style={{\n top: box?.top ?? 0,\n height: box?.height ?? 0,\n opacity: box && visible ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {children}\n </div>\n );\n}\n\nfunction Button({variant = \"secondary\",size = \"md\",className = \"\",...props}: React.ButtonHTMLAttributes<HTMLButtonElement> & {variant?: \"secondary\" | \"ghost\" | \"accent\";size?: \"sm\" | \"md\"}) {\n const variants = {secondary:\"bg-[var(--surface)] text-[var(--ink)] shadow-[var(--shadow-btn)] hover:bg-[var(--inset)]\", ghost:\"bg-[var(--hover-2)] text-[var(--ink)] hover:bg-[var(--line-strong)]\", accent:\"bg-[var(--accent)] text-white hover:bg-[var(--accent-ink)] shadow-[inset_0_1px_0_rgba(255,255,255,0.14)]\"};\n return <button type=\"button\" className={`inline-flex items-center justify-center font-medium select-none transition-[transform,background-color,opacity] duration-150 ease-out active:scale-[0.96] disabled:opacity-50 disabled:pointer-events-none ${variants[variant]} ${size === \"sm\" ? \"h-[27px] px-3 text-[13px] leading-none rounded-full gap-1.5\" : \"px-4 py-[9px] text-sm leading-none rounded-full gap-2\"} ${className}`} {...props}/>;\n}\n\n/* Smooth shadow values: shadow-plugin, Copyright (c) Florian Kiem, MIT. */\nconst scopedStyles = `.mova-ai-approval-card{\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--radius-control:8px;\n width:100%;max-width:320px;min-width:0;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:14px;line-height:1.5;letter-spacing:-.01em;color:var(--ink);}\n.mova-ai-approval-card[data-theme=\"dark\"]{\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n.mova-ai-approval-card .primitive-card-pad{padding:12px} .mova-ai-approval-card .primitive-card-bar{padding:10px 12px} .mova-ai-approval-card .primitive-card-footer{padding:10px} .mova-ai-approval-card .primitive-icon-button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;border-radius:8px}\n.mova-ai-approval-card :is(button,input,[tabindex]):focus-visible{outline:2px solid var(--accent)!important;outline-offset:2px} .mova-ai-approval-card button:not(:disabled){cursor:pointer} .mova-ai-approval-card code{overflow-wrap:anywhere} .mova-ai-approval-card *, .mova-ai-approval-card *::before,.mova-ai-approval-card *::after{box-sizing:border-box}\n@keyframes mova-ai-approval-card-pop-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n@keyframes mova-ai-approval-card-fade-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-approval-card-shimmer-text{from{background-position:150% center}to{background-position:-50% center}}\n@media(prefers-reduced-motion:reduce){.mova-ai-approval-card *, .mova-ai-approval-card *::before, .mova-ai-approval-card *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}`;\n\nexport function AIApprovalCard({theme = \"light\", ...props}: React.ComponentProps<typeof ApprovalCard> & {theme?: \"light\" | \"dark\"} = {}) {\n return <div className=\"mova-ai-approval-card\" data-theme={theme}><style>{scopedStyles}</style><ApprovalCard {...props} /></div>;\n}\n"
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-code-block",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-code-block.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-code-block.tsx",
|
|
7
|
+
"registryFile": "ai-code-block.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AICodeBlock",
|
|
10
|
+
"CodeBlockLabels",
|
|
11
|
+
"CodeBlockProps",
|
|
12
|
+
"CodePiece",
|
|
13
|
+
"DiffRow"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": [],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"previewMode": "client",
|
|
18
|
+
"content": "\"use client\";\n\"use client\";\n\n/** Adapted from Beautiful UI (https://github.com/slev12397/beautiful-ui).\n * Copyright (c) 2026 Shane Levine. MIT License; see LICENSE.third-party. */\n\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * CODE BLOCK\n * A light editor panel with two versions (switch in the card):\n * · Code — a line-numbered listing\n * · Diff — a unified diff: old/new gutters, a green/red accent\n * bar and row tint, plus word-level add/del highlights.\n * Both share syntax coloring, insets, and wrapping behavior.\n * ───────────────────────────────────────────────────────── */\n\nconst FILE = \"churn.ts\";\n\nconst CODE_LINES = [\n \"export async function churnBatch() {\",\n ' const flavor = await getFlavor(\"pistachio\");',\n \" const base = await dairy.fetch({ flavor });\",\n ' await freezer.store(base, { temp: \"-16C\" });',\n \" if (!base.approved) return null;\",\n \" return base.gallons;\",\n \"}\",\n];\n\n/* A single run of code within a diff row; `change` tints it as an add/del. */\nexport type CodePiece = { text: string; change?: \"add\" | \"del\" };\n/* One row of a unified diff: old/new line numbers, its kind, and its pieces. */\nexport type DiffRow = {\n old: number | null;\n cur: number | null;\n type: \"ctx\" | \"add\" | \"del\";\n pieces: CodePiece[];\n};\n/* Prominent copy strings on the code block. */\nexport type CodeBlockLabels = { copy: string; copied: string };\n\n// Back-compat internal aliases for the local component signatures.\ntype Piece = CodePiece;\ntype Row = DiffRow;\n\nconst DIFF: Row[] = [\n { old: 1, cur: 1, type: \"ctx\", pieces: [{ text: \"export async function churnBatch() {\" }] },\n { old: 2, cur: 2, type: \"ctx\", pieces: [{ text: ' const flavor = await getFlavor(\"pistachio\");' }] },\n { old: 3, cur: 3, type: \"ctx\", pieces: [{ text: \" const base = await dairy.fetch({ flavor });\" }] },\n { old: 4, cur: null, type: \"del\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-14C\"', change: \"del\" }, { text: \" });\" }] },\n { old: null, cur: 4, type: \"add\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-16C\"', change: \"add\" }, { text: \" });\" }] },\n { old: null, cur: 5, type: \"add\", pieces: [{ text: \" if (!base.approved) return null;\" }] },\n { old: 5, cur: 6, type: \"ctx\", pieces: [{ text: \" return base.gallons;\" }] },\n { old: 6, cur: 7, type: \"ctx\", pieces: [{ text: \"}\" }] },\n];\n\nconst HATCH = \"repeating-linear-gradient(45deg, var(--red) 0, var(--red) 1.5px, transparent 1.5px, transparent 3px)\";\n\n/* light syntax coloring — keywords/imports/conditionals, functions, strings & numbers */\nconst KEYWORDS = new Set([\"import\", \"from\", \"export\", \"default\", \"async\", \"function\", \"const\", \"let\", \"var\", \"await\", \"return\", \"if\", \"else\", \"for\", \"while\", \"new\", \"throw\", \"try\", \"catch\", \"null\", \"true\", \"false\", \"undefined\"]);\nconst TOKEN = /(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`[^`]*`|\\b\\d+(?:\\.\\d+)?\\b|\\b(?:import|from|export|default|async|function|const|let|var|await|return|if|else|for|while|new|throw|try|catch|null|true|false|undefined)\\b|[A-Za-z_$][\\w$]*(?=\\s*\\())/g;\n\nfunction highlight(text: string): ReactNode[] {\n const nodes: ReactNode[] = [];\n let last = 0;\n let k = 0;\n for (const m of text.matchAll(TOKEN)) {\n const idx = m.index ?? 0;\n const t = m[0];\n if (idx > last) nodes.push(<span key={k++}>{text.slice(last, idx)}</span>);\n let color: string;\n let weight: number | undefined;\n if (/^[\"'`]/.test(t) || /^\\d/.test(t)) color = \"var(--orange)\"; // string / number\n else if (KEYWORDS.has(t)) color = \"var(--accent-ink)\"; // keyword / import / conditional\n else { color = \"var(--ink)\"; weight = 500; } // function call\n nodes.push(<span key={k++} style={{ color, fontWeight: weight }}>{t}</span>);\n last = idx + t.length;\n }\n if (last < text.length) nodes.push(<span key={k++}>{text.slice(last)}</span>);\n return nodes;\n}\n\nfunction Pieces({ pieces }: { pieces: Piece[] }) {\n return (\n <>\n {pieces.map((p, i) => {\n if (p.change) {\n const add = p.change === \"add\";\n return (\n <span\n key={i}\n className=\"rounded-[3px]\"\n style={{\n background: `color-mix(in srgb, var(--${add ? \"green\" : \"red\"}) 18%, transparent)`,\n padding: \"0 2px\",\n margin: \"0 -1px\",\n boxDecorationBreak: \"clone\",\n WebkitBoxDecorationBreak: \"clone\",\n }}\n >\n {highlight(p.text)}\n </span>\n );\n }\n return <span key={i}>{highlight(p.text)}</span>;\n })}\n </>\n );\n}\n\nfunction FileIcon() {\n return (\n <svg aria-hidden width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"shrink-0 text-[var(--ink-3)]\">\n <path d=\"M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5\" />\n </svg>\n );\n}\n\nconst DEFAULT_LABELS: CodeBlockLabels = { copy: \"Copy\", copied: \"Copied\" };\n\nexport type CodeBlockProps = {\n /** Which view to render — \"Code\" (line-numbered listing) or \"Diff\". */\n variant?: string;\n /** The lines shown in the Code view. */\n lines?: string[];\n /** Raw text placed on the clipboard by Copy. Defaults to `lines` joined. */\n code?: string;\n /** The unified-diff rows shown in the Diff view. */\n diff?: DiffRow[];\n /** Filename shown in the header. */\n filename?: string;\n /** Prominent copy strings. */\n labels?: Partial<CodeBlockLabels>;\n /** Called with the copied text after a successful copy. */\n onCopy?: (text: string) => void;\n};\n\nfunction CodeBlock({\n variant,\n lines = CODE_LINES,\n code,\n diff = DIFF,\n filename = FILE,\n labels,\n onCopy,\n}: CodeBlockProps) {\n const [copied, setCopied] = useState(false);\n const [view, setView] = useState(\"Code\");\n const isDiff = (variant ?? view) === \"Diff\";\n const [copyError, setCopyError] = useState(false);\n const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => () => {if (copyTimer.current) clearTimeout(copyTimer.current);}, []);\n const text = { ...DEFAULT_LABELS, ...labels };\n const raw = code ?? (isDiff ? diff.map((row) => `${row.type === \"add\" ? \"+\" : row.type === \"del\" ? \"-\" : \" \"}${row.pieces.map((piece) => piece.text).join(\"\")}`).join(\"\\n\") : lines.join(\"\\n\"));\n\n const copy = useCallback(() => {\n setCopyError(false);\n if (!navigator.clipboard) {setCopyError(true); return;}\n navigator.clipboard.writeText(raw).then(() => {\n setCopied(true);\n onCopy?.(raw);\n if (copyTimer.current) clearTimeout(copyTimer.current);\n copyTimer.current = setTimeout(() => setCopied(false), 1500);\n }).catch(() => setCopyError(true));\n }, [raw, onCopy]);\n\n const added = diff.filter((r) => r.type === \"add\").length;\n const removed = diff.filter((r) => r.type === \"del\").length;\n\n return (\n <div className=\"w-full max-w-105 overflow-hidden rounded-[10px] bg-[var(--surface)] shadow-[var(--shadow-card)]\">\n {/* header — file · (diff stat | copy) */}\n <div className=\"flex min-h-11 flex-wrap items-center gap-2 border-b border-[var(--line)] px-4 text-[12.5px]\">\n <span className=\"inline-flex min-w-0 items-center gap-[7px]\">\n <FileIcon />\n <span className=\"truncate font-mono leading-none text-[var(--ink)]\">{filename}</span>\n </span>\n\n {variant === undefined && <div role=\"group\" aria-label=\"Code view\" className=\"ml-auto flex shrink-0 items-center rounded-[6px] bg-[var(--field)] p-0.5\">\n {[\"Code\", \"Diff\"].map((item) => <button key={item} type=\"button\" aria-pressed={view === item} onClick={() => {setView(item);setCopied(false);setCopyError(false);}} className={`rounded-[4px] px-1.5 py-1 text-[11px] leading-none ${view === item ? \"bg-[var(--surface)] text-[var(--ink)] shadow-[var(--shadow-btn)]\" : \"text-[var(--ink-3)]\"}`}>{item}</button>)}\n </div>}\n {isDiff && (\n <span className=\"ml-auto inline-flex items-center gap-2 font-mono text-[12px] leading-none tabular-nums\">\n <span className=\"text-[var(--green)]\">+{added}</span>\n <span className=\"text-[var(--red)]\">-{removed}</span>\n </span>\n )}\n <button\n type=\"button\"\n aria-label={isDiff ? \"Copy diff\" : \"Copy code\"}\n aria-live=\"polite\"\n onClick={copy}\n className={`-mr-1 ml-auto flex h-6 items-center gap-1 rounded-[6px] px-1.5 text-[12px]\n font-medium transition-colors duration-100 hover:bg-[var(--hover)]\n ${copied ? \"text-[var(--green)]\" : \"text-[var(--ink-3)] hover:text-[var(--ink)]\"}`}\n >\n {copied ? (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n ) : (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2.5\" /><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" /></svg>\n )}\n {copied ? text.copied : text.copy}\n </button>\n </div>\n {copyError && <div role=\"alert\" className=\"px-4 py-1 text-[11px] text-[var(--red)]\">Clipboard unavailable. Select the code to copy.</div>}\n\n {/* body — equal 12px inset on top / left / right; lines wrap */}\n <div className=\"py-3 font-mono text-[12.5px] leading-[1.65] text-[var(--ink-2)]\">\n {isDiff ? (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-[var(--line)]\" />\n {diff.map((r, i) => {\n const add = r.type === \"add\";\n const del = r.type === \"del\";\n // one gutter column: removals keep the old number, additions/context show the new one\n const num = del ? r.old : r.cur;\n return (\n <div\n key={i}\n className={`relative grid grid-cols-[20px_minmax(0,1fr)] items-start\n ${add ? \"bg-[var(--green-tint)]\" : del ? \"bg-[var(--red-tint)]\" : \"\"}`}\n >\n {(add || del) && (\n <span className=\"absolute inset-y-0 left-0 w-[3px]\" style={{ background: add ? \"var(--green)\" : HATCH }} />\n )}\n <span className={`select-none text-center text-[11px] ${add ? \"text-[var(--green)]\" : del ? \"text-[var(--red)]\" : \"text-[var(--ink-3)]\"}`}>{num ?? \"\"}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">\n <Pieces pieces={r.pieces} />\n </code>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-[var(--line)]\" />\n {lines.map((line, i) => (\n <div key={i} className=\"grid grid-cols-[20px_minmax(0,1fr)] items-start\">\n <span className=\"select-none text-center text-[11px] text-[var(--ink-3)]\">{i + 1}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">{highlight(line)}</code>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* Smooth shadow values: shadow-plugin, Copyright (c) Florian Kiem, MIT. */\nconst scopedStyles = `.mova-ai-code-block{\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--radius-control:8px;\n width:100%;max-width:420px;min-width:0;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:14px;line-height:1.5;letter-spacing:-.01em;color:var(--ink);}\n.mova-ai-code-block[data-theme=\"dark\"]{\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n.mova-ai-code-block .primitive-card-pad{padding:12px} .mova-ai-code-block .primitive-card-bar{padding:10px 12px} .mova-ai-code-block .primitive-card-footer{padding:10px} .mova-ai-code-block .primitive-icon-button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;border-radius:8px}\n.mova-ai-code-block :is(button,input,[tabindex]):focus-visible{outline:2px solid var(--accent)!important;outline-offset:2px} .mova-ai-code-block button:not(:disabled){cursor:pointer} .mova-ai-code-block code{overflow-wrap:anywhere} .mova-ai-code-block *, .mova-ai-code-block *::before,.mova-ai-code-block *::after{box-sizing:border-box}\n@keyframes mova-ai-code-block-pop-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n@keyframes mova-ai-code-block-fade-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-code-block-shimmer-text{from{background-position:150% center}to{background-position:-50% center}}\n@media(prefers-reduced-motion:reduce){.mova-ai-code-block *, .mova-ai-code-block *::before, .mova-ai-code-block *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}`;\n\nexport function AICodeBlock({theme = \"light\", ...props}: CodeBlockProps & {theme?: \"light\" | \"dark\"} = {}) {\n return <div className=\"mova-ai-code-block\" data-theme={theme}><style>{scopedStyles}</style><CodeBlock {...props} /></div>;\n}\n"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-context-meter",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-context-meter.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-context-meter.tsx",
|
|
7
|
+
"registryFile": "ai-context-meter.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AIContextBreakdownItem",
|
|
10
|
+
"AIContextMeter",
|
|
11
|
+
"AIContextMeterProps"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": [
|
|
14
|
+
"framer-motion"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"previewMode": "client",
|
|
18
|
+
"content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-context-meter (MIT). See ../patterns/smoothui.LICENSE.\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst VIEWBOX = 32;\nconst CENTER = VIEWBOX / 2;\nconst RADIUS = 13;\nconst STROKE_WIDTH = 3;\nconst DEFAULT_WARNING_AT = 0.8;\nconst DEFAULT_DANGER_AT = 0.95;\nconst WARNING_COLOR = \"oklch(78% 0.16 75)\";\nconst DANGER_COLOR = \"oklch(63% 0.21 25)\";\n\nexport type AIContextBreakdownItem = { label: string; tokens: number };\n\nexport type AIContextMeterProps = {\n breakdown?: AIContextBreakdownItem[];\n className?: string;\n dangerAt?: number;\n limit: number;\n used: number;\n warningAt?: number;\n};\n\nconst formatTokens = (tokens: number): string => {\n if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;\n if (tokens >= 10_000) return `${Math.round(tokens / 1000)}k`;\n if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;\n return String(tokens);\n};\n\nexport function AIContextMeter({\n breakdown,\n className = \"\",\n dangerAt = DEFAULT_DANGER_AT,\n limit,\n used,\n warningAt = DEFAULT_WARNING_AT,\n}: AIContextMeterProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(false);\n const fraction = limit > 0 ? Math.min(1, Math.max(0, used / limit)) : 0;\n const percent = Math.round(fraction * 100);\n const color = fraction >= dangerAt ? DANGER_COLOR : fraction >= warningAt ? WARNING_COLOR : \"currentColor\";\n const hasBreakdown = Boolean(breakdown?.length);\n\n return (\n <div className={`relative inline-block ${className}`}>\n <button\n aria-expanded={hasBreakdown ? isOpen : undefined}\n aria-label={`Context window ${percent}% used, ${formatTokens(used)} of ${formatTokens(limit)} tokens`}\n className=\"flex cursor-pointer items-center gap-1.5 rounded-lg px-1 py-0.5 text-muted-foreground text-xs transition-colors hover:text-foreground\"\n disabled={!hasBreakdown}\n onBlur={() => setIsOpen(false)}\n onClick={() => setIsOpen((current) => !current)}\n onFocus={() => hasBreakdown && setIsOpen(true)}\n onMouseEnter={() => hasBreakdown && setIsOpen(true)}\n onMouseLeave={() => setIsOpen(false)}\n type=\"button\"\n >\n <svg aria-hidden=\"true\" className=\"h-4 w-4 -rotate-90\" viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}>\n <circle cx={CENTER} cy={CENTER} fill=\"none\" r={RADIUS} stroke=\"currentColor\" strokeOpacity={0.2} strokeWidth={STROKE_WIDTH} />\n <motion.circle\n animate={{ stroke: color, strokeDasharray: `${fraction} ${1 - fraction}` }}\n cx={CENTER}\n cy={CENTER}\n fill=\"none\"\n pathLength={1}\n r={RADIUS}\n strokeLinecap=\"round\"\n strokeWidth={STROKE_WIDTH}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n />\n </svg>\n <span className=\"tabular-nums\">{formatTokens(used)}/{formatTokens(limit)}</span>\n </button>\n\n <AnimatePresence>\n {isOpen && hasBreakdown ? (\n <motion.div\n animate={{ opacity: 1, scale: 1, y: 0 }}\n className=\"absolute bottom-full left-0 z-50 mb-1.5 w-56 rounded-xl border border-border bg-background p-2.5 shadow-lg\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, scale: 0.96, y: 4 }}\n initial={shouldReduceMotion ? { opacity: 1, scale: 1, y: 0 } : { opacity: 0, scale: 0.96, y: 4 }}\n style={{ transformOrigin: \"bottom left\" }}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: EASE_OUT }}\n >\n <ul className=\"list-none space-y-1\">\n {breakdown?.map((item) => (\n <li className=\"flex items-baseline justify-between gap-3 text-xs\" key={item.label}>\n <span className=\"truncate text-muted-foreground\">{item.label}</span>\n <span className=\"shrink-0 text-foreground tabular-nums\">{formatTokens(item.tokens)}</span>\n </li>\n ))}\n </ul>\n </motion.div>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AIContextMeter;\n"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-fine-tune-card",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-fine-tune-card.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-fine-tune-card.tsx",
|
|
7
|
+
"registryFile": "ai-fine-tune-card.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AIFineTuneCard",
|
|
10
|
+
"FineTuneCardLabels",
|
|
11
|
+
"FineTuneCardProps",
|
|
12
|
+
"FineTuneField",
|
|
13
|
+
"FineTuneState"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": [],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"previewMode": "client",
|
|
18
|
+
"content": "\"use client\";\n\"use client\";\n\n/** Adapted from Beautiful UI (https://github.com/slev12397/beautiful-ui).\n * Copyright (c) 2026 Shane Levine. MIT License; see LICENSE.third-party.\n * Original card geometry, interactions, and motion are preserved. */\nimport * as React from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * FINE-TUNE CARD — compact interactive inspector.\n * Number fields scrub: hover the label for an ↔ cursor and\n * drag to adjust, use ↑/↓ (⇧ for ×10), or type directly.\n * ───────────────────────────────────────────────────────── */\n\nfunction ScrubField({\n label,\n value,\n onChange,\n min,\n max,\n step = 1,\n suffix = \"\",\n active,\n}: {\n label: string;\n value: number;\n onChange: (v: number) => void;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n active?: boolean;\n}) {\n const drag = useRef<{ x: number; v: number } | null>(null);\n const [draft, setDraft] = useState(String(value));\n useEffect(() => setDraft(String(value)), [value]);\n const clamp = (v: number) => Math.min(max, Math.max(min, Number((Math.round(v / step) * step).toFixed(8))));\n\n const commitDraft = () => {\n const parsed = draft.trim() === \"\" ? NaN : Number(draft);\n if (!Number.isFinite(parsed)) { setDraft(String(value)); return; }\n const next = clamp(parsed);\n setDraft(String(next));\n if (next !== value) onChange(next);\n };\n\n return (\n <label\n className=\"flex h-6.5 min-w-0 items-center gap-1 rounded-[6px] py-1 pr-1 pl-0.5\n transition-[background-color,box-shadow] duration-200\"\n style={{\n background: active ? \"var(--accent-tint)\" : \"var(--field)\",\n boxShadow: active ? \"0 0 0 1px var(--accent)\" : \"none\",\n }}\n >\n {/* scrub handle */}\n <span\n role=\"slider\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n tabIndex={0}\n onPointerDown={(e) => {\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n drag.current = { x: e.clientX, v: value };\n }}\n onPointerMove={(e) => {\n if (!drag.current) return;\n onChange(clamp(drag.current.v + ((e.clientX - drag.current.x) / 2) * step));\n }}\n onPointerUp={() => (drag.current = null)}\n onPointerCancel={() => (drag.current = null)}\n onLostPointerCapture={() => (drag.current = null)}\n onKeyDown={(e) => {\n const mult = e.shiftKey ? 10 : 1;\n if (e.key === \"ArrowUp\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n onChange(clamp(value + step * mult));\n } else if (e.key === \"ArrowDown\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n onChange(clamp(value - step * mult));\n }\n }}\n className=\"flex h-full shrink-0 cursor-ew-resize touch-none items-center rounded-[4px]\n px-0.5 text-[12px] text-[var(--ink-3)] select-none hover:text-[var(--ink-2)] focus-visible:text-[var(--accent-ink)]\n focus-visible:outline-none\"\n >\n {label}\n </span>\n <input\n inputMode=\"decimal\"\n value={draft}\n onChange={(event) => {\n // Keep incomplete numbers editable; validation happens on commit.\n if (/^-?\\d*\\.?\\d*$/.test(event.target.value)) setDraft(event.target.value);\n }}\n onBlur={commitDraft}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") { event.preventDefault(); commitDraft(); }\n if (event.key === \"Escape\") { event.preventDefault(); event.stopPropagation(); setDraft(String(value)); }\n }}\n aria-label={`${label} value`}\n className=\"min-w-0 flex-1 bg-transparent text-[12px] text-[var(--ink)] tabular-nums outline-none\"\n />\n {suffix && <span className=\"shrink-0 pr-0.5 text-[11.5px] text-[var(--ink-3)]\">{suffix}</span>}\n </label>\n );\n}\n\nconst SEGMENTS = [\"row\", \"col\", \"grid\"] as const;\n\nfunction SegmentIcon({ kind }: { kind: string }) {\n const dot = \"size-1.5 rounded-[2px] border-[1.2px] border-current\";\n if (kind === \"row\")\n return <span className=\"flex gap-0.5\">{[0, 1, 2].map((i) => <span key={i} className={dot} />)}</span>;\n if (kind === \"col\")\n return <span className=\"flex flex-col gap-0.5\">{[0, 1].map((i) => <span key={i} className={dot} />)}</span>;\n return (\n <span className=\"grid grid-cols-2 gap-0.5\">\n {[0, 1, 2, 3].map((i) => <span key={i} className={dot} />)}\n </span>\n );\n}\n\n/* A single scrub-able number property. `value` is the initial/default value. */\nexport type FineTuneField = {\n key: string;\n label: string;\n value: number;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n};\n/* Prominent copy strings on the card. */\nexport type FineTuneCardLabels = {\n title: string;\n layout: string;\n type: string;\n placeholder: string;\n adjust: string;\n edited: string;\n};\n/* The editable state emitted by `onChange`. */\nexport type FineTuneState = {\n segment: number;\n values: Record<string, number>;\n type: string;\n};\n\nconst FIELDS: FineTuneField[] = [\n { key: \"width\", label: \"W\", value: 324, min: 40, max: 999 },\n { key: \"height\", label: \"H\", value: 96, min: 24, max: 999 },\n { key: \"radius\", label: \"Radius\", value: 28, min: 0, max: 64 },\n { key: \"opacity\", label: \"Opacity\", value: 100, min: 0, max: 100, suffix: \"%\" },\n];\n\nconst OPTIONS = [\"Seasonal\", \"Classic\", \"Limited\"];\n\nconst DEFAULT_LABELS: FineTuneCardLabels = {\n title: \"Flavor card\",\n layout: \"Layout\",\n type: \"Type\",\n placeholder: \"Select type\",\n adjust: \"Adjust\",\n edited: \"Edited\",\n};\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const rows: T[][] = [];\n for (let i = 0; i < items.length; i += size) rows.push(items.slice(i, i + size));\n return rows;\n}\n\nexport type FineTuneCardProps = {\n /** Accepted for gallery/registry parity; not used by this card. */\n variant?: string;\n /** The scrub-able properties shown in the layout grid (rendered in pairs). */\n fields?: FineTuneField[];\n /** Options offered in the Type menu. */\n options?: string[];\n /** Prominent copy strings. */\n labels?: Partial<FineTuneCardLabels>;\n /** Called with the full editable state whenever the user edits it. */\n onChange?: (state: FineTuneState) => void;\n};\n\nfunction FineTuneCard({\n fields = FIELDS,\n options = OPTIONS,\n labels,\n onChange,\n}: FineTuneCardProps) {\n const text = { ...DEFAULT_LABELS, ...labels };\n const [seg, setSeg] = useState(0);\n const [values, setValues] = useState<Record<string, number>>(() =>\n Object.fromEntries(fields.map((f) => [f.key, f.value])),\n );\n const [menuOpen, setMenuOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n useEffect(() => {\n if (!menuOpen) return;\n const dismiss = (event: PointerEvent) => { if (!menuRef.current?.contains(event.target as Node)) setMenuOpen(false); };\n document.addEventListener(\"pointerdown\", dismiss);\n return () => document.removeEventListener(\"pointerdown\", dismiss);\n }, [menuOpen]);\n const [typeValue, setTypeValue] = useState(text.placeholder);\n\n const selectSeg = (i: number) => {\n setSeg(i);\n onChange?.({ segment: i, values, type: typeValue });\n };\n const setValue = (key: string, v: number) => {\n const next = { ...values, [key]: v };\n setValues(next);\n onChange?.({ segment: seg, values: next, type: typeValue });\n };\n const selectType = (value: string) => {\n setTypeValue(value);\n setMenuOpen(false);\n triggerRef.current?.focus();\n onChange?.({ segment: seg, values, type: value });\n };\n\n const changed = fields.some((f) => values[f.key] !== f.value);\n const done = seg !== 0 || changed || typeValue !== text.placeholder;\n\n return (\n <div className=\"relative w-full max-w-60 rounded-[10px] bg-[var(--surface)] shadow-[var(--shadow-raised)]\">\n {/* header */}\n <div className=\"primitive-card-bar flex items-center justify-between border-b border-[var(--line)]\">\n <span className=\"text-[13px] font-medium text-[var(--ink)]\">{text.title}</span>\n {done ? (\n <span\n className=\"flex items-center gap-1.5 text-[12px] font-medium text-[var(--green)]\"\n style={{ animation: \"mova-ai-fine-tune-card-pop-in 250ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M20 6L9 17l-5-5\" />\n </svg>\n {text.edited}\n </span>\n ) : (\n <span className=\"flex items-center gap-1.5\">\n <span className=\"flex size-4.5 items-center justify-center rounded-[5px] border border-[var(--accent)]/30 bg-[var(--accent-tint)]\">\n <svg width=\"9\" height=\"9\" viewBox=\"0 0 24 24\" fill=\"var(--accent)\">\n <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />\n </svg>\n </span>\n <span\n className=\"bg-clip-text text-[12px] font-medium text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(90deg, var(--accent) 35%, var(--accent-ink) 50%, var(--accent) 65%)\",\n backgroundSize: \"200% 100%\",\n animation: \"mova-ai-fine-tune-card-shimmer-text 1.4s linear infinite\",\n }}\n >\n {text.adjust}\n </span>\n </span>\n )}\n </div>\n\n {/* layout section */}\n <div className=\"primitive-card-pad flex flex-col gap-2 border-b border-[var(--line)]\">\n <p className=\"text-[12.5px] font-medium text-[var(--ink)]\">{text.layout}</p>\n {/* segmented control: gray track, raised white thumb */}\n <div className=\"relative grid grid-cols-3 rounded-[8px] bg-[var(--field)] p-0.5\">\n <span\n aria-hidden\n className=\"absolute inset-y-0.5 rounded-[6px] bg-[var(--surface)] shadow-[var(--shadow-btn)] transition-transform duration-300\"\n style={{\n width: \"calc((100% - 4px) / 3)\",\n left: 2,\n transform: `translateX(${seg * 100}%)`,\n transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n }}\n />\n {SEGMENTS.map((s, i) => (\n <button\n key={s}\n type=\"button\"\n aria-label={`${s} layout`}\n aria-pressed={i === seg}\n onClick={() => selectSeg(i)}\n className={`relative z-10 flex h-6 items-center justify-center transition-colors duration-200\n ${i === seg ? \"text-[var(--accent)]\" : \"text-[var(--ink-3)]\"}`}\n >\n <SegmentIcon kind={s} />\n </button>\n ))}\n </div>\n {chunk(fields, 2).map((pair, ri) => (\n <div key={ri} className=\"grid min-w-0 grid-cols-2 gap-2\">\n {pair.map((f) => (\n <ScrubField\n key={f.key}\n label={f.label}\n value={values[f.key]}\n onChange={(v) => setValue(f.key, v)}\n min={f.min}\n max={f.max}\n step={f.step}\n suffix={f.suffix}\n active={values[f.key] !== f.value}\n />\n ))}\n </div>\n ))}\n </div>\n\n {/* interaction section */}\n <div className=\"primitive-card-footer flex items-center justify-between\">\n <span className=\"text-[12px] text-[var(--ink-3)]\">{text.type}</span>\n <div ref={menuRef} className=\"relative -mr-0.5 w-30\" onBlur={(event) => {if (!event.currentTarget.contains(event.relatedTarget)) setMenuOpen(false);}} onKeyDown={(event) => {\n if (event.key === \"Escape\") {setMenuOpen(false); triggerRef.current?.focus();}\n if (menuOpen && (event.key === \"ArrowDown\" || event.key === \"ArrowUp\")) {\n event.preventDefault();\n const items = Array.from(menuRef.current?.querySelectorAll<HTMLButtonElement>(\"[role=menuitemradio]\") ?? []);\n const current = items.indexOf(document.activeElement as HTMLButtonElement);\n items[(current + (event.key === \"ArrowDown\" ? 1 : -1) + items.length) % items.length]?.focus();\n }\n }}>\n <button\n type=\"button\"\n ref={triggerRef}\n aria-haspopup=\"menu\"\n aria-label={text.type}\n aria-expanded={menuOpen}\n onClick={() => setMenuOpen((current) => !current)}\n className=\"flex h-6.5 w-full items-center justify-between rounded-[6px] bg-[var(--inset)] py-1 pr-1 pl-2\n shadow-[var(--shadow-hairline)] transition-shadow duration-200 focus-visible:outline-none\"\n style={{ boxShadow: menuOpen ? \"0 0 0 1px var(--accent)\" : undefined }}\n >\n <span className={`text-[12px] ${typeValue !== text.placeholder ? \"text-[var(--ink)]\" : \"text-[var(--ink-3)]\"}`}>\n {typeValue}\n </span>\n <svg\n width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className=\"transition-transform duration-200\"\n style={{ transform: menuOpen ? \"rotate(180deg)\" : \"rotate(0)\" }}\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </button>\n\n {menuOpen && (\n <div\n role=\"menu\"\n aria-label={text.type}\n className=\"absolute right-0 bottom-8 z-10 w-30 rounded-[10px] bg-[var(--surface)] p-1 shadow-[var(--shadow-raised)]\"\n style={{\n animation: \"mova-ai-fine-tune-card-pop-in 200ms cubic-bezier(0.23,1,0.32,1) both\",\n transformOrigin: \"bottom right\",\n }}\n >\n <GlideMenu className=\"flex flex-col gap-px\" highlightClassName=\"inset-x-0 rounded-[6px] bg-[var(--field)]\">\n {options.map((item) => (\n <button\n key={item}\n data-menu-row\n role=\"menuitemradio\"\n aria-checked={item === typeValue}\n type=\"button\"\n onClick={() => selectType(item)}\n className={`relative z-10 flex h-6.5 w-full items-center rounded-[6px] px-2 text-left text-[12.5px] text-[var(--ink)] ${\n item === typeValue ? \"bg-[var(--field)] group-hover/glide-menu:bg-transparent\" : \"\"\n }`}\n >\n {item}\n </button>\n ))}\n </GlideMenu>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n\ntype GlideMenuProps = {\n children: ReactNode;\n className?: string;\n highlightClassName?: string;\n rowSelector?: string;\n};\n\n/** A single hover layer that glides between interactive menu rows. */\nfunction GlideMenu({\n children,\n className = \"\",\n highlightClassName = \"inset-x-0 rounded-[8px] bg-[var(--hover)]\",\n rowSelector = \"[data-menu-row]\",\n}: GlideMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<{ top: number; height: number } | null>(null);\n const [visible, setVisible] = useState(false);\n\n const moveTo = (target: EventTarget | null) => {\n const container = ref.current;\n if (!(target instanceof Element) || !container) return;\n const row = target.closest(rowSelector);\n if (!(row instanceof HTMLElement) || !container.contains(row)) return;\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n setBox({ top: rowRect.top - containerRect.top, height: rowRect.height });\n setVisible(true);\n };\n\n return (\n <div\n ref={ref}\n onMouseOver={(event) => moveTo(event.target)}\n onMouseLeave={() => setVisible(false)}\n onFocusCapture={(event) => moveTo(event.target)}\n onBlurCapture={(event) => {\n if (!ref.current?.contains(event.relatedTarget as Node | null)) setVisible(false);\n }}\n className={`group/glide-menu relative ${className}`}\n >\n <span\n aria-hidden\n className={`pointer-events-none absolute ${highlightClassName}`}\n style={{\n top: box?.top ?? 0,\n height: box?.height ?? 0,\n opacity: box && visible ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/* Smooth shadow values: shadow-plugin, Copyright (c) Florian Kiem, MIT. */\nconst scopedStyles = `.mova-ai-fine-tune-card{\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--radius-control:8px;\n width:100%;max-width:240px;min-width:0;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:14px;line-height:1.5;letter-spacing:-.01em;color:var(--ink);}\n.mova-ai-fine-tune-card[data-theme=\"dark\"]{\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n.mova-ai-fine-tune-card .primitive-card-pad{padding:12px} .mova-ai-fine-tune-card .primitive-card-bar{padding:10px 12px} .mova-ai-fine-tune-card .primitive-card-footer{padding:10px} .mova-ai-fine-tune-card .primitive-icon-button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;border-radius:8px}\n.mova-ai-fine-tune-card :is(button,input,[tabindex]):focus-visible{outline:2px solid var(--accent)!important;outline-offset:2px} .mova-ai-fine-tune-card button:not(:disabled){cursor:pointer} .mova-ai-fine-tune-card code{overflow-wrap:anywhere} .mova-ai-fine-tune-card *, .mova-ai-fine-tune-card *::before,.mova-ai-fine-tune-card *::after{box-sizing:border-box}\n@keyframes mova-ai-fine-tune-card-pop-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n@keyframes mova-ai-fine-tune-card-fade-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-fine-tune-card-shimmer-text{from{background-position:150% center}to{background-position:-50% center}}\n@media(prefers-reduced-motion:reduce){.mova-ai-fine-tune-card *, .mova-ai-fine-tune-card *::before, .mova-ai-fine-tune-card *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}`;\n\nexport function AIFineTuneCard({theme = \"light\", ...props}: React.ComponentProps<typeof FineTuneCard> & {theme?: \"light\" | \"dark\"} = {}) {\n return <div className=\"mova-ai-fine-tune-card\" data-theme={theme}><style>{scopedStyles}</style><FineTuneCard {...props} /></div>;\n}\n"
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-prompt-bar",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-prompt-bar.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-prompt-bar.tsx",
|
|
7
|
+
"registryFile": "ai-prompt-bar.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AIPromptBar"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [
|
|
12
|
+
"glimm"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"previewMode": "client",
|
|
16
|
+
"content": "\"use client\";\n\"use client\";\n\n// SPDX-License-Identifier: MIT\n// Adapted from Beautiful UI, Copyright (c) 2026 Shane Levine.\n// Original composer geometry, controls, glide motion, and Glimm sweep are retained.\n\nimport { useEffect, useLayoutEffect, useRef, useState, useId } from \"react\";\nimport type { Palette, ShaderController, SweepHandle } from \"glimm\";\n\ntype SpeechRecognizer = {\n lang: string; interimResults: boolean;\n onresult: ((event: { results: ArrayLike<{ 0: { transcript: string } }> }) => void) | null;\n onend: (() => void) | null; onerror: (() => void) | null;\n start(): void; stop(): void; abort(): void;\n};\n/* Shadow tokens from shadow-plugin 2.1.1.\nMIT License\n\nCopyright (c) 2026 Florian Kiem\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\nconst COMPONENT_STYLES = `[data-mova-prompt] {--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--shadow-lg: 0 25px 50px 0 rgba(0, 0, 0, 0.05), 0 12px 24px 0 rgba(0, 0, 0, 0.04),\n 0 6px 12px 0 rgba(0, 0, 0, 0.03), 0 3px 6px 0 rgba(0, 0, 0, 0.02),\n 0 1.5px 3px 0 rgba(0, 0, 0, 0.02); font-family:Inter,ui-sans-serif,system-ui,sans-serif; font-size:14px;line-height:1.5;letter-spacing:-0.01em;color:var(--ink);\n /* surfaces — cool, blue-tinted neutrals */\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n /* ink ramp */\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n /* borders — solid and crisp, not alpha */\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n /* accent */\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n /* semantic */\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n /* chart overlays — neutral system colors with enough contrast on either theme */\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n /* shadows — our solid hairline ring + shadow-plugin's smooth layered stacks\n * (--shadow-xs…lg come from shadow-plugin/unprefixed) so every surface reads\n * consistently across the app. */\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);}\n[data-mova-prompt][data-theme=\"dark\"] {\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n /* Elevation rings go to a subtle *light* hairline in dark mode: the ring\n * paints over the surface behind the element, so a low-alpha white edge\n * dissolves into the shadow as one continuous stroke (per smooth-shadow-ring),\n * graduated by how far the surface floats. */\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n[data-mova-prompt] :is(button,input,textarea):focus-visible { outline:2px solid var(--accent);outline-offset:2px; }\n[data-mova-prompt] button { cursor:pointer; } [data-mova-prompt] button:disabled { cursor:default; }\n\n@keyframes mova-prompt-pop {from {opacity:0;transform:scale(.95)} to {opacity:1;transform:scale(1)}}\n@keyframes mova-prompt-eq {0%,100% {transform:scaleY(.35)} 50% {transform:scaleY(1)}}\n@media(prefers-reduced-motion:reduce) { [data-mova-prompt], [data-mova-prompt] * {animation:none!important;transition:none!important;scroll-behavior:auto!important;} }\n`;\n\n/* The built-in \"prism\" palette is only cyan→indigo→magenta, so a sweep\n * reads as blue/purple. Build a true full-spectrum rainbow instead. */\ntype GlimmRuntime = typeof import(\"glimm\");\n\n\n/* ─────────────────────────────────────────────────────────\n * PROMPT BAR\n * A composer with real controls: attach, @ data sources,\n * / commands, a model picker, dictation, and send.\n * Type @ or / to open the menus; ↑↓ + Enter to pick.\n * Variants: Rounded (card radius) · Pill (full radius).\n * ───────────────────────────────────────────────────────── */\n\nfunction Icon({ children, size = 15, strokeWidth = 1.8 }: { children: React.ReactNode; size?: number; strokeWidth?: number }) {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n {children}\n </svg>\n );\n}\n\nconst GLYPHS: Record<string, React.ReactNode> = {\n clip: <path d=\"m21.4 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\" />,\n chart: <path d=\"M4 20V10M10 20V4M16 20v-7M22 20H2\" />,\n layers: <g><path d=\"M12 2 2 7l10 5 10-5-10-5z\" /><path d=\"M2 17l10 5 10-5M2 12l10 5 10-5\" /></g>,\n globe: <g><circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\" /></g>,\n};\n\n/* real product marks, inline so the file stays self-contained */\nconst BRANDS: Record<string, React.ReactNode> = {\n figma: (\n <svg width=\"11\" height=\"16\" viewBox=\"0 0 38 57\" aria-hidden=\"true\">\n <path d=\"M9.5 57A9.5 9.5 0 0 0 19 47.5V38H9.5a9.5 9.5 0 0 0 0 19z\" fill=\"#0ACF83\" />\n <path d=\"M0 28.5A9.5 9.5 0 0 1 9.5 19H19v19H9.5A9.5 9.5 0 0 1 0 28.5z\" fill=\"#A259FF\" />\n <path d=\"M0 9.5A9.5 9.5 0 0 1 9.5 0H19v19H9.5A9.5 9.5 0 0 1 0 9.5z\" fill=\"#F24E1E\" />\n <path d=\"M19 0h9.5a9.5 9.5 0 1 1 0 19H19V0z\" fill=\"#FF7262\" />\n <path d=\"M38 28.5a9.5 9.5 0 1 1-19 0 9.5 9.5 0 0 1 19 0z\" fill=\"#1ABCFE\" />\n </svg>\n ),\n slack: (\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 127 127\" aria-hidden=\"true\">\n <path d=\"M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z\" fill=\"#E01E5A\" />\n <path d=\"M47 27.2c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.7 39.7.8 47 .8c7.3 0 13.2 5.9 13.2 13.2v13.2H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.3.7 54.4.7 47.1c0-7.3 5.9-13.2 13.2-13.2H47z\" fill=\"#36C5F0\" />\n <path d=\"M99.9 47.1c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V47.1zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.9C66.9 6.6 72.8.7 80.1.7c7.3 0 13.2 5.9 13.2 13.2v33.2z\" fill=\"#2EB67D\" />\n <path d=\"M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z\" fill=\"#ECB22E\" />\n </svg>\n ),\n gmail: (\n <svg width=\"15\" height=\"12\" viewBox=\"0 0 256 193\" aria-hidden=\"true\">\n <path d=\"M58.182 192.05V93.14L27.507 65.077 0 49.504v125.091c0 9.658 7.825 17.455 17.455 17.455h40.727Z\" fill=\"#4285F4\" />\n <path d=\"M197.818 192.05h40.727c9.659 0 17.455-7.826 17.455-17.455V49.505l-31.156 17.837-27.026 25.798v98.91Z\" fill=\"#34A853\" />\n <path d=\"m58.182 93.14-4.174-38.647 4.174-36.989L128 69.868l69.818-52.364 4.669 34.992-4.669 40.644L128 145.504 58.182 93.14Z\" fill=\"#EA4335\" />\n <path d=\"M197.818 17.504V93.14L256 49.504V26.231c0-21.585-24.64-33.89-41.89-20.945l-16.292 12.218Z\" fill=\"#FBBC04\" />\n <path d=\"m0 49.504 26.759 20.07L58.182 93.14V17.504L41.89 5.286C24.61-7.66 0 4.646 0 26.23v23.273Z\" fill=\"#C5221F\" />\n </svg>\n ),\n};\n\ntype Source = {\n key: string;\n name: string;\n desc: string;\n glyph?: string;\n brand?: string;\n attach?: boolean;\n connect?: boolean;\n};\n\nconst SOURCES: Source[] = [\n { key: \"attach\", name: \"Add photos & files\", desc: \"Upload from your computer\", glyph: \"clip\", attach: true },\n { key: \"scoop\", name: \"Scoop Data\", desc: \"Sales & churn metrics\", glyph: \"chart\" },\n { key: \"flavors\", name: \"Flavor records\", desc: \"26 makers, tags, links\", glyph: \"layers\" },\n { key: \"web\", name: \"Web search\", desc: \"Real-time news and info\", glyph: \"globe\" },\n { key: \"figma\", name: \"Figma\", desc: \"Design-to-code workflows\", brand: \"figma\" },\n { key: \"slack\", name: \"Slack\", desc: \"Read and manage Slack\", brand: \"slack\" },\n { key: \"gmail\", name: \"Gmail\", desc: \"Read and manage Gmail\", brand: \"gmail\", connect: true },\n];\n\nconst COMMANDS = [\n { key: \"compare\", name: \"/compare\", desc: \"Flavor vs. last summer\" },\n { key: \"churn-plan\", name: \"/churn-plan\", desc: \"Draft a churn schedule\" },\n { key: \"restock\", name: \"/restock\", desc: \"Build a reorder list\" },\n { key: \"draft-email\", name: \"/draft-email\", desc: \"Write a supplier email\" },\n { key: \"summarize\", name: \"/summarize\", desc: \"Digest the thread so far\" },\n];\n\nconst MODELS = [\n { key: \"sprinkles-5\", name: \"Sprinkles 5\", tag: \"Flagship\" },\n { key: \"vanilla-1\", name: \"Vanilla 1\", tag: \"Basic\" },\n { key: \"freezer-burn\", name: \"Freezer Burn 0.4\", tag: \"Stale\" },\n];\n\n\n\n/* self-running demo: walk the @ menu, then the / menu, and repeat.\n * Any pointer or key interaction hands control to the user. */\nconst AUTO_STEPS: {\n draft: string;\n active?: number;\n connect?: boolean;\n modelOpen?: boolean;\n model?: string;\n hold: number;\n}[] = [\n { draft: \"\", connect: false, model: \"vanilla-1\", hold: 1100 },\n { draft: \"@\", active: 0, hold: 900 },\n { draft: \"@\", active: 1, hold: 620 },\n { draft: \"@\", active: 4, hold: 620 },\n { draft: \"@\", active: 6, hold: 700 },\n { draft: \"@\", active: 6, connect: true, hold: 1000 },\n { draft: \"\", hold: 700 },\n { draft: \"/\", active: 0, hold: 900 },\n { draft: \"/\", active: 1, hold: 620 },\n { draft: \"/\", active: 3, hold: 1000 },\n { draft: \"\", hold: 800 },\n // open the model picker and upgrade to the flagship → rainbow sweep\n { draft: \"\", modelOpen: true, hold: 1200 },\n { draft: \"\", model: \"sprinkles-5\", hold: 2400 },\n { draft: \"\", hold: 900 },\n];\n\n/* the last @word or /word being typed, if any */\nfunction parseToken(draft: string): { kind: \"at\" | \"slash\"; query: string; start: number } | null {\n const match = /(^|\\s)([@/])([\\w-]*)$/.exec(draft);\n if (!match) return null;\n return {\n kind: match[2] === \"@\" ? \"at\" : \"slash\",\n query: match[3].toLowerCase(),\n start: match.index + match[1].length,\n };\n}\n\nexport function AIPromptBar({\n theme = \"dark\",\n className = \"\",\n variant = \"Rounded\",\n demo = true,\n tall = false,\n placeholder,\n onSend,\n}: {\n theme?: \"dark\" | \"light\";\n className?: string;\n variant?: string;\n /** the self-running walkthrough; turn off when embedding in a real surface */\n demo?: boolean;\n /** hero sizing: a multi-line input with controls on their own row */\n tall?: boolean;\n placeholder?: string;\n /** The first argument preserves the original text-only callback API. */\n onSend?: (text: string, submission: {\n files: File[];\n sources: Array<{ key: string; name: string; desc: string }>;\n model: { key: string; name: string; tag: string };\n }) => void;\n}) {\n const sourceMenuId = useId();\n const modelMenuId = useId();\n const [feedback, setFeedback] = useState(\"\");\n const fileRef = useRef<HTMLInputElement>(null);\n const recognitionRef = useRef<SpeechRecognizer | null>(null);\n const sweepRef = useRef<SweepHandle | null>(null);\n const glimmRuntimeRef = useRef<{ api: GlimmRuntime; rainbow: Palette } | null>(null);\n const pendingSweepRef = useRef(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const pill = variant === \"Pill\";\n const [draft, setDraft] = useState(\"\");\n const [dismissed, setDismissed] = useState(false);\n const [plusOpen, setPlusOpen] = useState(false);\n const [modelOpen, setModelOpen] = useState(false);\n const [model, setModel] = useState(MODELS[1]);\n const [attachments, setAttachments] = useState<File[]>([]);\n const [selectedSources, setSelectedSources] = useState<Source[]>([]);\n const [connected, setConnected] = useState(false);\n const [active, setActive] = useState(0);\n const [listening, setListening] = useState(false);\n const [auto, setAuto] = useState(demo);\n const [autoStep, setAutoStep] = useState(0);\n const [expanded, setExpanded] = useState(false);\n const wide = expanded || tall;\n const [rowBox, setRowBox] = useState<{ top: number; height: number } | null>(null);\n const [engaged, setEngaged] = useState(false);\n const [modelBox, setModelBox] = useState<{ top: number; height: number } | null>(null);\n const [modelHovered, setModelHovered] = useState<number | null>(null);\n const [modelMenuLeft, setModelMenuLeft] = useState(0);\n const [modelMenuBottom, setModelMenuBottom] = useState(0);\n const composerAnchorRef = useRef<HTMLDivElement>(null);\n const controlsRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const measureRef = useRef<HTMLSpanElement>(null);\n const modelRef = useRef<HTMLButtonElement>(null);\n const modelMenuRef = useRef<HTMLDivElement>(null);\n const rowRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const modelRowRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const glimmRef = useRef<HTMLCanvasElement>(null);\n const shaderRef = useRef<ShaderController | null>(null);\n const sweepingRef = useRef(false);\n\n /* hand control to the user: stop the demo loop, and when they aim at\n * the input itself, clear the demo's leftover draft for a clean start */\n const takeOver = (event: { target: EventTarget | null }) => {\n setAuto(false);\n if (auto && event.target === inputRef.current) setDraft(\"\");\n };\n\n const token = dismissed ? null : parseToken(draft);\n const menu: \"at\" | \"slash\" | null = plusOpen ? \"at\" : token?.kind ?? null;\n const query = plusOpen ? \"\" : token?.query ?? \"\";\n\n const rows: { key: string; name: string; desc: string }[] =\n menu === \"at\"\n ? SOURCES.filter((s) => s.name.toLowerCase().includes(query))\n : menu === \"slash\"\n ? COMMANDS.filter((c) => c.name.slice(1).startsWith(query))\n : [];\n\n useEffect(() => {\n setActive(0);\n setEngaged(false);\n }, [menu, query]);\n\n /* a single highlight glides to the active row instead of each row\n * toggling its own background — matches the gliding pill in the nav */\n useLayoutEffect(() => {\n const target = rowRefs.current[active];\n if (target) setRowBox({ top: target.offsetTop, height: target.offsetHeight });\n }, [menu, query, active, connected, rows.length]);\n\n /* same gliding highlight in the model menu — floats to the hovered\n * row, falling back to the currently-selected model */\n const modelIndex = MODELS.findIndex((m) => m.key === model.key);\n useLayoutEffect(() => {\n if (!modelOpen) return;\n const target = modelRowRefs.current[modelHovered ?? modelIndex];\n if (target) setModelBox({ top: target.offsetTop, height: target.offsetHeight });\n }, [modelOpen, modelHovered, modelIndex]);\n\n /* The menu is outside the clipped composer. Reposition it when the\n * preview resizes as well as when it opens. */\n useLayoutEffect(() => {\n if (!modelOpen || !composerAnchorRef.current || !modelRef.current) return;\n const reposition = () => {\n const anchorRect = composerAnchorRef.current?.getBoundingClientRect();\n const triggerRect = modelRef.current?.getBoundingClientRect();\n if (!anchorRect || !triggerRect) return;\n const menuWidth = modelMenuRef.current?.offsetWidth ?? 208;\n setModelMenuLeft(Math.max(0, Math.min(triggerRect.left - anchorRect.left, anchorRect.width - menuWidth)));\n setModelMenuBottom(anchorRect.bottom - triggerRect.top + 8);\n };\n reposition();\n const observer = new ResizeObserver(reposition);\n observer.observe(composerAnchorRef.current);\n window.addEventListener(\"resize\", reposition);\n return () => {\n observer.disconnect();\n window.removeEventListener(\"resize\", reposition);\n };\n }, [modelOpen, wide, model.name]);\n\n useEffect(() => {\n if (!modelOpen) setModelHovered(null);\n }, [modelOpen]);\n\n /* Build the shader with a pinned hue phase. createShader seeds its\n * internal hueShift from Math.random(), which made the sweep a different\n * colour on every reload — pin it so the rainbow is identical each time. */\n const makeShader = () => {\n const canvas = glimmRef.current;\n const runtime = glimmRuntimeRef.current;\n if (!runtime || !canvas || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return null;\n const random = Math.random;\n Math.random = () => 0;\n try {\n return runtime.api.createShader({\n canvas,\n palette: runtime.rainbow,\n direction: \"ltr\",\n bandTight: 10,\n swellAmount: 0.85,\n });\n } catch {\n return null;\n } finally {\n Math.random = random;\n }\n };\n\n const stopSweep = () => {\n pendingSweepRef.current = false;\n sweepingRef.current = false;\n sweepRef.current?.cancel();\n sweepRef.current = null;\n shaderRef.current?.destroy();\n shaderRef.current = null;\n };\n\n /* Glimm shader lives inside the composer, invisible at rest. Selecting\n * the flagship model fires a one-shot rainbow sweep across the interior. */\n useEffect(() => {\n let disposed = false;\n const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const motionChanged = () => { if (motionQuery.matches) stopSweep(); };\n motionQuery.addEventListener(\"change\", motionChanged);\n // Glimm is ESM-only: keep module evaluation safe for CJS and SSR consumers.\n void import(\"glimm\").then((api) => {\n if (disposed) return;\n const { ACCENTS, accentChain } = api;\n glimmRuntimeRef.current = {\n api,\n rainbow: accentChain([ACCENTS.red, ACCENTS.orange, ACCENTS.yellow, ACCENTS.green, ACCENTS.cyan, ACCENTS.blue, ACCENTS.purple]),\n };\n if (pendingSweepRef.current) celebrate();\n }).catch(() => {\n // The composer remains functional if the optional visual cannot load.\n if (!disposed) pendingSweepRef.current = false;\n });\n return () => {\n disposed = true;\n motionQuery.removeEventListener(\"change\", motionChanged);\n stopSweep();\n glimmRuntimeRef.current = null;\n recognitionRef.current?.abort();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const celebrate = () => {\n if (sweepingRef.current) return;\n if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n const runtime = glimmRuntimeRef.current;\n if (!runtime) { pendingSweepRef.current = true; return; }\n pendingSweepRef.current = false;\n // Recreate the shader per sweep so uTime restarts at 0 — the hue phase\n // (which drifts with time) is then identical on every trigger.\n shaderRef.current?.destroy();\n const shader = makeShader();\n shaderRef.current = shader;\n if (!shader) return;\n sweepingRef.current = true;\n const sweep = runtime.api.playSweep(shader, {\n palette: runtime.rainbow,\n direction: \"ltr\",\n sweepMs: 570,\n outroMs: 80,\n peakAlpha: 1.3,\n bandTight: 10,\n brightness: 1.4,\n swellAmount: 1,\n waveSpeed: 1.8,\n easing: \"easeOutExpo\",\n });\n sweepRef.current = sweep;\n sweep.done.finally(() => {\n if (sweepRef.current === sweep) {\n sweepingRef.current = false;\n sweepRef.current = null;\n shader.destroy();\n if (shaderRef.current === shader) shaderRef.current = null;\n }\n });\n };\n\n const selectModel = (next: (typeof MODELS)[number]) => {\n setModel(next);\n setModelOpen(false);\n if (next.key === \"sprinkles-5\") celebrate();\n else stopSweep();\n };\n\n /* autoplay: apply the current step, then advance after its hold */\n useEffect(() => {\n if (!auto || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n const step = AUTO_STEPS[autoStep % AUTO_STEPS.length];\n setDraft(step.draft);\n if (step.active !== undefined) setActive(step.active);\n if (step.connect !== undefined) setConnected(step.connect);\n if (step.modelOpen !== undefined) setModelOpen(step.modelOpen);\n if (step.model) {\n const next = MODELS.find((m) => m.key === step.model);\n if (next) selectModel(next);\n }\n const t = setTimeout(() => setAutoStep((s) => s + 1), step.hold);\n return () => clearTimeout(t);\n }, [auto, autoStep]);\n\n const toggleDictation = () => {\n if (listening) { recognitionRef.current?.stop(); return; }\n const speechWindow = window as Window & { SpeechRecognition?: new () => SpeechRecognizer; webkitSpeechRecognition?: new () => SpeechRecognizer };\n const Recognition = speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition;\n if (!Recognition) { setFeedback(\"Dictation is unavailable in this browser. You can type your message instead.\"); return; }\n const recognition = new Recognition();\n recognitionRef.current = recognition;\n recognition.lang = navigator.language;\n recognition.interimResults = false;\n recognition.onresult = (event) => {\n const text = Array.from(event.results).map((result) => result[0].transcript).join(\" \");\n setDraft((current) => `${current.trimEnd()} ${text}`.trimStart());\n setFeedback(\"Dictation added to your message.\");\n };\n recognition.onend = () => { setListening(false); inputRef.current?.focus(); };\n recognition.onerror = () => { setListening(false); setFeedback(\"Dictation could not start. Check microphone access or type your message.\"); };\n try { recognition.start(); setListening(true); setFeedback(\"Listening through your microphone…\"); }\n catch { setListening(false); setFeedback(\"Dictation is unavailable right now.\"); }\n };\n\n /* Move wrapped text above the controls, then grow to a compact maximum. */\n useLayoutEffect(() => {\n const input = inputRef.current;\n const controls = controlsRef.current;\n const measure = measureRef.current;\n const modelButton = modelRef.current;\n if (!input || !controls || !measure || !modelButton) return;\n\n const fixedControlsWidth = 28 * 3 + modelButton.offsetWidth;\n const inlineGaps = 4 * 4;\n const inlineInputWidth = controls.clientWidth - fixedControlsWidth - inlineGaps;\n const needsFullWidth = draft.includes(\"\\n\") || measure.offsetWidth + 8 > inlineInputWidth;\n if (needsFullWidth !== expanded) {\n setExpanded(needsFullWidth);\n }\n\n const minHeight = 28;\n const maxHeight = 100;\n input.style.height = \"0px\";\n const contentHeight = input.scrollHeight;\n input.style.height = `${Math.min(Math.max(contentHeight, minHeight), maxHeight)}px`;\n input.style.overflowY = contentHeight > maxHeight ? \"auto\" : \"hidden\";\n }, [draft, expanded]);\n\n /* clicking anywhere outside the composer closes the open menus */\n useEffect(() => {\n if (!modelOpen && !plusOpen) return;\n const close = (event: PointerEvent) => {\n if (!rootRef.current?.contains(event.target as Node)) {\n setDismissed(true);\n setModelOpen(false);\n setPlusOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [modelOpen, plusOpen]);\n\n const closeMenus = () => {\n setPlusOpen(false);\n setModelOpen(false);\n };\n\n const pick = (row: { key: string; name: string }) => {\n const source = SOURCES.find((s) => s.key === row.key);\n if (source?.attach) {\n fileRef.current?.click();\n if (token) setDraft(draft.slice(0, token.start));\n } else if (menu === \"at\") {\n if (source) setSelectedSources((current) => current.some((item) => item.key === source.key) ? current : [...current, source]);\n if (source?.connect) { setConnected(true); setFeedback(`${source.name} selected for this demo. Connect your account in your app to use live data.`); }\n setDraft(`${token ? draft.slice(0, token.start) : draft}@${row.name} `);\n } else {\n setDraft(`${token ? draft.slice(0, token.start) : draft}${row.name} `);\n }\n setPlusOpen(false);\n setDismissed(false);\n inputRef.current?.focus();\n };\n\n const canSend = draft.trim().length > 0 || attachments.length > 0;\n const send = () => {\n if (!canSend) return;\n onSend?.(draft.trim(), {\n files: [...attachments],\n sources: selectedSources.filter((source) => draft.includes(`@${source.name}`)).map(({ key, name, desc }) => ({ key, name, desc })),\n model: { ...model },\n });\n setFeedback(`Sent${attachments.length ? ` with ${attachments.length} attachment${attachments.length === 1 ? \"\" : \"s\"}` : \"\"}: ${draft.trim() || attachments.map((file) => file.name).join(\", \")}`);\n setDraft(\"\");\n setAttachments([]);\n setSelectedSources([]);\n closeMenus();\n };\n\n return (\n <div\n ref={rootRef}\n data-mova-prompt\n data-theme={theme}\n data-promptbar\n className={`${demo ? \"flex min-h-[384px] w-full max-w-[420px] flex-col justify-end pb-8\" : \"w-full\"} ${className}`}\n onPointerDownCapture={takeOver}\n onKeyDownCapture={(event) => { takeOver(event); if (event.key === \"Escape\") { event.stopPropagation(); setDismissed(true); closeMenus(); inputRef.current?.focus(); } }}\n >\n <style>{COMPONENT_STYLES}</style>\n <input ref={fileRef} type=\"file\" multiple className=\"hidden\" aria-label=\"Attach files\" onChange={(event) => { const files = Array.from(event.target.files ?? []); setAttachments((current) => [...current, ...files]); setFeedback(`${files.length} file${files.length === 1 ? \"\" : \"s\"} attached.`); event.target.value = \"\"; inputRef.current?.focus(); }} />\n {/* composer is the anchor — menus grow up from its top edge */}\n <div ref={composerAnchorRef} className=\"relative\">\n {/* ── @ / slash menu ─────────────────────────────── */}\n {menu && (\n <div\n id={sourceMenuId}\n role=\"listbox\" aria-label=\"Sources and commands\"\n onMouseLeave={() => setEngaged(false)}\n className=\"absolute inset-x-0 bottom-full z-10 mb-2 max-h-[min(320px,60dvh)] overflow-y-auto rounded-[10px] bg-[var(--surface)] p-1 shadow-[var(--shadow-raised)]\"\n style={{ animation: \"mova-prompt-pop 180ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"bottom center\" }}\n >\n {/* single gliding highlight — appears once a row is hovered */}\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-1 rounded-[6px] bg-[var(--hover)]\"\n style={{\n top: rowBox?.top ?? 0,\n height: rowBox?.height ?? 0,\n opacity: rowBox && engaged && rows.length > 0 ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {rows.map((row, i) => {\n const source = menu === \"at\" ? SOURCES.find((s) => s.key === row.key) : undefined;\n return (\n <button\n key={row.key}\n id={`${sourceMenuId}-${i}`} role=\"option\" aria-selected={i === active}\n type=\"button\"\n ref={(el) => {\n rowRefs.current[i] = el;\n }}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => {\n setActive(i);\n setEngaged(true);\n }}\n onFocus={() => { setActive(i); setEngaged(true); }}\n onClick={() => pick(row)}\n className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[6px] px-2 text-left\"\n >\n {source && (\n <span className=\"flex size-5.5 shrink-0 items-center justify-center text-[var(--ink-2)]\">\n {source.brand ? BRANDS[source.brand] : <Icon size={15}>{GLYPHS[source.glyph ?? \"clip\"]}</Icon>}\n </span>\n )}\n <span className=\"shrink-0 text-[12.5px] font-medium text-[var(--ink)]\">\n {row.name}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[12px] text-[var(--ink-3)]\">{row.desc}</span>\n {source?.connect && (\n <span\n className={`shrink-0 text-[12px] font-medium transition-colors duration-100 ${\n connected ? \"text-[var(--green)]\" : \"text-[var(--accent-ink)] hover:underline\"\n }`}\n >\n {connected ? \"Connected\" : \"Connect\"}\n </span>\n )}\n </button>\n );\n })}\n {rows.length === 0 && (\n <div className=\"flex h-9 items-center px-2 text-[12px] text-[var(--ink-3)]\">\n No matches for “{query}”\n </div>\n )}\n <div className=\"mt-1 border-t border-[var(--line)] px-2 pt-1.5 pb-1 text-[11px] text-[var(--ink-3)]\">\n {menu === \"at\" ? \"Type to search sources & files\" : \"Type to search commands\"}\n </div>\n </div>\n )}\n\n {/* ── model menu ─────────────────────────────────── */}\n {modelOpen && (\n <div\n ref={modelMenuRef}\n id={modelMenuId}\n role=\"group\" aria-label=\"Available models\"\n onKeyDown={(event) => { if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") { event.preventDefault(); const current = modelRowRefs.current.indexOf(document.activeElement as HTMLButtonElement); modelRowRefs.current[(current + (event.key === \"ArrowDown\" ? 1 : MODELS.length - 1)) % MODELS.length]?.focus(); } }}\n onMouseLeave={() => setModelHovered(null)}\n className=\"absolute z-10 w-52 max-w-full rounded-[10px] bg-[var(--surface)] p-1 shadow-[var(--shadow-raised)]\"\n style={{ left: modelMenuLeft, bottom: modelMenuBottom, animation: \"mova-prompt-pop 180ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"bottom left\" }}\n >\n {/* single gliding highlight — floats to the hovered / selected row */}\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-1 rounded-[6px] bg-[var(--hover)]\"\n style={{\n top: modelBox?.top ?? 0,\n height: modelBox?.height ?? 0,\n opacity: modelBox && modelHovered !== null ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {MODELS.map((m, i) => (\n <button\n key={m.key}\n type=\"button\"\n ref={(el) => {\n modelRowRefs.current[i] = el;\n }}\n onMouseDown={(event) => event.preventDefault()}\n onFocus={() => setModelHovered(i)}\n onMouseEnter={() => setModelHovered(i)}\n onClick={() => {\n selectModel(m);\n inputRef.current?.focus();\n }}\n className=\"relative z-10 flex h-7.5 w-full items-center gap-2 rounded-[6px] px-2 text-left\"\n >\n <span className=\"min-w-0 flex-1 truncate text-[12.5px] font-medium text-[var(--ink)]\">{m.name}</span>\n <span className=\"shrink-0 text-[11px] text-[var(--ink-3)]\">{m.tag}</span>\n <span className={`shrink-0 text-[var(--ink)] ${m.key === model.key ? \"\" : \"invisible\"}`}>\n <Icon size={13} strokeWidth={2.5}><path d=\"M20 6L9 17l-5-5\" /></Icon>\n </span>\n </button>\n ))}\n </div>\n )}\n\n {/* ── composer ───────────────────────────────────── */}\n <div\n className={`relative isolate flex flex-col overflow-hidden border border-[var(--line)] bg-[var(--surface)] shadow-[var(--shadow-card)] transition-[border-color,border-radius] duration-150 focus-within:border-[var(--line-strong)] ${\n tall ? \"gap-2.5 p-3.5\" : \"gap-1.5 p-1.5\"\n } ${\n pill ? (attachments.length > 0 || wide ? \"rounded-[24px]\" : \"rounded-full\") : tall ? \"rounded-[22px]\" : \"rounded-[14px]\"\n }`}\n >\n {/* rainbow glimm sweep — plays across the interior on model change.\n explicit w/h: a <canvas> is a replaced element and won't stretch\n to inset-0 alone, which feeds back into the shader's ResizeObserver. */}\n <canvas\n ref={glimmRef}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 -z-10 h-full w-full\"\n style={{ borderRadius: \"inherit\" }}\n />\n <span\n ref={measureRef}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute invisible whitespace-pre text-[13px] leading-[18px]\"\n >\n {draft}\n </span>\n\n {attachments.length > 0 && (\n <div className={`flex flex-wrap gap-1.5 pt-0.5 ${pill ? \"px-1\" : \"px-0.5\"}`}>\n {attachments.map((file, i) => (\n <span\n key={`${file.name}-${i}`}\n className={`flex h-6.5 items-center gap-1.5 bg-[var(--field)] py-1 pr-1 pl-1.5 text-[11.5px] text-[var(--ink-2)] shadow-[var(--shadow-hairline)] ${\n pill ? \"rounded-full\" : \"rounded-[6px]\"\n }`}\n style={{ animation: \"mova-prompt-pop 200ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n <Icon size={12}><g><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /></g></Icon>\n <span className=\"max-w-36 truncate\">{file.name}</span>\n <button\n type=\"button\"\n aria-label={`Remove ${file.name}`}\n onClick={() => setAttachments((current) => current.filter((_, j) => j !== i))}\n className={`-my-1 flex size-6 items-center justify-center text-[var(--ink-3)] transition-colors duration-100 hover:bg-[color-mix(in_srgb,var(--line)_70%,transparent)] hover:text-[var(--ink)] ${\n pill ? \"rounded-full\" : \"rounded-[5px]\"\n }`}\n >\n <Icon size={10} strokeWidth={2.5}><path d=\"M18 6L6 18M6 6l12 12\" /></Icon>\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div\n ref={controlsRef}\n className={`grid items-end gap-x-1 gap-y-1.5 ${\n wide\n ? \"grid-cols-[28px_auto_minmax(0,1fr)_28px_28px]\"\n : \"grid-cols-[28px_minmax(0,1fr)_auto_28px_28px]\"\n }`}\n >\n <button\n type=\"button\"\n aria-label=\"Add attachments and sources\"\n aria-expanded={plusOpen}\n onClick={() => {\n setModelOpen(false);\n setPlusOpen((current) => !current);\n inputRef.current?.focus();\n }}\n className={`flex size-7 shrink-0 items-center justify-center justify-self-start text-[var(--ink-3)] transition-[background-color,color,transform] duration-150 hover:bg-[var(--hover)] hover:text-[var(--ink)] active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${plusOpen ? \"bg-[var(--hover)] text-[var(--ink)]\" : \"\"} ${wide ? \"col-start-1 row-start-2\" : \"col-start-1 row-start-1\"}`}\n >\n <Icon size={16} strokeWidth={2}><path d=\"M12 5v14M5 12h14\" /></Icon>\n </button>\n\n <textarea\n ref={inputRef}\n rows={1}\n value={draft}\n onChange={(event) => {\n setDraft(event.target.value);\n setDismissed(false);\n setPlusOpen(false);\n }}\n onKeyDown={(event) => {\n if (event.nativeEvent.isComposing) return;\n if (menu && rows.length > 0) {\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n setEngaged(true);\n setActive((current) => (current + (event.key === \"ArrowDown\" ? 1 : rows.length - 1)) % rows.length);\n return;\n }\n if ((event.key === \"Enter\" && !event.shiftKey) || event.key === \"Tab\") {\n event.preventDefault();\n pick(rows[active]);\n return;\n }\n }\n if (event.key === \"Escape\") {\n setDismissed(true);\n closeMenus();\n return;\n }\n if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n event.preventDefault();\n send();\n }\n }}\n placeholder={listening ? \"Listening…\" : placeholder ?? \"Write a message…\"}\n aria-label=\"Prompt\"\n aria-expanded={!!menu}\n aria-controls={menu ? sourceMenuId : undefined}\n aria-activedescendant={menu && rows[active] ? `${sourceMenuId}-${active}` : undefined}\n className={`${tall ? \"min-h-[68px] px-2 py-2 text-[14px] leading-5\" : \"min-h-7 px-1 py-[5px] text-[13px] leading-[18px]\"} min-w-0 w-full resize-none bg-transparent text-[var(--ink)] outline-none [overflow-wrap:anywhere] placeholder:text-[var(--ink-3)] ${\n wide ? \"col-span-full col-start-1 row-start-1\" : \"col-start-2 row-start-1\"\n }`}\n />\n\n {/* model picker */}\n <button\n ref={modelRef}\n type=\"button\"\n aria-expanded={modelOpen}\n aria-label=\"Choose model\"\n aria-controls={modelMenuId}\n onKeyDown={(event) => { if (event.key === \"ArrowDown\") { event.preventDefault(); setPlusOpen(false); setDismissed(true); setModelOpen(true); requestAnimationFrame(() => modelRowRefs.current[modelIndex]?.focus()); } }}\n onClick={() => {\n setPlusOpen(false);\n setDismissed(true);\n setModelOpen((current) => !current);\n }}\n className={`flex h-7 shrink-0 items-center gap-1 px-1.5 text-[12px] font-medium text-[var(--ink-2)] transition-colors duration-150 hover:bg-[var(--hover)] hover:text-[var(--ink)] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${wide ? \"col-start-2 row-start-2 justify-self-start\" : \"col-start-3 row-start-1\"}`}\n >\n {model.name}\n <span className=\"text-[var(--ink-3)]\">\n <Icon size={11} strokeWidth={2.4}><path d=\"M6 9l6 6 6-6\" /></Icon>\n </span>\n </button>\n\n {/* dictation */}\n <button\n type=\"button\"\n aria-label={listening ? \"Stop dictation\" : \"Start dictation\"}\n aria-pressed={listening}\n onClick={toggleDictation}\n className={`flex size-7 shrink-0 items-center justify-center transition-[background-color,color,transform] duration-150 active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${listening ? \"bg-[var(--accent-tint)] text-[var(--accent-ink)]\" : \"text-[var(--ink-3)] hover:bg-[var(--hover)] hover:text-[var(--ink)]\"} ${wide ? \"col-start-4 row-start-2\" : \"col-start-4 row-start-1\"}`}\n >\n {listening ? (\n <span className=\"flex h-3.5 items-center gap-[2.5px]\">\n {[0, 1, 2].map((i) => (\n <span\n key={i}\n className=\"w-[2.5px] rounded-full bg-current\"\n style={{ height: \"100%\", animation: `mova-prompt-eq 900ms ease-in-out ${i * 150}ms infinite` }}\n />\n ))}\n </span>\n ) : (\n <Icon size={15} strokeWidth={2}><g><path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z\" /><path d=\"M19 10v2a7 7 0 0 1-14 0v-2M12 19v3\" /></g></Icon>\n )}\n </button>\n\n {/* send — tactile square (round in the pill variant) */}\n <button\n type=\"button\"\n aria-label=\"Send\"\n disabled={!canSend}\n onClick={send}\n className={`flex size-7 shrink-0 items-center justify-center transition-[background-color,color,transform] duration-200 enabled:active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${wide ? \"col-start-5 row-start-2\" : \"col-start-5 row-start-1\"}`}\n style={{\n background: canSend ? \"var(--ink)\" : \"var(--line-strong)\",\n color: canSend ? \"var(--surface)\" : \"var(--ink-2)\",\n }}\n >\n <Icon size={16} strokeWidth={2.4}><path d=\"M12 19V5M5 12l7-7 7 7\" /></Icon>\n </button>\n </div>\n </div>\n </div>\n <p role=\"status\" className=\"mt-2 min-h-4 break-words text-[11px] text-[var(--ink-3)]\">{feedback}</p>\n </div>\n );\n}\n\nexport default AIPromptBar;\n"
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-sidebar-nav",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-sidebar-nav.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-sidebar-nav.tsx",
|
|
7
|
+
"registryFile": "ai-sidebar-nav.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AISidebarNav"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [
|
|
12
|
+
"lucide-react"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"previewMode": "client",
|
|
16
|
+
"content": "\"use client\";\n\"use client\";\n\n// SPDX-License-Identifier: MIT\n// Adapted from Beautiful UI, Copyright (c) 2026 Shane Levine.\n// Retains the original 224px / 52px rail and gliding navigation motion.\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { LogOut as IconArrowBoxLeft, Check as IconCheckmark1Small, ChevronDown as IconChevronDownSmall, X as IconCrossSmall, SquarePen as IconEditBig, House as IconHome, Search as IconMagnifyingGlass, Plus as IconPlusMedium, IceCream as IconPopsicle2, Settings as IconSettingsGear1, PanelLeftClose as IconSidebarLeftArrow, UserPlus as IconUserAdd } from \"lucide-react\";\n\n/* Shadow tokens from shadow-plugin 2.1.1.\nMIT License\n\nCopyright (c) 2026 Florian Kiem\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\nconst COMPONENT_STYLES = `[data-mova-sidebar] {--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--shadow-lg: 0 25px 50px 0 rgba(0, 0, 0, 0.05), 0 12px 24px 0 rgba(0, 0, 0, 0.04),\n 0 6px 12px 0 rgba(0, 0, 0, 0.03), 0 3px 6px 0 rgba(0, 0, 0, 0.02),\n 0 1.5px 3px 0 rgba(0, 0, 0, 0.02); font-family:Inter,ui-sans-serif,system-ui,sans-serif; font-size:14px;line-height:1.5;letter-spacing:-0.01em;color:var(--ink);\n /* surfaces — cool, blue-tinted neutrals */\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n /* ink ramp */\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n /* borders — solid and crisp, not alpha */\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n /* accent */\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n /* semantic */\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n /* chart overlays — neutral system colors with enough contrast on either theme */\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n /* shadows — our solid hairline ring + shadow-plugin's smooth layered stacks\n * (--shadow-xs…lg come from shadow-plugin/unprefixed) so every surface reads\n * consistently across the app. */\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);}\n[data-mova-sidebar][data-theme=\"dark\"] {\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n /* Elevation rings go to a subtle *light* hairline in dark mode: the ring\n * paints over the surface behind the element, so a low-alpha white edge\n * dissolves into the shadow as one continuous stroke (per smooth-shadow-ring),\n * graduated by how far the surface floats. */\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n[data-mova-sidebar] :is(button,input,textarea):focus-visible { outline:2px solid var(--accent);outline-offset:2px; }\n[data-mova-sidebar] button { cursor:pointer; } [data-mova-sidebar] button:disabled { cursor:default; }\n[data-mova-sidebar] .sidebar-row{\n width: 208px;\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: var(--sidebar-easing);\n}\n\n[data-mova-sidebar] .sidebar-copy{\n opacity: 1;\n transform: translateX(0);\n transition:\n opacity var(--sidebar-copy-duration) ease-out,\n transform var(--sidebar-copy-duration) var(--sidebar-easing);\n}\n\n[data-mova-sidebar] .sidebar-logo,\n[data-mova-sidebar] .sidebar-collapse-control,\n[data-mova-sidebar] .sidebar-expand-control{\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: ease-out;\n}\n\n[data-mova-sidebar] .sidebar-expand-control{\n pointer-events: none;\n opacity: 0;\n}\n\n[data-mova-sidebar] .sidebar-glide-highlight{\n right: 8px;\n left: 8px;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-row{\n width: 36px;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-copy{\n pointer-events: none;\n opacity: 0;\n transform: translateX(calc(var(--sidebar-copy-offset) * -1));\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-workspace-control{\n pointer-events: none;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-logo,\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-collapse-control{\n pointer-events: none;\n opacity: 0;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-expand-control{\n pointer-events: auto;\n opacity: 1;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-glide-highlight{\n right: auto;\n width: 36px;\n}\n\n\n@keyframes mova-sidebar-pop {from {opacity:0;transform:scale(.95)} to {opacity:1;transform:scale(1)}}\n@keyframes mova-sidebar-eq {0%,100% {transform:scaleY(.35)} 50% {transform:scaleY(1)}}\n@media(prefers-reduced-motion:reduce) { [data-mova-sidebar], [data-mova-sidebar] * {animation:none!important;transition:none!important;scroll-behavior:auto!important;} }\n`;\n\ntype GlideMenuProps = {\n children: ReactNode;\n className?: string;\n highlightClassName?: string;\n rowSelector?: string;\n};\n\n/** A single hover layer that glides between interactive menu rows. */\nfunction GlideMenu({\n children,\n className = \"\",\n highlightClassName = \"inset-x-0 rounded-[8px] bg-[var(--hover)]\",\n rowSelector = \"[data-menu-row]\",\n}: GlideMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<{ top: number; height: number } | null>(null);\n const [visible, setVisible] = useState(false);\n\n const moveTo = (target: EventTarget | null) => {\n const container = ref.current;\n if (!(target instanceof Element) || !container) return;\n const row = target.closest(rowSelector);\n if (!(row instanceof HTMLElement) || !container.contains(row)) return;\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n setBox({ top: rowRect.top - containerRect.top, height: rowRect.height });\n setVisible(true);\n };\n\n return (\n <div\n ref={ref}\n onMouseOver={(event) => moveTo(event.target)}\n onMouseLeave={() => setVisible(false)}\n onFocusCapture={(event) => moveTo(event.target)}\n onBlurCapture={(event) => {\n if (!ref.current?.contains(event.relatedTarget as Node | null)) setVisible(false);\n }}\n className={`group/glide-menu relative ${className}`}\n >\n <span\n aria-hidden\n className={`pointer-events-none absolute ${highlightClassName}`}\n style={{\n top: box?.top ?? 0,\n height: box?.height ?? 0,\n opacity: box && visible ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/* ─────────────────────────────────────────────────────────\n * SIDEBAR NAV\n * Shared by the design-system preview and the harness shell:\n * compact workspace switcher, primary navigation, searchable\n * chat history, and a collapse that preserves icon alignment.\n * ───────────────────────────────────────────────────────── */\n\nconst WORKSPACE = { key: \"creamery\", name: \"Creamery Ops\", monogram: \"C\" };\n\nconst NAV_ITEMS = [\n { key: \"home\", label: \"Home\", icon: <IconHome size={18} /> },\n { key: \"invite\", label: \"Invite users\", icon: <IconUserAdd size={18} />, count: \"3/10\" },\n];\n\ntype SidebarRecent = {\n id: string;\n label: string;\n prompt?: string;\n};\n\nconst DEFAULT_RECENTS: SidebarRecent[] = [\n { id: \"suppliers\", label: \"Supplier records\" },\n { id: \"todos\", label: \"Urgent to-dos this morning\" },\n { id: \"flavor\", label: \"Flavor page ticket\" },\n { id: \"workload\", label: \"Workload summary\" },\n { id: \"offboarding\", label: \"Off-board a supplier\" },\n { id: \"restock\", label: \"Batch restock function\" },\n { id: \"edits\", label: \"Propose flavor edits\" },\n { id: \"subway\", label: \"Subway surfing\" },\n];\n\ntype SidebarNavProps = {\n theme?: \"dark\" | \"light\";\n onWorkspaceAction?: (action: string) => void;\n activeTitle?: string | null;\n className?: string;\n fill?: boolean;\n onNewChat?: () => void;\n onPick?: (id: string, label: string, prompt?: string) => void;\n /** controlled primary-nav selection (e.g. \"home\" | \"invite\") */\n activeNav?: string;\n onNavigate?: (key: string) => void;\n /** footer call-to-action — defaults to the demo \"Upgrade\" button */\n footerLabel?: string;\n footerIcon?: ReactNode;\n onFooterClick?: () => void;\n recents?: SidebarRecent[];\n variant?: string;\n};\n\nconst SIDEBAR_MOTION = {\n expandedWidth: 224,\n collapsedWidth: 52,\n duration: 280,\n copyDuration: 180,\n copyOffset: 8,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n};\n\n/* ─────────────────────────────────────────────────────────\n * CHAT SEARCH STORYBOARD\n *\n * 0ms search is triggered; Chats label begins fading\n * 0ms field grows right → left from the search control\n * 180ms field fills the row; cursor is focused and ready\n * ───────────────────────────────────────────────────────── */\nconst CHAT_SEARCH_MOTION = {\n duration: 180,\n closedWidth: 28,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n};\n\nfunction GlideGroup({ children }: { children: ReactNode }) {\n return (\n <GlideMenu\n rowSelector=\"[data-row]\"\n highlightClassName=\"sidebar-glide-highlight rounded-[7px] bg-[var(--hover-2)]\"\n className=\"group/glide flex flex-col gap-px\"\n >\n {children}\n </GlideMenu>\n );\n}\n\nfunction RailButton({\n icon,\n label,\n active = false,\n count,\n onClick,\n}: {\n icon: ReactNode;\n label: string;\n active?: boolean;\n count?: string;\n onClick?: () => void;\n}) {\n return (\n <button\n data-row\n aria-label={label}\n aria-current={active ? \"page\" : undefined}\n type=\"button\"\n onClick={onClick}\n className={`sidebar-row relative z-10 mx-2 flex h-8 items-center rounded-[8px] px-2 text-left\n transition-[width,background-color,color,transform] duration-150 active:scale-[0.98]\n ${active ? \"bg-[var(--hover-2)] group-hover/glide:bg-transparent\" : \"\"}`}\n >\n <span className={`flex size-5 shrink-0 items-center justify-center ${active ? \"text-[var(--ink)]\" : \"text-[var(--ink-2)]\"}`}>\n {icon}\n </span>\n <span className={`sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[14px] font-medium ${active ? \"text-[var(--ink)]\" : \"text-[var(--ink-2)]\"}`}>\n {label}\n </span>\n {count && (\n <span className=\"sidebar-copy mr-2 shrink-0 text-[12px] font-medium tabular-nums text-[var(--ink-3)]\">\n {count}\n </span>\n )}\n </button>\n );\n}\n\nfunction WorkspaceMenu({\n position,\n theme,\n onAction,\n onClose,\n}: {\n theme: \"dark\" | \"light\";\n onAction: (action: string) => void;\n position: { top: number; left: number };\n onClose: () => void;\n}) {\n const menuRef = useRef<HTMLDivElement>(null);\n useEffect(() => { menuRef.current?.querySelector<HTMLButtonElement>(\"button\")?.focus(); }, []);\n return createPortal(\n <div ref={menuRef}\n data-mova-sidebar data-theme={theme}\n data-workspace-menu\n role=\"group\" aria-label=\"Workspace menu\"\n onKeyDown={(event) => { if (event.key === \"Escape\") { event.preventDefault(); onClose(); } if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") { event.preventDefault(); const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button\")); const index = buttons.indexOf(document.activeElement as HTMLButtonElement); buttons[(index + (event.key === \"ArrowDown\" ? 1 : buttons.length - 1)) % buttons.length]?.focus(); } }}\n className=\"fixed z-50 w-64 rounded-[14px] bg-[var(--surface)] p-1.5 shadow-[var(--shadow-overlay)]\"\n style={{\n top: position.top,\n left: position.left,\n maxWidth: \"calc(100vw - 16px)\",\n maxHeight: \"calc(100dvh - 16px)\", overflowY: \"auto\",\n animation: \"mova-sidebar-pop 180ms cubic-bezier(0.23,1,0.32,1) both\",\n transformOrigin: \"top left\",\n }}\n >\n <style>{COMPONENT_STYLES}</style>\n <GlideMenu className=\"flex flex-col gap-px\" highlightClassName=\"inset-x-0 rounded-[8px] bg-[var(--hover-2)]\">\n <button\n data-menu-row\n type=\"button\"\n onClick={onClose}\n className=\"relative z-10 flex h-10 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-[7px] bg-[var(--ink)] text-[11px] font-semibold text-[var(--surface)]\">\n {WORKSPACE.monogram}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] font-medium text-[var(--ink)]\">{WORKSPACE.name}</span>\n <span className=\"shrink-0 text-[var(--ink)]\"><IconCheckmark1Small size={18} /></span>\n </button>\n <div className=\"my-1 h-px bg-[var(--line)]\" />\n {[\n { label: \"New workspace\", icon: <IconPlusMedium size={16} /> },\n { label: \"Workspace settings\", icon: <IconSettingsGear1 size={16} /> },\n { label: \"Invite team members\", icon: <IconUserAdd size={16} /> },\n ].map((item) => (\n <button\n key={item.label}\n data-menu-row\n type=\"button\"\n onClick={() => { onAction(item.label); onClose(); }}\n className=\"relative z-10 flex h-9 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-5 shrink-0 items-center justify-center text-[var(--ink-2)]\">{item.icon}</span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] text-[var(--ink)]\">{item.label}</span>\n </button>\n ))}\n <div className=\"my-1 h-px bg-[var(--line)]\" />\n <button\n data-menu-row\n type=\"button\"\n onClick={() => { onAction(\"Sign out\"); onClose(); }}\n className=\"relative z-10 flex h-9 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-5 shrink-0 items-center justify-center text-[var(--ink-2)]\"><IconArrowBoxLeft size={16} /></span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] text-[var(--ink)]\">Sign out</span>\n </button>\n </GlideMenu>\n </div>,\n document.body,\n );\n}\n\nexport function AISidebarNav({\n theme = \"dark\",\n onWorkspaceAction,\n activeTitle,\n className = \"\",\n fill = false,\n onNewChat,\n onPick,\n activeNav,\n onNavigate,\n footerLabel = \"Upgrade\",\n footerIcon,\n onFooterClick,\n recents = DEFAULT_RECENTS,\n}: SidebarNavProps) {\n const [feedback, setFeedback] = useState(\"\");\n const rootRef = useRef<HTMLElement>(null);\n const searchButtonRef = useRef<HTMLButtonElement>(null);\n const expandRef = useRef<HTMLButtonElement>(null);\n const collapseRef = useRef<HTMLButtonElement>(null);\n const [collapsed, setCollapsed] = useState(false);\n const [internalNav, setInternalNav] = useState(\"chats\");\n const currentNav = activeNav ?? internalNav;\n const selectNav = (key: string) => {\n setInternalNav(key);\n setFeedback(key === \"home\" ? \"Home selected\" : key === \"invite\" ? \"Invite users selected\" : \"Chats selected\");\n onNavigate?.(key);\n };\n const [demoActiveTitle, setDemoActiveTitle] = useState<string | null>(null);\n const [workspaceOpen, setWorkspaceOpen] = useState(false);\n const [workspacePosition, setWorkspacePosition] = useState({ top: 0, left: 0 });\n const [searchOpen, setSearchOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const workspaceButtonRef = useRef<HTMLButtonElement>(null);\n const searchRef = useRef<HTMLInputElement>(null);\n\n const selectedTitle = activeTitle === undefined ? demoActiveTitle : activeTitle;\n const visibleRecents = recents.filter((item) => item.label.toLowerCase().includes(query.trim().toLowerCase()));\n\n useEffect(() => {\n if (!workspaceOpen) return;\n const close = (event: PointerEvent) => {\n const target = event.target as Element;\n if (!workspaceButtonRef.current?.contains(target) && !target.closest(\"[data-workspace-menu]\")) {\n setWorkspaceOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [workspaceOpen]);\n\n useEffect(() => {\n if (!workspaceOpen) return;\n const close = () => { setWorkspaceOpen(false); workspaceButtonRef.current?.focus(); };\n window.addEventListener(\"resize\", close);\n return () => { window.removeEventListener(\"resize\", close); };\n }, [workspaceOpen]);\n\n useEffect(() => {\n if (searchOpen) searchRef.current?.focus();\n }, [searchOpen]);\n\n const collapse = () => {\n setCollapsed(true);\n requestAnimationFrame(() => expandRef.current?.focus());\n setWorkspaceOpen(false);\n setSearchOpen(false);\n setQuery(\"\");\n };\n\n return (\n <aside\n ref={rootRef}\n data-mova-sidebar data-theme={theme}\n data-sidebar-collapsed={collapsed}\n aria-label=\"Workspace navigation\"\n className={`relative flex shrink-0 overflow-hidden transition-[width] ${fill ? \"h-full\" : \"h-[600px]\"} ${className}`}\n style={{\n width: collapsed ? SIDEBAR_MOTION.collapsedWidth : SIDEBAR_MOTION.expandedWidth,\n transitionDuration: `${SIDEBAR_MOTION.duration}ms`,\n transitionTimingFunction: SIDEBAR_MOTION.easing,\n \"--sidebar-copy-duration\": `${SIDEBAR_MOTION.copyDuration}ms`,\n \"--sidebar-copy-offset\": `${SIDEBAR_MOTION.copyOffset}px`,\n \"--sidebar-easing\": SIDEBAR_MOTION.easing,\n } as CSSProperties}\n >\n <style>{COMPONENT_STYLES}</style>\n <div className=\"flex min-h-0 w-[224px] shrink-0 flex-col\">\n <div className=\"relative mb-2.5 h-10 shrink-0\">\n <button\n ref={workspaceButtonRef}\n data-workspace-trigger\n type=\"button\"\n aria-expanded={workspaceOpen}\n aria-hidden={collapsed}\n tabIndex={collapsed ? -1 : 0}\n onClick={() => {\n if (!workspaceOpen && workspaceButtonRef.current) {\n const rect = workspaceButtonRef.current.getBoundingClientRect();\n setWorkspacePosition({ top: Math.min(rect.bottom + 6, Math.max(8, window.innerHeight - 260)), left: Math.max(8, Math.min(rect.left, window.innerWidth - 264)) });\n }\n setWorkspaceOpen((open) => !open);\n }}\n className=\"sidebar-workspace-control absolute left-2 top-1 flex h-8 w-[164px] items-center rounded-[8px] px-2 text-left transition-[background-color,transform] duration-100 hover:bg-[var(--hover-2)] active:scale-[0.99]\"\n >\n <span className=\"sidebar-logo flex size-5 shrink-0 items-center justify-center text-[var(--ink)]\">\n <IconPopsicle2 size={18} />\n </span>\n <span className=\"sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[14px] font-medium text-[var(--ink-2)]\">\n {WORKSPACE.name}\n </span>\n <span className=\"sidebar-copy ml-1 flex shrink-0 text-[var(--ink-3)]\">\n <IconChevronDownSmall size={16} />\n </span>\n </button>\n\n {workspaceOpen && <WorkspaceMenu theme={theme} position={workspacePosition} onAction={(action) => { setFeedback(`${action} selected`); onWorkspaceAction?.(action); }} onClose={() => { setWorkspaceOpen(false); workspaceButtonRef.current?.focus(); }} />}\n\n <button\n type=\"button\"\n ref={collapseRef}\n aria-label=\"Collapse sidebar\"\n aria-hidden={collapsed}\n tabIndex={collapsed ? -1 : 0}\n onClick={collapse}\n className=\"sidebar-collapse-control absolute right-2 top-1 flex size-8 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[opacity,background-color,color] duration-150 hover:bg-[var(--hover-2)] hover:text-[var(--ink)]\"\n >\n <IconSidebarLeftArrow size={18} />\n </button>\n <button\n type=\"button\"\n ref={expandRef}\n aria-label=\"Expand sidebar\"\n aria-hidden={!collapsed}\n tabIndex={collapsed ? 0 : -1}\n onClick={() => { setCollapsed(false); requestAnimationFrame(() => collapseRef.current?.focus()); }}\n className=\"sidebar-expand-control absolute left-2 top-0.5 flex size-9 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[opacity,background-color,color] duration-150 hover:bg-[var(--hover-2)] hover:text-[var(--ink)]\"\n >\n <IconSidebarLeftArrow size={18} className=\"rotate-180\" />\n </button>\n </div>\n\n <GlideGroup>\n <RailButton\n icon={<IconEditBig size={18} />}\n label=\"New chat\"\n onClick={() => {\n if (activeTitle === undefined) setDemoActiveTitle(null);\n selectNav(\"chats\");\n setFeedback(\"New chat ready\");\n onNewChat?.();\n }}\n />\n {NAV_ITEMS.map((item) => (\n <RailButton\n key={item.key}\n icon={item.icon}\n label={item.label}\n count={item.count}\n active={currentNav === item.key}\n onClick={() => selectNav(item.key)}\n />\n ))}\n </GlideGroup>\n\n <div className=\"mt-3 min-h-0 flex-1 overflow-y-auto\" inert={collapsed}>\n <div className=\"sidebar-copy relative mx-2 mb-1 h-8\">\n <div\n aria-hidden={searchOpen}\n className={`absolute inset-0 flex items-center gap-1.5 px-2 text-[12.5px] font-medium text-[var(--ink-3)] transition-[opacity,transform] ${searchOpen ? \"pointer-events-none -translate-x-1 opacity-0\" : \"translate-x-0 opacity-100\"}`}\n style={{ transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms`, transitionTimingFunction: CHAT_SEARCH_MOTION.easing }}\n >\n <IconChevronDownSmall size={16} />\n <span>Chats</span>\n </div>\n\n <button\n type=\"button\"\n ref={searchButtonRef}\n aria-label=\"Search chats\"\n tabIndex={searchOpen ? -1 : 0}\n aria-expanded={searchOpen}\n onClick={() => setSearchOpen(true)}\n className={`absolute right-0 top-0 z-10 flex size-8 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[opacity,background-color,color,transform] hover:bg-[var(--hover-2)] hover:text-[var(--ink)] active:scale-[0.96] ${searchOpen ? \"pointer-events-none opacity-0\" : \"opacity-100\"}`}\n style={{ transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms` }}\n >\n <IconMagnifyingGlass size={16} />\n </button>\n\n <div\n className={`absolute right-0 top-0 z-20 flex h-8 items-center overflow-hidden rounded-[8px] bg-[var(--field)] text-[var(--ink-3)] shadow-[var(--shadow-hairline)] transition-[width,opacity] focus-within:text-[var(--ink-2)] ${searchOpen ? \"pointer-events-auto opacity-100\" : \"pointer-events-none opacity-0\"}`}\n style={{\n width: searchOpen ? \"100%\" : CHAT_SEARCH_MOTION.closedWidth,\n transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms`,\n transitionTimingFunction: CHAT_SEARCH_MOTION.easing,\n }}\n >\n <span className=\"ml-2 flex shrink-0 items-center justify-center\">\n <IconMagnifyingGlass size={15} />\n </span>\n <input\n ref={searchRef}\n tabIndex={searchOpen ? 0 : -1}\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setSearchOpen(false);\n setQuery(\"\");\n searchButtonRef.current?.focus();\n }\n }}\n placeholder=\"Search chats\"\n aria-label=\"Search chat history\"\n className=\"ml-1.5 min-w-0 flex-1 bg-transparent text-[13px] font-medium text-[var(--ink)] outline-none placeholder:text-[var(--ink-3)]\"\n />\n <button\n type=\"button\"\n aria-label=\"Close chat search\"\n tabIndex={searchOpen ? 0 : -1}\n onClick={() => {\n setSearchOpen(false);\n setQuery(\"\");\n searchButtonRef.current?.focus();\n }}\n className=\"flex size-8 shrink-0 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[background-color,color,transform] duration-150 hover:bg-[var(--hover-2)] hover:text-[var(--ink)] active:scale-[0.96]\"\n >\n <IconCrossSmall size={16} />\n </button>\n </div>\n </div>\n\n <GlideGroup>\n {visibleRecents.map((item) => {\n const active = item.label === selectedTitle;\n return (\n <button\n key={item.id}\n data-row\n type=\"button\"\n title={item.label}\n aria-current={active ? \"page\" : undefined}\n onClick={() => {\n selectNav(\"chats\");\n if (activeTitle === undefined) setDemoActiveTitle(item.label);\n onPick?.(item.id, item.label, item.prompt);\n }}\n className={`sidebar-row relative z-10 mx-2 flex h-8 items-center rounded-[8px] px-2 text-left transition-[width,background-color,color,transform] duration-150 active:scale-[0.98] ${\n active ? \"bg-[var(--hover-2)] group-hover/glide:bg-transparent\" : \"\"\n }`}\n >\n <span className={`sidebar-copy min-w-0 flex-1 truncate text-[14px] font-medium ${active ? \"text-[var(--ink)]\" : \"text-[var(--ink-2)]\"}`}>\n {item.label}\n </span>\n </button>\n );\n })}\n {query && visibleRecents.length === 0 && (\n <div className=\"sidebar-copy mx-2 px-2 py-2 text-[12.5px] text-[var(--ink-3)]\">No chats found</div>\n )}\n </GlideGroup>\n </div>\n\n <div inert={collapsed} className=\"sidebar-copy mx-2 mt-3 w-[208px] border-t border-[var(--line)] pt-3\">\n <button\n type=\"button\"\n onClick={() => { setFeedback(`${footerLabel} selected`); (onFooterClick ?? onNewChat)?.(); }}\n className=\"flex h-8 w-full items-center justify-center gap-1.5 rounded-[8px] bg-[var(--hover-2)] text-[12.5px] font-medium text-[var(--ink)] transition-[background-color,transform] duration-150 hover:bg-[var(--line-strong)] active:scale-[0.98]\"\n >\n {footerIcon}\n {footerLabel}\n </button>\n <p role=\"status\" className=\"mt-2 text-[11px] text-[var(--ink-3)]\">{feedback}</p>\n </div>\n </div>\n </aside>\n );\n}\n\nexport default AISidebarNav;\n"
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-sources",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-sources.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-sources.tsx",
|
|
7
|
+
"registryFile": "ai-sources.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AISource",
|
|
10
|
+
"AISources",
|
|
11
|
+
"AISourcesProps"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": [
|
|
14
|
+
"framer-motion",
|
|
15
|
+
"lucide-react"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"previewMode": "client",
|
|
19
|
+
"content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-sources (MIT). See ../patterns/smoothui.LICENSE.\nimport { ChevronRight, Globe } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { type ReactNode, useId, useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst STACK_LIMIT = 3;\nconst STACK_OVERLAP_PX = 8;\nconst FAN_GAP_PX = 3;\nconst ROW_STAGGER = 0.035;\n\nexport type AISource = { favicon?: ReactNode; id: string; snippet?: string; title: string; url: string };\nexport type AISourcesProps = { className?: string; defaultOpen?: boolean; label?: string; sources: AISource[] };\n\nconst hostOf = (url: string): string => {\n try { return new URL(url).hostname.replace(/^www\\./, \"\"); } catch { return url; }\n};\n\nconst Favicon = ({ size, source }: { size: number; source: AISource }) => (\n <span className=\"relative flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border bg-background text-muted-foreground\" style={{ height: size, width: size }}>\n <Globe aria-hidden=\"true\" size={size * 0.6} />\n {source.favicon ? <span className=\"absolute inset-0 flex size-full items-center justify-center *:size-full *:object-cover\">{source.favicon}</span> : null}\n </span>\n);\n\nexport function AISources({ className = \"\", defaultOpen = false, label = \"Sources\", sources }: AISourcesProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const [isHovered, setIsHovered] = useState(false);\n const listId = useId();\n const stacked = sources.slice(0, STACK_LIMIT);\n const overflow = sources.length - stacked.length;\n const fanned = isHovered && !isOpen && !shouldReduceMotion;\n\n return (\n <div className={`w-full ${className}`}>\n <button\n aria-expanded={isOpen}\n aria-controls={listId}\n className=\"flex cursor-pointer items-center gap-2 rounded-lg py-1 text-muted-foreground text-sm transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2\"\n onBlur={() => setIsHovered(false)}\n onClick={() => setIsOpen((current) => !current)}\n onFocus={() => setIsHovered(true)}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n type=\"button\"\n >\n <span className=\"flex items-center\">\n {stacked.map((source, index) => (\n <motion.span\n animate={{ marginLeft: index === 0 ? 0 : fanned ? FAN_GAP_PX : -STACK_OVERLAP_PX }}\n className=\"relative block\"\n key={source.id}\n style={{ zIndex: stacked.length - index }}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n >\n <Favicon size={18} source={source} />\n </motion.span>\n ))}\n {overflow > 0 ? <span className=\"ml-1 tabular-nums\">+{overflow}</span> : null}\n </span>\n <span>{label}</span>\n <motion.span animate={{ rotate: isOpen ? 90 : 0 }} className=\"flex size-4 items-center justify-center\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <ChevronRight aria-hidden=\"true\" size={14} />\n </motion.span>\n </button>\n\n <AnimatePresence initial={false}>\n {isOpen ? (\n <motion.ul\n id={listId}\n animate={{ height: \"auto\", opacity: 1 }}\n className=\"mt-1 list-none overflow-hidden\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { height: 0, opacity: 0 }}\n initial={shouldReduceMotion ? { height: \"auto\", opacity: 1 } : { height: 0, opacity: 0 }}\n transition={shouldReduceMotion ? { duration: 0 } : { height: SPRING_DEFAULT, opacity: { duration: 0.18, ease: EASE_OUT } }}\n >\n {sources.map((source, index) => (\n <motion.li animate={{ opacity: 1, y: 0 }} initial={shouldReduceMotion ? { opacity: 1, y: 0 } : { opacity: 0, y: 4 }} key={source.id} transition={shouldReduceMotion ? { duration: 0 } : { ...SPRING_DEFAULT, delay: index * ROW_STAGGER }}>\n <a className=\"flex items-start gap-2.5 rounded-lg px-1 py-1.5 no-underline transition-colors hover:bg-muted\" href={source.url} rel=\"noopener noreferrer\" target=\"_blank\">\n <motion.span className=\"mt-0.5 block shrink-0\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <Favicon size={18} source={source} />\n </motion.span>\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-foreground text-sm\">{source.title}</span>\n <span className=\"block truncate text-muted-foreground text-xs\">{source.snippet ?? hostOf(source.url)}</span>\n </span>\n </a>\n </motion.li>\n ))}\n </motion.ul>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AISources;\n"
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-suggestions",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-suggestions.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-suggestions.tsx",
|
|
7
|
+
"registryFile": "ai-suggestions.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AISuggestion",
|
|
10
|
+
"AISuggestions",
|
|
11
|
+
"AISuggestionsProps"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": [
|
|
14
|
+
"framer-motion"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"previewMode": "client",
|
|
18
|
+
"content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-suggestions (MIT). See ../patterns/smoothui.LICENSE.\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { useMemo } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst STAGGER_SECONDS = 0.045;\n\nexport type AISuggestion = { id: string; label: string };\nexport type AISuggestionsProps = {\n className?: string;\n label?: string;\n onSelect?: (suggestion: AISuggestion) => void;\n suggestions: AISuggestion[];\n};\n\nconst centreOutOrder = (count: number): number[] => {\n const middle = (count - 1) / 2;\n return Array.from({ length: count }, (_, index) => Math.abs(index - middle));\n};\n\nexport function AISuggestions({ className = \"\", label, onSelect, suggestions }: AISuggestionsProps) {\n const shouldReduceMotion = useReducedMotion();\n const delays = useMemo(() => centreOutOrder(suggestions.length), [suggestions.length]);\n\n return (\n <div className={`flex w-full flex-col gap-2 ${className}`}>\n {label ? <p className=\"text-muted-foreground text-xs uppercase tracking-wide\">{label}</p> : null}\n <ul className=\"flex list-none flex-wrap gap-2\">\n <AnimatePresence initial>\n {suggestions.map((suggestion, index) => (\n <motion.li\n animate={{ opacity: 1, scale: 1, y: 0 }}\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, scale: 0.94 }}\n initial={shouldReduceMotion ? { opacity: 1, scale: 1, y: 0 } : { opacity: 0, scale: 0.94, y: 6 }}\n key={suggestion.id}\n layout={!shouldReduceMotion}\n transition={shouldReduceMotion ? { duration: 0 } : { ...SPRING_DEFAULT, delay: (delays[index] ?? 0) * STAGGER_SECONDS }}\n >\n <motion.button\n className=\"cursor-pointer rounded-full border border-border bg-background px-3 py-1.5 text-left text-foreground text-sm transition-colors hover:border-foreground/30 hover:bg-muted\"\n onClick={() => onSelect?.(suggestion)}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n type=\"button\"\n whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}\n >\n {suggestion.label}\n </motion.button>\n </motion.li>\n ))}\n </AnimatePresence>\n </ul>\n </div>\n );\n}\n\nexport default AISuggestions;\n"
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-task-list",
|
|
3
|
+
"tier": "free",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"file": "ai-task-list.tsx",
|
|
6
|
+
"sourcePath": "ui/ai/ai-task-list.tsx",
|
|
7
|
+
"registryFile": "ai-task-list.json",
|
|
8
|
+
"exports": [
|
|
9
|
+
"AITask",
|
|
10
|
+
"AITaskList",
|
|
11
|
+
"AITaskListProps",
|
|
12
|
+
"AITaskStatus"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": [
|
|
15
|
+
"framer-motion"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"previewMode": "client",
|
|
19
|
+
"content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-task-list (MIT). See ../patterns/smoothui.LICENSE.\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { Fragment, type ReactNode } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\nconst CHECK_PATH = \"M 3.5 7.5 L 6 10 L 10.5 4.5\";\nconst BOX_SIZE = 14;\nconst UNDERLINE_SECONDS = 1.4;\nconst SUCCESS_COLOR = \"oklch(72% 0.17 150)\";\nconst DANGER_COLOR = \"oklch(63% 0.21 25)\";\n\nexport type AITaskStatus = \"pending\" | \"running\" | \"done\" | \"failed\";\nexport type AITask = { children?: AITask[]; id: string; label: string; note?: string; status: AITaskStatus };\nexport type AITaskListProps = { className?: string; label?: string; tasks: AITask[] };\n\nconst flatten = (tasks: AITask[]): AITask[] => tasks.flatMap((task) => [task, ...flatten(task.children ?? [])]);\nconst boxStroke = (status: AITaskStatus): string => status === \"failed\" ? DANGER_COLOR : status === \"done\" ? SUCCESS_COLOR : \"currentColor\";\n\nfunction TaskBox({ status, shouldReduceMotion }: { shouldReduceMotion: boolean; status: AITaskStatus }) {\n const isDone = status === \"done\";\n const isFailed = status === \"failed\";\n return (\n <span className=\"mt-0.5 flex size-3.5 shrink-0 items-center justify-center\">\n <svg aria-hidden=\"true\" className=\"size-3.5\" viewBox={`0 0 ${BOX_SIZE} ${BOX_SIZE}`}>\n <motion.rect\n animate={{ fillOpacity: isDone ? 0.12 : 0, stroke: boxStroke(status) }}\n fill={isFailed ? DANGER_COLOR : SUCCESS_COLOR}\n height={12}\n rx={3.5}\n strokeWidth={1.4}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }}\n width={12}\n x={1}\n y={1}\n />\n {isDone ? <path className=\"ai-task-draw\" d={CHECK_PATH} fill=\"none\" pathLength={1} stroke={SUCCESS_COLOR} strokeDasharray=\"1 1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.8} /> : null}\n {isFailed ? <path className=\"ai-task-draw\" d=\"M 5 5 L 9 9 M 9 5 L 5 9\" fill=\"none\" pathLength={1} stroke={DANGER_COLOR} strokeDasharray=\"1 1\" strokeLinecap=\"round\" strokeWidth={1.8} /> : null}\n </svg>\n </span>\n );\n}\n\nfunction TaskRow({ depth, shouldReduceMotion, task }: { depth: number; shouldReduceMotion: boolean; task: AITask }) {\n const isRunning = task.status === \"running\";\n const isDone = task.status === \"done\";\n return (\n <motion.li\n animate={{ opacity: isDone ? 0.65 : 1, y: isDone && !shouldReduceMotion ? 1 : 0 }}\n className=\"list-none\"\n style={{ paddingLeft: depth * 20 }}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n >\n <span className=\"relative flex items-start gap-2 py-1.5\">\n <span className=\"sr-only\">{task.status}: </span>\n <TaskBox shouldReduceMotion={shouldReduceMotion} status={task.status} />\n <span className=\"min-w-0 flex-1 break-words text-foreground text-xs leading-snug\">{task.label}</span>\n {task.note ? <span className=\"max-w-[35%] truncate text-muted-foreground text-xs tabular-nums\">{task.note}</span> : null}\n {isRunning && !shouldReduceMotion ? (\n <motion.span\n animate={{ backgroundPositionX: [\"0%\", \"200%\"] }}\n className=\"pointer-events-none absolute inset-x-0 bottom-0 h-px\"\n style={{ backgroundImage: \"linear-gradient(90deg, transparent 0%, currentColor 50%, transparent 100%)\", backgroundSize: \"50% 100%\", opacity: 0.5 }}\n transition={{ duration: UNDERLINE_SECONDS, ease: EASE_IN_OUT, repeat: Number.POSITIVE_INFINITY }}\n />\n ) : null}\n </span>\n </motion.li>\n );\n}\n\nexport function AITaskList({ className = \"\", label = \"Plan\", tasks }: AITaskListProps) {\n const shouldReduceMotion = Boolean(useReducedMotion());\n const all = flatten(tasks);\n const done = all.filter((task) => task.status === \"done\").length;\n return (\n <div className={`w-full rounded-lg border border-border bg-background p-3 ${className}`}>\n <div className=\"mb-1.5 flex items-baseline justify-between\">\n <p className=\"font-medium text-foreground text-sm\">{label}</p>\n <p className=\"text-muted-foreground text-xs tabular-nums\">{done}/{all.length}</p>\n </div>\n <style>{`\n .ai-task-draw { stroke-dashoffset: 0; }\n @media (prefers-reduced-motion: no-preference) {\n .ai-task-draw { animation: ai-task-draw 200ms cubic-bezier(0.23, 1, 0.32, 1) both; }\n }\n @keyframes ai-task-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n `}</style>\n <ul className=\"list-none\">\n {(() => {\n const renderTasks = (items: AITask[], depth: number): ReactNode => items.map((task) => <Fragment key={task.id}><TaskRow depth={depth} shouldReduceMotion={shouldReduceMotion} task={task} />{renderTasks(task.children ?? [], depth + 1)}</Fragment>);\n return renderTasks(tasks, 0);\n })()}\n </ul>\n </div>\n );\n}\n\nexport default AITaskList;\n"
|
|
20
|
+
}
|