@abuzareal/contour-ui 0.1.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.
Files changed (179) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +111 -0
  4. package/RELEASING.md +37 -0
  5. package/SECURITY.md +11 -0
  6. package/dist/components/hero/SceneBoundary.d.ts +14 -0
  7. package/dist/components/hero/SceneBoundary.js +11 -0
  8. package/dist/components/hero/Sculpture.d.ts +4 -0
  9. package/dist/components/hero/Sculpture.js +54 -0
  10. package/dist/components/three/ObjectStudy.d.ts +7 -0
  11. package/dist/components/three/ObjectStudy.js +65 -0
  12. package/dist/components/ui/Alert.d.ts +13 -0
  13. package/dist/components/ui/Alert.js +15 -0
  14. package/dist/components/ui/Avatar.d.ts +7 -0
  15. package/dist/components/ui/Avatar.js +12 -0
  16. package/dist/components/ui/Badge.d.ts +11 -0
  17. package/dist/components/ui/Badge.js +5 -0
  18. package/dist/components/ui/BrandMark.d.ts +7 -0
  19. package/dist/components/ui/BrandMark.js +5 -0
  20. package/dist/components/ui/Breadcrumbs.d.ts +10 -0
  21. package/dist/components/ui/Breadcrumbs.js +8 -0
  22. package/dist/components/ui/Button.d.ts +12 -0
  23. package/dist/components/ui/Button.js +6 -0
  24. package/dist/components/ui/Card.d.ts +13 -0
  25. package/dist/components/ui/Card.js +5 -0
  26. package/dist/components/ui/Checkbox.d.ts +9 -0
  27. package/dist/components/ui/Checkbox.js +14 -0
  28. package/dist/components/ui/CountUp.d.ts +9 -0
  29. package/dist/components/ui/CountUp.js +48 -0
  30. package/dist/components/ui/DataTable.d.ts +16 -0
  31. package/dist/components/ui/DataTable.js +24 -0
  32. package/dist/components/ui/DepthButton.d.ts +6 -0
  33. package/dist/components/ui/DepthButton.js +4 -0
  34. package/dist/components/ui/Dialog.d.ts +12 -0
  35. package/dist/components/ui/Dialog.js +27 -0
  36. package/dist/components/ui/Disclosure.d.ts +11 -0
  37. package/dist/components/ui/Disclosure.js +5 -0
  38. package/dist/components/ui/Divider.d.ts +5 -0
  39. package/dist/components/ui/Divider.js +6 -0
  40. package/dist/components/ui/DropdownMenu.d.ts +14 -0
  41. package/dist/components/ui/DropdownMenu.js +38 -0
  42. package/dist/components/ui/EmptyState.d.ts +9 -0
  43. package/dist/components/ui/EmptyState.js +4 -0
  44. package/dist/components/ui/ExternalLink.d.ts +4 -0
  45. package/dist/components/ui/ExternalLink.js +5 -0
  46. package/dist/components/ui/Field.d.ts +15 -0
  47. package/dist/components/ui/Field.js +6 -0
  48. package/dist/components/ui/FlipCard.d.ts +8 -0
  49. package/dist/components/ui/FlipCard.js +9 -0
  50. package/dist/components/ui/IconButton.d.ts +11 -0
  51. package/dist/components/ui/IconButton.js +5 -0
  52. package/dist/components/ui/Kbd.d.ts +5 -0
  53. package/dist/components/ui/Kbd.js +4 -0
  54. package/dist/components/ui/Magnetic.d.ts +8 -0
  55. package/dist/components/ui/Magnetic.js +19 -0
  56. package/dist/components/ui/Marquee.d.ts +7 -0
  57. package/dist/components/ui/Marquee.js +7 -0
  58. package/dist/components/ui/Pagination.d.ts +7 -0
  59. package/dist/components/ui/Pagination.js +15 -0
  60. package/dist/components/ui/Popover.d.ts +10 -0
  61. package/dist/components/ui/Popover.js +18 -0
  62. package/dist/components/ui/ProgressBar.d.ts +7 -0
  63. package/dist/components/ui/ProgressBar.js +6 -0
  64. package/dist/components/ui/RadioGroup.d.ts +14 -0
  65. package/dist/components/ui/RadioGroup.js +11 -0
  66. package/dist/components/ui/Reveal.d.ts +9 -0
  67. package/dist/components/ui/Reveal.js +24 -0
  68. package/dist/components/ui/RevealHeading.d.ts +12 -0
  69. package/dist/components/ui/RevealHeading.js +4 -0
  70. package/dist/components/ui/ScrollProgress.d.ts +1 -0
  71. package/dist/components/ui/ScrollProgress.js +27 -0
  72. package/dist/components/ui/SectionLabel.d.ts +8 -0
  73. package/dist/components/ui/SectionLabel.js +5 -0
  74. package/dist/components/ui/SegmentedControl.d.ts +10 -0
  75. package/dist/components/ui/SegmentedControl.js +20 -0
  76. package/dist/components/ui/SelectField.d.ts +13 -0
  77. package/dist/components/ui/SelectField.js +10 -0
  78. package/dist/components/ui/Skeleton.d.ts +6 -0
  79. package/dist/components/ui/Skeleton.js +5 -0
  80. package/dist/components/ui/Slider.d.ts +12 -0
  81. package/dist/components/ui/Slider.js +10 -0
  82. package/dist/components/ui/SocialIcons.d.ts +6 -0
  83. package/dist/components/ui/SocialIcons.js +7 -0
  84. package/dist/components/ui/Spinner.d.ts +6 -0
  85. package/dist/components/ui/Spinner.js +8 -0
  86. package/dist/components/ui/Stat.d.ts +8 -0
  87. package/dist/components/ui/Stat.js +4 -0
  88. package/dist/components/ui/Stepper.d.ts +10 -0
  89. package/dist/components/ui/Stepper.js +15 -0
  90. package/dist/components/ui/Switch.d.ts +8 -0
  91. package/dist/components/ui/Switch.js +7 -0
  92. package/dist/components/ui/Tabs.d.ts +13 -0
  93. package/dist/components/ui/Tabs.js +41 -0
  94. package/dist/components/ui/Tag.d.ts +5 -0
  95. package/dist/components/ui/Tag.js +8 -0
  96. package/dist/components/ui/TextArea.d.ts +10 -0
  97. package/dist/components/ui/TextArea.js +23 -0
  98. package/dist/components/ui/TextField.d.ts +12 -0
  99. package/dist/components/ui/TextField.js +13 -0
  100. package/dist/components/ui/ThemeToggle.d.ts +5 -0
  101. package/dist/components/ui/ThemeToggle.js +9 -0
  102. package/dist/components/ui/TiltCard.d.ts +8 -0
  103. package/dist/components/ui/TiltCard.js +23 -0
  104. package/dist/components/ui/Timeline.d.ts +9 -0
  105. package/dist/components/ui/Timeline.js +6 -0
  106. package/dist/components/ui/ToastProvider.d.ts +5 -0
  107. package/dist/components/ui/ToastProvider.js +29 -0
  108. package/dist/components/ui/Tooltip.d.ts +11 -0
  109. package/dist/components/ui/Tooltip.js +16 -0
  110. package/dist/fonts.css +3 -0
  111. package/dist/hooks/useClipboardCopy.d.ts +5 -0
  112. package/dist/hooks/useClipboardCopy.js +21 -0
  113. package/dist/hooks/useDismiss.d.ts +3 -0
  114. package/dist/hooks/useDismiss.js +22 -0
  115. package/dist/hooks/useHeaderScroll.d.ts +7 -0
  116. package/dist/hooks/useHeaderScroll.js +40 -0
  117. package/dist/hooks/useIdleTrigger.d.ts +1 -0
  118. package/dist/hooks/useIdleTrigger.js +19 -0
  119. package/dist/hooks/useInitialHashScroll.d.ts +1 -0
  120. package/dist/hooks/useInitialHashScroll.js +28 -0
  121. package/dist/hooks/useMobileMenu.d.ts +3 -0
  122. package/dist/hooks/useMobileMenu.js +34 -0
  123. package/dist/hooks/useMotionPreference.d.ts +1 -0
  124. package/dist/hooks/useMotionPreference.js +13 -0
  125. package/dist/hooks/usePrintPreparation.d.ts +1 -0
  126. package/dist/hooks/usePrintPreparation.js +29 -0
  127. package/dist/hooks/useScrollSpy.d.ts +1 -0
  128. package/dist/hooks/useScrollSpy.js +36 -0
  129. package/dist/hooks/useSmoothScroll.d.ts +1 -0
  130. package/dist/hooks/useSmoothScroll.js +18 -0
  131. package/dist/hooks/useTheme.d.ts +5 -0
  132. package/dist/hooks/useTheme.js +98 -0
  133. package/dist/hooks/useThreeStage.d.ts +28 -0
  134. package/dist/hooks/useThreeStage.js +155 -0
  135. package/dist/hooks/useToast.d.ts +11 -0
  136. package/dist/hooks/useToast.js +9 -0
  137. package/dist/index.d.ts +129 -0
  138. package/dist/index.js +68 -0
  139. package/dist/kit/actions.css +209 -0
  140. package/dist/kit/data-display.css +496 -0
  141. package/dist/kit/effects.css +144 -0
  142. package/dist/kit/feedback.css +316 -0
  143. package/dist/kit/forms.css +363 -0
  144. package/dist/kit/index.css +9 -0
  145. package/dist/kit/navigation.css +262 -0
  146. package/dist/kit/overlays.css +299 -0
  147. package/dist/lib/a11y.d.ts +3 -0
  148. package/dist/lib/a11y.js +3 -0
  149. package/dist/lib/classNames.d.ts +6 -0
  150. package/dist/lib/classNames.js +4 -0
  151. package/dist/lib/format.d.ts +3 -0
  152. package/dist/lib/format.js +3 -0
  153. package/dist/lib/keyboard.d.ts +8 -0
  154. package/dist/lib/keyboard.js +22 -0
  155. package/dist/lib/motion.d.ts +3 -0
  156. package/dist/lib/motion.js +4 -0
  157. package/dist/lib/safeHref.d.ts +2 -0
  158. package/dist/lib/safeHref.js +14 -0
  159. package/dist/lib/storage.d.ts +10 -0
  160. package/dist/lib/storage.js +25 -0
  161. package/dist/lib/themeBootstrap.d.ts +6 -0
  162. package/dist/lib/themeBootstrap.js +19 -0
  163. package/dist/lib/threeMaterials.d.ts +9 -0
  164. package/dist/lib/threeMaterials.js +62 -0
  165. package/dist/lib/threeStudies.d.ts +16 -0
  166. package/dist/lib/threeStudies.js +39 -0
  167. package/dist/motion.css +59 -0
  168. package/dist/reset.css +180 -0
  169. package/dist/scroll.d.ts +1 -0
  170. package/dist/scroll.js +1 -0
  171. package/dist/styles.css +23 -0
  172. package/dist/theme-toggle.css +38 -0
  173. package/dist/three.d.ts +8 -0
  174. package/dist/three.js +5 -0
  175. package/dist/tokens.css +127 -0
  176. package/dist/types.d.ts +6 -0
  177. package/dist/types.js +1 -0
  178. package/dist/ui.css +178 -0
  179. package/package.json +101 -0
