@pallasoft/ps-core-components 2.3.1 → 2.5.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-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/components/ps-skeleton-card.d.ts +4 -0
- package/dist/components/ps-skeleton-card.js +9 -0
- package/dist/components/ps-skeleton-group.d.ts +7 -0
- package/dist/components/ps-skeleton-group.js +6 -0
- package/dist/components/ps-skeleton-list.d.ts +8 -0
- package/dist/components/ps-skeleton-list.js +10 -0
- package/dist/components/ps-skeleton-table.d.ts +7 -0
- package/dist/components/ps-skeleton-table.js +15 -0
- package/dist/components/ps-skeleton-text.d.ts +5 -0
- package/dist/components/ps-skeleton-text.js +12 -0
- package/dist/components/ps-skeleton.d.ts +3 -1
- package/dist/components/ps-skeleton.js +6 -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/constants/ps-skeleton-config.d.ts +3 -0
- package/dist/constants/ps-skeleton-config.js +6 -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-card.js +5 -0
- package/dist/esm/components/ps-skeleton-group.js +2 -0
- package/dist/esm/components/ps-skeleton-list.js +6 -0
- package/dist/esm/components/ps-skeleton-table.js +11 -0
- package/dist/esm/components/ps-skeleton-text.js +8 -0
- package/dist/esm/components/ps-skeleton.js +8 -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-skeleton-config.js +3 -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 +43 -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 +9 -0
- package/dist/index.js +9 -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,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
const getActionClassName = (variant = 'secondary') => {
|
|
4
|
+
if (variant === 'primary') {
|
|
5
|
+
return 'border-primary bg-primary text-white hover:bg-primary-dark';
|
|
6
|
+
}
|
|
7
|
+
return 'border-line bg-surface text-ink hover:border-primary/40 hover:text-primary';
|
|
8
|
+
};
|
|
9
|
+
export const PsEmptyState = ({ actions = [], badge, children, className, detail, description, eyebrow, icon, title, variant = 'page', }) => {
|
|
10
|
+
const isInline = variant === 'inline';
|
|
11
|
+
const Wrapper = isInline ? 'div' : 'main';
|
|
12
|
+
const Heading = isInline ? 'h3' : 'h1';
|
|
13
|
+
return (_jsx(Wrapper, { className: PallasoftClassNameService.merge(isInline
|
|
14
|
+
? 'flex items-center justify-center rounded-ps-panel border border-dashed border-line bg-mist/40 px-6 py-10 text-ink'
|
|
15
|
+
: 'relative flex min-h-screen items-center justify-center bg-mist px-4 py-10 text-ink', className), children: _jsx("section", { className: isInline
|
|
16
|
+
? 'relative w-full max-w-2xl text-center'
|
|
17
|
+
: 'ps-card relative w-full max-w-2xl p-6 sm:p-8', children: _jsxs("div", { className: isInline
|
|
18
|
+
? 'flex flex-col items-center gap-3'
|
|
19
|
+
: 'flex flex-col gap-5 sm:flex-row sm:items-start', children: [icon ? _jsx("div", { className: 'ps-icon-surface h-14 w-14', children: icon }) : null, _jsxs("div", { className: isInline ? 'min-w-0' : 'min-w-0 flex-1', children: [_jsxs("div", { className: isInline
|
|
20
|
+
? 'flex flex-wrap items-center justify-center gap-2'
|
|
21
|
+
: 'flex flex-wrap items-center gap-2', children: [eyebrow ? (_jsx("p", { className: 'text-xs font-semibold uppercase tracking-[0.22em] text-primary', children: eyebrow })) : null, eyebrow && badge ? _jsx("span", { className: 'h-1 w-1 rounded-full bg-line' }) : null, badge ? (_jsx("p", { className: 'text-xs font-semibold uppercase tracking-[0.22em] text-ink/40', children: badge })) : null] }), _jsx(Heading, { className: isInline
|
|
22
|
+
? 'text-base font-semibold text-ink'
|
|
23
|
+
: 'mt-3 text-3xl font-semibold tracking-tight sm:text-4xl', children: title }), description ? (_jsx("p", { className: isInline
|
|
24
|
+
? 'mt-1.5 text-sm leading-6 text-ink/60'
|
|
25
|
+
: 'mt-3 text-sm leading-6 text-ink/60', children: description })) : null, detail ? (_jsx("p", { className: 'mt-4 rounded-2xl border border-line bg-mist px-4 py-3 text-left text-xs leading-5 text-ink/60', children: detail })) : null, children ? _jsx("div", { className: 'mt-5 flex justify-center', children: children }) : null, actions.length > 0 ? (_jsx("div", { className: isInline
|
|
26
|
+
? 'mt-5 flex flex-col justify-center gap-2 sm:flex-row'
|
|
27
|
+
: 'mt-6 flex flex-col gap-2 sm:flex-row', children: actions.map((action) => (_jsx("a", { className: PallasoftClassNameService.merge('ps-focus-ring ps-pressable inline-flex items-center justify-center rounded-ps-control border px-4 py-3 text-sm font-semibold', getActionClassName(action.variant)), href: action.href, rel: action.rel, target: action.target, children: action.label }, `${action.href}-${action.label}`))) })) : null] })] }) }) }));
|
|
28
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsButton } from './ps-button';
|
|
4
|
+
const positionClassNames = {
|
|
5
|
+
'bottom-right': 'bottom-6 right-6',
|
|
6
|
+
'bottom-left': 'bottom-6 left-6',
|
|
7
|
+
'bottom-center': 'bottom-6 left-1/2 -translate-x-1/2',
|
|
8
|
+
};
|
|
9
|
+
export const PsFab = ({ className, position = 'bottom-right', ...props }) => (_jsx(PsButton, { size: 'large', variant: 'primary', className: PallasoftClassNameService.merge('fixed z-docked h-14 w-14 shrink-0 !px-0 shadow-lg', positionClassNames[position], className), ...props }));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import InfiniteScroll from 'react-infinite-scroll-component';
|
|
3
|
+
import { PsSpinner } from './ps-spinner';
|
|
4
|
+
const defaultLoader = (_jsx("div", { className: 'flex w-full justify-center py-8', children: _jsx(PsSpinner, {}) }));
|
|
5
|
+
export const PsInfiniteScroll = ({ children, className, dataLength, hasMore, loader, next, }) => (_jsx(InfiniteScroll, { className: className, dataLength: dataLength, hasMore: hasMore, loader: loader !== null && loader !== undefined ? loader : defaultLoader, next: next, children: children }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
const densityClassNames = {
|
|
4
|
+
compact: 'py-2',
|
|
5
|
+
normal: 'py-2.5',
|
|
6
|
+
};
|
|
7
|
+
const variantClassNames = {
|
|
8
|
+
card: 'ps-surface px-3',
|
|
9
|
+
row: 'border-b border-line/70 px-0 last:border-b-0',
|
|
10
|
+
};
|
|
11
|
+
export const PsListTile = ({ children, className, density = 'normal', eyebrow, isPrivateTitle = false, subtitle, title, variant = 'row', }) => (_jsxs("div", { className: PallasoftClassNameService.merge('min-w-0 overflow-hidden', variantClassNames[variant], densityClassNames[density], className), children: [eyebrow ? _jsx("p", { className: 'ps-eyebrow break-words', children: eyebrow }) : null, _jsx("p", { className: PallasoftClassNameService.merge('break-words text-sm font-semibold text-ink', eyebrow && 'mt-1', isPrivateTitle && 'private-number'), children: title }), subtitle ? (_jsx("p", { className: 'mt-1 break-words text-sm leading-5 text-slate-500', children: subtitle })) : null, children] }));
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
export const PsMetricCard = ({ changeLabel, className, icon, label, value }) => (_jsx("section", { className: PallasoftClassNameService.merge('ps-metric-card min-w-0 p-3.5 sm:p-4', className), children: _jsxs("div", { className: 'flex items-start justify-between gap-3', children: [_jsxs("div", { className: 'min-w-0', children: [_jsx("p", { className: 'ps-eyebrow truncate', children: label }), _jsx("p", { className: 'mt-2 truncate text-2xl font-semibold text-ink', children: value }), changeLabel ? _jsx("p", { className: 'mt-1 text-sm text-slate-500', children: changeLabel }) : null] }), icon ? (_jsx("div", { className: 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary-soft text-primary', children: icon })) : null] }) }));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService, PallasoftZIndex } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsNavigationProgressConfig } from '../constants/ps-navigation-progress-config';
|
|
4
|
+
import { usePsNavigationProgress } from '../hooks/use-ps-navigation-progress';
|
|
5
|
+
export const PsNavigationProgress = ({ className, color = PsNavigationProgressConfig.defaultColor, height = PsNavigationProgressConfig.defaultHeight, isLoading, label = PsNavigationProgressConfig.defaultLabel, progress, shadow = true, }) => {
|
|
6
|
+
const { isAnimated, isVisible, width } = usePsNavigationProgress(isLoading !== undefined
|
|
7
|
+
? isLoading
|
|
8
|
+
: progress !== undefined &&
|
|
9
|
+
progress > 0 &&
|
|
10
|
+
progress < PsNavigationProgressConfig.completeWidth);
|
|
11
|
+
return (_jsx("div", { "aria-hidden": isVisible ? undefined : 'true', "aria-label": label, "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": Math.round(width), className: PallasoftClassNameService.merge('pointer-events-none fixed inset-x-0 top-0', className), role: 'progressbar', style: { height, zIndex: PallasoftZIndex.Toast }, children: _jsx("div", { className: 'h-full origin-left ease-out', style: {
|
|
12
|
+
backgroundColor: color,
|
|
13
|
+
boxShadow: shadow ? `0 0 10px ${color}, 0 0 5px ${color}` : undefined,
|
|
14
|
+
opacity: isVisible ? 1 : 0,
|
|
15
|
+
transitionDuration: isAnimated
|
|
16
|
+
? `${PsNavigationProgressConfig.transitionMs}ms, ${PsNavigationProgressConfig.fadeMs}ms`
|
|
17
|
+
: '0ms',
|
|
18
|
+
transitionProperty: 'width, opacity',
|
|
19
|
+
width: `${Math.min(Math.max(width, 0), 100)}%`,
|
|
20
|
+
} }) }));
|
|
21
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
const toneClassNames = {
|
|
4
|
+
danger: 'bg-rose-50 text-rose-700 ring-rose-100 dark:bg-rose-500/18 dark:text-rose-100 dark:ring-rose-300/22',
|
|
5
|
+
neutral: 'bg-slate-100 text-slate-700 ring-slate-200 dark:bg-white/10 dark:text-slate-100 dark:ring-white/12',
|
|
6
|
+
primary: 'bg-primary-soft text-ink ring-primary/12 dark:bg-primary/22 dark:text-white dark:ring-primary/32',
|
|
7
|
+
success: 'bg-emerald-50 text-emerald-700 ring-emerald-100 dark:bg-emerald-400/18 dark:text-emerald-50 dark:ring-emerald-200/22',
|
|
8
|
+
warning: 'bg-amber-50 text-amber-800 ring-amber-100 dark:bg-amber-400/18 dark:text-amber-50 dark:ring-amber-200/24',
|
|
9
|
+
};
|
|
10
|
+
export const PsPill = ({ children, className, tone = 'neutral' }) => (_jsx("span", { className: PallasoftClassNameService.merge('inline-flex min-h-8 min-w-[5.5rem] max-w-full items-center justify-center rounded-full px-5 py-1 text-center text-[0.72rem] font-black capitalize tracking-[0.01em] ring-1 ring-inset', toneClassNames[tone], className), children: _jsx("span", { className: 'min-w-0 break-words', children: children }) }));
|
|
@@ -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,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsSkeleton } from './ps-skeleton';
|
|
4
|
+
import { PsSkeletonGroup } from './ps-skeleton-group';
|
|
5
|
+
export const PsSkeletonCard = ({ className }) => (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex flex-col gap-3 rounded-[1.25rem] border border-line bg-surface p-5', className), children: [_jsx(PsSkeleton, { className: 'h-2.5 w-24' }), _jsx(PsSkeleton, { className: 'h-6 w-32' })] }));
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const PsSkeletonGroup = ({ children, className, label = 'Loading', }) => (_jsxs("div", { "aria-busy": 'true', className: className, role: 'status', children: [_jsx("span", { className: 'sr-only', children: label }), children] }));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsSkeletonConfig } from '../constants/ps-skeleton-config';
|
|
4
|
+
import { PsSkeleton } from './ps-skeleton';
|
|
5
|
+
import { PsSkeletonGroup } from './ps-skeleton-group';
|
|
6
|
+
export const PsSkeletonList = ({ className, hasLeading = false, hasTrailing = true, rows = 4, title = false, }) => (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex flex-col', className), children: [title ? _jsx(PsSkeleton, { className: 'mx-3 mb-3 h-2.5 w-24' }) : null, Array.from({ length: rows }, (_, index) => (_jsxs("div", { className: 'flex min-h-11 items-center gap-3 px-3 py-2.5', children: [hasLeading ? _jsx(PsSkeleton, { shape: 'circle' }) : null, _jsx(PsSkeleton, { className: PsSkeletonConfig.lineWidths[index % PsSkeletonConfig.lineWidths.length] }), hasTrailing ? _jsx(PsSkeleton, { className: 'ml-auto h-2.5 w-20 shrink-0' }) : null] }, index)))] }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsSkeletonConfig } from '../constants/ps-skeleton-config';
|
|
4
|
+
import { PsSkeleton } from './ps-skeleton';
|
|
5
|
+
import { PsSkeletonGroup } from './ps-skeleton-group';
|
|
6
|
+
export const PsSkeletonTable = ({ className, columns = 4, hasToolbar = false, rows = 6, }) => {
|
|
7
|
+
const gridTemplateColumns = `minmax(0, 2fr) repeat(${Math.max(columns - 1, 0)}, minmax(0, 1fr))`;
|
|
8
|
+
return (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex flex-col gap-3', className), children: [hasToolbar ? (_jsxs("div", { className: 'flex items-center gap-3', children: [_jsx(PsSkeleton, { className: 'max-w-sm', shape: 'block' }), _jsx(PsSkeleton, { className: 'ml-auto w-24', shape: 'block' })] })) : null, _jsxs("div", { className: 'overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [_jsx("div", { className: 'grid gap-4 bg-mist/70 px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (_, index) => (_jsx(PsSkeleton, { className: 'h-2.5 w-16' }, index))) }), Array.from({ length: rows }, (_, rowIndex) => (_jsx("div", { className: 'grid items-center gap-4 border-t border-line px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (__, columnIndex) => (_jsx(PsSkeleton, { className: columnIndex === columns - 1 && columns > 1
|
|
9
|
+
? 'ml-auto w-16'
|
|
10
|
+
: PsSkeletonConfig.lineWidths[(rowIndex + columnIndex) % PsSkeletonConfig.lineWidths.length] }, columnIndex))) }, rowIndex)))] })] }));
|
|
11
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsSkeletonConfig } from '../constants/ps-skeleton-config';
|
|
4
|
+
import { PsSkeleton } from './ps-skeleton';
|
|
5
|
+
import { PsSkeletonGroup } from './ps-skeleton-group';
|
|
6
|
+
export const PsSkeletonText = ({ className, lines = 3 }) => (_jsx(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex flex-col gap-2.5', className), children: Array.from({ length: lines }, (_, index) => (_jsx(PsSkeleton, { className: index === lines - 1 && lines > 1
|
|
7
|
+
? 'w-2/3'
|
|
8
|
+
: PsSkeletonConfig.lineWidths[index % PsSkeletonConfig.lineWidths.length] }, index))) }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
const shapeClassNames = {
|
|
4
|
+
block: 'h-11 w-full rounded-ps-control',
|
|
5
|
+
circle: 'h-9 w-9 shrink-0 rounded-full',
|
|
6
|
+
line: 'h-3 w-full rounded-full',
|
|
7
|
+
};
|
|
8
|
+
export const PsSkeleton = ({ className, shape = 'line' }) => (_jsx("span", { "aria-hidden": 'true', className: PallasoftClassNameService.merge('ps-skeleton', shapeClassNames[shape], 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
|
+
};
|