@abuzareal/contour-ui 0.1.1 → 0.2.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 (70) hide show
  1. package/CHANGELOG.md +86 -11
  2. package/CODE_OF_CONDUCT.md +5 -0
  3. package/CONTRIBUTING.md +15 -0
  4. package/NEXTJS.md +51 -0
  5. package/README.md +176 -76
  6. package/RELEASING.md +14 -28
  7. package/SECURITY.md +1 -1
  8. package/dist/components/hero/SceneBoundary.js +1 -0
  9. package/dist/components/hero/Sculpture.js +6 -4
  10. package/dist/components/three/ObjectStudy.js +7 -3
  11. package/dist/components/ui/Alert.js +1 -0
  12. package/dist/components/ui/BrandMark.js +1 -0
  13. package/dist/components/ui/Checkbox.js +1 -0
  14. package/dist/components/ui/CountUp.js +1 -0
  15. package/dist/components/ui/DataTable.js +1 -0
  16. package/dist/components/ui/Dialog.js +1 -0
  17. package/dist/components/ui/DropdownMenu.d.ts +7 -1
  18. package/dist/components/ui/DropdownMenu.js +62 -14
  19. package/dist/components/ui/FlipCard.js +1 -0
  20. package/dist/components/ui/Magnetic.js +1 -0
  21. package/dist/components/ui/Pagination.js +1 -0
  22. package/dist/components/ui/Popover.d.ts +4 -1
  23. package/dist/components/ui/Popover.js +7 -5
  24. package/dist/components/ui/RadioGroup.js +1 -0
  25. package/dist/components/ui/Reveal.js +1 -0
  26. package/dist/components/ui/ScrollProgress.js +1 -0
  27. package/dist/components/ui/SegmentedControl.js +1 -0
  28. package/dist/components/ui/SelectField.d.ts +11 -1
  29. package/dist/components/ui/SelectField.js +6 -4
  30. package/dist/components/ui/Slider.js +1 -0
  31. package/dist/components/ui/Switch.js +1 -0
  32. package/dist/components/ui/Tabs.d.ts +4 -1
  33. package/dist/components/ui/Tabs.js +12 -5
  34. package/dist/components/ui/Tag.js +1 -0
  35. package/dist/components/ui/TextArea.d.ts +8 -1
  36. package/dist/components/ui/TextArea.js +21 -7
  37. package/dist/components/ui/TextField.d.ts +10 -1
  38. package/dist/components/ui/TextField.js +17 -6
  39. package/dist/components/ui/ThemeToggle.js +1 -0
  40. package/dist/components/ui/TiltCard.js +1 -0
  41. package/dist/components/ui/ToastProvider.js +1 -0
  42. package/dist/components/ui/Tooltip.js +1 -0
  43. package/dist/fonts/dm-mono-LICENSE.txt +93 -0
  44. package/dist/fonts/dm-mono-latin-400-normal.woff2 +0 -0
  45. package/dist/fonts/dm-sans-LICENSE.txt +93 -0
  46. package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
  47. package/dist/fonts/space-grotesk-LICENSE.txt +93 -0
  48. package/dist/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
  49. package/dist/fonts-latin.css +35 -0
  50. package/dist/hooks/useClipboardCopy.js +1 -0
  51. package/dist/hooks/useControlRef.d.ts +6 -0
  52. package/dist/hooks/useControlRef.js +13 -0
  53. package/dist/hooks/useDismiss.js +1 -0
  54. package/dist/hooks/useFormReset.d.ts +3 -0
  55. package/dist/hooks/useFormReset.js +31 -0
  56. package/dist/hooks/useHeaderScroll.js +1 -0
  57. package/dist/hooks/useIdleTrigger.js +1 -0
  58. package/dist/hooks/useInitialHashScroll.js +1 -0
  59. package/dist/hooks/useMobileMenu.js +1 -0
  60. package/dist/hooks/useMotionPreference.js +1 -0
  61. package/dist/hooks/useOpenState.d.ts +2 -0
  62. package/dist/hooks/useOpenState.js +15 -0
  63. package/dist/hooks/usePrintPreparation.js +1 -0
  64. package/dist/hooks/useScrollSpy.js +1 -0
  65. package/dist/hooks/useSmoothScroll.js +1 -0
  66. package/dist/hooks/useTheme.js +1 -0
  67. package/dist/hooks/useThreeStage.d.ts +4 -0
  68. package/dist/hooks/useThreeStage.js +165 -128
  69. package/dist/hooks/useToast.js +1 -0
  70. package/package.json +29 -12
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  /** Shared Three.js stage: renderer, studio lighting, resizing, a visibility-aware render loop, pointer tracking, and cleanup. */
2
3
  import { useEffect, useRef } from "react";
