@shohojdhara/atomix 0.4.8 → 0.4.9
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/atomix.config.ts +58 -1
- package/dist/atomix.css +148 -120
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +1 -1
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1227 -122
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1052 -41
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2086 -1035
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1620 -600
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +441 -270
- package/dist/index.esm.js +1900 -638
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1935 -670
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +6 -3
- package/scripts/atomix-cli.js +148 -4
- package/scripts/cli/__tests__/basic.test.js +3 -2
- package/scripts/cli/__tests__/clean.test.js +278 -0
- package/scripts/cli/__tests__/component-validator.test.js +433 -0
- package/scripts/cli/__tests__/generator.test.js +613 -0
- package/scripts/cli/__tests__/glass-motion.test.js +256 -0
- package/scripts/cli/__tests__/integration.test.js +719 -108
- package/scripts/cli/__tests__/migrate.test.js +74 -0
- package/scripts/cli/__tests__/security.test.js +206 -0
- package/scripts/cli/__tests__/test-setup.js +3 -1
- package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
- package/scripts/cli/__tests__/token-provider.test.js +361 -0
- package/scripts/cli/__tests__/utils.test.js +5 -5
- package/scripts/cli/commands/benchmark.js +105 -0
- package/scripts/cli/commands/build-theme.js +4 -1
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +135 -14
- package/scripts/cli/commands/init.js +45 -18
- package/scripts/cli/commands/migrate.js +106 -0
- package/scripts/cli/commands/sync-tokens.js +206 -0
- package/scripts/cli/commands/theme-bridge.js +248 -0
- package/scripts/cli/commands/tokens.js +157 -0
- package/scripts/cli/commands/validate.js +194 -0
- package/scripts/cli/internal/ai-engine.js +156 -0
- package/scripts/cli/internal/component-validator.js +443 -0
- package/scripts/cli/internal/config-loader.js +162 -0
- package/scripts/cli/internal/filesystem.js +102 -2
- package/scripts/cli/internal/generator.js +359 -39
- package/scripts/cli/internal/glass-generator.js +398 -0
- package/scripts/cli/internal/hook-generator.js +369 -0
- package/scripts/cli/internal/hooks.js +61 -0
- package/scripts/cli/internal/itcss-generator.js +565 -0
- package/scripts/cli/internal/motion-generator.js +679 -0
- package/scripts/cli/internal/template-engine.js +301 -0
- package/scripts/cli/internal/theme-bridge.js +664 -0
- package/scripts/cli/internal/tokens/engine.js +122 -0
- package/scripts/cli/internal/tokens/provider.js +34 -0
- package/scripts/cli/internal/tokens/providers/figma.js +50 -0
- package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
- package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
- package/scripts/cli/internal/tokens/token-provider.js +443 -0
- package/scripts/cli/internal/tokens/token-validator.js +513 -0
- package/scripts/cli/internal/validator.js +276 -0
- package/scripts/cli/internal/wizard.js +60 -6
- package/scripts/cli/mappings.js +23 -0
- package/scripts/cli/migration-tools.js +164 -94
- package/scripts/cli/plugins/style-dictionary.js +46 -0
- package/scripts/cli/templates/README.md +525 -95
- package/scripts/cli/templates/common-templates.js +40 -14
- package/scripts/cli/templates/components/react-component.ts +282 -0
- package/scripts/cli/templates/config/project-config.ts +112 -0
- package/scripts/cli/templates/hooks/use-component.ts +477 -0
- package/scripts/cli/templates/index.js +19 -4
- package/scripts/cli/templates/index.ts +171 -0
- package/scripts/cli/templates/next-templates.js +72 -0
- package/scripts/cli/templates/react-templates.js +70 -126
- package/scripts/cli/templates/scss-templates.js +35 -35
- package/scripts/cli/templates/stories/storybook-story.ts +241 -0
- package/scripts/cli/templates/styles/scss-component.ts +255 -0
- package/scripts/cli/templates/tests/vitest-test.ts +229 -0
- package/scripts/cli/templates/token-templates.js +337 -1
- package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
- package/scripts/cli/templates/types/component-types.ts +145 -0
- package/scripts/cli/templates/utils/testing-utils.ts +144 -0
- package/scripts/cli/templates/vanilla-templates.js +39 -0
- package/scripts/cli/token-manager.js +8 -2
- package/scripts/cli/utils/cache-manager.js +240 -0
- package/scripts/cli/utils/detector.js +46 -0
- package/scripts/cli/utils/diagnostics.js +289 -0
- package/scripts/cli/utils/error.js +45 -3
- package/scripts/cli/utils/helpers.js +24 -0
- package/scripts/cli/utils/logger.js +1 -1
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +4 -38
- package/scripts/cli/utils.js +46 -0
- package/src/components/Accordion/Accordion.stories.tsx +0 -18
- package/src/components/Accordion/Accordion.test.tsx +0 -17
- package/src/components/Accordion/Accordion.tsx +0 -4
- package/src/components/AtomixGlass/AtomixGlass.tsx +102 -2
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +125 -12
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/animation-system.ts +578 -0
- package/src/components/AtomixGlass/shader-utils.ts +4 -1
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
- package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
- package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
- package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
- package/src/components/Avatar/Avatar.tsx +1 -1
- package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
- package/src/components/Button/Button.stories.tsx +10 -0
- package/src/components/Button/Button.test.tsx +16 -11
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +12 -12
- package/src/components/Form/Select.tsx +62 -3
- package/src/components/Modal/Modal.tsx +14 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
- package/src/components/Slider/Slider.stories.tsx +3 -3
- package/src/components/Slider/Slider.tsx +38 -0
- package/src/components/Steps/Steps.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +77 -8
- package/src/components/Testimonial/Testimonial.tsx +1 -1
- package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
- package/src/components/TypedButton/TypedButton.tsx +39 -0
- package/src/components/TypedButton/index.ts +2 -0
- package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
- package/src/lib/composables/index.ts +4 -7
- package/src/lib/composables/types.ts +45 -0
- package/src/lib/composables/useAccordion.ts +0 -7
- package/src/lib/composables/useAtomixGlass.ts +144 -5
- package/src/lib/composables/useChartExport.ts +3 -13
- package/src/lib/composables/useDropdown.ts +66 -0
- package/src/lib/composables/useFocusTrap.ts +80 -0
- package/src/lib/composables/usePerformanceMonitor.ts +448 -0
- package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
- package/src/lib/composables/useResponsiveGlass.ts +441 -0
- package/src/lib/composables/useTooltip.ts +16 -0
- package/src/lib/composables/useTypedButton.ts +66 -0
- package/src/lib/config/index.ts +62 -5
- package/src/lib/constants/components.ts +55 -0
- package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
- package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
- package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
- package/src/lib/types/components.ts +37 -11
- package/src/lib/types/glass.ts +35 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/utils/displacement-generator.ts +1 -1
- package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
- package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
- package/src/styles/06-components/_components.testbutton.scss +212 -0
- package/src/styles/06-components/_components.testtypecheck.scss +212 -0
- package/src/styles/06-components/_components.typedbutton.scss +212 -0
- package/src/styles/99-utilities/_index.scss +1 -0
- package/src/styles/99-utilities/_utilities.text.scss +1 -1
- package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
- package/src/styles/06-components/old.chart.styles.scss +0 -2788
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestTypeCheck Hook Types
|
|
3
|
+
* Generated by Atomix CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Component State Interface
|
|
8
|
+
*/
|
|
9
|
+
export interface ComponentState {
|
|
10
|
+
isActive: boolean;
|
|
11
|
+
isHovered: boolean;
|
|
12
|
+
isFocused: boolean;
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
variant: 'primary' | 'secondary' | 'outline' | string;
|
|
15
|
+
size: 'sm' | 'md' | 'lg' | string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Event Handler Types
|
|
20
|
+
*/
|
|
21
|
+
export type ClickHandler = (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
22
|
+
export type HoverHandler = (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
23
|
+
export type FocusHandler = (event: React.FocusEvent<HTMLDivElement>) => void;
|
|
24
|
+
export type BlurHandler = (event: React.FocusEvent<HTMLDivElement>) => void;
|
|
25
|
+
export type StateChangeHandler = (state: ComponentState) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Variant Configuration
|
|
29
|
+
*/
|
|
30
|
+
export type VariantType = 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Size Configuration
|
|
34
|
+
*/
|
|
35
|
+
export type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Glass Effect Configuration
|
|
39
|
+
*/
|
|
40
|
+
export interface GlassConfig {
|
|
41
|
+
displacementScale?: number;
|
|
42
|
+
blurAmount?: number;
|
|
43
|
+
saturation?: number;
|
|
44
|
+
elasticity?: number;
|
|
45
|
+
}
|
|
@@ -23,6 +23,13 @@ import {
|
|
|
23
23
|
lerp,
|
|
24
24
|
} from '../../components/AtomixGlass/glass-utils';
|
|
25
25
|
import { updateAtomixGlassStyles } from './useAtomixGlassStyles';
|
|
26
|
+
// Phase 1: Time-Based Animation System
|
|
27
|
+
import {
|
|
28
|
+
createAnimationLoop,
|
|
29
|
+
createFBMEngine,
|
|
30
|
+
getFBMConfigForQuality,
|
|
31
|
+
liquidGlassWithTime,
|
|
32
|
+
} from '../../components/AtomixGlass/animation-system';
|
|
26
33
|
|
|
27
34
|
const { CONSTANTS } = ATOMIX_GLASS;
|
|
28
35
|
|
|
@@ -151,6 +158,13 @@ interface UseAtomixGlassOptions extends Omit<AtomixGlassProps, 'children'> {
|
|
|
151
158
|
wrapperRef?: React.RefObject<HTMLDivElement>;
|
|
152
159
|
children?: React.ReactNode;
|
|
153
160
|
isFixedOrSticky?: boolean;
|
|
161
|
+
// Phase 1: Time-Based Animation System
|
|
162
|
+
withLiquidBlur?: boolean;
|
|
163
|
+
animationQuality?: 'low' | 'medium' | 'high';
|
|
164
|
+
timeSpeed?: number;
|
|
165
|
+
noiseAmplitude?: number;
|
|
166
|
+
noiseFrequency?: number;
|
|
167
|
+
displacementStrength?: number;
|
|
154
168
|
}
|
|
155
169
|
|
|
156
170
|
interface UseAtomixGlassReturn {
|
|
@@ -182,6 +196,10 @@ interface UseAtomixGlassReturn {
|
|
|
182
196
|
// Transform calculations
|
|
183
197
|
transformStyle: string;
|
|
184
198
|
|
|
199
|
+
// Phase 1: Animation System - Shader time control
|
|
200
|
+
getShaderTime: () => number;
|
|
201
|
+
applyTimeBasedDistortion: (uv: { x: number; y: number }) => { x: number; y: number };
|
|
202
|
+
|
|
185
203
|
// Event handlers
|
|
186
204
|
handleMouseEnter: () => void;
|
|
187
205
|
handleMouseLeave: () => void;
|
|
@@ -218,6 +236,14 @@ export function useAtomixGlass({
|
|
|
218
236
|
padding,
|
|
219
237
|
withLiquidBlur,
|
|
220
238
|
isFixedOrSticky = false,
|
|
239
|
+
// Phase 1: Animation System Props
|
|
240
|
+
withTimeAnimation = ATOMIX_GLASS.DEFAULTS.WITH_TIME_ANIMATION,
|
|
241
|
+
animationSpeed = ATOMIX_GLASS.DEFAULTS.ANIMATION_SPEED,
|
|
242
|
+
withMultiLayerDistortion = ATOMIX_GLASS.DEFAULTS.WITH_MULTI_LAYER_DISTORTION,
|
|
243
|
+
distortionOctaves = ATOMIX_GLASS.DEFAULTS.DISTORTION_OCTAVES,
|
|
244
|
+
distortionLacunarity = ATOMIX_GLASS.DEFAULTS.DISTORTION_LACUNARITY,
|
|
245
|
+
distortionGain = ATOMIX_GLASS.DEFAULTS.DISTORTION_GAIN,
|
|
246
|
+
distortionQuality = ATOMIX_GLASS.DEFAULTS.DISTORTION_QUALITY,
|
|
221
247
|
}: UseAtomixGlassOptions): UseAtomixGlassReturn {
|
|
222
248
|
// State
|
|
223
249
|
const [isHovered, setIsHovered] = useState(false);
|
|
@@ -243,6 +269,115 @@ export function useAtomixGlass({
|
|
|
243
269
|
const [userPrefersHighContrast, setUserPrefersHighContrast] = useState(false);
|
|
244
270
|
const [detectedOverLight, setDetectedOverLight] = useState(false);
|
|
245
271
|
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// Phase 1: Time-Based Animation System (Feature 1.1)
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
// Animation state refs
|
|
277
|
+
const animationFrameIdRef = useRef<number | null>(null);
|
|
278
|
+
const animationStartTimeRef = useRef<number>(0);
|
|
279
|
+
const elapsedTimeRef = useRef<number>(0);
|
|
280
|
+
const shaderTimeRef = useRef<number>(0);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get FBM configuration based on quality preset or custom values
|
|
284
|
+
*/
|
|
285
|
+
const fbmConfig = useMemo(() => {
|
|
286
|
+
// If quality preset is provided, use it as base
|
|
287
|
+
const preset = getFBMConfigForQuality(distortionQuality);
|
|
288
|
+
|
|
289
|
+
// Override with custom values if provided
|
|
290
|
+
return {
|
|
291
|
+
octaves: distortionOctaves ?? preset.octaves,
|
|
292
|
+
lacunarity: distortionLacunarity ?? preset.lacunarity,
|
|
293
|
+
gain: distortionGain ?? preset.gain,
|
|
294
|
+
};
|
|
295
|
+
}, [distortionQuality, distortionOctaves, distortionLacunarity, distortionGain]);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create FBM engine for multi-layer distortion
|
|
299
|
+
*/
|
|
300
|
+
const fbmEngine = useMemo(() => {
|
|
301
|
+
if (!withMultiLayerDistortion) return null;
|
|
302
|
+
return createFBMEngine(fbmConfig);
|
|
303
|
+
}, [withMultiLayerDistortion, fbmConfig]);
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Determine effective animation settings
|
|
307
|
+
*/
|
|
308
|
+
const effectiveReducedMotion = useMemo(
|
|
309
|
+
() => reducedMotion || userPrefersReducedMotion,
|
|
310
|
+
[reducedMotion, userPrefersReducedMotion]
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const effectiveWithTimeAnimation = useMemo(() => {
|
|
314
|
+
return withTimeAnimation && !effectiveReducedMotion;
|
|
315
|
+
}, [withTimeAnimation, effectiveReducedMotion]);
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Animation loop for time-based effects
|
|
319
|
+
*/
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
if (!effectiveWithTimeAnimation || typeof window === 'undefined') {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let lastFrameTime = performance.now();
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Animation frame handler
|
|
329
|
+
*/
|
|
330
|
+
const animate = (currentTime: number) => {
|
|
331
|
+
// Calculate delta time
|
|
332
|
+
const deltaTime = currentTime - lastFrameTime;
|
|
333
|
+
lastFrameTime = currentTime;
|
|
334
|
+
|
|
335
|
+
// Apply animation speed multiplier
|
|
336
|
+
const scaledDelta = deltaTime * animationSpeed;
|
|
337
|
+
elapsedTimeRef.current += scaledDelta;
|
|
338
|
+
shaderTimeRef.current = elapsedTimeRef.current;
|
|
339
|
+
|
|
340
|
+
// Continue animation loop
|
|
341
|
+
animationFrameIdRef.current = requestAnimationFrame(animate);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Start animation
|
|
345
|
+
animationStartTimeRef.current = performance.now();
|
|
346
|
+
animationFrameIdRef.current = requestAnimationFrame(animate);
|
|
347
|
+
|
|
348
|
+
// Cleanup
|
|
349
|
+
return () => {
|
|
350
|
+
if (animationFrameIdRef.current !== null) {
|
|
351
|
+
cancelAnimationFrame(animationFrameIdRef.current);
|
|
352
|
+
animationFrameIdRef.current = null;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}, [effectiveWithTimeAnimation, animationSpeed]);
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get current shader time for animations
|
|
359
|
+
*/
|
|
360
|
+
const getShaderTime = useCallback(() => {
|
|
361
|
+
return shaderTimeRef.current;
|
|
362
|
+
}, []);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Apply time-based distortion to UV coordinates
|
|
366
|
+
*/
|
|
367
|
+
const applyTimeBasedDistortion = useCallback(
|
|
368
|
+
(uv: { x: number; y: number }): { x: number; y: number } => {
|
|
369
|
+
if (!effectiveWithTimeAnimation || !fbmEngine) {
|
|
370
|
+
return uv;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const time = shaderTimeRef.current;
|
|
374
|
+
|
|
375
|
+
// Apply liquid glass distortion with time
|
|
376
|
+
return liquidGlassWithTime(uv, time, fbmConfig);
|
|
377
|
+
},
|
|
378
|
+
[effectiveWithTimeAnimation, fbmEngine, fbmConfig]
|
|
379
|
+
);
|
|
380
|
+
|
|
246
381
|
// Memoized derived values
|
|
247
382
|
const effectiveBorderRadius = useMemo(() => {
|
|
248
383
|
if (borderRadius !== undefined) {
|
|
@@ -259,10 +394,6 @@ export function useAtomixGlass({
|
|
|
259
394
|
cachedRectRef
|
|
260
395
|
});
|
|
261
396
|
|
|
262
|
-
const effectiveReducedMotion = useMemo(
|
|
263
|
-
() => reducedMotion || userPrefersReducedMotion,
|
|
264
|
-
[reducedMotion, userPrefersReducedMotion]
|
|
265
|
-
);
|
|
266
397
|
|
|
267
398
|
const effectiveHighContrast = useMemo(
|
|
268
399
|
() => highContrast || userPrefersHighContrast,
|
|
@@ -701,6 +832,12 @@ export function useAtomixGlass({
|
|
|
701
832
|
const tick = () => {
|
|
702
833
|
if (!lerpActiveRef.current) return;
|
|
703
834
|
|
|
835
|
+
// Add ref validity check to prevent memory leaks
|
|
836
|
+
if (!glassRef.current || !wrapperRef?.current) {
|
|
837
|
+
lerpActiveRef.current = false;
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
704
841
|
const cur = internalMouseOffsetRef.current;
|
|
705
842
|
const tgt = targetMouseOffsetRef.current;
|
|
706
843
|
|
|
@@ -726,7 +863,7 @@ export function useAtomixGlass({
|
|
|
726
863
|
|
|
727
864
|
// Imperative style update with the smoothed values
|
|
728
865
|
updateAtomixGlassStyles(
|
|
729
|
-
wrapperRef
|
|
866
|
+
wrapperRef.current,
|
|
730
867
|
glassRef.current,
|
|
731
868
|
{
|
|
732
869
|
mouseOffset: internalMouseOffsetRef.current,
|
|
@@ -916,6 +1053,8 @@ export function useAtomixGlass({
|
|
|
916
1053
|
mouseOffset, // This is now static (refs or props) unless prop changes
|
|
917
1054
|
overLightConfig,
|
|
918
1055
|
transformStyle,
|
|
1056
|
+
getShaderTime,
|
|
1057
|
+
applyTimeBasedDistortion,
|
|
919
1058
|
handleMouseEnter,
|
|
920
1059
|
handleMouseLeave,
|
|
921
1060
|
handleMouseDown,
|
|
@@ -139,20 +139,10 @@ export function useChartExport() {
|
|
|
139
139
|
|
|
140
140
|
// Export as PDF
|
|
141
141
|
const exportAsPDF = useCallback(
|
|
142
|
-
async (
|
|
143
|
-
|
|
144
|
-
// For now, we'll convert to canvas and then to PDF
|
|
145
|
-
const canvas = await svgToCanvas(svgElement, options);
|
|
146
|
-
|
|
147
|
-
// This would require jsPDF library
|
|
148
|
-
// const pdf = new jsPDF();
|
|
149
|
-
// const imgData = canvas.toDataURL('image/png');
|
|
150
|
-
// pdf.addImage(imgData, 'PNG', 0, 0);
|
|
151
|
-
// pdf.save(options.filename || 'chart.pdf');
|
|
152
|
-
|
|
153
|
-
console.warn('PDF export requires jsPDF library to be installed');
|
|
142
|
+
async (_svgElement: SVGSVGElement, _options: ExportOptions): Promise<void> => {
|
|
143
|
+
console.warn('PDF export requires a PDF library to be installed');
|
|
154
144
|
},
|
|
155
|
-
[
|
|
145
|
+
[]
|
|
156
146
|
);
|
|
157
147
|
|
|
158
148
|
// Export data as CSV
|
|
@@ -107,6 +107,72 @@ export const useDropdown = ({
|
|
|
107
107
|
};
|
|
108
108
|
}, [isOpen, closeOnEscape, setIsOpen]);
|
|
109
109
|
|
|
110
|
+
// Handle arrow key navigation
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!isOpen || !menuRef.current) return undefined;
|
|
113
|
+
|
|
114
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
115
|
+
const menu = menuRef.current;
|
|
116
|
+
if (!menu) return;
|
|
117
|
+
|
|
118
|
+
const items = Array.from(
|
|
119
|
+
menu.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])')
|
|
120
|
+
);
|
|
121
|
+
if (items.length === 0) return;
|
|
122
|
+
|
|
123
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLElement);
|
|
124
|
+
|
|
125
|
+
switch (event.key) {
|
|
126
|
+
case 'ArrowDown':
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
129
|
+
const nextItem = items[nextIndex];
|
|
130
|
+
if (nextItem) nextItem.focus();
|
|
131
|
+
break;
|
|
132
|
+
case 'ArrowUp':
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
const prevIndex = (currentIndex - 1 + items.length) % items.length;
|
|
135
|
+
const prevItem = items[prevIndex];
|
|
136
|
+
if (prevItem) prevItem.focus();
|
|
137
|
+
break;
|
|
138
|
+
case 'Home':
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
const firstItem = items[0];
|
|
141
|
+
if (firstItem) firstItem.focus();
|
|
142
|
+
break;
|
|
143
|
+
case 'End':
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
const lastItem = items[items.length - 1];
|
|
146
|
+
if (lastItem) lastItem.focus();
|
|
147
|
+
break;
|
|
148
|
+
case 'Tab':
|
|
149
|
+
// Close dropdown on tab
|
|
150
|
+
setIsOpen(false);
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
161
|
+
};
|
|
162
|
+
}, [isOpen, setIsOpen]);
|
|
163
|
+
|
|
164
|
+
// Focus management when dropdown opens
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (isOpen && menuRef.current) {
|
|
167
|
+
const firstItem = menuRef.current.querySelector<HTMLElement>(
|
|
168
|
+
'[role="menuitem"]:not([disabled])'
|
|
169
|
+
);
|
|
170
|
+
if (firstItem) {
|
|
171
|
+
setTimeout(() => firstItem.focus(), 0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, [isOpen]);
|
|
175
|
+
|
|
110
176
|
// Helper function to get the flipped placement if needed
|
|
111
177
|
const getFlippedPlacement = useCallback(
|
|
112
178
|
(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to trap focus within an element
|
|
5
|
+
*/
|
|
6
|
+
export function useFocusTrap(isOpen: boolean) {
|
|
7
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
8
|
+
const previouslyFocusedElement = useRef<HTMLElement | null>(null);
|
|
9
|
+
|
|
10
|
+
const getFocusableElements = useCallback(() => {
|
|
11
|
+
if (!containerRef.current) return [];
|
|
12
|
+
|
|
13
|
+
return Array.from(
|
|
14
|
+
containerRef.current.querySelectorAll<HTMLElement>(
|
|
15
|
+
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
16
|
+
)
|
|
17
|
+
).filter((el) => {
|
|
18
|
+
// Check if visible
|
|
19
|
+
const style = window.getComputedStyle(el);
|
|
20
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
21
|
+
});
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const handleKeyDown = useCallback(
|
|
25
|
+
(event: KeyboardEvent) => {
|
|
26
|
+
if (event.key !== 'Tab') return;
|
|
27
|
+
|
|
28
|
+
const focusableElements = getFocusableElements();
|
|
29
|
+
if (focusableElements.length === 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const firstElement = focusableElements[0] as HTMLElement;
|
|
34
|
+
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
|
|
35
|
+
|
|
36
|
+
if (event.shiftKey) {
|
|
37
|
+
// Shift + Tab
|
|
38
|
+
if (document.activeElement === firstElement) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
lastElement.focus();
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// Tab
|
|
44
|
+
if (document.activeElement === lastElement) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
firstElement.focus();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
[getFocusableElements]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (isOpen) {
|
|
55
|
+
previouslyFocusedElement.current = document.activeElement as HTMLElement;
|
|
56
|
+
|
|
57
|
+
const focusableElements = getFocusableElements();
|
|
58
|
+
if (focusableElements.length > 0 && focusableElements[0]) {
|
|
59
|
+
// Delay focus slightly to ensure element is rendered
|
|
60
|
+
const firstEl = focusableElements[0] as HTMLElement;
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
firstEl.focus();
|
|
63
|
+
}, 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
67
|
+
} else {
|
|
68
|
+
if (previouslyFocusedElement.current) {
|
|
69
|
+
previouslyFocusedElement.current.focus();
|
|
70
|
+
}
|
|
71
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
76
|
+
};
|
|
77
|
+
}, [isOpen, getFocusableElements, handleKeyDown]);
|
|
78
|
+
|
|
79
|
+
return containerRef;
|
|
80
|
+
}
|