@1889ca/ui 0.1.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 +27 -0
- package/src/components/ContextMenu.css +50 -0
- package/src/components/ContextMenu.jsx +54 -0
- package/src/components/DraggablePanel.css +73 -0
- package/src/components/DraggablePanel.jsx +66 -0
- package/src/components/EditableValue.css +32 -0
- package/src/components/EditableValue.jsx +82 -0
- package/src/components/FolderCard.css +131 -0
- package/src/components/FolderCard.jsx +53 -0
- package/src/components/InlineEdit.css +30 -0
- package/src/components/InlineEdit.jsx +47 -0
- package/src/components/ItemCard.css +111 -0
- package/src/components/ItemCard.jsx +42 -0
- package/src/components/ItemGrid.css +13 -0
- package/src/components/ItemGrid.jsx +31 -0
- package/src/components/Lightbox.css +80 -0
- package/src/components/Lightbox.jsx +41 -0
- package/src/components/Modal.css +72 -0
- package/src/components/Modal.jsx +23 -0
- package/src/components/PageHeader.css +76 -0
- package/src/components/PageHeader.jsx +45 -0
- package/src/components/SlidePanel.css +82 -0
- package/src/components/SlidePanel.jsx +37 -0
- package/src/components/ZoomControls.css +51 -0
- package/src/components/ZoomControls.jsx +19 -0
- package/src/hooks/useItemDrag.js +81 -0
- package/src/hooks/useKeyboardShortcuts.js +36 -0
- package/src/hooks/useLightboxNav.js +39 -0
- package/src/hooks/useMultiSelect.js +48 -0
- package/src/icons/common.jsx +44 -0
- package/src/index.js +41 -0
- package/src/styles/primitives.css +115 -0
- package/src/styles/reset.css +61 -0
- package/src/styles/tokens.css +52 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
|
+
|
|
3
|
+
.ui-zoom-controls {
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: 0.15rem;
|
|
7
|
+
background: var(--ui-surface);
|
|
8
|
+
backdrop-filter: blur(var(--ui-glass-blur));
|
|
9
|
+
border: 1px solid var(--ui-border);
|
|
10
|
+
border-radius: var(--ui-radius);
|
|
11
|
+
padding: 0.2rem 0.35rem;
|
|
12
|
+
box-shadow: var(--ui-shadow);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.ui-zoom-btn {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
width: 28px;
|
|
20
|
+
height: 28px;
|
|
21
|
+
background: transparent;
|
|
22
|
+
border: none;
|
|
23
|
+
border-radius: var(--ui-radius-sm);
|
|
24
|
+
color: var(--ui-text-muted);
|
|
25
|
+
font-size: 0.85rem;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
transition: all 0.15s var(--ui-ease);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ui-zoom-btn:hover {
|
|
31
|
+
background: var(--ui-surface-raised);
|
|
32
|
+
color: var(--ui-text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ui-zoom-btn:active {
|
|
36
|
+
transform: scale(0.92);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.ui-zoom-fit {
|
|
40
|
+
font-size: 0.72rem;
|
|
41
|
+
width: auto;
|
|
42
|
+
padding: 0 0.45rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ui-zoom-val {
|
|
46
|
+
font-size: 0.7rem;
|
|
47
|
+
color: var(--ui-text-muted);
|
|
48
|
+
min-width: 2.4rem;
|
|
49
|
+
text-align: center;
|
|
50
|
+
font-variant-numeric: tabular-nums;
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-components/rules.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compact zoom control bar: -, percentage, +, Fit.
|
|
5
|
+
*/
|
|
6
|
+
export default function ZoomControls({ zoom, setZoom, resetView, min = 0.2, max = 5, step = 0.25 }) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="ui-zoom-controls">
|
|
9
|
+
<button className="ui-zoom-btn" type="button" title="Zoom out"
|
|
10
|
+
onClick={() => setZoom((z) => Math.max(min, +(z - step).toFixed(2)))}>−</button>
|
|
11
|
+
<span className="ui-zoom-val">{Math.round(zoom * 100)}%</span>
|
|
12
|
+
<button className="ui-zoom-btn" type="button" title="Zoom in"
|
|
13
|
+
onClick={() => setZoom((z) => Math.min(max, +(z + step).toFixed(2)))}>+</button>
|
|
14
|
+
{resetView && (
|
|
15
|
+
<button className="ui-zoom-btn ui-zoom-fit" onClick={resetView} title="Fit to view" type="button">Fit</button>
|
|
16
|
+
)}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-hooks/rules.md */
|
|
2
|
+
import { useState, useRef, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Drag-and-drop hook for moving items between folders/containers.
|
|
6
|
+
*
|
|
7
|
+
* @param {Set} selectedIds - Currently selected item IDs
|
|
8
|
+
* @param {function} onMove - Called with (itemIds, targetId) when items are dropped on a target
|
|
9
|
+
* @returns {Object} Drag handlers and state
|
|
10
|
+
*/
|
|
11
|
+
export default function useItemDrag(selectedIds, onMove) {
|
|
12
|
+
const [dragOverTarget, setDragOverTarget] = useState(null);
|
|
13
|
+
const [dragOverDesktop, setDragOverDesktop] = useState(false);
|
|
14
|
+
const dragDataRef = useRef(null);
|
|
15
|
+
|
|
16
|
+
const onItemDragStart = useCallback((e, itemId) => {
|
|
17
|
+
const ids = selectedIds.has(itemId) ? [...selectedIds] : [itemId];
|
|
18
|
+
dragDataRef.current = ids;
|
|
19
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
20
|
+
e.dataTransfer.setData('text/plain', String(itemId));
|
|
21
|
+
e.currentTarget.classList.add('dragging');
|
|
22
|
+
}, [selectedIds]);
|
|
23
|
+
|
|
24
|
+
const onItemDragEnd = useCallback((e) => {
|
|
25
|
+
e.currentTarget.classList.remove('dragging');
|
|
26
|
+
dragDataRef.current = null;
|
|
27
|
+
setDragOverTarget(null);
|
|
28
|
+
setDragOverDesktop(false);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const onTargetDragOver = useCallback((e, targetId) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
e.dataTransfer.dropEffect = 'move';
|
|
34
|
+
setDragOverTarget(targetId);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const onTargetDragLeave = useCallback((e, targetId) => {
|
|
38
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
39
|
+
setDragOverTarget((cur) => cur === targetId ? null : cur);
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const onTargetDrop = useCallback((e, targetId) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setDragOverTarget(null);
|
|
46
|
+
const ids = dragDataRef.current;
|
|
47
|
+
if (ids && ids.length && onMove) onMove(ids, targetId);
|
|
48
|
+
}, [onMove]);
|
|
49
|
+
|
|
50
|
+
const onDesktopDragOver = useCallback((e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.dataTransfer.dropEffect = 'move';
|
|
53
|
+
setDragOverDesktop(true);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const onDesktopDragLeave = useCallback((e) => {
|
|
57
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
58
|
+
setDragOverDesktop(false);
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const onDesktopDrop = useCallback((e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
setDragOverDesktop(false);
|
|
65
|
+
const ids = dragDataRef.current;
|
|
66
|
+
if (ids && ids.length && onMove) onMove(ids, null);
|
|
67
|
+
}, [onMove]);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
dragOverTarget,
|
|
71
|
+
dragOverDesktop,
|
|
72
|
+
onItemDragStart,
|
|
73
|
+
onItemDragEnd,
|
|
74
|
+
onTargetDragOver,
|
|
75
|
+
onTargetDragLeave,
|
|
76
|
+
onTargetDrop,
|
|
77
|
+
onDesktopDragOver,
|
|
78
|
+
onDesktopDragLeave,
|
|
79
|
+
onDesktopDrop,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-hooks/rules.md */
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register keyboard shortcuts. Ignores events from input/textarea.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} shortcuts - Map of key to handler: { 'Space': fn, 'Escape': fn, 'Meta+a': fn }
|
|
8
|
+
* @param {Array} deps - Dependency array for the effect
|
|
9
|
+
*/
|
|
10
|
+
export default function useKeyboardShortcuts(shortcuts, deps = []) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
function onKeyDown(e) {
|
|
13
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
14
|
+
|
|
15
|
+
for (const [combo, handler] of Object.entries(shortcuts)) {
|
|
16
|
+
if (!handler) continue;
|
|
17
|
+
const parts = combo.split('+');
|
|
18
|
+
const key = parts[parts.length - 1];
|
|
19
|
+
const needsMeta = parts.includes('Meta') || parts.includes('Ctrl');
|
|
20
|
+
const needsShift = parts.includes('Shift');
|
|
21
|
+
|
|
22
|
+
const metaMatch = needsMeta ? (e.metaKey || e.ctrlKey) : !(e.metaKey || e.ctrlKey);
|
|
23
|
+
const shiftMatch = needsShift ? e.shiftKey : !e.shiftKey;
|
|
24
|
+
|
|
25
|
+
if (e.key === key && metaMatch && shiftMatch) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
handler(e);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
document.addEventListener('keydown', onKeyDown);
|
|
34
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
35
|
+
}, deps);
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-hooks/rules.md */
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lightbox navigation hook. Manages open/close state and prev/next within a filtered list.
|
|
6
|
+
*
|
|
7
|
+
* @param {Array} items - All items (must have `id` property)
|
|
8
|
+
* @param {function} hasImage - Predicate: (item) => boolean, filters items for lightbox navigation
|
|
9
|
+
* @returns {Object} { lightboxId, lightboxItem, open, close, prev, next, canPrev, canNext }
|
|
10
|
+
*/
|
|
11
|
+
export default function useLightboxNav(items, hasImage) {
|
|
12
|
+
const [lightboxId, setLightboxId] = useState(null);
|
|
13
|
+
|
|
14
|
+
const withImages = items.filter(hasImage);
|
|
15
|
+
const idx = withImages.findIndex((item) => item.id === lightboxId);
|
|
16
|
+
const lightboxItem = idx !== -1 ? withImages[idx] : null;
|
|
17
|
+
|
|
18
|
+
const open = useCallback((id) => setLightboxId(id), []);
|
|
19
|
+
const close = useCallback(() => setLightboxId(null), []);
|
|
20
|
+
|
|
21
|
+
const prev = useCallback(() => {
|
|
22
|
+
if (idx > 0) setLightboxId(withImages[idx - 1].id);
|
|
23
|
+
}, [withImages, idx]);
|
|
24
|
+
|
|
25
|
+
const next = useCallback(() => {
|
|
26
|
+
if (idx < withImages.length - 1) setLightboxId(withImages[idx + 1].id);
|
|
27
|
+
}, [withImages, idx]);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
lightboxId,
|
|
31
|
+
lightboxItem,
|
|
32
|
+
open,
|
|
33
|
+
close,
|
|
34
|
+
prev,
|
|
35
|
+
next,
|
|
36
|
+
canPrev: idx > 0,
|
|
37
|
+
canNext: idx < withImages.length - 1,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-hooks/rules.md */
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multi-select hook with Cmd+click, Shift+click, and Ctrl+A support.
|
|
6
|
+
* Returns selection state + handlers to wire into item components.
|
|
7
|
+
*
|
|
8
|
+
* @param {Array} items - Array of items with `id` property
|
|
9
|
+
* @returns {Object} { selectedIds, lastSelectedId, handleClick, selectAll, clearSelection, setSelectedIds }
|
|
10
|
+
*/
|
|
11
|
+
export default function useMultiSelect(items) {
|
|
12
|
+
const [selectedIds, setSelectedIds] = useState(new Set());
|
|
13
|
+
const [lastSelectedId, setLastSelectedId] = useState(null);
|
|
14
|
+
|
|
15
|
+
const handleClick = useCallback((e, itemId) => {
|
|
16
|
+
if (e.detail === 2) return; // skip, let doubleClick handle it
|
|
17
|
+
|
|
18
|
+
if (e.metaKey || e.ctrlKey) {
|
|
19
|
+
setSelectedIds((prev) => {
|
|
20
|
+
const next = new Set(prev);
|
|
21
|
+
next.has(itemId) ? next.delete(itemId) : next.add(itemId);
|
|
22
|
+
return next;
|
|
23
|
+
});
|
|
24
|
+
setLastSelectedId(itemId);
|
|
25
|
+
} else if (e.shiftKey && lastSelectedId !== null) {
|
|
26
|
+
const ids = items.map((item) => item.id);
|
|
27
|
+
const si = ids.indexOf(lastSelectedId);
|
|
28
|
+
const ei = ids.indexOf(itemId);
|
|
29
|
+
if (si !== -1 && ei !== -1) {
|
|
30
|
+
const [from, to] = [Math.min(si, ei), Math.max(si, ei)];
|
|
31
|
+
setSelectedIds(new Set(ids.slice(from, to + 1)));
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
setSelectedIds(new Set([itemId]));
|
|
35
|
+
setLastSelectedId(itemId);
|
|
36
|
+
}
|
|
37
|
+
}, [items, lastSelectedId]);
|
|
38
|
+
|
|
39
|
+
const selectAll = useCallback(() => {
|
|
40
|
+
setSelectedIds(new Set(items.map((item) => item.id)));
|
|
41
|
+
}, [items]);
|
|
42
|
+
|
|
43
|
+
const clearSelection = useCallback(() => {
|
|
44
|
+
setSelectedIds(new Set());
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return { selectedIds, lastSelectedId, handleClick, selectAll, clearSelection, setSelectedIds };
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-icons/rules.md */
|
|
2
|
+
|
|
3
|
+
export function DownloadIcon({ size = 14 }) {
|
|
4
|
+
return (
|
|
5
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
6
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
7
|
+
<polyline points="7 10 12 15 17 10" />
|
|
8
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
9
|
+
</svg>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FolderIcon({ size = 18 }) {
|
|
14
|
+
return (
|
|
15
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
16
|
+
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ChevronLeftIcon({ size = 20 }) {
|
|
22
|
+
return (
|
|
23
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
24
|
+
<path d="M15 18l-6-6 6-6" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ChevronRightIcon({ size = 20 }) {
|
|
30
|
+
return (
|
|
31
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
32
|
+
<path d="M9 18l6-6-6-6" />
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function CloseIcon({ size = 16 }) {
|
|
38
|
+
return (
|
|
39
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
40
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
41
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
42
|
+
</svg>
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Styles — import order matters */
|
|
2
|
+
import './styles/tokens.css';
|
|
3
|
+
import './styles/reset.css';
|
|
4
|
+
import './styles/primitives.css';
|
|
5
|
+
|
|
6
|
+
/* Component styles */
|
|
7
|
+
import './components/ContextMenu.css';
|
|
8
|
+
import './components/Lightbox.css';
|
|
9
|
+
import './components/DraggablePanel.css';
|
|
10
|
+
import './components/EditableValue.css';
|
|
11
|
+
import './components/InlineEdit.css';
|
|
12
|
+
import './components/Modal.css';
|
|
13
|
+
import './components/ZoomControls.css';
|
|
14
|
+
import './components/PageHeader.css';
|
|
15
|
+
import './components/ItemGrid.css';
|
|
16
|
+
import './components/ItemCard.css';
|
|
17
|
+
import './components/FolderCard.css';
|
|
18
|
+
import './components/SlidePanel.css';
|
|
19
|
+
|
|
20
|
+
/* Components */
|
|
21
|
+
export { default as ContextMenu } from './components/ContextMenu.jsx';
|
|
22
|
+
export { default as Lightbox } from './components/Lightbox.jsx';
|
|
23
|
+
export { default as DraggablePanel } from './components/DraggablePanel.jsx';
|
|
24
|
+
export { default as EditableValue } from './components/EditableValue.jsx';
|
|
25
|
+
export { default as InlineEdit } from './components/InlineEdit.jsx';
|
|
26
|
+
export { default as Modal } from './components/Modal.jsx';
|
|
27
|
+
export { default as ZoomControls } from './components/ZoomControls.jsx';
|
|
28
|
+
export { default as PageHeader } from './components/PageHeader.jsx';
|
|
29
|
+
export { default as ItemGrid } from './components/ItemGrid.jsx';
|
|
30
|
+
export { default as ItemCard } from './components/ItemCard.jsx';
|
|
31
|
+
export { default as FolderCard } from './components/FolderCard.jsx';
|
|
32
|
+
export { default as SlidePanel } from './components/SlidePanel.jsx';
|
|
33
|
+
|
|
34
|
+
/* Hooks */
|
|
35
|
+
export { default as useMultiSelect } from './hooks/useMultiSelect.js';
|
|
36
|
+
export { default as useItemDrag } from './hooks/useItemDrag.js';
|
|
37
|
+
export { default as useLightboxNav } from './hooks/useLightboxNav.js';
|
|
38
|
+
export { default as useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js';
|
|
39
|
+
|
|
40
|
+
/* Icons */
|
|
41
|
+
export { DownloadIcon, FolderIcon, ChevronLeftIcon, ChevronRightIcon, CloseIcon } from './icons/common.jsx';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-styles/rules.md */
|
|
2
|
+
|
|
3
|
+
/* Shared primitive classes */
|
|
4
|
+
|
|
5
|
+
.ui-btn {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
gap: 0.5rem;
|
|
10
|
+
padding: 0.5rem 1.25rem;
|
|
11
|
+
border-radius: var(--ui-radius);
|
|
12
|
+
border: 1px solid var(--ui-border);
|
|
13
|
+
font-size: 0.85rem;
|
|
14
|
+
font-weight: 500;
|
|
15
|
+
transition: all 0.2s var(--ui-ease);
|
|
16
|
+
white-space: nowrap;
|
|
17
|
+
backdrop-filter: blur(var(--ui-glass-blur));
|
|
18
|
+
background: var(--ui-surface);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.ui-btn-primary {
|
|
22
|
+
background: var(--ui-accent);
|
|
23
|
+
border-color: transparent;
|
|
24
|
+
color: #fff;
|
|
25
|
+
box-shadow: 0 0 16px var(--ui-accent-glow);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ui-btn-primary:hover {
|
|
29
|
+
background: var(--ui-accent-hover);
|
|
30
|
+
box-shadow: 0 0 24px var(--ui-accent-glow);
|
|
31
|
+
transform: translateY(-1px);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ui-btn-ghost {
|
|
35
|
+
background: var(--ui-surface);
|
|
36
|
+
color: var(--ui-text-muted);
|
|
37
|
+
border: 1px solid var(--ui-border);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ui-btn-ghost:hover {
|
|
41
|
+
color: var(--ui-text);
|
|
42
|
+
border-color: var(--ui-border-strong);
|
|
43
|
+
background: var(--ui-surface-raised);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ui-btn-danger {
|
|
47
|
+
background: var(--ui-danger);
|
|
48
|
+
border-color: transparent;
|
|
49
|
+
color: #fff;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.ui-btn-danger:hover {
|
|
53
|
+
background: color-mix(in srgb, var(--ui-danger) 85%, #000);
|
|
54
|
+
box-shadow: 0 0 16px var(--ui-danger-glow);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ui-btn-danger-ghost {
|
|
58
|
+
background: transparent;
|
|
59
|
+
color: var(--ui-danger);
|
|
60
|
+
border-color: var(--ui-danger);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ui-btn-danger-ghost:hover {
|
|
64
|
+
color: var(--ui-danger);
|
|
65
|
+
border-color: var(--ui-danger);
|
|
66
|
+
box-shadow: 0 0 12px var(--ui-danger-glow);
|
|
67
|
+
background: color-mix(in srgb, var(--ui-danger) 10%, transparent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.ui-btn:disabled {
|
|
71
|
+
opacity: 0.4;
|
|
72
|
+
cursor: not-allowed;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ui-btn-sm {
|
|
77
|
+
padding: 0.25rem 0.6rem;
|
|
78
|
+
font-size: 0.78rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ui-card {
|
|
82
|
+
background: var(--ui-surface);
|
|
83
|
+
backdrop-filter: blur(var(--ui-glass-blur));
|
|
84
|
+
border: 1px solid var(--ui-border);
|
|
85
|
+
border-radius: var(--ui-radius-lg);
|
|
86
|
+
padding: 1.5rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ui-input {
|
|
90
|
+
background: var(--ui-surface);
|
|
91
|
+
backdrop-filter: blur(var(--ui-glass-blur));
|
|
92
|
+
border: 1px solid var(--ui-border);
|
|
93
|
+
border-radius: var(--ui-radius);
|
|
94
|
+
color: var(--ui-text);
|
|
95
|
+
padding: 0.5rem 0.75rem;
|
|
96
|
+
font-size: 0.85rem;
|
|
97
|
+
outline: none;
|
|
98
|
+
transition: border-color 0.2s var(--ui-ease), box-shadow 0.2s var(--ui-ease);
|
|
99
|
+
width: 100%;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.ui-input:focus {
|
|
103
|
+
border-color: var(--ui-accent);
|
|
104
|
+
box-shadow: 0 0 0 3px var(--ui-accent-subtle);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ui-input::placeholder {
|
|
108
|
+
color: var(--ui-text-subtle);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ui-page {
|
|
112
|
+
min-height: 100dvh;
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-styles/rules.md */
|
|
2
|
+
|
|
3
|
+
/* Minimal reset for @mcm/ui consumers */
|
|
4
|
+
*,
|
|
5
|
+
*::before,
|
|
6
|
+
*::after {
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
html {
|
|
13
|
+
font-size: 16px;
|
|
14
|
+
-webkit-text-size-adjust: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background-color: var(--ui-bg);
|
|
19
|
+
color: var(--ui-text);
|
|
20
|
+
font-family: var(--ui-font-sans);
|
|
21
|
+
line-height: 1.6;
|
|
22
|
+
min-height: 100dvh;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
a {
|
|
26
|
+
color: var(--ui-accent);
|
|
27
|
+
text-decoration: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
a:hover {
|
|
31
|
+
text-decoration: underline;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
button {
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
font-family: inherit;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
input,
|
|
40
|
+
textarea,
|
|
41
|
+
select {
|
|
42
|
+
font-family: inherit;
|
|
43
|
+
font-size: inherit;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
img {
|
|
47
|
+
max-width: 100%;
|
|
48
|
+
display: block;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.sr-only {
|
|
52
|
+
position: absolute;
|
|
53
|
+
width: 1px;
|
|
54
|
+
height: 1px;
|
|
55
|
+
padding: 0;
|
|
56
|
+
margin: -1px;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
clip: rect(0, 0, 0, 0);
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
border: 0;
|
|
61
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Contract: contracts/packages-ui-styles/rules.md */
|
|
2
|
+
|
|
3
|
+
/* Design tokens — shared across all @mcm/ui consumers */
|
|
4
|
+
:root {
|
|
5
|
+
/* Core palette */
|
|
6
|
+
--ui-bg: #08080c;
|
|
7
|
+
--ui-surface: rgba(255, 255, 255, 0.04);
|
|
8
|
+
--ui-surface-raised: rgba(255, 255, 255, 0.07);
|
|
9
|
+
--ui-surface-solid: #111116;
|
|
10
|
+
--ui-border: rgba(255, 255, 255, 0.08);
|
|
11
|
+
--ui-border-subtle: rgba(255, 255, 255, 0.05);
|
|
12
|
+
--ui-border-strong: rgba(255, 255, 255, 0.14);
|
|
13
|
+
|
|
14
|
+
/* Accent — indigo-violet */
|
|
15
|
+
--ui-accent: #7c6cf0;
|
|
16
|
+
--ui-accent-hover: #6a5bd8;
|
|
17
|
+
--ui-accent-glow: rgba(124, 108, 240, 0.25);
|
|
18
|
+
--ui-accent-subtle: rgba(124, 108, 240, 0.08);
|
|
19
|
+
--ui-accent-muted: rgba(124, 108, 240, 0.15);
|
|
20
|
+
|
|
21
|
+
/* Semantic */
|
|
22
|
+
--ui-danger: #f06068;
|
|
23
|
+
--ui-danger-glow: rgba(240, 96, 104, 0.2);
|
|
24
|
+
--ui-success: #34d399;
|
|
25
|
+
|
|
26
|
+
/* Text */
|
|
27
|
+
--ui-text: #e8e6f0;
|
|
28
|
+
--ui-text-muted: #8a879c;
|
|
29
|
+
--ui-text-subtle: #56536a;
|
|
30
|
+
|
|
31
|
+
/* Radius */
|
|
32
|
+
--ui-radius-sm: 6px;
|
|
33
|
+
--ui-radius: 10px;
|
|
34
|
+
--ui-radius-lg: 14px;
|
|
35
|
+
|
|
36
|
+
/* Typography */
|
|
37
|
+
--ui-font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
38
|
+
--ui-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
39
|
+
|
|
40
|
+
/* Glass */
|
|
41
|
+
--ui-glass-blur: 16px;
|
|
42
|
+
--ui-glass-blur-heavy: 24px;
|
|
43
|
+
|
|
44
|
+
/* Shadows & glow */
|
|
45
|
+
--ui-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
46
|
+
--ui-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 1px rgba(255, 255, 255, 0.05);
|
|
47
|
+
--ui-shadow-float: 0 12px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
|
48
|
+
|
|
49
|
+
/* Transitions */
|
|
50
|
+
--ui-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
51
|
+
--ui-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
52
|
+
}
|