@@ -0,0 +1 @@
1
+ export default function useMotionPreference(): boolean;
@@ -0,0 +1,13 @@
1
+ /** Tracks the user's `prefers-reduced-motion` setting and updates when it changes. */
2
+ import { useEffect, useState } from "react";
3
+ const query = "(prefers-reduced-motion: reduce)";
4
+ export default function useMotionPreference() {
5
+ const [reducedMotion, setReducedMotion] = useState(() => typeof window !== "undefined" && window.matchMedia(query).matches);
6
+ useEffect(() => {
7
+ const media = window.matchMedia(query);
8
+ const update = () => setReducedMotion(media.matches);
9
+ media.addEventListener("change", update);
10
+ return () => media.removeEventListener("change", update);
11
+ }, []);
12
+ return reducedMotion;
13
+ }
@@ -0,0 +1 @@
1
+ export default function usePrintPreparation(): void;
@@ -0,0 +1,29 @@
1
+ /** Prints the full page: opens collapsed disclosures and uses the light theme, then restores both. */
2
+ import { useEffect } from "react";
3
+ export default function usePrintPreparation() {
4
+ useEffect(() => {
5
+ const root = document.documentElement;
6
+ let expanded = [];
7
+ let screenTheme;
8
+ const prepare = () => {
9
+ expanded = [
10
+ ...document.querySelectorAll("details:not([open])"),
11
+ ];
12
+ expanded.forEach((details) => (details.open = true));
13
+ screenTheme = root.dataset.theme;
14
+ root.dataset.theme = "light";
15
+ };
16
+ const restore = () => {
17
+ expanded.forEach((details) => (details.open = false));
18
+ expanded = [];
19
+ if (screenTheme)
20
+ root.dataset.theme = screenTheme;
21
+ };
22
+ window.addEventListener("beforeprint", prepare);
23
+ window.addEventListener("afterprint", restore);
24
+ return () => {
25
+ window.removeEventListener("beforeprint", prepare);
26
+ window.removeEventListener("afterprint", restore);
27
+ };
28
+ }, []);
29
+ }
@@ -0,0 +1 @@
1
+ export default function useScrollSpy(ids: string[]): string;
@@ -0,0 +1,36 @@
1
+ /** Returns the id of the last listed element whose top has scrolled past the activation line. */
2
+ import { useEffect, useState } from "react";
3
+ /** Fraction of the viewport height at which an element becomes "current". */
4
+ const activationLine = 0.3;
5
+ export default function useScrollSpy(ids) {
6
+ const [activeId, setActiveId] = useState("");
7
+ const key = ids.join("|");
8
+ useEffect(() => {
9
+ const elements = key
10
+ .split("|")
11
+ .map((id) => document.getElementById(id))
12
+ .filter((element) => element !== null);
13
+ let frame = 0;
14
+ const update = () => {
15
+ cancelAnimationFrame(frame);
16
+ frame = requestAnimationFrame(() => {
17
+ const line = window.innerHeight * activationLine;
18
+ let current = "";
19
+ elements.forEach((element) => {
20
+ if (element.getBoundingClientRect().top <= line)
21
+ current = element.id;
22
+ });
23
+ setActiveId(current);
24
+ });
25
+ };
26
+ update();
27
+ window.addEventListener("scroll", update, { passive: true });
28
+ window.addEventListener("resize", update);
29
+ return () => {
30
+ cancelAnimationFrame(frame);
31
+ window.removeEventListener("scroll", update);
32
+ window.removeEventListener("resize", update);
33
+ };
34
+ }, [key]);
35
+ return activeId;
36
+ }
@@ -0,0 +1 @@
1
+ export default function useSmoothScroll(enabled: boolean): void;
@@ -0,0 +1,18 @@
1
+ /** Enables Lenis smooth scrolling (and animated anchor jumps) while motion is allowed. */
2
+ import { useEffect } from "react";
3
+ import Lenis from "lenis";
4
+ export default function useSmoothScroll(enabled) {
5
+ useEffect(() => {
6
+ if (!enabled)
7
+ return;
8
+ const lenis = new Lenis({
9
+ autoRaf: true,
10
+ duration: 1.05,
11
+ smoothWheel: true,
12
+ syncTouch: false,
13
+ // Lenis honours each section's CSS scroll-margin-top, so no extra offset.
14
+ anchors: true,
15
+ });
16
+ return () => lenis.destroy();
17
+ }, [enabled]);
18
+ }
@@ -0,0 +1,5 @@
1
+ export type Theme = "light" | "dark";
2
+ export default function useTheme(animate?: boolean): {
3
+ theme: Theme;
4
+ toggle: (origin: HTMLElement) => void;
5
+ };
@@ -0,0 +1,98 @@
1
+ /** Light/dark theme state: persisted choice, system fallback, and the animated switch. */
2
+ import { useCallback, useEffect, useSyncExternalStore } from "react";
3
+ import { flushSync } from "react-dom";
4
+ import { readStorage, storageKeys, writeStorage } from "../lib/storage.js";
5
+ import { themeColors } from "../lib/themeBootstrap.js";
6
+ const revealDuration = 1100;
7
+ const revealEasing = "cubic-bezier(0.76, 0, 0.24, 1)";
8
+ const readTheme = () => typeof document !== "undefined" &&
9
+ document.documentElement.dataset.theme === "dark"
10
+ ? "dark"
11
+ : "light";
12
+ function applyTheme(theme) {
13
+ document.documentElement.dataset.theme = theme;
14
+ document
15
+ .querySelector('meta[name="theme-color"]')
16
+ ?.setAttribute("content", themeColors[theme]);
17
+ window.dispatchEvent(new Event("contour-theme-change"));
18
+ }
19
+ /** Grows the new theme as a circle from `origin` while the old page recedes. */
20
+ function animateReveal(transition, origin) {
21
+ const bounds = origin.getBoundingClientRect();
22
+ const x = bounds.left + bounds.width / 2;
23
+ const y = bounds.top + bounds.height / 2;
24
+ const radius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y));
25
+ const timing = { duration: revealDuration, easing: revealEasing };
26
+ transition.ready
27
+ .then(() => {
28
+ document.documentElement.animate({
29
+ clipPath: [
30
+ `circle(0px at ${x}px ${y}px)`,
31
+ `circle(${radius}px at ${x}px ${y}px)`,
32
+ ],
33
+ }, { ...timing, pseudoElement: "::view-transition-new(root)" });
34
+ document.documentElement.animate({ transform: ["scale(1)", "scale(0.965)"], opacity: [1, 0.55] }, { ...timing, pseudoElement: "::view-transition-old(root)" });
35
+ })
36
+ .catch(() => { });
37
+ }
38
+ function subscribe(onChange) {
39
+ if (typeof window === "undefined")
40
+ return () => { };
41
+ const observer = new MutationObserver(onChange);
42
+ observer.observe(document.documentElement, {
43
+ attributes: true,
44
+ attributeFilter: ["data-theme"],
45
+ });
46
+ const onStorage = (event) => {
47
+ if (event.key !== storageKeys.theme && event.key !== null)
48
+ return;
49
+ const stored = readStorage("local", storageKeys.theme);
50
+ applyTheme(stored === "light" || stored === "dark"
51
+ ? stored
52
+ : window.matchMedia("(prefers-color-scheme: dark)").matches
53
+ ? "dark"
54
+ : "light");
55
+ };
56
+ window.addEventListener("contour-theme-change", onChange);
57
+ window.addEventListener("storage", onStorage);
58
+ return () => {
59
+ observer.disconnect();
60
+ window.removeEventListener("contour-theme-change", onChange);
61
+ window.removeEventListener("storage", onStorage);
62
+ };
63
+ }
64
+ export default function useTheme(animate = true) {
65
+ const theme = useSyncExternalStore(subscribe, readTheme, () => "light");
66
+ useEffect(() => {
67
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
68
+ const followSystem = () => {
69
+ if (readStorage("local", storageKeys.theme) !== null)
70
+ return;
71
+ const next = media.matches ? "dark" : "light";
72
+ applyTheme(next);
73
+ };
74
+ media.addEventListener("change", followSystem);
75
+ return () => media.removeEventListener("change", followSystem);
76
+ }, []);
77
+ const toggle = useCallback((origin) => {
78
+ const next = readTheme() === "dark" ? "light" : "dark";
79
+ writeStorage("local", storageKeys.theme, next);
80
+ const commit = () => {
81
+ flushSync(() => applyTheme(next));
82
+ };
83
+ if (!animate ||
84
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
85
+ commit();
86
+ return;
87
+ }
88
+ if (typeof document.startViewTransition !== "function") {
89
+ const root = document.documentElement;
90
+ root.classList.add("theme-fading");
91
+ commit();
92
+ window.setTimeout(() => root.classList.remove("theme-fading"), 500);
93
+ return;
94
+ }
95
+ animateReveal(document.startViewTransition(commit), origin);
96
+ }, [animate]);
97
+ return { theme, toggle };
98
+ }
@@ -0,0 +1,28 @@
1
+ /** Shared Three.js stage: renderer, studio lighting, resizing, a visibility-aware render loop, pointer tracking, and cleanup. */
2
+ import { type RefObject } from "react";
3
+ import * as THREE from "three";
4
+ export type StageFrame = {
5
+ delta: number;
6
+ elapsed: number;
7
+ /** Pointer offset from the stage centre, each axis in -0.5..0.5. */
8
+ pointer: THREE.Vector2;
9
+ };
10
+ export type StageScene = {
11
+ update: (frame: StageFrame) => void;
12
+ /** Snaps to a still pose when motion is turned off. */
13
+ rest: () => void;
14
+ dispose: () => void;
15
+ };
16
+ export type StageContext = {
17
+ scene: THREE.Scene;
18
+ camera: THREE.PerspectiveCamera;
19
+ renderer: THREE.WebGLRenderer;
20
+ };
21
+ type StageOptions = {
22
+ motionEnabled: boolean;
23
+ setup: (context: StageContext) => StageScene;
24
+ /** Camera z for a given aspect ratio, keeping the subject framed. */
25
+ cameraDistance: (aspect: number) => number;
26
+ };
27
+ export default function useThreeStage(hostRef: RefObject<HTMLDivElement>, { motionEnabled, setup, cameraDistance }: StageOptions): void;
28
+ export {};
@@ -0,0 +1,155 @@
1
+ /** Shared Three.js stage: renderer, studio lighting, resizing, a visibility-aware render loop, pointer tracking, and cleanup. */
2
+ import { useEffect, useRef } from "react";
3
+ import * as THREE from "three";
4
+ import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
5
+ const maxPixelRatio = 1.75;
6
+ export default function useThreeStage(hostRef, { motionEnabled, setup, cameraDistance }) {
7
+ const motionRef = useRef(motionEnabled);
8
+ const syncRef = useRef(undefined);
9
+ // Setup and framing run once per mount; later prop changes are not re-applied.
10
+ const setupRef = useRef({ setup, cameraDistance });
11
+ useEffect(() => {
12
+ motionRef.current = motionEnabled;
13
+ syncRef.current?.();
14
+ }, [motionEnabled]);
15
+ useEffect(() => {
16
+ const host = hostRef.current;
17
+ if (!host)
18
+ return;
19
+ let renderer;
20
+ try {
21
+ renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
22
+ }
23
+ catch {
24
+ host.dataset.state = "unavailable";
25
+ return;
26
+ }
27
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, maxPixelRatio));
28
+ renderer.setClearColor(0x000000, 0);
29
+ // Neutral tone mapping keeps the lime close to the CSS accent colour.
30
+ renderer.toneMapping = THREE.NeutralToneMapping;
31
+ renderer.toneMappingExposure = 1.05;
32
+ renderer.domElement.setAttribute("aria-hidden", "true");
33
+ host.appendChild(renderer.domElement);
34
+ // Studio lighting shared by every stage so materials read consistently.
35
+ const scene = new THREE.Scene();
36
+ const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 30);
37
+ camera.position.set(0, 0, 10.5);
38
+ const environment = new RoomEnvironment();
39
+ const generator = new THREE.PMREMGenerator(renderer);
40
+ const environmentMap = generator.fromScene(environment, 0.04);
41
+ scene.environment = environmentMap.texture;
42
+ environment.dispose();
43
+ generator.dispose();
44
+ const keyLight = new THREE.DirectionalLight(0xffffff, 4);
45
+ keyLight.position.set(4, 5, 6);
46
+ const rimLight = new THREE.DirectionalLight(0xcad9ff, 3);
47
+ rimLight.position.set(-5, 1, -2);
48
+ scene.add(keyLight, rimLight, new THREE.AmbientLight(0xffffff, 0.7));
49
+ const stage = setupRef.current.setup({ scene, camera, renderer });
50
+ // Loop state: frames run only while visible, focused, and motion is on.
51
+ const pointer = new THREE.Vector2();
52
+ let visible = true;
53
+ let contextLost = false;
54
+ let running = false;
55
+ let frame = 0;
56
+ let elapsed = 0;
57
+ let previousTime = 0;
58
+ const render = () => {
59
+ if (contextLost)
60
+ return;
61
+ renderer.render(scene, camera);
62
+ host.dataset.state = "ready";
63
+ };
64
+ const resize = () => {
65
+ const { width, height } = host.getBoundingClientRect();
66
+ if (!width || !height)
67
+ return;
68
+ camera.aspect = width / height;
69
+ camera.position.z = setupRef.current.cameraDistance(camera.aspect);
70
+ camera.updateProjectionMatrix();
71
+ renderer.setSize(width, height);
72
+ render();
73
+ };
74
+ const trackPointer = (event) => {
75
+ if (event.pointerType === "touch" || !motionRef.current)
76
+ return;
77
+ const bounds = host.getBoundingClientRect();
78
+ pointer.set((event.clientX - bounds.left) / bounds.width - 0.5, (event.clientY - bounds.top) / bounds.height - 0.5);
79
+ };
80
+ const resetPointer = () => pointer.set(0, 0);
81
+ const animate = (time) => {
82
+ if (!running)
83
+ return;
84
+ const delta = Math.min(Math.max(time - previousTime, 0) / 1000, 0.05);
85
+ previousTime = time;
86
+ elapsed += delta;
87
+ stage.update({ delta, elapsed, pointer });
88
+ render();
89
+ frame = requestAnimationFrame(animate);
90
+ };
91
+ /** Starts or stops the loop to match visibility and motion; snaps to rest when paused. */
92
+ const sync = () => {
93
+ const motion = motionRef.current;
94
+ const shouldRun = motion && visible && !document.hidden && !contextLost;
95
+ if (shouldRun && !running) {
96
+ running = true;
97
+ previousTime = performance.now();
98
+ frame = requestAnimationFrame(animate);
99
+ }
100
+ else if (!shouldRun && running) {
101
+ running = false;
102
+ cancelAnimationFrame(frame);
103
+ }
104
+ if (!motion) {
105
+ pointer.set(0, 0);
106
+ stage.rest();
107
+ render();
108
+ }
109
+ };
110
+ syncRef.current = sync;
111
+ const loseContext = (event) => {
112
+ event.preventDefault();
113
+ contextLost = true;
114
+ host.dataset.state = "unavailable";
115
+ sync();
116
+ };
117
+ const restoreContext = () => {
118
+ contextLost = false;
119
+ render();
120
+ sync();
121
+ };
122
+ const visibilityObserver = new IntersectionObserver(([entry]) => {
123
+ visible = entry.isIntersecting;
124
+ if (visible)
125
+ render();
126
+ sync();
127
+ });
128
+ const resizeObserver = new ResizeObserver(resize);
129
+ visibilityObserver.observe(host);
130
+ resizeObserver.observe(host);
131
+ host.addEventListener("pointermove", trackPointer);
132
+ host.addEventListener("pointerleave", resetPointer);
133
+ document.addEventListener("visibilitychange", sync);
134
+ renderer.domElement.addEventListener("webglcontextlost", loseContext);
135
+ renderer.domElement.addEventListener("webglcontextrestored", restoreContext);
136
+ resize();
137
+ sync();
138
+ return () => {
139
+ running = false;
140
+ syncRef.current = undefined;
141
+ cancelAnimationFrame(frame);
142
+ visibilityObserver.disconnect();
143
+ resizeObserver.disconnect();
144
+ host.removeEventListener("pointermove", trackPointer);
145
+ host.removeEventListener("pointerleave", resetPointer);
146
+ document.removeEventListener("visibilitychange", sync);
147
+ renderer.domElement.removeEventListener("webglcontextlost", loseContext);
148
+ renderer.domElement.removeEventListener("webglcontextrestored", restoreContext);
149
+ stage.dispose();
150
+ environmentMap.dispose();
151
+ renderer.dispose();
152
+ renderer.domElement.remove();
153
+ };
154
+ }, [hostRef]);
155
+ }
@@ -0,0 +1,11 @@
1
+ import type { AlertTone } from "../components/ui/Alert.js";
2
+ export type ToastInput = {
3
+ title: string;
4
+ message?: string;
5
+ tone?: AlertTone;
6
+ };
7
+ export type ToastContextValue = {
8
+ show: (toast: ToastInput) => void;
9
+ };
10
+ export declare const ToastContext: import("react").Context<ToastContextValue | null>;
11
+ export default function useToast(): ToastContextValue;
@@ -0,0 +1,9 @@
1
+ /** Toast context: lets any component queue a notification rendered by ToastProvider. */
2
+ import { createContext, useContext } from "react";
3
+ export const ToastContext = createContext(null);
4
+ export default function useToast() {
5
+ const context = useContext(ToastContext);
6
+ if (!context)
7
+ throw new Error("useToast must be used inside ToastProvider");
8
+ return context;
9
+ }
@@ -0,0 +1,129 @@
1
+ export { default as Alert } from "./components/ui/Alert.js";
2
+ export type * from "./components/ui/Alert.js";
3
+ export { default as Avatar } from "./components/ui/Avatar.js";
4
+ export type * from "./components/ui/Avatar.js";
5
+ export { default as Badge } from "./components/ui/Badge.js";
6
+ export type * from "./components/ui/Badge.js";
7
+ export { default as BrandMark } from "./components/ui/BrandMark.js";
8
+ export type * from "./components/ui/BrandMark.js";
9
+ export { default as Breadcrumbs } from "./components/ui/Breadcrumbs.js";
10
+ export type * from "./components/ui/Breadcrumbs.js";
11
+ export { default as Button } from "./components/ui/Button.js";
12
+ export type * from "./components/ui/Button.js";
13
+ export { default as Card } from "./components/ui/Card.js";
14
+ export type * from "./components/ui/Card.js";
15
+ export { default as Checkbox } from "./components/ui/Checkbox.js";
16
+ export type * from "./components/ui/Checkbox.js";
17
+ export { default as CountUp } from "./components/ui/CountUp.js";
18
+ export type * from "./components/ui/CountUp.js";
19
+ export { default as DataTable } from "./components/ui/DataTable.js";
20
+ export type * from "./components/ui/DataTable.js";
21
+ export { default as DepthButton } from "./components/ui/DepthButton.js";
22
+ export type * from "./components/ui/DepthButton.js";
23
+ export { default as Dialog } from "./components/ui/Dialog.js";
24
+ export type * from "./components/ui/Dialog.js";
25
+ export { default as Disclosure } from "./components/ui/Disclosure.js";
26
+ export type * from "./components/ui/Disclosure.js";
27
+ export { default as Divider } from "./components/ui/Divider.js";
28
+ export type * from "./components/ui/Divider.js";
29
+ export { default as DropdownMenu } from "./components/ui/DropdownMenu.js";
30
+ export type * from "./components/ui/DropdownMenu.js";
31
+ export { default as EmptyState } from "./components/ui/EmptyState.js";
32
+ export type * from "./components/ui/EmptyState.js";
33
+ export { default as ExternalLink } from "./components/ui/ExternalLink.js";
34
+ export type * from "./components/ui/ExternalLink.js";
35
+ export { default as Field } from "./components/ui/Field.js";
36
+ export type * from "./components/ui/Field.js";
37
+ export { default as FlipCard } from "./components/ui/FlipCard.js";
38
+ export type * from "./components/ui/FlipCard.js";
39
+ export { default as IconButton } from "./components/ui/IconButton.js";
40
+ export type * from "./components/ui/IconButton.js";
41
+ export { default as Kbd } from "./components/ui/Kbd.js";
42
+ export type * from "./components/ui/Kbd.js";
43
+ export { default as Magnetic } from "./components/ui/Magnetic.js";
44
+ export type * from "./components/ui/Magnetic.js";
45
+ export { default as Marquee } from "./components/ui/Marquee.js";
46
+ export type * from "./components/ui/Marquee.js";
47
+ export { default as Pagination } from "./components/ui/Pagination.js";
48
+ export type * from "./components/ui/Pagination.js";
49
+ export { default as Popover } from "./components/ui/Popover.js";
50
+ export type * from "./components/ui/Popover.js";
51
+ export { default as ProgressBar } from "./components/ui/ProgressBar.js";
52
+ export type * from "./components/ui/ProgressBar.js";
53
+ export { default as RadioGroup } from "./components/ui/RadioGroup.js";
54
+ export type * from "./components/ui/RadioGroup.js";
55
+ export { default as Reveal } from "./components/ui/Reveal.js";
56
+ export type * from "./components/ui/Reveal.js";
57
+ export { default as RevealHeading } from "./components/ui/RevealHeading.js";
58
+ export type * from "./components/ui/RevealHeading.js";
59
+ export { default as ScrollProgress } from "./components/ui/ScrollProgress.js";
60
+ export type * from "./components/ui/ScrollProgress.js";
61
+ export { default as SectionLabel } from "./components/ui/SectionLabel.js";
62
+ export type * from "./components/ui/SectionLabel.js";
63
+ export { default as SegmentedControl } from "./components/ui/SegmentedControl.js";
64
+ export type * from "./components/ui/SegmentedControl.js";
65
+ export { default as SelectField } from "./components/ui/SelectField.js";
66
+ export type * from "./components/ui/SelectField.js";
67
+ export { default as Skeleton } from "./components/ui/Skeleton.js";
68
+ export type * from "./components/ui/Skeleton.js";
69
+ export { default as Slider } from "./components/ui/Slider.js";
70
+ export type * from "./components/ui/Slider.js";
71
+ export { GitHubIcon, LinkedInIcon } from "./components/ui/SocialIcons.js";
72
+ export type * from "./components/ui/SocialIcons.js";
73
+ export { default as Spinner } from "./components/ui/Spinner.js";
74
+ export type * from "./components/ui/Spinner.js";
75
+ export { default as Stat } from "./components/ui/Stat.js";
76
+ export type * from "./components/ui/Stat.js";
77
+ export { default as Stepper } from "./components/ui/Stepper.js";
78
+ export type * from "./components/ui/Stepper.js";
79
+ export { default as Switch } from "./components/ui/Switch.js";
80
+ export type * from "./components/ui/Switch.js";
81
+ export { default as Tabs } from "./components/ui/Tabs.js";
82
+ export type * from "./components/ui/Tabs.js";
83
+ export { default as Tag } from "./components/ui/Tag.js";
84
+ export type * from "./components/ui/Tag.js";
85
+ export { default as TextArea } from "./components/ui/TextArea.js";
86
+ export type * from "./components/ui/TextArea.js";
87
+ export { default as TextField } from "./components/ui/TextField.js";
88
+ export type * from "./components/ui/TextField.js";
89
+ export { default as ThemeToggle } from "./components/ui/ThemeToggle.js";
90
+ export type * from "./components/ui/ThemeToggle.js";
91
+ export { default as TiltCard } from "./components/ui/TiltCard.js";
92
+ export type * from "./components/ui/TiltCard.js";
93
+ export { default as Timeline } from "./components/ui/Timeline.js";
94
+ export type * from "./components/ui/Timeline.js";
95
+ export { default as ToastProvider } from "./components/ui/ToastProvider.js";
96
+ export type * from "./components/ui/ToastProvider.js";
97
+ export { default as Tooltip } from "./components/ui/Tooltip.js";
98
+ export type * from "./components/ui/Tooltip.js";
99
+ export { default as useClipboardCopy } from "./hooks/useClipboardCopy.js";
100
+ export type * from "./hooks/useClipboardCopy.js";
101
+ export { default as useDismiss } from "./hooks/useDismiss.js";
102
+ export type * from "./hooks/useDismiss.js";
103
+ export { default as useHeaderScroll } from "./hooks/useHeaderScroll.js";
104
+ export type * from "./hooks/useHeaderScroll.js";
105
+ export { default as useIdleTrigger } from "./hooks/useIdleTrigger.js";
106
+ export type * from "./hooks/useIdleTrigger.js";
107
+ export { default as useInitialHashScroll } from "./hooks/useInitialHashScroll.js";
108
+ export type * from "./hooks/useInitialHashScroll.js";
109
+ export { default as useMobileMenu } from "./hooks/useMobileMenu.js";
110
+ export type * from "./hooks/useMobileMenu.js";
111
+ export { default as useMotionPreference } from "./hooks/useMotionPreference.js";
112
+ export type * from "./hooks/useMotionPreference.js";
113
+ export { default as usePrintPreparation } from "./hooks/usePrintPreparation.js";
114
+ export type * from "./hooks/usePrintPreparation.js";
115
+ export { default as useScrollSpy } from "./hooks/useScrollSpy.js";
116
+ export type * from "./hooks/useScrollSpy.js";
117
+ export { default as useTheme } from "./hooks/useTheme.js";
118
+ export type * from "./hooks/useTheme.js";
119
+ export { default as useToast } from "./hooks/useToast.js";
120
+ export type * from "./hooks/useToast.js";
121
+ export * from "./lib/classNames.js";
122
+ export * from "./lib/format.js";
123
+ export * from "./lib/storage.js";
124
+ export * from "./lib/keyboard.js";
125
+ export * from "./lib/a11y.js";
126
+ export * from "./lib/motion.js";
127
+ export type { Section } from "./types.js";
128
+ export { default as SceneBoundary } from "./components/hero/SceneBoundary.js";
129
+ export { safeHref } from "./lib/safeHref.js";
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ export { default as Alert } from "./components/ui/Alert.js";
2
+ export { default as Avatar } from "./components/ui/Avatar.js";
3
+ export { default as Badge } from "./components/ui/Badge.js";
4
+ export { default as BrandMark } from "./components/ui/BrandMark.js";
5
+ export { default as Breadcrumbs } from "./components/ui/Breadcrumbs.js";
6
+ export { default as Button } from "./components/ui/Button.js";
7
+ export { default as Card } from "./components/ui/Card.js";
8
+ export { default as Checkbox } from "./components/ui/Checkbox.js";
9
+ export { default as CountUp } from "./components/ui/CountUp.js";
10
+ export { default as DataTable } from "./components/ui/DataTable.js";
11
+ export { default as DepthButton } from "./components/ui/DepthButton.js";
12
+ export { default as Dialog } from "./components/ui/Dialog.js";
13
+ export { default as Disclosure } from "./components/ui/Disclosure.js";
14
+ export { default as Divider } from "./components/ui/Divider.js";
15
+ export { default as DropdownMenu } from "./components/ui/DropdownMenu.js";
16
+ export { default as EmptyState } from "./components/ui/EmptyState.js";
17
+ export { default as ExternalLink } from "./components/ui/ExternalLink.js";
18
+ export { default as Field } from "./components/ui/Field.js";
19
+ export { default as FlipCard } from "./components/ui/FlipCard.js";
20
+ export { default as IconButton } from "./components/ui/IconButton.js";
21
+ export { default as Kbd } from "./components/ui/Kbd.js";
22
+ export { default as Magnetic } from "./components/ui/Magnetic.js";
23
+ export { default as Marquee } from "./components/ui/Marquee.js";
24
+ export { default as Pagination } from "./components/ui/Pagination.js";
25
+ export { default as Popover } from "./components/ui/Popover.js";
26
+ export { default as ProgressBar } from "./components/ui/ProgressBar.js";
27
+ export { default as RadioGroup } from "./components/ui/RadioGroup.js";
28
+ export { default as Reveal } from "./components/ui/Reveal.js";
29
+ export { default as RevealHeading } from "./components/ui/RevealHeading.js";
30
+ export { default as ScrollProgress } from "./components/ui/ScrollProgress.js";
31
+ export { default as SectionLabel } from "./components/ui/SectionLabel.js";
32
+ export { default as SegmentedControl } from "./components/ui/SegmentedControl.js";
33
+ export { default as SelectField } from "./components/ui/SelectField.js";
34
+ export { default as Skeleton } from "./components/ui/Skeleton.js";
35
+ export { default as Slider } from "./components/ui/Slider.js";
36
+ export { GitHubIcon, LinkedInIcon } from "./components/ui/SocialIcons.js";
37
+ export { default as Spinner } from "./components/ui/Spinner.js";
38
+ export { default as Stat } from "./components/ui/Stat.js";
39
+ export { default as Stepper } from "./components/ui/Stepper.js";
40
+ export { default as Switch } from "./components/ui/Switch.js";
41
+ export { default as Tabs } from "./components/ui/Tabs.js";
42
+ export { default as Tag } from "./components/ui/Tag.js";
43
+ export { default as TextArea } from "./components/ui/TextArea.js";
44
+ export { default as TextField } from "./components/ui/TextField.js";
45
+ export { default as ThemeToggle } from "./components/ui/ThemeToggle.js";
46
+ export { default as TiltCard } from "./components/ui/TiltCard.js";
47
+ export { default as Timeline } from "./components/ui/Timeline.js";
48
+ export { default as ToastProvider } from "./components/ui/ToastProvider.js";
49
+ export { default as Tooltip } from "./components/ui/Tooltip.js";
50
+ export { default as useClipboardCopy } from "./hooks/useClipboardCopy.js";
51
+ export { default as useDismiss } from "./hooks/useDismiss.js";
52
+ export { default as useHeaderScroll } from "./hooks/useHeaderScroll.js";
53
+ export { default as useIdleTrigger } from "./hooks/useIdleTrigger.js";
54
+ export { default as useInitialHashScroll } from "./hooks/useInitialHashScroll.js";
55
+ export { default as useMobileMenu } from "./hooks/useMobileMenu.js";
56
+ export { default as useMotionPreference } from "./hooks/useMotionPreference.js";
57
+ export { default as usePrintPreparation } from "./hooks/usePrintPreparation.js";
58
+ export { default as useScrollSpy } from "./hooks/useScrollSpy.js";
59
+ export { default as useTheme } from "./hooks/useTheme.js";
60
+ export { default as useToast } from "./hooks/useToast.js";
61
+ export * from "./lib/classNames.js";
62
+ export * from "./lib/format.js";
63
+ export * from "./lib/storage.js";
64
+ export * from "./lib/keyboard.js";
65
+ export * from "./lib/a11y.js";
66
+ export * from "./lib/motion.js";
67
+ export { default as SceneBoundary } from "./components/hero/SceneBoundary.js";
68
+ export { safeHref } from "./lib/safeHref.js";