@heyramzi/ui 6.1.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +32 -40
- package/README.md +10 -4
- package/package.json +31 -29
- package/scripts/sync-shadcn.mjs +29 -16
- package/scripts/sync-transitions.mjs +11 -11
- package/src/ai/ai-button/AiButton.svelte +86 -0
- package/src/ai/ai-button/index.ts +4 -0
- package/src/ai/confirmation/Confirmation.svelte +8 -8
- package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
- package/src/ai/generation-progress/GenerationProgress.svelte +354 -0
- package/src/ai/generation-progress/index.ts +7 -0
- package/src/ai/index.ts +16 -24
- package/src/ai/prompt-input/PromptInput.svelte +19 -1
- package/src/ai/prompt-input/PromptInputSubmit.svelte +9 -3
- package/src/ai/reasoning/Reasoning.svelte +11 -8
- package/src/ambient.d.ts +4 -0
- package/src/animations/README.md +17 -17
- package/src/animations/index.css +3 -14
- package/src/animations/upstream-coverage.test.ts +1 -1
- package/src/components/copy/clipboard.test.ts +206 -0
- package/src/components/copy/clipboard.ts +49 -0
- package/src/components/copy/copied.svelte.ts +39 -0
- package/src/components/copy/copy-button.svelte +175 -0
- package/src/components/copy/copy-button.test.ts +110 -0
- package/src/components/copy/index.ts +3 -0
- package/src/components/data-table/data-table.test.ts +134 -0
- package/src/components/data-table/data-table.ts +77 -0
- package/src/components/data-table/index.ts +20 -0
- package/src/components/data-table/infinite-scroll.svelte +69 -0
- package/src/components/data-table/pagination.svelte +62 -0
- package/src/components/data-table/row-arrow.svelte +28 -0
- package/src/components/data-table/row-context-menu.svelte +106 -0
- package/src/components/data-table/row-context-menu.test.ts +18 -0
- package/src/components/data-table/sortable-head.svelte +56 -0
- package/src/components/data-table/table-skeleton.svelte +83 -0
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +232 -0
- package/src/components/hold-to-confirm/index.ts +1 -0
- package/src/components/icon/icon.svelte +23 -0
- package/src/components/icon/index.ts +1 -0
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-flag/language-flag.svelte +60 -0
- package/src/components/list-page/detail-empty.svelte +50 -0
- package/src/components/list-page/detail-header.svelte +107 -0
- package/src/components/list-page/detail-panel.svelte +90 -0
- package/src/components/list-page/detail-row.svelte +36 -0
- package/src/components/list-page/detail-stack.svelte +33 -0
- package/src/components/list-page/empty-state.svelte +91 -0
- package/src/components/list-page/filter-item.svelte +49 -0
- package/src/components/list-page/filter-popover.svelte +88 -0
- package/src/components/list-page/filter-section.ts +12 -0
- package/src/components/list-page/grouped-list.svelte +116 -0
- package/src/components/list-page/index.ts +28 -0
- package/src/components/list-page/list-page-shell.svelte +195 -0
- package/src/components/list-page/list-table.ts +19 -0
- package/src/components/list-page/master-detail.svelte +206 -0
- package/src/components/list-page/master-detail.test.ts +37 -0
- package/src/components/list-page/notice-bar.svelte +68 -0
- package/src/components/list-page/prev-next.svelte +104 -0
- package/src/components/list-page/queue-rail.ts +25 -0
- package/src/components/list-page/rail-body.svelte +29 -0
- package/src/components/list-page/rail-empty.svelte +51 -0
- package/src/components/list-page/rail-head.svelte +49 -0
- package/src/components/list-page/rail-row.svelte +163 -0
- package/src/components/list-page/section-host.ts +24 -0
- package/src/components/list-page/section-label.svelte +39 -0
- package/src/components/list-page/step.test.ts +21 -0
- package/src/components/list-page/step.ts +10 -0
- package/src/components/list-page/toolbar-search.svelte +63 -0
- package/src/components/list-page/toolbar-trigger.ts +23 -0
- package/src/components/list-page/types.ts +1 -0
- package/src/components/locale-switch/index.ts +1 -0
- package/src/components/locale-switch/locale-switch.svelte +59 -0
- package/src/components/mark/index.ts +1 -0
- package/src/components/mark/mark.svelte +70 -0
- package/src/components/open-in/index.ts +6 -0
- package/src/components/open-in/open-in.svelte +106 -0
- package/src/components/picker/index.ts +4 -0
- package/src/components/picker/mark-picker.svelte +84 -0
- package/src/components/picker/picker.svelte +143 -0
- package/src/components/picker/trigger.ts +42 -0
- package/src/components/picker/types.ts +26 -0
- package/src/components/segmented/animated-tabs.svelte +175 -0
- package/src/components/segmented/collapse.test.ts +49 -0
- package/src/components/segmented/collapse.ts +39 -0
- package/src/components/segmented/index.ts +41 -0
- package/src/components/segmented/section-runs.svelte +79 -0
- package/src/components/segmented/segment.svelte +106 -0
- package/src/components/segmented/segmented-filter.svelte +125 -0
- package/src/components/segmented/segmented-nav.svelte +82 -0
- package/src/components/segmented/segmented-overflow.svelte +85 -0
- package/src/components/segmented/segmented-track.svelte +181 -0
- package/src/components/segmented/segmented.test.ts +20 -0
- package/src/components/segmented/segmented.ts +80 -0
- package/src/components/star-rating-input/index.ts +9 -0
- package/src/components/star-rating-input/star-rating-input.svelte +256 -0
- package/src/components/svg/index.ts +1 -0
- package/src/components/svg/svg.svelte +9 -0
- package/src/effects/border-beam/BorderBeam.svelte +1 -1
- package/src/effects/image-comparison/ImageComparison.css +42 -8
- package/src/effects/image-comparison/ImageComparison.svelte +138 -21
- package/src/effects/image-comparison/compare.test.ts +17 -0
- package/src/effects/image-comparison/compare.ts +5 -0
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +338 -0
- package/src/effects/magic-card/MagicCard.css +47 -0
- package/src/effects/magic-card/MagicCard.svelte +57 -0
- package/src/effects/marquee/Marquee.css +27 -1
- package/src/effects/marquee/Marquee.svelte +67 -3
- package/src/effects/reveal/index.ts +2 -1
- package/src/effects/reveal/observe.test.ts +60 -0
- package/src/effects/reveal/observe.ts +33 -1
- package/src/effects/reveal/types.ts +7 -0
- package/src/effects/theme-toggle/ThemeToggle.css +199 -6
- package/src/effects/theme-toggle/ThemeToggle.svelte +51 -26
- package/src/gallery.test.ts +52 -0
- package/src/index.ts +5 -1
- package/src/lib/panel.ts +7 -1
- package/src/lib/sliding-pill.ts +68 -0
- package/src/lib/theme-transition.ts +41 -13
- package/src/lib/tone.ts +48 -0
- package/src/shadcn/accordion/accordion-trigger.svelte +15 -6
- package/src/shadcn/alert/alert.svelte +3 -1
- package/src/shadcn/badge/badge.svelte +8 -7
- package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +2 -2
- package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +2 -2
- package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
- package/src/shadcn/calendar/calendar-next-button.svelte +2 -2
- package/src/shadcn/calendar/calendar-prev-button.svelte +2 -2
- package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
- package/src/shadcn/calendar/calendar.svelte +6 -0
- package/src/shadcn/code-block/code-block-actions.svelte +23 -0
- package/src/shadcn/code-block/code-block-content.svelte +55 -0
- package/src/shadcn/code-block/code-block-copy-button.svelte +10 -0
- package/src/shadcn/code-block/code-block-footer.svelte +20 -0
- package/src/shadcn/code-block/code-block-header.svelte +23 -0
- package/src/shadcn/code-block/code-block-line.svelte +71 -0
- package/src/shadcn/code-block/code-block-title.svelte +23 -0
- package/src/shadcn/code-block/code-block.svelte +47 -0
- package/src/shadcn/code-block/index.ts +42 -0
- package/src/shadcn/command/index.ts +3 -0
- package/src/shadcn/field/index.ts +3 -0
- package/src/shadcn/field/simple-field.svelte +71 -0
- package/src/shadcn/field/simple-field.test.ts +47 -0
- package/src/shadcn/hooks/highlighter.ts +132 -0
- package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
- package/src/shadcn/kbd/index.ts +10 -0
- package/src/shadcn/kbd/kbd-group.svelte +20 -0
- package/src/shadcn/kbd/kbd.svelte +23 -0
- package/src/shadcn/message-scroller/index.ts +28 -0
- package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
- package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
- package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
- package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
- package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
- package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
- package/src/shadcn/radio-group/radio-group-item.svelte +3 -2
- package/src/shadcn/sheet/sheet-content.svelte +1 -0
- package/src/shadcn/sheet/sheet.css +48 -0
- package/src/shadcn/sonner/sonner.svelte +7 -10
- package/src/shadcn/tabs/tabs-list.svelte +13 -2
- package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
- package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
- package/src/shadcn/trace/index.ts +29 -0
- package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible.svelte +20 -0
- package/src/shadcn/trace/trace-description.svelte +20 -0
- package/src/shadcn/trace/trace-icon.svelte +24 -0
- package/src/shadcn/trace/trace-line.svelte +31 -0
- package/src/shadcn/trace/trace-title.svelte +20 -0
- package/src/shadcn/trace/trace.svelte +23 -0
- package/src/ai/composer/Composer.css +0 -108
- package/src/ai/composer/Composer.svelte +0 -111
- package/src/ai/composer/index.ts +0 -10
- package/src/ai/conversation/Conversation.svelte +0 -28
- package/src/ai/conversation/ConversationContent.svelte +0 -35
- package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
- package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
- package/src/ai/conversation/index.ts +0 -33
- package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
- package/src/animations/accordion.css +0 -58
- package/src/animations/icon-swap.css +0 -43
- package/src/animations/notification-badge.css +0 -81
- package/src/animations/panel.css +0 -86
- package/src/animations/reasoning-stream.css +0 -49
- package/src/animations/skeleton-reveal.css +0 -97
- package/src/animations/streaming-text.css +0 -31
- package/src/animations/tabs-sliding.css +0 -83
- package/src/animations/texts-reveal.css +0 -70
- package/src/animations/tooltip.css +0 -72
- package/src/animations/view-transition-reveal.ts +0 -74
- package/src/charts/animated-value.css +0 -7
- package/src/charts/animated-value.svelte +0 -110
- package/src/charts/area-chart.css +0 -29
- package/src/charts/area-chart.svelte +0 -195
- package/src/charts/bar-chart.css +0 -119
- package/src/charts/bar-chart.svelte +0 -269
- package/src/charts/charts.css +0 -201
- package/src/charts/crosshair.svelte +0 -35
- package/src/charts/donut-chart.css +0 -56
- package/src/charts/donut-chart.svelte +0 -158
- package/src/charts/edge-fade.svelte +0 -43
- package/src/charts/edge-fade.ts +0 -16
- package/src/charts/figure.css +0 -100
- package/src/charts/figure.svelte +0 -122
- package/src/charts/gauge-chart.css +0 -94
- package/src/charts/gauge-chart.svelte +0 -303
- package/src/charts/hover-band.test.ts +0 -30
- package/src/charts/hover-band.ts +0 -21
- package/src/charts/hover-focus.svelte +0 -85
- package/src/charts/index.ts +0 -30
- package/src/charts/legend.css +0 -131
- package/src/charts/legend.svelte +0 -112
- package/src/charts/line-chart.css +0 -26
- package/src/charts/line-chart.svelte +0 -256
- package/src/charts/motion.ts +0 -19
- package/src/charts/number-chart.css +0 -168
- package/src/charts/number-chart.svelte +0 -177
- package/src/charts/primitives/chart-container.svelte +0 -57
- package/src/charts/primitives/chart-style.svelte +0 -36
- package/src/charts/primitives/chart-tooltip.svelte +0 -161
- package/src/charts/primitives/chart-utils.ts +0 -54
- package/src/charts/primitives/index.ts +0 -11
- package/src/charts/reveal.svelte +0 -61
- package/src/charts/ring-chart.css +0 -96
- package/src/charts/ring-chart.svelte +0 -158
- package/src/charts/sparkline.css +0 -45
- package/src/charts/sparkline.svelte +0 -140
- package/src/charts/stat-card.css +0 -103
- package/src/charts/stat-card.svelte +0 -267
- package/src/charts/time-series-chart.css +0 -28
- package/src/charts/time-series-chart.svelte +0 -179
- package/src/charts/trend-pill.css +0 -38
- package/src/charts/trend-pill.svelte +0 -48
- package/src/lib/hooks/is-mobile.svelte.ts +0 -11
- package/src/lib/utils.ts +0 -45
- package/src/showcase/AiSection.svelte +0 -157
- package/src/showcase/AnimationsSection.svelte +0 -1658
- package/src/showcase/ChartsSection.svelte +0 -458
- package/src/showcase/EffectsSection.svelte +0 -152
- package/src/showcase/ShadcnSection.svelte +0 -27
- package/src/showcase/ShowcaseImport.svelte +0 -126
- package/src/showcase/ShowcaseList.svelte +0 -132
- package/src/showcase/ShowcaseNav.svelte +0 -247
- package/src/showcase/ShowcaseSection.svelte +0 -112
- package/src/showcase/ShowcaseShell.svelte +0 -55
- package/src/showcase/ShowcaseStage.svelte +0 -342
- package/src/showcase/ShowcaseVariant.svelte +0 -109
- package/src/showcase/assets/image-reveal/art-1.jpg +0 -0
- package/src/showcase/assets/image-reveal/art-2.jpg +0 -0
- package/src/showcase/assets/image-reveal/art-3.jpg +0 -0
- package/src/showcase/check.svg +0 -1
- package/src/showcase/chevron-arms.svg +0 -2
- package/src/showcase/chevron-down.svg +0 -1
- package/src/showcase/copy.svg +0 -2
- package/src/showcase/demos/ai/confirmation.svelte +0 -59
- package/src/showcase/demos/ai/conversation.svelte +0 -59
- package/src/showcase/demos/ai/loading-state.svelte +0 -19
- package/src/showcase/demos/ai/prompt-input.svelte +0 -57
- package/src/showcase/demos/ai/reasoning.svelte +0 -19
- package/src/showcase/demos/ai/thinking-dots.svelte +0 -15
- package/src/showcase/demos/ai/voice-waveform.svelte +0 -24
- package/src/showcase/demos/effects/animated-beam.svelte +0 -50
- package/src/showcase/demos/effects/animated-group.svelte +0 -47
- package/src/showcase/demos/effects/border-beam-default.svelte +0 -20
- package/src/showcase/demos/effects/border-beam-line.svelte +0 -20
- package/src/showcase/demos/effects/border-beam-mono.svelte +0 -20
- package/src/showcase/demos/effects/border-beam-sunset.svelte +0 -20
- package/src/showcase/demos/effects/image-comparison.svelte +0 -25
- package/src/showcase/demos/effects/image-reveal.svelte +0 -49
- package/src/showcase/demos/effects/in-view.svelte +0 -41
- package/src/showcase/demos/effects/integrations-hub.svelte +0 -82
- package/src/showcase/demos/effects/magnetic.svelte +0 -31
- package/src/showcase/demos/effects/marquee.svelte +0 -33
- package/src/showcase/demos/effects/progressive-blur.svelte +0 -47
- package/src/showcase/demos/effects/spotlight.svelte +0 -40
- package/src/showcase/demos/effects/text-effect.svelte +0 -55
- package/src/showcase/demos/shadcn/accordion.svelte +0 -45
- package/src/showcase/demos/shadcn/alert-dialog.svelte +0 -23
- package/src/showcase/demos/shadcn/alert.svelte +0 -30
- package/src/showcase/demos/shadcn/attachment.svelte +0 -71
- package/src/showcase/demos/shadcn/avatar.svelte +0 -30
- package/src/showcase/demos/shadcn/badge.svelte +0 -26
- package/src/showcase/demos/shadcn/breadcrumb.svelte +0 -34
- package/src/showcase/demos/shadcn/bubble.svelte +0 -36
- package/src/showcase/demos/shadcn/button.svelte +0 -11
- package/src/showcase/demos/shadcn/calendar.svelte +0 -8
- package/src/showcase/demos/shadcn/card.svelte +0 -39
- package/src/showcase/demos/shadcn/checkbox.svelte +0 -39
- package/src/showcase/demos/shadcn/collapsible.svelte +0 -20
- package/src/showcase/demos/shadcn/command.svelte +0 -48
- package/src/showcase/demos/shadcn/context-menu.svelte +0 -54
- package/src/showcase/demos/shadcn/demo-harness.svelte +0 -12
- package/src/showcase/demos/shadcn/demos.test.ts +0 -20
- package/src/showcase/demos/shadcn/dialog.svelte +0 -38
- package/src/showcase/demos/shadcn/dropdown-menu.svelte +0 -59
- package/src/showcase/demos/shadcn/empty.svelte +0 -29
- package/src/showcase/demos/shadcn/field.svelte +0 -115
- package/src/showcase/demos/shadcn/hover-card.svelte +0 -32
- package/src/showcase/demos/shadcn/index.ts +0 -358
- package/src/showcase/demos/shadcn/input-otp.svelte +0 -19
- package/src/showcase/demos/shadcn/input.svelte +0 -5
- package/src/showcase/demos/shadcn/label.svelte +0 -11
- package/src/showcase/demos/shadcn/marker.svelte +0 -30
- package/src/showcase/demos/shadcn/message.svelte +0 -75
- package/src/showcase/demos/shadcn/popover.svelte +0 -36
- package/src/showcase/demos/shadcn/progress.svelte +0 -13
- package/src/showcase/demos/shadcn/radio-group.svelte +0 -19
- package/src/showcase/demos/shadcn/scroll-area.svelte +0 -18
- package/src/showcase/demos/shadcn/select.svelte +0 -29
- package/src/showcase/demos/shadcn/separator.svelte +0 -12
- package/src/showcase/demos/shadcn/sheet.svelte +0 -48
- package/src/showcase/demos/shadcn/skeleton.svelte +0 -11
- package/src/showcase/demos/shadcn/slider.svelte +0 -7
- package/src/showcase/demos/shadcn/sonner.svelte +0 -18
- package/src/showcase/demos/shadcn/spinner.svelte +0 -11
- package/src/showcase/demos/shadcn/switch.svelte +0 -9
- package/src/showcase/demos/shadcn/table.svelte +0 -76
- package/src/showcase/demos/shadcn/tabs.svelte +0 -62
- package/src/showcase/demos/shadcn/textarea.svelte +0 -5
- package/src/showcase/demos/shadcn/toggle-group.svelte +0 -18
- package/src/showcase/demos/shadcn/toggle.svelte +0 -14
- package/src/showcase/demos/shadcn/tooltip.svelte +0 -13
- package/src/showcase/heart.svg +0 -1
- package/src/showcase/index.ts +0 -38
- package/src/showcase/kit.test.ts +0 -72
- package/src/showcase/kit.ts +0 -17
- package/src/showcase/plus.svg +0 -1
- package/src/showcase/showcase-shell.css +0 -91
- package/src/showcase/stacked-section.css +0 -15
- package/src/showcase/success-check.svg +0 -1
- package/src/showcase/types.ts +0 -29
- package/src/svg-raw.d.ts +0 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Action } from "svelte/action";
|
|
2
|
+
import type { InViewActionOptions, RevealViewOptions } from "./types.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Runs `onChange` when `el` enters or leaves the viewport. Returns the teardown.
|
|
@@ -31,3 +32,34 @@ export function observeInView(
|
|
|
31
32
|
observer.observe(el);
|
|
32
33
|
return () => observer.disconnect();
|
|
33
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Svelte action that calls back when the node enters or leaves the viewport.
|
|
38
|
+
* Options are read at call time, so a `use:onInView={...}` update swaps the callbacks.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <section use:onInView={{ amount: 0.2, onEnter: () => (visible = true) }}>
|
|
42
|
+
*/
|
|
43
|
+
export const onInView: Action<HTMLElement, InViewActionOptions | undefined> = (node, initial) => {
|
|
44
|
+
let options = initial ?? {};
|
|
45
|
+
let entered = false;
|
|
46
|
+
|
|
47
|
+
const { amount = 0.1, margin, once } = options;
|
|
48
|
+
|
|
49
|
+
const stop = observeInView(node, { amount, margin, once }, (inView) => {
|
|
50
|
+
if (inView && !entered) {
|
|
51
|
+
entered = true;
|
|
52
|
+
options.onEnter?.();
|
|
53
|
+
} else if (!inView && entered) {
|
|
54
|
+
entered = false;
|
|
55
|
+
options.onLeave?.();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
update(next) {
|
|
61
|
+
options = next ?? {};
|
|
62
|
+
},
|
|
63
|
+
destroy: stop,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -23,3 +23,10 @@ export type RevealViewOptions = {
|
|
|
23
23
|
/** `rootMargin` for the observer, e.g. `"0px 0px -15% 0px"`. */
|
|
24
24
|
margin?: string;
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
export type InViewActionOptions = RevealViewOptions & {
|
|
28
|
+
/** Called when the node enters the viewport. */
|
|
29
|
+
onEnter?: () => void;
|
|
30
|
+
/** Called when the node leaves the viewport. Only fires when `once` is false. */
|
|
31
|
+
onLeave?: () => void;
|
|
32
|
+
};
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
::view-transition-old(root),
|
|
2
|
-
::view-transition-new(root) {
|
|
3
|
-
animation: none;
|
|
4
|
-
mix-blend-mode: normal;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
1
|
.theme-toggle:active {
|
|
8
2
|
transform: scale(0.97);
|
|
9
3
|
}
|
|
@@ -19,3 +13,202 @@
|
|
|
19
13
|
background: color-mix(in oklch, var(--color-background) 12%, transparent);
|
|
20
14
|
color: var(--color-background);
|
|
21
15
|
}
|
|
16
|
+
|
|
17
|
+
/* The slide variant: a pill track with a thumb that moves to the active side.
|
|
18
|
+
A caller sets brand colours with custom properties on the class:
|
|
19
|
+
--theme-toggle-thumb-light, --theme-toggle-thumb-dark, --theme-toggle-track,
|
|
20
|
+
--theme-toggle-track-hover, --theme-toggle-sun, --theme-toggle-moon,
|
|
21
|
+
--theme-toggle-thumb-icon and --theme-toggle-glow. */
|
|
22
|
+
.theme-toggle-slide {
|
|
23
|
+
--tt-thumb: 1.5rem;
|
|
24
|
+
--tt-pad: 0.25rem;
|
|
25
|
+
position: relative;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
box-sizing: content-box;
|
|
29
|
+
width: calc(var(--tt-thumb) * 2);
|
|
30
|
+
height: var(--tt-thumb);
|
|
31
|
+
padding: var(--tt-pad);
|
|
32
|
+
border: 0;
|
|
33
|
+
border-radius: 9999px;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
background: var(
|
|
36
|
+
--theme-toggle-track,
|
|
37
|
+
color-mix(in oklch, var(--color-foreground) 10%, transparent)
|
|
38
|
+
);
|
|
39
|
+
transition:
|
|
40
|
+
background-color 300ms,
|
|
41
|
+
box-shadow 300ms,
|
|
42
|
+
transform 300ms;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.theme-toggle-slide--lg {
|
|
46
|
+
--tt-thumb: 2rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.theme-toggle-slide:hover {
|
|
50
|
+
background: var(
|
|
51
|
+
--theme-toggle-track-hover,
|
|
52
|
+
color-mix(in oklch, var(--color-foreground) 20%, transparent)
|
|
53
|
+
);
|
|
54
|
+
box-shadow: 0 0 15px color-mix(in oklch, var(--theme-toggle-glow, transparent) 30%, transparent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.theme-toggle-slide:active {
|
|
58
|
+
transform: scale(0.97);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.theme-toggle-slide:focus-visible {
|
|
62
|
+
outline: 2px solid var(--color-ring, var(--color-primary));
|
|
63
|
+
outline-offset: 2px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.theme-toggle-slide__glow {
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: 0;
|
|
69
|
+
border-radius: inherit;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
opacity: 0;
|
|
72
|
+
background: radial-gradient(
|
|
73
|
+
circle at var(--tt-glow-x, 25%) 50%,
|
|
74
|
+
color-mix(in oklch, var(--theme-toggle-glow, transparent) 20%, transparent),
|
|
75
|
+
transparent 70%
|
|
76
|
+
);
|
|
77
|
+
transition: opacity 500ms;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.theme-toggle-slide:hover .theme-toggle-slide__glow {
|
|
81
|
+
opacity: 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.theme-toggle-slide[data-state="dark"] {
|
|
85
|
+
--tt-glow-x: 75%;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.theme-toggle-slide__icon {
|
|
89
|
+
position: absolute;
|
|
90
|
+
display: flex;
|
|
91
|
+
opacity: 0.3;
|
|
92
|
+
transform: scale(0.75);
|
|
93
|
+
transition:
|
|
94
|
+
opacity 300ms,
|
|
95
|
+
transform 300ms;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.theme-toggle-slide__icon--sun {
|
|
99
|
+
left: calc(var(--tt-pad) + (var(--tt-thumb) - 14px) / 2);
|
|
100
|
+
color: var(--theme-toggle-sun, var(--color-muted-foreground));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.theme-toggle-slide__icon--moon {
|
|
104
|
+
right: calc(var(--tt-pad) + (var(--tt-thumb) - 14px) / 2);
|
|
105
|
+
color: var(--theme-toggle-moon, var(--color-muted-foreground));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.theme-toggle-slide[data-state="light"] .theme-toggle-slide__icon--sun,
|
|
109
|
+
.theme-toggle-slide[data-state="dark"] .theme-toggle-slide__icon--moon {
|
|
110
|
+
opacity: 0.7;
|
|
111
|
+
transform: scale(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.theme-toggle-slide[data-state="light"] .theme-toggle-slide__icon--sun {
|
|
115
|
+
animation: theme-toggle-spin 8s linear infinite;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.theme-toggle-slide[data-state="dark"] .theme-toggle-slide__icon--moon {
|
|
119
|
+
animation: theme-toggle-wiggle 2s ease-in-out infinite;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.theme-toggle-slide__thumb {
|
|
123
|
+
--tt-thumb-color: var(--theme-toggle-thumb-light, var(--color-primary));
|
|
124
|
+
position: relative;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
width: var(--tt-thumb);
|
|
129
|
+
height: var(--tt-thumb);
|
|
130
|
+
border-radius: 9999px;
|
|
131
|
+
background: var(--tt-thumb-color);
|
|
132
|
+
color: var(--theme-toggle-thumb-icon, var(--color-primary-foreground));
|
|
133
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
134
|
+
transition:
|
|
135
|
+
transform 500ms cubic-bezier(0.165, 0.84, 0.44, 1),
|
|
136
|
+
background-color 500ms;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.theme-toggle-slide[data-state="dark"] .theme-toggle-slide__thumb {
|
|
140
|
+
--tt-thumb-color: var(--theme-toggle-thumb-dark, var(--color-primary));
|
|
141
|
+
transform: translateX(var(--tt-thumb));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.theme-toggle-slide__ring {
|
|
145
|
+
position: absolute;
|
|
146
|
+
inset: 0;
|
|
147
|
+
border-radius: inherit;
|
|
148
|
+
background: color-mix(in oklch, var(--tt-thumb-color) 50%, transparent);
|
|
149
|
+
animation: theme-toggle-pulse 2s ease-in-out infinite;
|
|
150
|
+
pointer-events: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.theme-toggle-slide__mark {
|
|
154
|
+
position: absolute;
|
|
155
|
+
transition:
|
|
156
|
+
opacity 300ms,
|
|
157
|
+
transform 300ms;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.theme-toggle-slide__mark--sun {
|
|
161
|
+
opacity: 1;
|
|
162
|
+
transform: scale(1) rotate(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.theme-toggle-slide__mark--moon {
|
|
166
|
+
opacity: 0;
|
|
167
|
+
transform: scale(0) rotate(-90deg);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.theme-toggle-slide[data-state="dark"] .theme-toggle-slide__mark--sun {
|
|
171
|
+
opacity: 0;
|
|
172
|
+
transform: scale(0) rotate(90deg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.theme-toggle-slide[data-state="dark"] .theme-toggle-slide__mark--moon {
|
|
176
|
+
opacity: 1;
|
|
177
|
+
transform: scale(1) rotate(0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@keyframes theme-toggle-spin {
|
|
181
|
+
to {
|
|
182
|
+
transform: rotate(360deg);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@keyframes theme-toggle-wiggle {
|
|
187
|
+
0%,
|
|
188
|
+
100% {
|
|
189
|
+
transform: rotate(-5deg);
|
|
190
|
+
}
|
|
191
|
+
50% {
|
|
192
|
+
transform: rotate(5deg);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@keyframes theme-toggle-pulse {
|
|
197
|
+
0%,
|
|
198
|
+
100% {
|
|
199
|
+
transform: scale(1);
|
|
200
|
+
opacity: 0;
|
|
201
|
+
}
|
|
202
|
+
50% {
|
|
203
|
+
transform: scale(1.5);
|
|
204
|
+
opacity: 0.4;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@media (prefers-reduced-motion: reduce) {
|
|
209
|
+
.theme-toggle-slide,
|
|
210
|
+
.theme-toggle-slide * {
|
|
211
|
+
animation: none !important;
|
|
212
|
+
transition: none !important;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
// =========================
|
|
5
5
|
import "./ThemeToggle.css";
|
|
6
6
|
import { cn } from "@heyramzi/utils";
|
|
7
|
-
import
|
|
8
|
-
import MoonIcon from "@lucide/svelte/icons/moon";
|
|
9
|
-
import SunIcon from "@lucide/svelte/icons/sun";
|
|
7
|
+
import { Monitor, Moon, Sun } from "phosphor-svelte";
|
|
10
8
|
import { revealTheme } from "../../lib/theme-transition.js";
|
|
11
9
|
|
|
12
10
|
// =========================
|
|
@@ -15,13 +13,15 @@
|
|
|
15
13
|
interface Props {
|
|
16
14
|
/** Used when `mode` is absent. */
|
|
17
15
|
isDark?: boolean;
|
|
18
|
-
/** The icon follows the mode: Sun, Moon or Monitor. */
|
|
16
|
+
/** The icon follows the mode: Sun, Moon or Monitor. The slide variant has no third state, so `system` reads as light. */
|
|
19
17
|
mode?: "light" | "dark" | "system";
|
|
20
18
|
/** Fires after the wipe has started. The caller flips the theme here. */
|
|
21
19
|
onToggle: (event: MouseEvent) => void;
|
|
22
|
-
|
|
20
|
+
/** `slide` is a pill track with a thumb that moves between a sun and a moon. */
|
|
21
|
+
variant?: "outline" | "ghost" | "inverted" | "slide";
|
|
23
22
|
/** Alias for `variant="inverted"`. */
|
|
24
23
|
inverted?: boolean;
|
|
24
|
+
/** `sm` is 32px tall, `default` is 40px, in every variant. */
|
|
25
25
|
size?: "default" | "sm";
|
|
26
26
|
/** Overrides the aria-label and the screen-reader text. */
|
|
27
27
|
label?: string;
|
|
@@ -78,24 +78,49 @@
|
|
|
78
78
|
<!-- =========================== -->
|
|
79
79
|
<!-- MARKUP -->
|
|
80
80
|
<!-- =========================== -->
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</
|
|
81
|
+
{#if style === "slide"}
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onclick={handleClick}
|
|
85
|
+
aria-label={text}
|
|
86
|
+
{title}
|
|
87
|
+
data-state={active === "dark" ? "dark" : "light"}
|
|
88
|
+
class={cn("theme-toggle-slide", size === "default" && "theme-toggle-slide--lg", className)}
|
|
89
|
+
>
|
|
90
|
+
<span class="theme-toggle-slide__glow" aria-hidden="true"></span>
|
|
91
|
+
<span class="theme-toggle-slide__icon theme-toggle-slide__icon--sun" aria-hidden="true">
|
|
92
|
+
<Sun size={14} weight="fill" />
|
|
93
|
+
</span>
|
|
94
|
+
<span class="theme-toggle-slide__icon theme-toggle-slide__icon--moon" aria-hidden="true">
|
|
95
|
+
<Moon size={14} weight="fill" />
|
|
96
|
+
</span>
|
|
97
|
+
<span class="theme-toggle-slide__thumb" aria-hidden="true">
|
|
98
|
+
<span class="theme-toggle-slide__ring"></span>
|
|
99
|
+
<Sun size={14} weight="fill" class="theme-toggle-slide__mark theme-toggle-slide__mark--sun" />
|
|
100
|
+
<Moon size={14} weight="fill" class="theme-toggle-slide__mark theme-toggle-slide__mark--moon" />
|
|
101
|
+
</span>
|
|
102
|
+
<span class="sr-only">{text}</span>
|
|
103
|
+
</button>
|
|
104
|
+
{:else}
|
|
105
|
+
<button
|
|
106
|
+
onclick={handleClick}
|
|
107
|
+
aria-label={text}
|
|
108
|
+
{title}
|
|
109
|
+
class={cn(
|
|
110
|
+
"theme-toggle relative flex cursor-pointer items-center justify-center rounded-lg transition-all duration-300",
|
|
111
|
+
size === "sm" ? "size-8" : "size-10",
|
|
112
|
+
style === "inverted" && "theme-toggle--inverted",
|
|
113
|
+
style === "ghost" && "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
|
|
114
|
+
style === "outline" &&
|
|
115
|
+
"border border-border/60 bg-muted/30 text-muted-foreground hover:border-border hover:text-foreground",
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<Sun class={iconClass(active === "light", "rotate-90")} />
|
|
120
|
+
<Moon class={iconClass(active === "dark", "-rotate-90")} />
|
|
121
|
+
{#if mode}
|
|
122
|
+
<Monitor class={iconClass(active === "system", "rotate-90")} />
|
|
123
|
+
{/if}
|
|
124
|
+
<span class="sr-only">{text}</span>
|
|
125
|
+
</button>
|
|
126
|
+
{/if}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The gallery lives on the site, one copy of every demo, and it has to hold everything this package ships.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* These read the site's files off disk, so an animation without a stage or a synced primitive without a demo fails here and not in front of a reader.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
12
|
+
|
|
13
|
+
/** The runner's cwd is the workspace root or the package; find the package. */
|
|
14
|
+
function packageRoot(): string {
|
|
15
|
+
let dir = process.cwd();
|
|
16
|
+
for (;;) {
|
|
17
|
+
if (existsSync(join(dir, ".upstream", "shadcn.lock.json"))) return dir;
|
|
18
|
+
const candidate = join(dir, "packages", "ui");
|
|
19
|
+
if (existsSync(join(candidate, ".upstream", "shadcn.lock.json"))) return candidate;
|
|
20
|
+
const parent = dirname(dir);
|
|
21
|
+
if (parent === dir) throw new Error("packages/ui not found");
|
|
22
|
+
dir = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PKG = packageRoot();
|
|
27
|
+
const SITE = join(PKG, "..", "..", "site", "src", "lib");
|
|
28
|
+
|
|
29
|
+
describe("site gallery coverage", () => {
|
|
30
|
+
it("gives every animation a stage", () => {
|
|
31
|
+
const section = readFileSync(join(SITE, "demos", "motion.svelte"), "utf8");
|
|
32
|
+
const missing = readdirSync(join(PKG, "src", "animations"))
|
|
33
|
+
.filter((file) => file.endsWith(".css") && file !== "index.css")
|
|
34
|
+
.filter((file) => !section.includes(`animations/${file.replace(".css", "")}`));
|
|
35
|
+
expect(missing).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("lists every synced shadcn demo", () => {
|
|
39
|
+
const manifest = readFileSync(join(SITE, "shadcn-demos", "index.ts"), "utf8");
|
|
40
|
+
const vendored = readdirSync(join(PKG, ".upstream", "shadcn-demos"))
|
|
41
|
+
.filter((file) => file.endsWith(".svelte"))
|
|
42
|
+
.map((file) => file.replace(".svelte", ""));
|
|
43
|
+
expect(vendored.filter((name) => !manifest.includes(`name: "${name}"`))).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps a demo file behind every listed primitive", () => {
|
|
47
|
+
const manifest = readFileSync(join(SITE, "shadcn-demos", "index.ts"), "utf8");
|
|
48
|
+
const listed = [...manifest.matchAll(/name: "([a-z-]+)"/g)].map((m) => m[1]);
|
|
49
|
+
expect(listed.length).toBeGreaterThan(0);
|
|
50
|
+
expect(listed.filter((name) => !existsSync(join(SITE, "shadcn-demos", `${name}.svelte`)))).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type { AnimatedBeamProps, AnchorPosition } from "./effects/animated-beam/
|
|
|
16
16
|
|
|
17
17
|
export { default as Spotlight } from "./effects/spotlight/Spotlight.svelte";
|
|
18
18
|
|
|
19
|
+
export { default as MagicCard } from "./effects/magic-card/MagicCard.svelte";
|
|
20
|
+
|
|
19
21
|
export { default as Marquee } from "./effects/marquee/Marquee.svelte";
|
|
20
22
|
|
|
21
23
|
export { default as ImageReveal } from "./effects/image-reveal/ImageReveal.svelte";
|
|
@@ -40,10 +42,12 @@ export type {
|
|
|
40
42
|
|
|
41
43
|
export { default as ProgressiveBlur } from "./effects/progressive-blur/ProgressiveBlur.svelte";
|
|
42
44
|
|
|
45
|
+
export { default as InteractiveHoverButton } from "./effects/interactive-hover-button/InteractiveHoverButton.svelte";
|
|
46
|
+
|
|
43
47
|
export { default as ImageComparison } from "./effects/image-comparison/ImageComparison.svelte";
|
|
44
48
|
|
|
45
49
|
export { magnetic } from "./effects/magnetic/magnetic.js";
|
|
46
50
|
export type { MagneticOptions } from "./effects/magnetic/magnetic.js";
|
|
47
51
|
|
|
48
52
|
export { revealTheme } from "./lib/theme-transition.js";
|
|
49
|
-
export type { RevealThemeOptions } from "./lib/theme-transition.js";
|
|
53
|
+
export type { RevealOrigin, RevealThemeOptions } from "./lib/theme-transition.js";
|
package/src/lib/panel.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// different props, and their edges drifted because the classes were typed twice.
|
|
5
5
|
|
|
6
6
|
// Dashboards used `Card`, so /ads had a ring and a shadow where /social had a
|
|
7
|
-
// hairline. Flat, one hairline, `rounded-xl
|
|
7
|
+
// hairline. Flat, one hairline, `rounded-xl`.
|
|
8
8
|
export const panelShellClass = "overflow-hidden rounded-xl border border-border bg-card";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -13,3 +13,9 @@ export const panelShellClass = "overflow-hidden rounded-xl border border-border
|
|
|
13
13
|
*/
|
|
14
14
|
export const panelHeaderClass =
|
|
15
15
|
"flex min-h-11 flex-wrap items-center justify-between gap-x-3 gap-y-1 border-b border-border px-4 py-2";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The inset well for a control block inside a panel. A visible step at the
|
|
19
|
+
* same depth, for filter rows and grouped controls.
|
|
20
|
+
*/
|
|
21
|
+
export const insetWellClass = "rounded-lg border border-border bg-muted/40 p-2";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Attachment } from "svelte/attachments";
|
|
2
|
+
|
|
3
|
+
/** The travelling highlight behind the active tab. Attach it to a span inside the list. */
|
|
4
|
+
|
|
5
|
+
// WHY attach to the pill, not the list: the pill finds its list via
|
|
6
|
+
// `parentElement`, so any strip that renders one span gets the motion.
|
|
7
|
+
|
|
8
|
+
// WHY measure: the active item's width is its label. The selector covers the
|
|
9
|
+
// three ways a strip marks it: a tab, a nav link, a toggle.
|
|
10
|
+
|
|
11
|
+
// Per transitions.dev `tabs-sliding`: first paint and resizes write the
|
|
12
|
+
// position with the transition off, so the pill snaps in instead of flying.
|
|
13
|
+
const ACTIVE = '[data-state="active"],[aria-current="page"],[aria-pressed="true"]';
|
|
14
|
+
|
|
15
|
+
export const slidingPill: Attachment<HTMLElement> = (pill) => {
|
|
16
|
+
const list = pill.parentElement;
|
|
17
|
+
if (!list) return;
|
|
18
|
+
|
|
19
|
+
let settled = false;
|
|
20
|
+
|
|
21
|
+
function move(animate: boolean) {
|
|
22
|
+
const active = list!.querySelector<HTMLElement>(ACTIVE);
|
|
23
|
+
if (!active) {
|
|
24
|
+
pill.style.opacity = "0";
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
pill.style.opacity = "1";
|
|
28
|
+
if (animate) {
|
|
29
|
+
pill.style.transform = `translateX(${active.offsetLeft}px)`;
|
|
30
|
+
pill.style.width = `${active.offsetWidth}px`;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Suspend, write, reflow, restore. Without the reflow the browser
|
|
34
|
+
// coalesces the two writes and the snap animates anyway.
|
|
35
|
+
pill.style.transition = "none";
|
|
36
|
+
pill.style.transform = `translateX(${active.offsetLeft}px)`;
|
|
37
|
+
pill.style.width = `${active.offsetWidth}px`;
|
|
38
|
+
void pill.offsetWidth;
|
|
39
|
+
pill.style.transition = "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Position first, THEN hand the highlight over. Swapped, one frame draws both
|
|
43
|
+
// the trigger's own background and a zero-width pill: a flicker on hydration.
|
|
44
|
+
move(false);
|
|
45
|
+
list.dataset.pill = "on";
|
|
46
|
+
settled = true;
|
|
47
|
+
|
|
48
|
+
// The selection lives in an attribute bits-ui owns, and a badge arriving
|
|
49
|
+
// changes a trigger's width without any attribute changing.
|
|
50
|
+
const attributes = new MutationObserver(() => move(settled));
|
|
51
|
+
attributes.observe(list, {
|
|
52
|
+
subtree: true,
|
|
53
|
+
childList: true,
|
|
54
|
+
attributes: true,
|
|
55
|
+
attributeFilter: ["data-state", "aria-current", "aria-pressed"],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// A list that reflows (a rail collapsing, a font landing) re-measures
|
|
59
|
+
// without motion: nothing was chosen, so nothing should travel.
|
|
60
|
+
const resize = new ResizeObserver(() => move(false));
|
|
61
|
+
resize.observe(list);
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
attributes.disconnect();
|
|
65
|
+
resize.disconnect();
|
|
66
|
+
delete list.dataset.pill;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -3,41 +3,45 @@ export interface RevealThemeOptions {
|
|
|
3
3
|
skip?: boolean;
|
|
4
4
|
/** Wipe length in ms. */
|
|
5
5
|
duration?: number;
|
|
6
|
+
/** Wipe easing, any CSS easing value. */
|
|
7
|
+
easing?: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
/** Where the wipe grows from: a click, an element, or a point in the viewport. */
|
|
11
|
+
export type RevealOrigin = MouseEvent | Element | { x: number; y: number };
|
|
12
|
+
|
|
8
13
|
const DEFAULT_DURATION = 400;
|
|
14
|
+
const STYLE_ID = "heyramzi-reveal-theme";
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Run `apply` inside a circular wipe that grows from the centre of the clicked
|
|
12
|
-
* element. Falls back to a plain `apply()` when the
|
|
13
|
-
* transitions, the user prefers reduced motion, or `skip`
|
|
14
|
-
*
|
|
15
|
-
* The page also needs `::view-transition-old(root)` and `::view-transition-new(root)`
|
|
16
|
-
* set to `animation: none`. `ThemeToggle` ships that CSS; a bare caller adds its own.
|
|
18
|
+
* element, the element, or a point. Falls back to a plain `apply()` when the
|
|
19
|
+
* browser has no view transitions, the user prefers reduced motion, or `skip`
|
|
20
|
+
* is set. It adds the `::view-transition-*(root)` reset itself.
|
|
17
21
|
*/
|
|
18
22
|
export function revealTheme(
|
|
19
|
-
|
|
23
|
+
origin: RevealOrigin,
|
|
20
24
|
apply: () => void,
|
|
21
25
|
options: RevealThemeOptions = {},
|
|
22
26
|
): void {
|
|
23
|
-
const { skip = false, duration = DEFAULT_DURATION } = options;
|
|
24
|
-
const
|
|
27
|
+
const { skip = false, duration = DEFAULT_DURATION, easing = "ease-in-out" } = options;
|
|
28
|
+
const point = resolveOrigin(origin);
|
|
25
29
|
|
|
26
30
|
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
27
31
|
|
|
28
|
-
if (skip || prefersReduced || !document.startViewTransition || !
|
|
32
|
+
if (skip || prefersReduced || !document.startViewTransition || !point) {
|
|
29
33
|
apply();
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
const
|
|
34
|
-
const x = rect.left + rect.width / 2;
|
|
35
|
-
const y = rect.top + rect.height / 2;
|
|
37
|
+
const { x, y } = point;
|
|
36
38
|
const endRadius = Math.hypot(
|
|
37
39
|
Math.max(x, window.innerWidth - x),
|
|
38
40
|
Math.max(y, window.innerHeight - y),
|
|
39
41
|
);
|
|
40
42
|
|
|
43
|
+
ensureStyles();
|
|
44
|
+
|
|
41
45
|
const transition = document.startViewTransition(() => {
|
|
42
46
|
apply();
|
|
43
47
|
});
|
|
@@ -45,7 +49,31 @@ export function revealTheme(
|
|
|
45
49
|
transition.ready.then(() => {
|
|
46
50
|
document.documentElement.animate(
|
|
47
51
|
{ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`] },
|
|
48
|
-
{ duration, easing
|
|
52
|
+
{ duration, easing, pseudoElement: "::view-transition-new(root)" },
|
|
49
53
|
);
|
|
50
54
|
});
|
|
51
55
|
}
|
|
56
|
+
|
|
57
|
+
function centreOf(element: Element): { x: number; y: number } {
|
|
58
|
+
const rect = element.getBoundingClientRect();
|
|
59
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A click on a child still wipes from the control that owns the handler.
|
|
63
|
+
function resolveOrigin(origin: RevealOrigin): { x: number; y: number } | null {
|
|
64
|
+
if (origin instanceof Element) return centreOf(origin);
|
|
65
|
+
if (origin instanceof MouseEvent) {
|
|
66
|
+
return origin.currentTarget instanceof Element
|
|
67
|
+
? centreOf(origin.currentTarget)
|
|
68
|
+
: { x: origin.clientX, y: origin.clientY };
|
|
69
|
+
}
|
|
70
|
+
return origin;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureStyles(): void {
|
|
74
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
75
|
+
const style = document.createElement("style");
|
|
76
|
+
style.id = STYLE_ID;
|
|
77
|
+
style.textContent = `::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal;}`;
|
|
78
|
+
document.head.appendChild(style);
|
|
79
|
+
}
|
package/src/lib/tone.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type Tone = "neutral" | "accent" | "info" | "success" | "warning" | "destructive";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The severity scale, as Tailwind classes for a chip and as CSS colors for the
|
|
5
|
+
* dot. Tokens are plain CSS variables, so a host app can retheme them. The
|
|
6
|
+
* accent tone reads `--tone-accent` and falls back to `--color-primary`, for a
|
|
7
|
+
* host whose primary is a neutral and whose accent is a brand color.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const TONE_CLASS: Record<Tone, string> = {
|
|
11
|
+
neutral: "border-border bg-muted/40 text-muted-foreground",
|
|
12
|
+
accent:
|
|
13
|
+
"border-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_30%,transparent)] bg-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_10%,transparent)] text-[color:var(--tone-accent,var(--color-primary))]",
|
|
14
|
+
info: "border-info/30 bg-info/10 text-info-800 dark:text-info-400",
|
|
15
|
+
success: "border-success/30 bg-success/10 text-success-800 dark:text-success-400",
|
|
16
|
+
warning: "border-warning/30 bg-warning/10 text-warning-800 dark:text-warning-400",
|
|
17
|
+
destructive: "border-destructive/30 bg-destructive/10 text-destructive",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** The same scale as text, for a verdict inside a line of muted facts. */
|
|
21
|
+
export const TONE_TEXT: Record<Tone, string> = {
|
|
22
|
+
neutral: "text-muted-foreground",
|
|
23
|
+
accent: "text-[color:var(--tone-accent,var(--color-primary))]",
|
|
24
|
+
info: "text-info-800 dark:text-info-400",
|
|
25
|
+
success: "text-success-800 dark:text-success-400",
|
|
26
|
+
warning: "text-warning-800 dark:text-warning-400",
|
|
27
|
+
destructive: "text-destructive",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The same scale as icon color. */
|
|
31
|
+
export const TONE_ICON: Record<Tone, string> = {
|
|
32
|
+
neutral: "text-muted-foreground",
|
|
33
|
+
accent: "text-[color:var(--tone-accent,var(--color-primary))]",
|
|
34
|
+
info: "text-info",
|
|
35
|
+
success: "text-success",
|
|
36
|
+
warning: "text-warning",
|
|
37
|
+
destructive: "text-destructive",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The scale as CSS colors, for the SVG dot. */
|
|
41
|
+
export const TONE_COLOR: Record<Tone, string> = {
|
|
42
|
+
neutral: "var(--color-muted-foreground)",
|
|
43
|
+
accent: "var(--tone-accent,var(--color-primary))",
|
|
44
|
+
info: "var(--color-info)",
|
|
45
|
+
success: "var(--color-success)",
|
|
46
|
+
warning: "var(--color-warning)",
|
|
47
|
+
destructive: "var(--color-destructive)",
|
|
48
|
+
};
|