3
4
  import * as THREE from "three";
@@ -16,140 +17,176 @@ export default function useThreeStage(hostRef, { motionEnabled, setup, cameraDis
16
17
  const host = hostRef.current;
17
18
  if (!host)
18
19
  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);
20
+ const disposers = [];
21
+ const own = (dispose) => {
22
+ let disposed = false;
23
+ const release = () => {
24
+ if (disposed)
25
+ return;
26
+ disposed = true;
27
+ dispose();
28
+ };
29
+ disposers.push(release);
30
+ return release;
79
31
  };
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);
32
+ const cleanup = () => {
33
+ syncRef.current = undefined;
34
+ for (const dispose of disposers.reverse()) {
35
+ try {
36
+ dispose();
37
+ }
38
+ catch (error) {
39
+ console.error("Contour stage cleanup failed", error);
40
+ }
99
41
  }
100
- else if (!shouldRun && running) {
42
+ disposers.length = 0;
43
+ };
44
+ try {
45
+ const renderer = new THREE.WebGLRenderer({
46
+ alpha: true,
47
+ antialias: true,
48
+ });
49
+ own(() => renderer.dispose());
50
+ own(() => renderer.domElement.remove());
51
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, maxPixelRatio));
52
+ renderer.setClearColor(0x000000, 0);
53
+ // Neutral tone mapping keeps the lime close to the CSS accent colour.
54
+ renderer.toneMapping = THREE.NeutralToneMapping;
55
+ renderer.toneMappingExposure = 1.05;
56
+ renderer.domElement.setAttribute("aria-hidden", "true");
57
+ host.appendChild(renderer.domElement);
58
+ // Studio lighting shared by every stage so materials read consistently.
59
+ const scene = new THREE.Scene();
60
+ const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 30);
61
+ camera.position.set(0, 0, 10.5);
62
+ const environment = new RoomEnvironment();
63
+ const releaseEnvironment = own(() => environment.dispose());
64
+ const generator = new THREE.PMREMGenerator(renderer);
65
+ const releaseGenerator = own(() => generator.dispose());
66
+ const environmentMap = generator.fromScene(environment, 0.04);
67
+ own(() => environmentMap.dispose());
68
+ scene.environment = environmentMap.texture;
69
+ releaseEnvironment();
70
+ releaseGenerator();
71
+ const keyLight = new THREE.DirectionalLight(0xffffff, 4);
72
+ keyLight.position.set(4, 5, 6);
73
+ const rimLight = new THREE.DirectionalLight(0xcad9ff, 3);
74
+ rimLight.position.set(-5, 1, -2);
75
+ scene.add(keyLight, rimLight, new THREE.AmbientLight(0xffffff, 0.7));
76
+ const stage = setupRef.current.setup({
77
+ scene,
78
+ camera,
79
+ renderer,
80
+ onDispose: own,
81
+ });
82
+ own(() => stage.dispose());
83
+ // Loop state: frames run only while visible, focused, and motion is on.
84
+ const pointer = new THREE.Vector2();
85
+ let visible = true;
86
+ let contextLost = false;
87
+ let running = false;
88
+ let frame = 0;
89
+ let elapsed = 0;
90
+ let previousTime = 0;
91
+ own(() => {
101
92
  running = false;
102
93
  cancelAnimationFrame(frame);
103
- }
104
- if (!motion) {
105
- pointer.set(0, 0);
106
- stage.rest();
94
+ });
95
+ const render = () => {
96
+ if (contextLost)
97
+ return;
98
+ renderer.render(scene, camera);
99
+ host.dataset.state = "ready";
100
+ };
101
+ const resize = () => {
102
+ const { width, height } = host.getBoundingClientRect();
103
+ if (!width || !height)
104
+ return;
105
+ camera.aspect = width / height;
106
+ camera.position.z = setupRef.current.cameraDistance(camera.aspect);
107
+ camera.updateProjectionMatrix();
108
+ renderer.setSize(width, height);
109
+ stage.resize?.();
107
110
  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)
