@pallasoft/ps-core-components 2.3.0 → 2.4.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/dist/components/ps-button.js +5 -5
- package/dist/components/ps-draggable-grid.d.ts +9 -12
- package/dist/components/ps-draggable-grid.js +72 -45
- package/dist/components/ps-navigation-progress.d.ts +3 -2
- package/dist/components/ps-navigation-progress.js +13 -22
- package/dist/components/ps-section-header.js +1 -1
- package/dist/constants/ps-draggable-grid.d.ts +17 -0
- package/dist/constants/ps-draggable-grid.js +26 -0
- package/dist/constants/ps-navigation-progress-config.d.ts +5 -0
- package/dist/constants/ps-navigation-progress-config.js +5 -0
- package/dist/esm/components/ps-accordion.js +5 -0
- package/dist/esm/components/ps-action-tile.js +7 -0
- package/dist/esm/components/ps-audio-spectrum.js +9 -0
- package/dist/esm/components/ps-badge.js +10 -0
- package/dist/esm/components/ps-button.js +16 -0
- package/dist/esm/components/ps-card.js +14 -0
- package/dist/esm/components/ps-draggable-grid.js +80 -0
- package/dist/esm/components/ps-empty-state.js +28 -0
- package/dist/esm/components/ps-fab.js +9 -0
- package/dist/esm/components/ps-infinite-scroll.js +5 -0
- package/dist/esm/components/ps-layer.js +3 -0
- package/dist/esm/components/ps-list-tile.js +11 -0
- package/dist/esm/components/ps-metric-card.js +3 -0
- package/dist/esm/components/ps-navigation-progress.js +21 -0
- package/dist/esm/components/ps-pill.js +10 -0
- package/dist/esm/components/ps-portable-text.js +34 -0
- package/dist/esm/components/ps-reflective-app-shell.js +13 -0
- package/dist/esm/components/ps-section-header.js +2 -0
- package/dist/esm/components/ps-site-header.js +11 -0
- package/dist/esm/components/ps-skeleton.js +3 -0
- package/dist/esm/components/ps-spinner.js +12 -0
- package/dist/esm/components/ps-theme-toggle.js +7 -0
- package/dist/esm/components/ps-voice-orb.js +100 -0
- package/dist/esm/constants/ps-draggable-grid.js +23 -0
- package/dist/esm/constants/ps-navigation-progress-config.js +12 -0
- package/dist/esm/constants/ps-voice-orb.js +85 -0
- package/dist/esm/contexts/ps-layer-context.js +2 -0
- package/dist/esm/hooks/use-ps-audio-spectrum.js +65 -0
- package/dist/esm/hooks/use-ps-exit-transition.js +29 -0
- package/dist/esm/hooks/use-ps-hide-on-scroll.js +25 -0
- package/dist/esm/hooks/use-ps-invisible-recaptcha.js +90 -0
- package/dist/esm/hooks/use-ps-layer-z-index.js +4 -0
- package/dist/esm/hooks/use-ps-navigation-progress.js +41 -0
- package/dist/esm/hooks/use-ps-route-loader-data.js +12 -0
- package/dist/esm/hooks/use-ps-viewport.js +18 -0
- package/dist/esm/index.js +38 -0
- package/dist/esm/interfaces/ps-component-props.js +1 -0
- package/dist/esm/models/ps-draggable-grid.js +1 -0
- package/dist/esm/models/ps-navigation-progress.js +1 -0
- package/dist/esm/models/ps-recaptcha-pending-request.js +1 -0
- package/dist/esm/models/ps-remix-react-module.js +1 -0
- package/dist/esm/models/ps-voice-orb.js +1 -0
- package/dist/hooks/use-ps-exit-transition.js +0 -1
- package/dist/hooks/use-ps-navigation-progress.d.ts +2 -0
- package/dist/hooks/use-ps-navigation-progress.js +45 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/models/ps-draggable-grid.d.ts +19 -0
- package/dist/models/ps-draggable-grid.js +2 -0
- package/dist/models/ps-navigation-progress.d.ts +5 -0
- package/dist/models/ps-navigation-progress.js +2 -0
- package/dist/models/ps-recaptcha-pending-request.d.ts +4 -0
- package/dist/models/ps-recaptcha-pending-request.js +2 -0
- package/dist/models/ps-remix-react-module.d.ts +3 -0
- package/dist/models/ps-remix-react-module.js +2 -0
- package/package.json +11 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PortableText as Text } from '@portabletext/react';
|
|
4
|
+
export const PsPortableText = ({ bodyClasses = 'text-ink', containerClasses = '', containerStyles = {}, headerClasses = 'text-ink', isLargeBodyFont = false, linkClassName = 'text-primary underline', LinkComponent = 'a', listClasses = 'text-ink', text, }) => {
|
|
5
|
+
const hasContainerStyles = Object.keys(containerStyles).length > 0;
|
|
6
|
+
const getBodyClass = hasContainerStyles ? '' : bodyClasses;
|
|
7
|
+
const getHeaderClass = hasContainerStyles ? '' : headerClasses;
|
|
8
|
+
const getListClass = hasContainerStyles ? '' : listClasses;
|
|
9
|
+
const components = {
|
|
10
|
+
marks: {
|
|
11
|
+
link: ({ children, value }) => {
|
|
12
|
+
const rel = !value.href.startsWith('/') ? 'noreferrer noopener' : undefined;
|
|
13
|
+
return (_jsx(LinkComponent, { href: value.href, rel: rel, className: linkClassName, "aria-label": value.children, children: children }));
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
list: {
|
|
17
|
+
bullet: ({ children }) => _jsx("ul", { className: 'mb-3 mt-1 list-disc ml-4', children: children }),
|
|
18
|
+
number: ({ children }) => _jsx("ol", { className: 'mb-3 mt-1 list-decimal ml-4', children: children }),
|
|
19
|
+
},
|
|
20
|
+
listItem: {
|
|
21
|
+
bullet: ({ children }) => (_jsx("li", { className: PallasoftClassNameService.merge('md:text-body-2 max-md:text-body-3', getListClass), children: children })),
|
|
22
|
+
number: ({ children }) => (_jsx("li", { className: PallasoftClassNameService.merge('md:text-body-2 max-md:text-body-3', getListClass), children: children })),
|
|
23
|
+
},
|
|
24
|
+
block: {
|
|
25
|
+
normal: ({ children }) => (_jsx("p", { className: PallasoftClassNameService.merge(isLargeBodyFont ? 'text-body-1' : 'text-body-2', 'whitespace-pre-line', getBodyClass), children: children })),
|
|
26
|
+
h2: ({ children }) => (_jsx("h2", { className: PallasoftClassNameService.merge('text-header-1 py-2', getHeaderClass), children: children })),
|
|
27
|
+
h3: ({ children }) => (_jsx("h3", { className: PallasoftClassNameService.merge('text-header-2 py-2', getHeaderClass), children: children })),
|
|
28
|
+
h4: ({ children }) => (_jsx("h4", { className: PallasoftClassNameService.merge('text-header-3 py-2', getHeaderClass), children: children })),
|
|
29
|
+
h5: ({ children }) => (_jsx("h5", { className: PallasoftClassNameService.merge('text-header-3 py-1', getHeaderClass), children: children })),
|
|
30
|
+
h6: ({ children }) => (_jsx("h6", { className: PallasoftClassNameService.merge('text-header-4 py-1', getHeaderClass), children: children })),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return (_jsx("div", { className: containerClasses, style: containerStyles, children: _jsx(Text, { value: text, components: components }) }));
|
|
34
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
4
|
+
export const PsReflectiveAppShell = ({ children, className, contentClassName, frameClassName, scrollResetKey, sidebar, sidebarWidth = '17rem', topbar, }) => {
|
|
5
|
+
const contentRef = useRef(null);
|
|
6
|
+
useLayoutEffect(() => {
|
|
7
|
+
if (scrollResetKey === undefined)
|
|
8
|
+
return;
|
|
9
|
+
contentRef.current?.scrollTo({ top: 0, left: 0 });
|
|
10
|
+
window.scrollTo({ top: 0, left: 0 });
|
|
11
|
+
}, [scrollResetKey]);
|
|
12
|
+
return (_jsx("section", { className: PallasoftClassNameService.merge('ps-reflective-frame min-h-screen text-ink lg:px-3 lg:py-3', className), children: _jsxs("div", { className: PallasoftClassNameService.merge('ps-liquid-strong mx-auto grid h-screen min-h-0 overflow-hidden border-line/70 bg-white/70 backdrop-blur-2xl transition-[grid-template-columns] duration-200 dark:border-white/10 dark:bg-surface/70 md:grid-cols-[var(--ps-sidebar-width)_minmax(0,1fr)] lg:h-[calc(100vh-1.5rem)] lg:max-w-[1720px] lg:rounded-[1.5rem] lg:border', frameClassName), style: { ['--ps-sidebar-width']: sidebarWidth }, children: [sidebar, _jsxs("div", { className: 'flex min-h-0 min-w-0 flex-col bg-white/32 dark:bg-white/[0.02]', children: [topbar, _jsx("main", { ref: contentRef, className: PallasoftClassNameService.merge('ps-scrollbar min-h-0 flex-1 overflow-y-auto px-4 py-5 ps-page-enter sm:px-5 lg:px-6 xl:px-7', contentClassName), children: children })] })] }) }));
|
|
13
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const PsSectionHeader = ({ description, eyebrow, leading, title, trailing, }) => (_jsxs("div", { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', children: [_jsxs("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [leading, eyebrow ? (_jsx("p", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.22em] text-ink/50', children: eyebrow })) : null, _jsx("h1", { className: 'break-words text-xl font-extrabold text-ink sm:text-2xl', children: title }), description ? (_jsx("p", { className: 'max-w-3xl text-sm leading-6 text-ink/60', children: description })) : null] }), trailing ? _jsx("div", { className: 'ml-auto flex shrink-0 items-center', children: trailing }) : null] }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
4
|
+
const logoLinkClassName = 'flex shrink-0 items-center gap-2.5';
|
|
5
|
+
export const PsSiteHeader = ({ centerContent, className, homeHref, label, LinkComponent, logoAlt, logoClassName, logoSrc, logoSrcDark, logoSrcMobile, logoSrcMobileDark, navClassName, onBack, showLabel = false, trailing, }) => {
|
|
6
|
+
const logoLabel = `${label} home`;
|
|
7
|
+
const renderLogo = (src, darkSrc, baseClassName) => darkSrc ? (_jsxs(_Fragment, { children: [_jsx("img", { alt: logoAlt !== null && logoAlt !== undefined ? logoAlt : label, className: PallasoftClassNameService.merge(baseClassName, 'dark:hidden', logoClassName), src: src }), _jsx("img", { alt: logoAlt !== null && logoAlt !== undefined ? logoAlt : label, className: PallasoftClassNameService.merge(baseClassName, 'hidden dark:block', logoClassName), src: darkSrc })] })) : (_jsx("img", { alt: logoAlt !== null && logoAlt !== undefined ? logoAlt : label, className: PallasoftClassNameService.merge(baseClassName, logoClassName), src: src }));
|
|
8
|
+
const logoContent = logoSrcMobile ? (_jsxs(_Fragment, { children: [_jsx("span", { className: 'hidden sm:contents', children: renderLogo(logoSrc, logoSrcDark, 'h-8 w-auto') }), _jsx("span", { className: 'contents sm:hidden', children: renderLogo(logoSrcMobile, logoSrcMobileDark, 'h-8 w-auto') }), showLabel ? _jsx("span", { className: 'text-base font-bold text-ink', children: label }) : null] })) : (_jsxs(_Fragment, { children: [renderLogo(logoSrc, logoSrcDark, 'h-8 w-auto'), showLabel ? _jsx("span", { className: 'text-base font-bold text-ink', children: label }) : null] }));
|
|
9
|
+
const logoLink = LinkComponent ? (_jsx(LinkComponent, { "aria-label": logoLabel, className: logoLinkClassName, to: homeHref, children: logoContent })) : (_jsx("a", { "aria-label": logoLabel, className: logoLinkClassName, href: homeHref, children: logoContent }));
|
|
10
|
+
return (_jsx("header", { className: PallasoftClassNameService.merge('fixed inset-x-0 top-0 z-header px-6 pt-4', className), children: _jsxs("nav", { className: PallasoftClassNameService.merge('ps-glass mx-auto flex max-w-6xl items-center gap-3 rounded-ps-card px-5 py-3', navClassName), children: [onBack ? (_jsx("button", { "aria-label": 'Back', className: 'ps-focus-ring flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-ink md:hidden', onClick: onBack, type: 'button', children: _jsx(PsIcon, { iconKey: PsIconKey.ChevronRight, width: 20, height: 20, className: 'rotate-180' }) })) : null, _jsx("div", { className: onBack ? 'hidden md:flex' : 'contents', children: logoLink }), _jsx("div", { className: 'flex min-w-0 flex-1 items-center justify-center gap-3', children: centerContent }), trailing ? _jsx("div", { className: 'flex shrink-0 items-center gap-2.5', children: trailing }) : null] }) }));
|
|
11
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
export const PsSkeleton = ({ className }) => (_jsx("div", { className: PallasoftClassNameService.merge('animate-pulse rounded-full bg-mist', className) }));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
const colorClassNames = {
|
|
4
|
+
current: 'text-current',
|
|
5
|
+
primary: 'text-primary',
|
|
6
|
+
};
|
|
7
|
+
const sizeClassNames = {
|
|
8
|
+
small: 'h-4 w-4 border-2',
|
|
9
|
+
default: 'h-6 w-6 border-2',
|
|
10
|
+
large: 'h-8 w-8 border-[3px]',
|
|
11
|
+
};
|
|
12
|
+
export const PsSpinner = ({ className, color = 'primary', label = 'Loading', size = 'default', ...props }) => (_jsxs("div", { className: PallasoftClassNameService.merge('inline-flex items-center justify-center', colorClassNames[color], className), ...props, children: [_jsx("span", { "aria-hidden": 'true', className: PallasoftClassNameService.merge('shrink-0 animate-spin rounded-full border-current border-t-transparent', sizeClassNames[size]) }), _jsx("span", { className: 'sr-only', children: label })] }));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService, PallasoftThemeMode, PallasoftThemeModeService, } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsIcon } from '@pallasoft/ps-icons';
|
|
4
|
+
export const PsThemeToggle = ({ className, mode, type = 'button', ...props }) => {
|
|
5
|
+
const isDarkMode = mode === PallasoftThemeMode.Dark;
|
|
6
|
+
return (_jsxs("button", { "aria-label": isDarkMode ? 'Switch to light mode' : 'Switch to dark mode', className: PallasoftClassNameService.merge('ps-focus-ring ps-glass flex h-11 items-center gap-2 rounded-2xl px-3 text-sm font-bold text-ink transition hover:border-primary/30 hover:text-primary', className), type: type, ...props, children: [_jsx(PsIcon, { className: 'h-5 w-5', iconKey: isDarkMode ? 'sun' : 'moon' }), _jsx("span", { className: 'hidden sm:inline', children: PallasoftThemeModeService.getNextMode(mode) === PallasoftThemeMode.Dark ? 'Dark' : 'Light' })] }));
|
|
7
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
4
|
+
import { PsVoiceOrbCadenceByState, PsVoiceOrbConfig, PsVoiceOrbThemes, } from '../constants/ps-voice-orb';
|
|
5
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
6
|
+
const approach = (current, target, factor) => current + (target - current) * factor;
|
|
7
|
+
const buildRimMask = (innerStopPercent) => `radial-gradient(circle, transparent ${innerStopPercent}%, black ${innerStopPercent + 6}%, black 92%, transparent 97%)`;
|
|
8
|
+
const initialRimMask = buildRimMask(PsVoiceOrbConfig.rimInnerStopPercent);
|
|
9
|
+
const buildBlobRadius = (phase, depth) => {
|
|
10
|
+
const harmonic = PsVoiceOrbConfig.blobSecondHarmonic;
|
|
11
|
+
const corner = (offset) => (50 +
|
|
12
|
+
Math.sin(phase + offset) * depth * 0.65 +
|
|
13
|
+
Math.sin(phase * harmonic + offset * 2) * depth * 0.35).toFixed(2);
|
|
14
|
+
return `${corner(0)}% ${corner(1.7)}% ${corner(3.4)}% ${corner(5.1)}% / ${corner(2.6)}% ${corner(4.3)}% ${corner(0.9)}% ${corner(6)}%`;
|
|
15
|
+
};
|
|
16
|
+
export const PsVoiceOrb = (props) => {
|
|
17
|
+
const size = typeof props.size === 'number' ? props.size : PsVoiceOrbConfig.defaultSize;
|
|
18
|
+
const level = typeof props.level === 'number' ? clamp(props.level, 0, 1) : 0;
|
|
19
|
+
const state = props.state ? props.state : props.variant === 'error' ? 'error' : 'idle';
|
|
20
|
+
const isError = state === 'error' || props.variant === 'error';
|
|
21
|
+
const cadence = PsVoiceOrbCadenceByState[state];
|
|
22
|
+
const theme = PsVoiceOrbThemes[isError ? 'error' : 'default'];
|
|
23
|
+
const containerRef = useRef(null);
|
|
24
|
+
const glowRef = useRef(null);
|
|
25
|
+
const glassRef = useRef(null);
|
|
26
|
+
const rimRef = useRef(null);
|
|
27
|
+
const coreRef = useRef(null);
|
|
28
|
+
const targetLevelRef = useRef(level);
|
|
29
|
+
const cadenceRef = useRef(cadence);
|
|
30
|
+
targetLevelRef.current = level;
|
|
31
|
+
cadenceRef.current = cadence;
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let frameId;
|
|
34
|
+
let lastFrameMs = performance.now();
|
|
35
|
+
let smoothedLevel = 0;
|
|
36
|
+
let spinDegrees = 0;
|
|
37
|
+
let wobblePhase = 0;
|
|
38
|
+
let pulsePhase = 0;
|
|
39
|
+
let orbitPhase = 0;
|
|
40
|
+
let smoothedPulse = 0;
|
|
41
|
+
let smoothedBlob = 0;
|
|
42
|
+
let smoothedOrbit = 0;
|
|
43
|
+
const tick = (now) => {
|
|
44
|
+
const deltaMs = Math.min(now - lastFrameMs, PsVoiceOrbConfig.maxFrameMs);
|
|
45
|
+
const deltaS = deltaMs / 1000;
|
|
46
|
+
lastFrameMs = now;
|
|
47
|
+
const activeCadence = cadenceRef.current;
|
|
48
|
+
spinDegrees = (spinDegrees + (deltaS / activeCadence.spinS) * 360) % 360;
|
|
49
|
+
wobblePhase += deltaS * activeCadence.wobbleHz * Math.PI * 2;
|
|
50
|
+
pulsePhase += deltaS * activeCadence.pulseHz * Math.PI * 2;
|
|
51
|
+
orbitPhase += deltaS * PsVoiceOrbConfig.coreOrbitHz * Math.PI * 2;
|
|
52
|
+
smoothedLevel = approach(smoothedLevel, targetLevelRef.current, PsVoiceOrbConfig.levelSmoothing);
|
|
53
|
+
smoothedPulse = approach(smoothedPulse, activeCadence.pulseDepth, PsVoiceOrbConfig.cadenceSmoothing);
|
|
54
|
+
smoothedBlob = approach(smoothedBlob, activeCadence.blobDepth, PsVoiceOrbConfig.blobSmoothing);
|
|
55
|
+
smoothedOrbit = approach(smoothedOrbit, activeCadence.orbitDepth, PsVoiceOrbConfig.blobSmoothing);
|
|
56
|
+
const voice = Math.pow(smoothedLevel, PsVoiceOrbConfig.levelCurve);
|
|
57
|
+
const pulse = ((Math.sin(pulsePhase) + 1) / 2) * smoothedPulse;
|
|
58
|
+
const amplitude = clamp(Math.max(voice, pulse), 0, 1);
|
|
59
|
+
const blobRadius = buildBlobRadius(wobblePhase, smoothedBlob);
|
|
60
|
+
if (containerRef.current) {
|
|
61
|
+
containerRef.current.style.transform = `scale(${1 + amplitude * 0.3})`;
|
|
62
|
+
}
|
|
63
|
+
if (glowRef.current) {
|
|
64
|
+
glowRef.current.style.opacity = `${0.4 + amplitude * 0.55}`;
|
|
65
|
+
glowRef.current.style.transform = `scale(${1.05 + amplitude * 0.7})`;
|
|
66
|
+
glowRef.current.style.borderRadius = blobRadius;
|
|
67
|
+
}
|
|
68
|
+
if (glassRef.current) {
|
|
69
|
+
glassRef.current.style.borderRadius = blobRadius;
|
|
70
|
+
}
|
|
71
|
+
if (rimRef.current) {
|
|
72
|
+
const mask = buildRimMask(PsVoiceOrbConfig.rimInnerStopPercent - amplitude * PsVoiceOrbConfig.rimAmplitudeSpread);
|
|
73
|
+
rimRef.current.style.transform = `rotate(${spinDegrees}deg)`;
|
|
74
|
+
rimRef.current.style.maskImage = mask;
|
|
75
|
+
rimRef.current.style.webkitMaskImage = mask;
|
|
76
|
+
}
|
|
77
|
+
if (coreRef.current) {
|
|
78
|
+
const radius = PsVoiceOrbConfig.coreOrbitRadiusPercent * smoothedOrbit;
|
|
79
|
+
const x = Math.cos(orbitPhase) * radius;
|
|
80
|
+
const y = Math.sin(orbitPhase * 1.3) * radius;
|
|
81
|
+
coreRef.current.style.opacity = `${smoothedOrbit * (0.55 + pulse * 0.45)}`;
|
|
82
|
+
coreRef.current.style.transform = `translate(calc(-50% + ${x.toFixed(2)}%), calc(-50% + ${y.toFixed(2)}%)) scale(${0.8 + pulse * 0.5})`;
|
|
83
|
+
}
|
|
84
|
+
frameId = requestAnimationFrame(tick);
|
|
85
|
+
};
|
|
86
|
+
frameId = requestAnimationFrame(tick);
|
|
87
|
+
return () => cancelAnimationFrame(frameId);
|
|
88
|
+
}, []);
|
|
89
|
+
return (_jsxs("span", { ref: containerRef, className: PallasoftClassNameService.merge('ps-voice-orb relative inline-flex shrink-0 items-center justify-center', props.className), style: { width: size, height: size }, children: [_jsx("span", { ref: glowRef, "aria-hidden": 'true', className: 'absolute inset-0 rounded-full opacity-40 blur-xl', style: { background: theme.rim } }), _jsxs("span", { ref: glassRef, "aria-hidden": 'true', className: 'absolute inset-0 overflow-hidden rounded-full', children: [_jsx("span", { className: 'absolute inset-0', style: { background: theme.glass } }), _jsx("span", { ref: rimRef, className: 'absolute inset-0', style: {
|
|
90
|
+
background: theme.rim,
|
|
91
|
+
maskImage: initialRimMask,
|
|
92
|
+
WebkitMaskImage: initialRimMask,
|
|
93
|
+
} }), _jsx("span", { ref: coreRef, className: 'absolute left-1/2 top-1/2 rounded-full opacity-0 blur-sm', style: {
|
|
94
|
+
background: theme.core,
|
|
95
|
+
width: `${PsVoiceOrbConfig.coreSizePercent}%`,
|
|
96
|
+
height: `${PsVoiceOrbConfig.coreSizePercent}%`,
|
|
97
|
+
} })] }), cadence.isRippling
|
|
98
|
+
? PsVoiceOrbConfig.rippleDelaysMs.map((delayMs) => (_jsx("span", { "aria-hidden": 'true', className: 'animate-ps-orb-ripple absolute inset-0 rounded-full border opacity-40', style: { borderColor: theme.ripple, animationDelay: `${delayMs}ms` } }, delayMs)))
|
|
99
|
+
: null] }));
|
|
100
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const PsDraggableGridConfig = {
|
|
2
|
+
activationDistance: 6,
|
|
3
|
+
dropDuration: 220,
|
|
4
|
+
dropEasing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
|
|
5
|
+
liftDuration: 200,
|
|
6
|
+
liftEasing: 'cubic-bezier(0.2, 0.9, 0.3, 1.2)',
|
|
7
|
+
placeholderOpacity: '0.4',
|
|
8
|
+
touchDelay: 180,
|
|
9
|
+
touchTolerance: 6,
|
|
10
|
+
};
|
|
11
|
+
export const PsDraggableGridClasses = {
|
|
12
|
+
handle: 'ps-focus-ring inline-flex h-7 w-7 shrink-0 cursor-grab touch-none items-center justify-center rounded-full text-slate-400/70 transition hover:bg-white/36 hover:text-slate-600 active:cursor-grabbing dark:text-slate-300/55 dark:hover:bg-white/8 dark:hover:text-slate-100',
|
|
13
|
+
item: 'min-w-0 rounded-[1.7rem]',
|
|
14
|
+
lifted: 'cursor-grabbing rounded-[1.7rem] ring-1 ring-primary/30',
|
|
15
|
+
placeholder: 'rounded-[1.7rem] outline-dashed outline-2 outline-offset-[-2px] outline-primary/35',
|
|
16
|
+
};
|
|
17
|
+
export const PsDraggableGridLiftFrames = [
|
|
18
|
+
{ boxShadow: '0 0 0 0 rgb(15 23 42 / 0)', transform: 'scale(1) rotate(0deg)' },
|
|
19
|
+
{
|
|
20
|
+
boxShadow: '0 28px 64px -18px rgb(15 23 42 / 0.5)',
|
|
21
|
+
transform: 'scale(1.035) rotate(-1.2deg)',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const PsNavigationProgressConfig = {
|
|
2
|
+
completeWidth: 100,
|
|
3
|
+
defaultColor: 'rgb(var(--ps-color-primary-rgb))',
|
|
4
|
+
defaultHeight: 3,
|
|
5
|
+
defaultLabel: 'Page loading',
|
|
6
|
+
fadeMs: 320,
|
|
7
|
+
startWidth: 12,
|
|
8
|
+
transitionMs: 240,
|
|
9
|
+
trickleFactor: 0.14,
|
|
10
|
+
trickleMax: 90,
|
|
11
|
+
trickleMs: 260,
|
|
12
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const PsVoiceOrbConfig = {
|
|
2
|
+
defaultSize: 64,
|
|
3
|
+
levelSmoothing: 0.18,
|
|
4
|
+
levelCurve: 0.55,
|
|
5
|
+
cadenceSmoothing: 0.06,
|
|
6
|
+
rimInnerStopPercent: 66,
|
|
7
|
+
rimAmplitudeSpread: 18,
|
|
8
|
+
maxFrameMs: 64,
|
|
9
|
+
rippleDelaysMs: [0, 500],
|
|
10
|
+
blobSmoothing: 0.05,
|
|
11
|
+
blobSecondHarmonic: 1.7,
|
|
12
|
+
coreSizePercent: 46,
|
|
13
|
+
coreOrbitRadiusPercent: 22,
|
|
14
|
+
coreOrbitHz: 0.8,
|
|
15
|
+
};
|
|
16
|
+
export const PsVoiceOrbCadenceByState = {
|
|
17
|
+
idle: {
|
|
18
|
+
spinS: 16,
|
|
19
|
+
wobbleHz: 0.12,
|
|
20
|
+
pulseHz: 0.25,
|
|
21
|
+
pulseDepth: 0.08,
|
|
22
|
+
blobDepth: 3,
|
|
23
|
+
orbitDepth: 0,
|
|
24
|
+
isRippling: false,
|
|
25
|
+
},
|
|
26
|
+
connecting: {
|
|
27
|
+
spinS: 2.5,
|
|
28
|
+
wobbleHz: 0.5,
|
|
29
|
+
pulseHz: 1.4,
|
|
30
|
+
pulseDepth: 0.3,
|
|
31
|
+
blobDepth: 12,
|
|
32
|
+
orbitDepth: 1,
|
|
33
|
+
isRippling: false,
|
|
34
|
+
},
|
|
35
|
+
listening: {
|
|
36
|
+
spinS: 5,
|
|
37
|
+
wobbleHz: 0.4,
|
|
38
|
+
pulseHz: 0.7,
|
|
39
|
+
pulseDepth: 0.16,
|
|
40
|
+
blobDepth: 6,
|
|
41
|
+
orbitDepth: 0,
|
|
42
|
+
isRippling: true,
|
|
43
|
+
},
|
|
44
|
+
thinking: {
|
|
45
|
+
spinS: 1.6,
|
|
46
|
+
wobbleHz: 0.55,
|
|
47
|
+
pulseHz: 0.9,
|
|
48
|
+
pulseDepth: 0.4,
|
|
49
|
+
blobDepth: 16,
|
|
50
|
+
orbitDepth: 1,
|
|
51
|
+
isRippling: false,
|
|
52
|
+
},
|
|
53
|
+
speaking: {
|
|
54
|
+
spinS: 3,
|
|
55
|
+
wobbleHz: 0.7,
|
|
56
|
+
pulseHz: 1.3,
|
|
57
|
+
pulseDepth: 0.22,
|
|
58
|
+
blobDepth: 8,
|
|
59
|
+
orbitDepth: 0,
|
|
60
|
+
isRippling: true,
|
|
61
|
+
},
|
|
62
|
+
error: {
|
|
63
|
+
spinS: 14,
|
|
64
|
+
wobbleHz: 0.15,
|
|
65
|
+
pulseHz: 0.6,
|
|
66
|
+
pulseDepth: 0.14,
|
|
67
|
+
blobDepth: 3,
|
|
68
|
+
orbitDepth: 0,
|
|
69
|
+
isRippling: false,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const PsVoiceOrbThemes = {
|
|
73
|
+
default: {
|
|
74
|
+
glass: 'radial-gradient(circle at 32% 28%, rgb(255 255 255 / 0.16), transparent 42%), radial-gradient(circle, rgb(var(--ps-color-primary-rgb) / 0.2), rgb(var(--ps-color-ink-rgb) / 0.94) 75%)',
|
|
75
|
+
rim: 'conic-gradient(rgb(var(--ps-color-primary-rgb)), rgb(var(--ps-color-accent-rgb)), rgb(var(--ps-color-secondary-rgb)), rgb(var(--ps-color-primary-dark-rgb)), rgb(var(--ps-color-primary-rgb)))',
|
|
76
|
+
ripple: 'rgb(var(--ps-color-primary-rgb) / 0.5)',
|
|
77
|
+
core: 'radial-gradient(circle, rgb(255 255 255 / 0.9), rgb(var(--ps-color-accent-rgb) / 0.7) 35%, transparent 70%)',
|
|
78
|
+
},
|
|
79
|
+
error: {
|
|
80
|
+
glass: 'radial-gradient(circle at 32% 28%, rgb(255 255 255 / 0.16), transparent 42%), radial-gradient(circle, rgb(var(--ps-color-danger-rgb) / 0.22), rgb(var(--ps-color-ink-rgb) / 0.94) 75%)',
|
|
81
|
+
rim: 'conic-gradient(rgb(var(--ps-color-danger-rgb)), rgb(255 255 255 / 0.85), rgb(var(--ps-color-danger-rgb) / 0.7), rgb(var(--ps-color-danger-rgb)))',
|
|
82
|
+
ripple: 'rgb(var(--ps-color-danger-rgb) / 0.5)',
|
|
83
|
+
core: 'radial-gradient(circle, rgb(255 255 255 / 0.9), rgb(var(--ps-color-danger-rgb) / 0.7) 35%, transparent 70%)',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
export const usePsAudioSpectrum = (audioElement, options = {}) => {
|
|
3
|
+
const DefaultBarCount = 12;
|
|
4
|
+
const DefaultFftSize = 128;
|
|
5
|
+
const DefaultSmoothing = 0.75;
|
|
6
|
+
const barCount = typeof options.barCount === 'number' ? options.barCount : DefaultBarCount;
|
|
7
|
+
const fftSize = typeof options.fftSize === 'number' ? options.fftSize : DefaultFftSize;
|
|
8
|
+
const smoothing = typeof options.smoothing === 'number' ? options.smoothing : DefaultSmoothing;
|
|
9
|
+
const [levels, setLevels] = useState(() => Array(barCount).fill(0));
|
|
10
|
+
const configRef = useRef({ barCount, fftSize, smoothing });
|
|
11
|
+
configRef.current = { barCount, fftSize, smoothing };
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!audioElement)
|
|
14
|
+
return;
|
|
15
|
+
const AudioContextClass = typeof window === 'undefined'
|
|
16
|
+
? undefined
|
|
17
|
+
: window.AudioContext || window.webkitAudioContext;
|
|
18
|
+
if (!AudioContextClass)
|
|
19
|
+
return;
|
|
20
|
+
const audioContext = new AudioContextClass();
|
|
21
|
+
const analyser = audioContext.createAnalyser();
|
|
22
|
+
analyser.fftSize = configRef.current.fftSize;
|
|
23
|
+
analyser.smoothingTimeConstant = configRef.current.smoothing;
|
|
24
|
+
const source = audioContext.createMediaElementSource(audioElement);
|
|
25
|
+
source.connect(analyser);
|
|
26
|
+
analyser.connect(audioContext.destination);
|
|
27
|
+
const buffer = new Uint8Array(analyser.frequencyBinCount);
|
|
28
|
+
let animationFrame;
|
|
29
|
+
const tick = () => {
|
|
30
|
+
analyser.getByteFrequencyData(buffer);
|
|
31
|
+
const activeBarCount = configRef.current.barCount;
|
|
32
|
+
const binsPerBar = Math.max(1, Math.floor(buffer.length / activeBarCount));
|
|
33
|
+
const nextLevels = [];
|
|
34
|
+
for (let bar = 0; bar < activeBarCount; bar++) {
|
|
35
|
+
let sum = 0;
|
|
36
|
+
const start = bar * binsPerBar;
|
|
37
|
+
const end = start + binsPerBar;
|
|
38
|
+
for (let bin = start; bin < end; bin++) {
|
|
39
|
+
sum += bin < buffer.length ? buffer[bin] : 0;
|
|
40
|
+
}
|
|
41
|
+
nextLevels.push(Math.min(1, sum / binsPerBar / 255));
|
|
42
|
+
}
|
|
43
|
+
setLevels(nextLevels);
|
|
44
|
+
animationFrame = requestAnimationFrame(tick);
|
|
45
|
+
};
|
|
46
|
+
const resumeAudioContext = () => {
|
|
47
|
+
if (audioContext.state === 'suspended')
|
|
48
|
+
audioContext.resume().catch(() => undefined);
|
|
49
|
+
};
|
|
50
|
+
const gestureEvents = ['touchend', 'mousedown', 'keydown'];
|
|
51
|
+
resumeAudioContext();
|
|
52
|
+
audioElement.addEventListener('play', resumeAudioContext);
|
|
53
|
+
gestureEvents.forEach((eventName) => document.addEventListener(eventName, resumeAudioContext, { once: true }));
|
|
54
|
+
animationFrame = requestAnimationFrame(tick);
|
|
55
|
+
return () => {
|
|
56
|
+
cancelAnimationFrame(animationFrame);
|
|
57
|
+
audioElement.removeEventListener('play', resumeAudioContext);
|
|
58
|
+
gestureEvents.forEach((eventName) => document.removeEventListener(eventName, resumeAudioContext));
|
|
59
|
+
source.disconnect();
|
|
60
|
+
analyser.disconnect();
|
|
61
|
+
audioContext.close().catch(() => undefined);
|
|
62
|
+
};
|
|
63
|
+
}, [audioElement]);
|
|
64
|
+
return { levels };
|
|
65
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
export const usePsExitTransition = (isOpen, durationMs = 180) => {
|
|
3
|
+
const [shouldRender, setShouldRender] = useState(isOpen);
|
|
4
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
5
|
+
const timeoutRef = useRef();
|
|
6
|
+
const hasRenderedRef = useRef(isOpen);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (timeoutRef.current)
|
|
9
|
+
clearTimeout(timeoutRef.current);
|
|
10
|
+
if (isOpen) {
|
|
11
|
+
setShouldRender(true);
|
|
12
|
+
setIsClosing(false);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!hasRenderedRef.current)
|
|
16
|
+
return;
|
|
17
|
+
setIsClosing(true);
|
|
18
|
+
timeoutRef.current = setTimeout(() => {
|
|
19
|
+
setShouldRender(false);
|
|
20
|
+
setIsClosing(false);
|
|
21
|
+
}, durationMs);
|
|
22
|
+
return () => {
|
|
23
|
+
if (timeoutRef.current)
|
|
24
|
+
clearTimeout(timeoutRef.current);
|
|
25
|
+
};
|
|
26
|
+
}, [isOpen, durationMs]);
|
|
27
|
+
hasRenderedRef.current = shouldRender;
|
|
28
|
+
return { isClosing, shouldRender };
|
|
29
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
export const usePsHideOnScroll = (options = {}) => {
|
|
3
|
+
const { threshold = 8 } = options;
|
|
4
|
+
const [hidden, setHidden] = useState(false);
|
|
5
|
+
const lastScrollY = useRef(0);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
lastScrollY.current = window.scrollY;
|
|
8
|
+
const onScroll = () => {
|
|
9
|
+
const currentScrollY = window.scrollY;
|
|
10
|
+
const delta = currentScrollY - lastScrollY.current;
|
|
11
|
+
if (currentScrollY <= 0) {
|
|
12
|
+
setHidden(false);
|
|
13
|
+
lastScrollY.current = currentScrollY;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (Math.abs(delta) < threshold)
|
|
17
|
+
return;
|
|
18
|
+
setHidden(delta > 0);
|
|
19
|
+
lastScrollY.current = currentScrollY;
|
|
20
|
+
};
|
|
21
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
22
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
23
|
+
}, [threshold]);
|
|
24
|
+
return hidden;
|
|
25
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
let scriptLoadPromise = null;
|
|
3
|
+
const explicitRenderQuery = 'render=explicit';
|
|
4
|
+
const loadRecaptchaScript = (scriptUrl) => {
|
|
5
|
+
if (scriptLoadPromise)
|
|
6
|
+
return scriptLoadPromise;
|
|
7
|
+
const scriptSrc = `${scriptUrl}?${explicitRenderQuery}`;
|
|
8
|
+
scriptLoadPromise = new Promise((resolve, reject) => {
|
|
9
|
+
const existing = document.querySelector(`script[src="${scriptSrc}"]`);
|
|
10
|
+
if (existing) {
|
|
11
|
+
existing.addEventListener('load', () => resolve());
|
|
12
|
+
existing.addEventListener('error', () => reject(new Error('Failed to load reCAPTCHA')));
|
|
13
|
+
if (window.grecaptcha)
|
|
14
|
+
resolve();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const script = document.createElement('script');
|
|
18
|
+
script.src = scriptSrc;
|
|
19
|
+
script.async = true;
|
|
20
|
+
script.defer = true;
|
|
21
|
+
script.onload = () => resolve();
|
|
22
|
+
script.onerror = () => reject(new Error('Failed to load reCAPTCHA'));
|
|
23
|
+
document.head.appendChild(script);
|
|
24
|
+
});
|
|
25
|
+
return scriptLoadPromise;
|
|
26
|
+
};
|
|
27
|
+
export const usePsInvisibleRecaptcha = ({ siteKey, scriptUrl, badgePosition = 'bottomright', }) => {
|
|
28
|
+
const [isReady, setIsReady] = useState(false);
|
|
29
|
+
const widgetIdRef = useRef(null);
|
|
30
|
+
const containerRef = useRef(null);
|
|
31
|
+
const pendingRef = useRef(null);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let isMounted = true;
|
|
34
|
+
const container = document.createElement('div');
|
|
35
|
+
container.style.display = 'none';
|
|
36
|
+
document.body.appendChild(container);
|
|
37
|
+
containerRef.current = container;
|
|
38
|
+
loadRecaptchaScript(scriptUrl)
|
|
39
|
+
.then(() => {
|
|
40
|
+
if (!isMounted || !window.grecaptcha)
|
|
41
|
+
return;
|
|
42
|
+
window.grecaptcha.ready(() => {
|
|
43
|
+
if (!isMounted || !window.grecaptcha || !containerRef.current)
|
|
44
|
+
return;
|
|
45
|
+
widgetIdRef.current = window.grecaptcha.render(containerRef.current, {
|
|
46
|
+
'sitekey': siteKey,
|
|
47
|
+
'size': 'invisible',
|
|
48
|
+
'badge': badgePosition,
|
|
49
|
+
'callback': (token) => {
|
|
50
|
+
pendingRef.current?.resolve(token);
|
|
51
|
+
pendingRef.current = null;
|
|
52
|
+
},
|
|
53
|
+
'expired-callback': () => {
|
|
54
|
+
pendingRef.current?.reject(new Error('reCAPTCHA expired'));
|
|
55
|
+
pendingRef.current = null;
|
|
56
|
+
},
|
|
57
|
+
'error-callback': () => {
|
|
58
|
+
pendingRef.current?.reject(new Error('reCAPTCHA verification failed'));
|
|
59
|
+
pendingRef.current = null;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
setIsReady(true);
|
|
63
|
+
});
|
|
64
|
+
})
|
|
65
|
+
.catch(() => {
|
|
66
|
+
if (isMounted)
|
|
67
|
+
setIsReady(false);
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
isMounted = false;
|
|
71
|
+
container.remove();
|
|
72
|
+
containerRef.current = null;
|
|
73
|
+
};
|
|
74
|
+
}, [siteKey, scriptUrl, badgePosition]);
|
|
75
|
+
const execute = useCallback(() => {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
if (!window.grecaptcha || widgetIdRef.current === null) {
|
|
78
|
+
reject(new Error('reCAPTCHA is not ready yet'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
pendingRef.current = { resolve, reject };
|
|
82
|
+
window.grecaptcha.execute(widgetIdRef.current);
|
|
83
|
+
}).finally(() => {
|
|
84
|
+
if (window.grecaptcha && widgetIdRef.current !== null) {
|
|
85
|
+
window.grecaptcha.reset(widgetIdRef.current);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
return { execute, isReady };
|
|
90
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { PallasoftZIndexService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsLayerContext } from '../contexts/ps-layer-context';
|
|
4
|
+
export const usePsLayerZIndex = (layer) => PallasoftZIndexService.above(useContext(PsLayerContext), layer);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { PsNavigationProgressConfig } from '../constants/ps-navigation-progress-config';
|
|
3
|
+
const hidden = { isAnimated: false, isVisible: false, width: 0 };
|
|
4
|
+
export const usePsNavigationProgress = (isLoading) => {
|
|
5
|
+
const [state, setState] = useState(hidden);
|
|
6
|
+
const timers = useRef([]);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (typeof window === 'undefined')
|
|
9
|
+
return;
|
|
10
|
+
const clear = () => {
|
|
11
|
+
timers.current.forEach((timer) => window.clearTimeout(timer));
|
|
12
|
+
timers.current.forEach((timer) => window.clearInterval(timer));
|
|
13
|
+
timers.current = [];
|
|
14
|
+
};
|
|
15
|
+
clear();
|
|
16
|
+
if (isLoading) {
|
|
17
|
+
setState({ isAnimated: false, isVisible: true, width: 0 });
|
|
18
|
+
timers.current.push(window.setTimeout(() => setState({
|
|
19
|
+
isAnimated: true,
|
|
20
|
+
isVisible: true,
|
|
21
|
+
width: PsNavigationProgressConfig.startWidth,
|
|
22
|
+
}), 0), window.setInterval(() => setState((current) => ({
|
|
23
|
+
...current,
|
|
24
|
+
width: current.width +
|
|
25
|
+
(PsNavigationProgressConfig.trickleMax - current.width) *
|
|
26
|
+
PsNavigationProgressConfig.trickleFactor,
|
|
27
|
+
})), PsNavigationProgressConfig.trickleMs));
|
|
28
|
+
return clear;
|
|
29
|
+
}
|
|
30
|
+
setState((current) => current.isVisible
|
|
31
|
+
? {
|
|
32
|
+
isAnimated: true,
|
|
33
|
+
isVisible: true,
|
|
34
|
+
width: PsNavigationProgressConfig.completeWidth,
|
|
35
|
+
}
|
|
36
|
+
: current);
|
|
37
|
+
timers.current.push(window.setTimeout(() => setState((current) => ({ ...current, isVisible: false })), PsNavigationProgressConfig.transitionMs), window.setTimeout(() => setState(hidden), PsNavigationProgressConfig.transitionMs + PsNavigationProgressConfig.fadeMs));
|
|
38
|
+
return clear;
|
|
39
|
+
}, [isLoading]);
|
|
40
|
+
return state;
|
|
41
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const createPsRouteLoaderDataHook = (routeId) => {
|
|
2
|
+
let cache;
|
|
3
|
+
return () => {
|
|
4
|
+
const { useRouteLoaderData } = require('@remix-run/react');
|
|
5
|
+
const data = useRouteLoaderData(routeId);
|
|
6
|
+
if (data !== undefined)
|
|
7
|
+
cache = data;
|
|
8
|
+
if (cache === undefined)
|
|
9
|
+
throw new Error(`No loader data found for route "${routeId}"`);
|
|
10
|
+
return cache;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { PallasoftViewportService, PallasoftViewportTier } from '@pallasoft/ps-design-system';
|
|
3
|
+
export const usePsViewport = (defaultTier = PallasoftViewportTier.Desktop) => {
|
|
4
|
+
const [tier, setTier] = useState(defaultTier);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const resolve = () => setTier(PallasoftViewportService.getTier(window.innerWidth));
|
|
7
|
+
resolve();
|
|
8
|
+
const tabletQuery = window.matchMedia(PallasoftViewportService.getTabletQuery());
|
|
9
|
+
const desktopQuery = window.matchMedia(PallasoftViewportService.getDesktopQuery());
|
|
10
|
+
tabletQuery.addEventListener('change', resolve);
|
|
11
|
+
desktopQuery.addEventListener('change', resolve);
|
|
12
|
+
return () => {
|
|
13
|
+
tabletQuery.removeEventListener('change', resolve);
|
|
14
|
+
desktopQuery.removeEventListener('change', resolve);
|
|
15
|
+
};
|
|
16
|
+
}, []);
|
|
17
|
+
return tier;
|
|
18
|
+
};
|