@1889ca/ui 0.5.1 → 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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/components/ContextMenu.css +5 -3
  3. package/src/components/ContextMenu.jsx +2 -14
  4. package/src/components/DraggablePanel.css +2 -1
  5. package/src/components/DraggablePanel.jsx +4 -17
  6. package/src/components/EmptyState.css +36 -0
  7. package/src/components/EmptyState.jsx +27 -0
  8. package/src/components/ItemCard.css +12 -1
  9. package/src/components/ItemCard.jsx +8 -3
  10. package/src/components/ItemGrid.css +22 -0
  11. package/src/components/Lightbox.css +12 -2
  12. package/src/components/Lightbox.jsx +2 -2
  13. package/src/components/LoginCard.css +1 -1
  14. package/src/components/Modal.css +5 -4
  15. package/src/components/PageHeader.css +15 -1
  16. package/src/components/Panel.css +1 -2
  17. package/src/components/Panel.jsx +2 -2
  18. package/src/components/Skeleton.css +79 -0
  19. package/src/components/Skeleton.jsx +47 -0
  20. package/src/components/SlidePanel.css +7 -1
  21. package/src/components/StatusBar.css +38 -1
  22. package/src/components/TabBar.css +53 -0
  23. package/src/components/TabBar.jsx +28 -0
  24. package/src/components/ThemeToggle.css +34 -0
  25. package/src/components/ThemeToggle.jsx +44 -0
  26. package/src/components/Toast.css +102 -0
  27. package/src/components/Toast.jsx +67 -0
  28. package/src/components/Toggle.css +53 -0
  29. package/src/components/Toggle.jsx +28 -0
  30. package/src/components/Tooltip.css +36 -0
  31. package/src/components/Tooltip.jsx +55 -0
  32. package/src/components/UserMenu.css +18 -1
  33. package/src/components/UserMenu.jsx +28 -18
  34. package/src/effects/Particles.jsx +117 -0
  35. package/src/effects/ambient-glow.css +67 -0
  36. package/src/effects/edge-light.css +69 -0
  37. package/src/effects/glass-tint.css +47 -0
  38. package/src/effects/glow-focus.css +64 -0
  39. package/src/effects/haptic.css +52 -0
  40. package/src/effects/particles.css +10 -0
  41. package/src/effects/scroll-parallax.css +46 -0
  42. package/src/hooks/useClickOutside.js +26 -0
  43. package/src/hooks/useLocalStorage.js +25 -0
  44. package/src/hooks/useMagnet.js +60 -0
  45. package/src/hooks/useMediaQuery.js +24 -0
  46. package/src/hooks/useScrollParallax.js +39 -0
  47. package/src/hooks/useSound.js +106 -0
  48. package/src/hooks/useTheme.js +90 -0
  49. package/src/index.js +36 -0
  50. package/src/styles/primitives.css +48 -1
  51. package/src/styles/tokens.css +89 -0
  52. package/src/utils/accentColors.js +85 -0
  53. package/src/utils/viewTransition.js +16 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1889ca/ui",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -4,7 +4,8 @@
4
4
  position: fixed;
5
5
  z-index: 1000;
6
6
  min-width: 170px;