111
+ };
112
+ const trackPointer = (event) => {
113
+ if (event.pointerType === "touch" || !motionRef.current)
114
+ return;
115
+ const bounds = host.getBoundingClientRect();
116
+ pointer.set((event.clientX - bounds.left) / bounds.width - 0.5, (event.clientY - bounds.top) / bounds.height - 0.5);
117
+ };
118
+ const resetPointer = () => pointer.set(0, 0);
119
+ const animate = (time) => {
120
+ if (!running)
121
+ return;
122
+ const delta = Math.min(Math.max(time - previousTime, 0) / 1000, 0.05);
123
+ previousTime = time;
124
+ elapsed += delta;
125
+ stage.update({ delta, elapsed, pointer });
126
+ render();
127
+ frame = requestAnimationFrame(animate);
128
+ };
129
+ /** Starts or stops the loop to match visibility and motion; snaps to rest when paused. */
130
+ const sync = () => {
131
+ const motion = motionRef.current;
132
+ const shouldRun = motion && visible && !document.hidden && !contextLost;
133
+ if (shouldRun && !running) {
134
+ running = true;
135
+ previousTime = performance.now();
136
+ frame = requestAnimationFrame(animate);
137
+ }
138
+ else if (!shouldRun && running) {
139
+ running = false;
140
+ cancelAnimationFrame(frame);
141
+ }
142
+ if (!motion) {
143
+ pointer.set(0, 0);
144
+ stage.rest();
145
+ render();
146
+ }
147
+ };
148
+ syncRef.current = sync;
149
+ const loseContext = (event) => {
150
+ event.preventDefault();
151
+ contextLost = true;
152
+ host.dataset.state = "unavailable";
153
+ sync();
154
+ };
155
+ const restoreContext = () => {
156
+ contextLost = false;
125
157
  render();
158
+ sync();
159
+ };
160
+ const visibilityObserver = new IntersectionObserver(([entry]) => {
161
+ visible = entry.isIntersecting;
162
+ if (visible)
163
+ render();
164
+ sync();
165
+ });
166
+ own(() => visibilityObserver.disconnect());
167
+ const resizeObserver = new ResizeObserver(resize);
168
+ own(() => resizeObserver.disconnect());
169
+ visibilityObserver.observe(host);
170
+ resizeObserver.observe(host);
171
+ host.addEventListener("pointermove", trackPointer);
172
+ own(() => host.removeEventListener("pointermove", trackPointer));
173
+ host.addEventListener("pointerleave", resetPointer);
174
+ own(() => host.removeEventListener("pointerleave", resetPointer));
175
+ document.addEventListener("visibilitychange", sync);
176
+ own(() => document.removeEventListener("visibilitychange", sync));
177
+ renderer.domElement.addEventListener("webglcontextlost", loseContext);
178
+ own(() => renderer.domElement.removeEventListener("webglcontextlost", loseContext));
179
+ renderer.domElement.addEventListener("webglcontextrestored", restoreContext);
180
+ own(() => renderer.domElement.removeEventListener("webglcontextrestored", restoreContext));
181
+ resize();
126
182
  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
- };
183
+ return cleanup;
184
+ }
185
+ catch (error) {
186
+ cleanup();
187
+ host.dataset.state = "unavailable";
188
+ console.error("Contour stage initialization failed", error);
189
+ return;
190
+ }
154
191
  }, [hostRef]);
155
192
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  /** Toast context: lets any component queue a notification rendered by ToastProvider. */
2
3
  import { createContext, useContext } from "react";
