@1889ca/ui 0.5.0 → 0.6.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/package.json +1 -1
- package/src/components/ContextMenu.css +5 -3
- package/src/components/ContextMenu.jsx +2 -14
- package/src/components/DraggablePanel.css +2 -1
- package/src/components/DraggablePanel.jsx +4 -17
- package/src/components/EmptyState.css +36 -0
- package/src/components/EmptyState.jsx +27 -0
- package/src/components/ItemCard.css +12 -1
- package/src/components/ItemCard.jsx +8 -3
- package/src/components/ItemGrid.css +22 -0
- package/src/components/Lightbox.css +12 -2
- package/src/components/Lightbox.jsx +2 -2
- package/src/components/LoginCard.css +1 -1
- package/src/components/Modal.css +5 -4
- package/src/components/PageHeader.css +15 -1
- package/src/components/Panel.css +53 -2
- package/src/components/Panel.jsx +25 -5
- package/src/components/Skeleton.css +79 -0
- package/src/components/Skeleton.jsx +47 -0
- package/src/components/SlidePanel.css +7 -1
- package/src/components/StatusBar.css +38 -1
- package/src/components/TabBar.css +53 -0
- package/src/components/TabBar.jsx +28 -0
- package/src/components/ThemeToggle.css +34 -0
- package/src/components/ThemeToggle.jsx +44 -0
- package/src/components/Toast.css +102 -0
- package/src/components/Toast.jsx +67 -0
- package/src/components/Toggle.css +53 -0
- package/src/components/Toggle.jsx +28 -0
- package/src/components/Tooltip.css +36 -0
- package/src/components/Tooltip.jsx +55 -0
- package/src/components/UserMenu.css +18 -1
- package/src/components/UserMenu.jsx +28 -18
- package/src/effects/Particles.jsx +117 -0
- package/src/effects/ambient-glow.css +67 -0
- package/src/effects/edge-light.css +69 -0
- package/src/effects/glass-tint.css +47 -0
- package/src/effects/glow-focus.css +64 -0
- package/src/effects/haptic.css +52 -0
- package/src/effects/particles.css +10 -0
- package/src/effects/scroll-parallax.css +46 -0
- package/src/hooks/useClickOutside.js +26 -0
- package/src/hooks/useLocalStorage.js +25 -0
- package/src/hooks/useMagnet.js +60 -0
- package/src/hooks/useMediaQuery.js +24 -0
- package/src/hooks/useScrollParallax.js +39 -0
- package/src/hooks/useSound.js +106 -0
- package/src/hooks/useTheme.js +90 -0
- package/src/index.js +36 -0
- package/src/styles/primitives.css +48 -1
- package/src/styles/tokens.css +89 -0
- package/src/utils/accentColors.js +85 -0
- package/src/utils/viewTransition.js +16 -0
package/package.json
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
position: fixed;
|
|
5
5
|
z-index: 1000;
|
|
6
6
|
min-width: 170px;
|
|
7
|
-
|
|
7
|
+
max-width: calc(100vw - 1rem);
|
|
8
|
+
background: var(--ui-glass);
|
|
8
9
|
backdrop-filter: blur(var(--ui-glass-blur-heavy));
|
|
9
10
|
border: 1px solid var(--ui-border);
|
|
10
11
|
border-top-color: var(--ui-border-strong);
|
|
@@ -12,11 +13,12 @@
|
|
|
12
13
|
padding: 4px;
|
|
13
14
|
box-shadow: var(--ui-shadow-float);
|
|
14
15
|
animation: ui-ctx-in 0.12s var(--ui-ease);
|
|
16
|
+
transform-origin: top left;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
@keyframes ui-ctx-in {
|
|
18
|
-
from { opacity: 0; transform:
|
|
19
|
-
to { opacity: 1; transform:
|
|
20
|
+
from { opacity: 0; transform: scaleY(0.8) scaleX(0.95); }
|
|
21
|
+
to { opacity: 1; transform: scaleY(1) scaleX(1); }
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
.ui-ctx-menu-item {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
+
import useClickOutside from '../hooks/useClickOutside.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Right-click context menu. Renders at (x, y) with a list of action items.
|
|
@@ -8,20 +9,7 @@ import { useEffect, useRef } from 'react';
|
|
|
8
9
|
export default function ContextMenu({ x, y, items, onClose }) {
|
|
9
10
|
const ref = useRef(null);
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
function onMouseDown(e) {
|
|
13
|
-
if (ref.current && !ref.current.contains(e.target)) onClose();
|
|
14
|
-
}
|
|
15
|
-
function onKey(e) {
|
|
16
|
-
if (e.key === 'Escape') onClose();
|
|
17
|
-
}
|
|
18
|
-
document.addEventListener('mousedown', onMouseDown);
|
|
19
|
-
document.addEventListener('keydown', onKey);
|
|
20
|
-
return () => {
|
|
21
|
-
document.removeEventListener('mousedown', onMouseDown);
|
|
22
|
-
document.removeEventListener('keydown', onKey);
|
|
23
|
-
};
|
|
24
|
-
}, [onClose]);
|
|
12
|
+
useClickOutside(ref, onClose);
|
|
25
13
|
|
|
26
14
|
useEffect(() => {
|
|
27
15
|
if (!ref.current) return;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
.ui-dp-panel {
|
|
4
4
|
position: absolute;
|
|
5
5
|
z-index: 30;
|
|
6
|
-
background:
|
|
6
|
+
background: var(--ui-glass);
|
|
7
7
|
backdrop-filter: blur(var(--ui-glass-blur-heavy));
|
|
8
8
|
border: 1px solid var(--ui-border);
|
|
9
9
|
border-top-color: var(--ui-border-strong);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
display: flex;
|
|
13
13
|
flex-direction: column;
|
|
14
14
|
min-width: 120px;
|
|
15
|
+
max-width: calc(100vw - 1rem);
|
|
15
16
|
user-select: none;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
|
-
import { useRef
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import useLocalStorage from '../hooks/useLocalStorage.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Floating panel with drag-to-move titlebar and optional vertical resize.
|
|
@@ -7,25 +8,11 @@ import { useRef, useState, useEffect } from 'react';
|
|
|
7
8
|
* @param {string} storagePrefix - localStorage key prefix (default: 'ui-panel')
|
|
8
9
|
*/
|
|
9
10
|
export default function DraggablePanel({ panelId, title, defaultPos, resizable, minHeight, maxHeight, className, children, onClose, storagePrefix = 'ui-panel' }) {
|
|
10
|
-
const [pos, setPos] =
|
|
11
|
-
|
|
12
|
-
catch { return defaultPos; }
|
|
13
|
-
});
|
|
14
|
-
const [height, setHeight] = useState(() => {
|
|
15
|
-
try { const s = localStorage.getItem(`${storagePrefix}-h-${panelId}`); return s ? Number(s) : (minHeight || 200); }
|
|
16
|
-
catch { return minHeight || 200; }
|
|
17
|
-
});
|
|
11
|
+
const [pos, setPos] = useLocalStorage(`${storagePrefix}-${panelId}`, defaultPos);
|
|
12
|
+
const [height, setHeight] = useLocalStorage(`${storagePrefix}-h-${panelId}`, minHeight || 200);
|
|
18
13
|
const dragRef = useRef(null);
|
|
19
14
|
const resizeRef = useRef(null);
|
|
20
15
|
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
try { localStorage.setItem(`${storagePrefix}-${panelId}`, JSON.stringify(pos)); } catch {}
|
|
23
|
-
}, [pos, panelId, storagePrefix]);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (resizable) try { localStorage.setItem(`${storagePrefix}-h-${panelId}`, String(height)); } catch {}
|
|
27
|
-
}, [height, panelId, resizable, storagePrefix]);
|
|
28
|
-
|
|
29
16
|
function handleDragStart(e) {
|
|
30
17
|
if (e.target.closest('.ui-dp-close') || e.target.closest('button') || e.target.closest('input') || e.target.closest('select')) return;
|
|
31
18
|
e.preventDefault();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
|
+
|
|
3
|
+
.ui-empty-state {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
text-align: center;
|
|
9
|
+
padding: 3rem 2rem;
|
|
10
|
+
gap: 0.75rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.ui-empty-state-icon {
|
|
14
|
+
color: var(--ui-text-subtle);
|
|
15
|
+
margin-bottom: 0.25rem;
|
|
16
|
+
opacity: 0.6;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ui-empty-state-title {
|
|
20
|
+
font-size: 0.95rem;
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
color: var(--ui-text-muted);
|
|
23
|
+
margin: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.ui-empty-state-desc {
|
|
27
|
+
font-size: 0.82rem;
|
|
28
|
+
color: var(--ui-text-subtle);
|
|
29
|
+
max-width: 280px;
|
|
30
|
+
line-height: 1.5;
|
|
31
|
+
margin: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ui-empty-state-action {
|
|
35
|
+
margin-top: 0.5rem;
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
|
+
import './EmptyState.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Empty state placeholder with icon, message, and optional action.
|
|
6
|
+
* @param {ReactNode} icon - Icon or illustration (optional, shows default if omitted)
|
|
7
|
+
* @param {string} title - Primary message
|
|
8
|
+
* @param {string} description - Secondary explanation (optional)
|
|
9
|
+
* @param {ReactNode} action - Action button or link (optional)
|
|
10
|
+
*/
|
|
11
|
+
export default function EmptyState({ icon, title, description, action }) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="ui-empty-state">
|
|
14
|
+
<div className="ui-empty-state-icon">
|
|
15
|
+
{icon || (
|
|
16
|
+
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
17
|
+
<rect x="8" y="8" width="32" height="32" rx="4" opacity="0.3" />
|
|
18
|
+
<path d="M20 24h8M24 20v8" opacity="0.5" />
|
|
19
|
+
</svg>
|
|
20
|
+
)}
|
|
21
|
+
</div>
|
|
22
|
+
<h3 className="ui-empty-state-title">{title}</h3>
|
|
23
|
+
{description && <p className="ui-empty-state-desc">{description}</p>}
|
|
24
|
+
{action && <div className="ui-empty-state-action">{action}</div>}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
.ui-item-card:hover {
|
|
51
51
|
border-color: var(--ui-border-strong);
|
|
52
52
|
transform: translateY(-2px) scale(1.01);
|
|
53
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
53
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(124, 108, 240, 0.08);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
.ui-item-card-thumb {
|
|
@@ -109,3 +109,14 @@
|
|
|
109
109
|
flex-shrink: 0;
|
|
110
110
|
font-variant-numeric: tabular-nums;
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
/* Selection pulse */
|
|
114
|
+
@keyframes ui-card-select-pulse {
|
|
115
|
+
0% { box-shadow: 0 0 0 1px var(--ui-accent), 0 0 24px var(--ui-accent-glow); }
|
|
116
|
+
50% { box-shadow: 0 0 0 2px var(--ui-accent), 0 0 32px var(--ui-accent-glow); }
|
|
117
|
+
100% { box-shadow: 0 0 0 1px var(--ui-accent), 0 0 24px var(--ui-accent-glow); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ui-item-card--selected .ui-item-card {
|
|
121
|
+
animation: ui-card-select-pulse 0.5s var(--ui-ease);
|
|
122
|
+
}
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
* @param {function} onDragStart - Drag start handler
|
|
16
16
|
* @param {function} onDragEnd - Drag end handler
|
|
17
17
|
*/
|
|
18
|
-
export default function ItemCard({ thumbnail, name, meta, selected, draggable, onClick, onDoubleClick, onContextMenu, onDragStart, onDragEnd }) {
|
|
18
|
+
export default function ItemCard({ thumbnail, name, meta, selected, draggable, glow, viewTransitionName, onClick, onDoubleClick, onContextMenu, onDragStart, onDragEnd }) {
|
|
19
19
|
return (
|
|
20
20
|
<li
|
|
21
|
-
className={`ui-item-card-wrap${selected ? ' ui-item-card--selected' : ''}`}
|
|
21
|
+
className={`ui-item-card-wrap${selected ? ' ui-item-card--selected' : ''}${glow !== false && thumbnail ? ' ui-ambient-glow' : ''}`}
|
|
22
22
|
draggable={draggable}
|
|
23
23
|
onClick={onClick}
|
|
24
24
|
onDoubleClick={onDoubleClick}
|
|
@@ -26,10 +26,15 @@ export default function ItemCard({ thumbnail, name, meta, selected, draggable, o
|
|
|
26
26
|
onDragStart={onDragStart}
|
|
27
27
|
onDragEnd={onDragEnd}
|
|
28
28
|
>
|
|
29
|
+
{glow !== false && thumbnail && (
|
|
30
|
+
<div className="ui-ambient-glow-source" aria-hidden="true">
|
|
31
|
+
<img src={thumbnail} alt="" draggable={false} />
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
29
34
|
<div className="ui-item-card">
|
|
30
35
|
<div className="ui-item-card-thumb">
|
|
31
36
|
{thumbnail
|
|
32
|
-
? <img src={thumbnail} alt={name} draggable={false} />
|
|
37
|
+
? <img src={thumbnail} alt={name} draggable={false} style={viewTransitionName ? { viewTransitionName } : undefined} />
|
|
33
38
|
: <div className="ui-item-card-thumb-empty" />}
|
|
34
39
|
</div>
|
|
35
40
|
<div className="ui-item-card-info">
|
|
@@ -11,3 +11,25 @@
|
|
|
11
11
|
background: var(--ui-accent-subtle);
|
|
12
12
|
border-radius: var(--ui-radius);
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
/* Staggered entrance animation for grid children */
|
|
16
|
+
.ui-item-grid > * {
|
|
17
|
+
animation: ui-grid-enter 0.4s var(--ui-ease) both;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@keyframes ui-grid-enter {
|
|
21
|
+
from { opacity: 0; transform: translateY(12px) scale(0.97); }
|
|
22
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ui-item-grid > *:nth-child(1) { animation-delay: 0ms; }
|
|
26
|
+
.ui-item-grid > *:nth-child(2) { animation-delay: 30ms; }
|
|
27
|
+
.ui-item-grid > *:nth-child(3) { animation-delay: 60ms; }
|
|
28
|
+
.ui-item-grid > *:nth-child(4) { animation-delay: 90ms; }
|
|
29
|
+
.ui-item-grid > *:nth-child(5) { animation-delay: 120ms; }
|
|
30
|
+
.ui-item-grid > *:nth-child(6) { animation-delay: 150ms; }
|
|
31
|
+
.ui-item-grid > *:nth-child(7) { animation-delay: 180ms; }
|
|
32
|
+
.ui-item-grid > *:nth-child(8) { animation-delay: 210ms; }
|
|
33
|
+
.ui-item-grid > *:nth-child(9) { animation-delay: 240ms; }
|
|
34
|
+
.ui-item-grid > *:nth-child(10) { animation-delay: 270ms; }
|
|
35
|
+
.ui-item-grid > *:nth-child(n+11) { animation-delay: 300ms; }
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
position: fixed;
|
|
52
52
|
top: 50%;
|
|
53
53
|
transform: translateY(-50%);
|
|
54
|
-
background:
|
|
54
|
+
background: var(--ui-glass);
|
|
55
55
|
backdrop-filter: blur(var(--ui-glass-blur));
|
|
56
56
|
border: 1px solid var(--ui-border);
|
|
57
57
|
color: var(--ui-text);
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
.ui-lightbox-nav:hover {
|
|
69
|
-
background:
|
|
69
|
+
background: var(--ui-glass-heavy);
|
|
70
70
|
border-color: var(--ui-border-strong);
|
|
71
71
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
|
|
72
72
|
}
|
|
@@ -78,3 +78,13 @@
|
|
|
78
78
|
|
|
79
79
|
.ui-lightbox-prev { left: 1.5rem; }
|
|
80
80
|
.ui-lightbox-next { right: 1.5rem; }
|
|
81
|
+
|
|
82
|
+
@media (max-width: 480px) {
|
|
83
|
+
.ui-lightbox-nav {
|
|
84
|
+
width: 36px;
|
|
85
|
+
height: 36px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.ui-lightbox-prev { left: 0.5rem; }
|
|
89
|
+
.ui-lightbox-next { right: 0.5rem; }
|
|
90
|
+
}
|
|
@@ -5,7 +5,7 @@ import { useEffect, useCallback } from 'react';
|
|
|
5
5
|
* Fullscreen image viewer with keyboard navigation.
|
|
6
6
|
* Space/Escape closes, ArrowLeft/ArrowRight navigates.
|
|
7
7
|
*/
|
|
8
|
-
export default function Lightbox({ src, name, onClose, onPrev, onNext }) {
|
|
8
|
+
export default function Lightbox({ src, name, onClose, onPrev, onNext, viewTransitionName }) {
|
|
9
9
|
const handleKey = useCallback((e) => {
|
|
10
10
|
if (e.key === 'Escape' || e.key === ' ') {
|
|
11
11
|
e.preventDefault();
|
|
@@ -23,7 +23,7 @@ export default function Lightbox({ src, name, onClose, onPrev, onNext }) {
|
|
|
23
23
|
return (
|
|
24
24
|
<div className="ui-lightbox-backdrop" onClick={onClose}>
|
|
25
25
|
<div className="ui-lightbox-content" onClick={(e) => e.stopPropagation()}>
|
|
26
|
-
<img src={src} alt={name} className="ui-lightbox-img" />
|
|
26
|
+
<img src={src} alt={name} className="ui-lightbox-img" style={viewTransitionName ? { viewTransitionName } : undefined} />
|
|
27
27
|
{name && <div className="ui-lightbox-caption">{name}</div>}
|
|
28
28
|
</div>
|
|
29
29
|
{onPrev && (
|
package/src/components/Modal.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
.ui-modal-backdrop {
|
|
4
4
|
position: fixed;
|
|
5
5
|
inset: 0;
|
|
6
|
-
background:
|
|
6
|
+
background: var(--ui-backdrop);
|
|
7
7
|
backdrop-filter: blur(4px);
|
|
8
8
|
display: flex;
|
|
9
9
|
align-items: center;
|
|
@@ -18,19 +18,20 @@
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
.ui-modal {
|
|
21
|
-
background:
|
|
21
|
+
background: var(--ui-glass-heavy);
|
|
22
22
|
backdrop-filter: blur(var(--ui-glass-blur-heavy));
|
|
23
23
|
border: 1px solid var(--ui-border);
|
|
24
24
|
border-top-color: var(--ui-border-strong);
|
|
25
25
|
border-radius: var(--ui-radius-lg);
|
|
26
26
|
width: 320px;
|
|
27
|
+
max-width: calc(100vw - 2rem);
|
|
27
28
|
box-shadow: var(--ui-shadow-float);
|
|
28
29
|
animation: ui-modal-in 0.25s var(--ui-ease-spring);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
@keyframes ui-modal-in {
|
|
32
|
-
from { opacity: 0; transform: scale(0.95) translateY(8px); }
|
|
33
|
-
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
33
|
+
from { opacity: 0; transform: scale(0.95) translateY(8px); filter: blur(4px); }
|
|
34
|
+
to { opacity: 1; transform: scale(1) translateY(0); filter: blur(0); }
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
.ui-modal-header {
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
justify-content: space-between;
|
|
7
7
|
padding: 0.75rem 1.5rem;
|
|
8
8
|
border-bottom: 1px solid var(--ui-border);
|
|
9
|
-
background:
|
|
9
|
+
background: var(--ui-glass-bar);
|
|
10
10
|
backdrop-filter: blur(var(--ui-glass-blur-heavy));
|
|
11
11
|
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03);
|
|
12
12
|
position: sticky;
|
|
13
13
|
top: 0;
|
|
14
14
|
z-index: 10;
|
|
15
|
+
gap: 0.5rem;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
.ui-page-header-left {
|
|
@@ -19,12 +20,20 @@
|
|
|
19
20
|
align-items: center;
|
|
20
21
|
gap: 0.5rem;
|
|
21
22
|
min-width: 0;
|
|
23
|
+
overflow: hidden;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
.ui-page-header-actions {
|
|
25
27
|
display: flex;
|
|
26
28
|
align-items: center;
|
|
27
29
|
gap: 0.5rem;
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (max-width: 480px) {
|
|
34
|
+
.ui-page-header {
|
|
35
|
+
padding: 0.75rem 0.75rem;
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
/* Breadcrumbs */
|
|
@@ -34,6 +43,8 @@
|
|
|
34
43
|
align-items: center;
|
|
35
44
|
gap: 0.4rem;
|
|
36
45
|
font-size: 0.85rem;
|
|
46
|
+
min-width: 0;
|
|
47
|
+
overflow: hidden;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
.ui-breadcrumb-segment {
|
|
@@ -62,6 +73,9 @@
|
|
|
62
73
|
font-size: 0.9rem;
|
|
63
74
|
font-weight: 600;
|
|
64
75
|
color: var(--ui-text);
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
text-overflow: ellipsis;
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/* Selection bar */
|
package/src/components/Panel.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
2
|
|
|
3
3
|
.ui-panel {
|
|
4
|
-
background:
|
|
4
|
+
background: var(--ui-glass);
|
|
5
5
|
backdrop-filter: blur(var(--ui-glass-blur-heavy));
|
|
6
6
|
border: 1px solid var(--ui-border);
|
|
7
7
|
border-top-color: var(--ui-border-strong);
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
display: flex;
|
|
11
11
|
flex-direction: column;
|
|
12
12
|
min-width: 120px;
|
|
13
|
-
overflow: hidden;
|
|
14
13
|
}
|
|
15
14
|
|
|
15
|
+
/* ── Titlebar ── */
|
|
16
|
+
|
|
16
17
|
.ui-panel-titlebar {
|
|
17
18
|
display: flex;
|
|
18
19
|
align-items: center;
|
|
@@ -53,6 +54,56 @@
|
|
|
53
54
|
background: var(--ui-surface-raised);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/* ── Tabs ── */
|
|
58
|
+
|
|
59
|
+
.ui-panel-tabs {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
padding: 0 0.35rem;
|
|
63
|
+
gap: 2px;
|
|
64
|
+
border-bottom: 1px solid var(--ui-border);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ui-panel-tab {
|
|
68
|
+
position: relative;
|
|
69
|
+
padding: 0.45rem 0.7rem;
|
|
70
|
+
background: none;
|
|
71
|
+
border: none;
|
|
72
|
+
color: var(--ui-text-subtle);
|
|
73
|
+
font-size: 0.7rem;
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
text-transform: uppercase;
|
|
76
|
+
letter-spacing: 0.05em;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
transition: color 0.15s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ui-panel-tab::after {
|
|
82
|
+
content: '';
|
|
83
|
+
position: absolute;
|
|
84
|
+
left: 0.35rem;
|
|
85
|
+
right: 0.35rem;
|
|
86
|
+
bottom: -1px;
|
|
87
|
+
height: 2px;
|
|
88
|
+
border-radius: 1px;
|
|
89
|
+
background: transparent;
|
|
90
|
+
transition: background 0.2s var(--ui-ease);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ui-panel-tab:hover {
|
|
94
|
+
color: var(--ui-text-muted);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ui-panel-tab--active {
|
|
98
|
+
color: var(--ui-accent);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ui-panel-tab--active::after {
|
|
102
|
+
background: var(--ui-accent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── Body ── */
|
|
106
|
+
|
|
56
107
|
.ui-panel-body {
|
|
57
108
|
flex: 1;
|
|
58
109
|
min-height: 0;
|
package/src/components/Panel.jsx
CHANGED
|
@@ -4,25 +4,45 @@
|
|
|
4
4
|
* Static glass panel — same chrome as DraggablePanel but positioned in flow.
|
|
5
5
|
* Use for sidebar sections, inspector panels, toolboxes, etc.
|
|
6
6
|
*
|
|
7
|
-
* @param {string} title - Titlebar text
|
|
7
|
+
* @param {string} title - Titlebar text (optional)
|
|
8
|
+
* @param {Array} tabs - [{id, label}] tab definitions (optional)
|
|
9
|
+
* @param {string} activeTab - Currently active tab id
|
|
10
|
+
* @param {function} onTabChange - Called with tab id on click
|
|
8
11
|
* @param {ReactNode} children - Panel body content
|
|
9
12
|
* @param {ReactNode} actions - Extra elements in the titlebar (right side, before close)
|
|
10
13
|
* @param {function} onClose - Close button handler (omit to hide close button)
|
|
11
14
|
* @param {string} className - Additional class names
|
|
12
15
|
* @param {boolean} flush - If true, removes body padding (for custom layouts)
|
|
13
16
|
*/
|
|
14
|
-
export default function Panel({ title, children, actions, onClose, className, flush }) {
|
|
17
|
+
export default function Panel({ title, tabs, activeTab, onTabChange, children, actions, onClose, className, flush, edgeLight = true }) {
|
|
18
|
+
const hasHeader = title || actions || onClose;
|
|
19
|
+
const hasTabs = tabs && tabs.length > 0;
|
|
20
|
+
|
|
15
21
|
return (
|
|
16
|
-
<div className={`ui-panel${className ? ` ${className}` : ''}`}>
|
|
17
|
-
{
|
|
22
|
+
<div className={`ui-panel${edgeLight ? ' ui-edge-light' : ''}${className ? ` ${className}` : ''}`}>
|
|
23
|
+
{hasHeader && (
|
|
18
24
|
<div className="ui-panel-titlebar">
|
|
19
|
-
<span className="ui-panel-title">{title}</span>
|
|
25
|
+
{title && <span className="ui-panel-title">{title}</span>}
|
|
20
26
|
<div className="ui-panel-actions">
|
|
21
27
|
{actions}
|
|
22
28
|
{onClose && <button className="ui-panel-close" type="button" onClick={onClose}>×</button>}
|
|
23
29
|
</div>
|
|
24
30
|
</div>
|
|
25
31
|
)}
|
|
32
|
+
{hasTabs && (
|
|
33
|
+
<nav className="ui-panel-tabs">
|
|
34
|
+
{tabs.map((tab) => (
|
|
35
|
+
<button
|
|
36
|
+
key={tab.id}
|
|
37
|
+
type="button"
|
|
38
|
+
className={`ui-panel-tab${activeTab === tab.id ? ' ui-panel-tab--active' : ''}`}
|
|
39
|
+
onClick={() => onTabChange?.(tab.id)}
|
|
40
|
+
>
|
|
41
|
+
{tab.label}
|
|
42
|
+
</button>
|
|
43
|
+
))}
|
|
44
|
+
</nav>
|
|
45
|
+
)}
|
|
26
46
|
<div className={`ui-panel-body${flush ? ' ui-panel-body--flush' : ''}`}>
|
|
27
47
|
{children}
|
|
28
48
|
</div>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
|
+
|
|
3
|
+
.ui-skeleton {
|
|
4
|
+
background: var(--ui-surface-raised);
|
|
5
|
+
border-radius: var(--ui-radius-sm);
|
|
6
|
+
position: relative;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ui-skeleton::after {
|
|
11
|
+
content: '';
|
|
12
|
+
position: absolute;
|
|
13
|
+
inset: 0;
|
|
14
|
+
background: linear-gradient(
|
|
15
|
+
90deg,
|
|
16
|
+
transparent 0%,
|
|
17
|
+
rgba(255, 255, 255, 0.04) 40%,
|
|
18
|
+
rgba(255, 255, 255, 0.06) 50%,
|
|
19
|
+
rgba(255, 255, 255, 0.04) 60%,
|
|
20
|
+
transparent 100%
|
|
21
|
+
);
|
|
22
|
+
animation: ui-skeleton-shimmer 1.8s var(--ui-ease) infinite;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes ui-skeleton-shimmer {
|
|
26
|
+
from { transform: translateX(-100%); }
|
|
27
|
+
to { transform: translateX(100%); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ui-skeleton--text {
|
|
31
|
+
height: 0.75rem;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ui-skeleton--circle {
|
|
36
|
+
border-radius: 50%;
|
|
37
|
+
width: 40px;
|
|
38
|
+
height: 40px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ui-skeleton--rect {
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 120px;
|
|
44
|
+
border-radius: var(--ui-radius);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ui-skeleton--card {
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 200px;
|
|
50
|
+
border-radius: var(--ui-radius-lg);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-skeleton-lines {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 0.5rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Pre-built card skeleton */
|
|
60
|
+
|
|
61
|
+
.ui-skeleton-card {
|
|
62
|
+
border-radius: var(--ui-radius);
|
|
63
|
+
border: 1px solid var(--ui-border);
|
|
64
|
+
background: var(--ui-surface);
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ui-skeleton-card-thumb {
|
|
69
|
+
aspect-ratio: 1;
|
|
70
|
+
border-radius: 0;
|
|
71
|
+
border-bottom: 1px solid var(--ui-border-subtle);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ui-skeleton-card-info {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 0.4rem;
|
|
78
|
+
padding: 0.6rem;
|
|
79
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
|
+
import './Skeleton.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shimmer loading placeholder.
|
|
6
|
+
* @param {string} variant - 'text' | 'circle' | 'rect' | 'card' (default: 'text')
|
|
7
|
+
* @param {string|number} width - CSS width (default: '100%')
|
|
8
|
+
* @param {string|number} height - CSS height (default: auto based on variant)
|
|
9
|
+
* @param {number} lines - Number of text lines for variant='text' (default: 1)
|
|
10
|
+
*/
|
|
11
|
+
export default function Skeleton({ variant = 'text', width, height, lines = 1 }) {
|
|
12
|
+
const style = {
|
|
13
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
14
|
+
height: typeof height === 'number' ? `${height}px` : height,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (variant === 'text' && lines > 1) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="ui-skeleton-lines">
|
|
20
|
+
{Array.from({ length: lines }, (_, i) => (
|
|
21
|
+
<div
|
|
22
|
+
key={i}
|
|
23
|
+
className="ui-skeleton ui-skeleton--text"
|
|
24
|
+
style={i === lines - 1 ? { ...style, width: '60%' } : style}
|
|
25
|
+
/>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return <div className={`ui-skeleton ui-skeleton--${variant}`} style={style} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pre-built skeleton that matches ItemCard layout.
|
|
36
|
+
*/
|
|
37
|
+
Skeleton.Card = function SkeletonCard() {
|
|
38
|
+
return (
|
|
39
|
+
<div className="ui-skeleton-card">
|
|
40
|
+
<div className="ui-skeleton ui-skeleton--rect ui-skeleton-card-thumb" />
|
|
41
|
+
<div className="ui-skeleton-card-info">
|
|
42
|
+
<div className="ui-skeleton ui-skeleton--text" style={{ width: '70%' }} />
|
|
43
|
+
<div className="ui-skeleton ui-skeleton--text" style={{ width: '40%' }} />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|