7
- background: rgba(18, 18, 24, 0.82);
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: scale(0.96); }
19
- to { opacity: 1; transform: scale(1); }
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
- useEffect(() => {
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: rgba(18, 18, 24, 0.8);
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, useState, useEffect } from 'react';
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] = useState(() => {
11
- try { const s = localStorage.getItem(`${storagePrefix}-${panelId}`); return s ? JSON.parse(s) : defaultPos; }
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: rgba(18, 18, 24, 0.6);
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: rgba(18, 18, 24, 0.85);
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 && (
@@ -8,7 +8,7 @@
8
8
  width: 100%;
9
9
  max-width: 400px;
10
10
  padding: 2rem;
11
- background: rgba(18, 18, 24, 0.7);
11
+ background: var(--ui-glass);
12
12
  backdrop-filter: blur(var(--ui-glass-blur-heavy));
13
13
  border: 1px solid var(--ui-border);
14
14
  border-top-color: var(--ui-border-strong);
@@ -3,7 +3,7 @@
3
3
  .ui-modal-backdrop {
4
4
  position: fixed;
5
5
  inset: 0;
6
- background: rgba(0, 0, 0, 0.5);
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: rgba(18, 18, 24, 0.85);
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: rgba(8, 8, 12, 0.75);
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 */
@@ -1,7 +1,7 @@
1
1
  /** Contract: contracts/packages-ui-components/rules.md */
2
2
 
3
3
  .ui-panel {
4
- background: rgba(18, 18, 24, 0.8);
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,7 +10,6 @@
10
10
  display: flex;
11
11
  flex-direction: column;
12
12
  min-width: 120px;
13
- overflow: hidden;
14
13
  }
15
14
 
16
15
  /* ── Titlebar ── */
@@ -14,12 +14,12 @@
14
14
  * @param {string} className - Additional class names
15
15
  * @param {boolean} flush - If true, removes body padding (for custom layouts)
16
16
  */
17
- export default function Panel({ title, tabs, activeTab, onTabChange, children, actions, onClose, className, flush }) {
17
+ export default function Panel({ title, tabs, activeTab, onTabChange, children, actions, onClose, className, flush, edgeLight = true }) {
18
18
  const hasHeader = title || actions || onClose;
19
19
  const hasTabs = tabs && tabs.length > 0;
20
20
 
21
21
  return (
22
- <div className={`ui-panel${className ? ` ${className}` : ''}`}>
22
+ <div className={`ui-panel${edgeLight ? ' ui-edge-light' : ''}${className ? ` ${className}` : ''}`}>
23
23
  {hasHeader && (
24
24
  <div className="ui-panel-titlebar">
25
25
  {title && <span className="ui-panel-title">{title}</span>}
@@ -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
+ };
@@ -4,7 +4,7 @@
4
4
  position: fixed;
5
5
  top: 0;
6
6
  height: 100dvh;
7
- background: rgba(12, 12, 18, 0.85);
7
+ background: var(--ui-glass-heavy);
8
8
  backdrop-filter: blur(var(--ui-glass-blur-heavy));
9
9
  border-right: 1px solid var(--ui-border);
10
10
  transition: transform 0.3s var(--ui-ease);
@@ -58,6 +58,12 @@
58
58
  padding: 0.75rem;
59
59
  }
60
60
 
61
+ @media (max-width: 480px) {
62
+ .ui-slide-panel {
63
+ max-width: 85vw;
64
+ }
65
+ }
66
+
61
67
  /* Tab button primitive */
62
68
  .ui-tab {
63
69
  flex: 1;
@@ -7,7 +7,7 @@
7
7
  padding: 0 1rem;
8
8
  height: 28px;
9
9
  border-top: 1px solid var(--ui-border);
10
- background: rgba(8, 8, 12, 0.75);
10
+ background: var(--ui-glass-bar);
11
11
  backdrop-filter: blur(var(--ui-glass-blur-heavy));
12
12
  box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.03);
13
13
  position: fixed;
@@ -18,6 +18,18 @@
18
18
  font-size: 0.72rem;
19
19
  color: var(--ui-text-muted);
20
20
  gap: 0.5rem;
21
+ overflow: hidden;
22
+ }
23
+
24
+ @media (max-width: 480px) {
25
+ .ui-statusbar {
26
+ padding: 0 0.5rem;
27
+ gap: 0.25rem;
28
+ }
29
+
30
+ .ui-statusbar-center {
31
+ display: none;
32
+ }
21
33
  }
22
34
 
23
35
  .ui-statusbar-left,
@@ -27,6 +39,7 @@
27
39
  align-items: center;
28
40
  gap: 0.6rem;
29
41
  min-width: 0;
42
+ overflow: hidden;
30
43
  }
31
44
 
32
45
  .ui-statusbar-left { justify-content: flex-start; }
@@ -116,3 +129,27 @@
116
129
  background: var(--ui-border);
117
130
  flex-shrink: 0;
118
131
  }
132
+
133
+ /* Progress shimmer */
134
+ .ui-sb-progress-fill {
135
+ position: relative;
136
+ overflow: hidden;
137
+ }
138
+
139
+ .ui-sb-progress-fill::after {
140
+ content: '';
141
+ position: absolute;
142
+ inset: 0;
143
+ background: linear-gradient(
144
+ 90deg,
145
+ transparent 0%,
146
+ rgba(255, 255, 255, 0.15) 50%,
147
+ transparent 100%
148
+ );
149
+ animation: ui-progress-shimmer 1.5s var(--ui-ease) infinite;
150
+ }
151
+
152
+ @keyframes ui-progress-shimmer {
153
+ from { transform: translateX(-100%); }
154
+ to { transform: translateX(100%); }
155
+ }