3
4
  export const ToastContext = createContext(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abuzareal/contour-ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Editorial React components, themes, restrained motion, and optional Three.js objects.",
6
6
  "type": "module",
@@ -10,7 +10,10 @@
10
10
  "LICENSE",
11
11
  "CHANGELOG.md",
12
12
  "SECURITY.md",
13
- "RELEASING.md"
13
+ "RELEASING.md",
14
+ "CONTRIBUTING.md",
15
+ "CODE_OF_CONDUCT.md",
16
+ "NEXTJS.md"
14
17
  ],
15
18
  "sideEffects": [
16
19
  "**/*.css"
@@ -18,24 +21,29 @@
18
21
  "exports": {
19
22
  ".": {
20
23
  "types": "./dist/index.d.ts",
21
- "import": "./dist/index.js"
24
+ "import": "./dist/index.js",
25
+ "default": "./dist/index.js"
22
26
  },
23
27
  "./three": {
24
28
  "types": "./dist/three.d.ts",
25
- "import": "./dist/three.js"
29
+ "import": "./dist/three.js",
30
+ "default": "./dist/three.js"
26
31
  },
27
32
  "./scroll": {
28
33
  "types": "./dist/scroll.d.ts",
29
- "import": "./dist/scroll.js"
34
+ "import": "./dist/scroll.js",
35
+ "default": "./dist/scroll.js"
30
36
  },
31
37
  "./theme-bootstrap": {
32
38
  "types": "./dist/lib/themeBootstrap.d.ts",
33
- "import": "./dist/lib/themeBootstrap.js"
39
+ "import": "./dist/lib/themeBootstrap.js",
40
+ "default": "./dist/lib/themeBootstrap.js"
34
41
  },
35
42
  "./styles.css": "./dist/styles.css",
36
43
  "./reset.css": "./dist/reset.css",
37
44
  "./tokens.css": "./dist/tokens.css",
38
- "./fonts.css": "./dist/fonts.css"
45
+ "./fonts.css": "./dist/fonts.css",
46
+ "./fonts-latin.css": "./dist/fonts-latin.css"
39
47
  },
40
48
  "scripts": {
41
49
  "build": "node scripts/build.mjs",
@@ -45,9 +53,12 @@
45
53
  "test:browser": "node --test tests/*.browser.mjs",
46
54
  "check:package": "node scripts/check-package.mjs",
47
55
  "validate": "npm run typecheck && npm run lint && npm run build && npm test && npm run test:browser && npm run check:package",
48
- "prepublishOnly": "npm run validate && npm run test:compat",
56
+ "prepublishOnly": "node scripts/verify-release.mjs",
49
57
  "test:consumer": "node scripts/test-consumer.mjs",
50
- "test:compat": "node scripts/test-compat.mjs"
58
+ "test:compat": "node scripts/test-compat.mjs",
59
+ "test:optional": "node scripts/test-optional-consumer.mjs",
60
+ "test:next": "npm run build && node scripts/test-next.mjs",
61
+ "verify:release": "node scripts/verify-release.mjs"
51
62
  },
52
63
  "dependencies": {
53
64
  "@fontsource-variable/dm-sans": "^5.3.0",
@@ -72,7 +83,6 @@
72
83
  "devDependencies": {
73
84
  "@playwright/test": "1.63.0",
74
85
  "@testing-library/react": "^16.3.3",
75
- "@types/node": "^24.13.3",
76
86
  "@types/react": "^18.3.5",
77
87
  "@types/react-dom": "^18.3.0",
78
88
  "@types/three": "^0.186.0",
@@ -97,8 +107,15 @@
97
107
  "type": "git",
98
108
  "url": "git+https://github.com/abuzareal/contour-ui.git"
99
109
  },
100
- "homepage": "https://github.com/abuzareal/contour-ui#readme",
110
+ "homepage": "https://contour.abuzr.in/",
101
111
  "bugs": {
102
112
  "url": "https://github.com/abuzareal/contour-ui/issues"
103
- }
113
+ },
114
+ "keywords": [
115
+ "react",
116
+ "typescript",
117
+ "components",
118
+ "design-system",
119
+ "contour-ui"
120
+ ]
104
121
  }