Merge pull request 'fix(web): style guide batch 3 — icons, font scale, spacing' (#85) from fix-style-guide-batch-3 into main
Reviewed-on: #85
This commit was merged in pull request #85.
This commit is contained in:
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "silo-web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.564.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.0.0"
|
||||
@@ -1499,6 +1500,15 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.564.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz",
|
||||
"integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.564.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.0.0"
|
||||
|
||||
@@ -113,9 +113,9 @@ export function AppShell() {
|
||||
onClick={toggleDensity}
|
||||
title={`Switch to ${density === "comfortable" ? "compact" : "comfortable"} view`}
|
||||
style={{
|
||||
padding: "0.2rem 0.5rem",
|
||||
fontSize: "0.7rem",
|
||||
borderRadius: "0.3rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
borderRadius: "0.375rem",
|
||||
cursor: "pointer",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
background: "var(--ctp-surface0)",
|
||||
@@ -130,7 +130,7 @@ export function AppShell() {
|
||||
onClick={logout}
|
||||
style={{
|
||||
padding: "0.35rem 0.75rem",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
borderRadius: "0.4rem",
|
||||
cursor: "pointer",
|
||||
border: "none",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label: string;
|
||||
@@ -24,76 +25,95 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
||||
};
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
const handleScroll = () => onClose();
|
||||
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
document.addEventListener('keydown', handleKey);
|
||||
window.addEventListener('scroll', handleScroll, true);
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
document.addEventListener("keydown", handleKey);
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
document.removeEventListener('keydown', handleKey);
|
||||
window.removeEventListener('scroll', handleScroll, true);
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
document.removeEventListener("keydown", handleKey);
|
||||
window.removeEventListener("scroll", handleScroll, true);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Clamp position to viewport
|
||||
const style: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
position: "fixed",
|
||||
left: Math.min(x, window.innerWidth - 220),
|
||||
top: Math.min(y, window.innerHeight - items.length * 32 - 16),
|
||||
zIndex: 9999,
|
||||
backgroundColor: 'var(--ctp-surface0)',
|
||||
border: '1px solid var(--ctp-surface1)',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '0.25rem 0',
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.5rem",
|
||||
padding: "0.25rem 0",
|
||||
minWidth: 200,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} style={style}>
|
||||
{items.map((item, i) =>
|
||||
item.divider ? (
|
||||
<div key={i} style={{ borderTop: '1px solid var(--ctp-surface1)', margin: '0.25rem 0' }} />
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
borderTop: "1px solid var(--ctp-surface1)",
|
||||
margin: "0.25rem 0",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => {
|
||||
if (item.onToggle) item.onToggle();
|
||||
else if (item.onClick) { item.onClick(); onClose(); }
|
||||
else if (item.onClick) {
|
||||
item.onClick();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
width: '100%',
|
||||
padding: '0.35rem 0.75rem',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: item.disabled ? 'var(--ctp-overlay0)' : 'var(--ctp-text)',
|
||||
fontSize: '0.85rem',
|
||||
cursor: item.disabled ? 'default' : 'pointer',
|
||||
textAlign: 'left',
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
width: "100%",
|
||||
padding: "0.35rem 0.75rem",
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: item.disabled ? "var(--ctp-overlay0)" : "var(--ctp-text)",
|
||||
fontSize: "var(--font-body)",
|
||||
cursor: item.disabled ? "default" : "pointer",
|
||||
textAlign: "left",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!item.disabled) e.currentTarget.style.backgroundColor = 'var(--ctp-surface1)';
|
||||
if (!item.disabled)
|
||||
e.currentTarget.style.backgroundColor = "var(--ctp-surface1)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
{item.checked !== undefined && (
|
||||
<span style={{
|
||||
width: 16, height: 16, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: '1px solid var(--ctp-overlay0)', borderRadius: 3,
|
||||
backgroundColor: item.checked ? 'var(--ctp-mauve)' : 'transparent',
|
||||
color: item.checked ? 'var(--ctp-crust)' : 'transparent',
|
||||
fontSize: '0.7rem', fontWeight: 700, flexShrink: 0,
|
||||
}}>
|
||||
{item.checked ? '✓' : ''}
|
||||
<span
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "1px solid var(--ctp-overlay0)",
|
||||
borderRadius: 3,
|
||||
backgroundColor: item.checked
|
||||
? "var(--ctp-mauve)"
|
||||
: "transparent",
|
||||
color: item.checked ? "var(--ctp-crust)" : "transparent",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{item.checked ? <Check size={14} /> : ""}
|
||||
</span>
|
||||
)}
|
||||
{item.label}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export interface TagOption {
|
||||
id: string;
|
||||
@@ -12,34 +13,45 @@ interface TagInputProps {
|
||||
searchFn: (query: string) => Promise<TagOption[]>;
|
||||
}
|
||||
|
||||
export function TagInput({ value, onChange, placeholder, searchFn }: TagInputProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
export function TagInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
searchFn,
|
||||
}: TagInputProps) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState<TagOption[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [highlighted, setHighlighted] = useState(0);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Debounced search
|
||||
const search = useCallback(
|
||||
(q: string) => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
if (q.trim() === '') {
|
||||
if (q.trim() === "") {
|
||||
// Show all results when input is empty but focused
|
||||
debounceRef.current = setTimeout(() => {
|
||||
searchFn('').then((opts) => {
|
||||
setResults(opts.filter((o) => !value.includes(o.id)));
|
||||
setHighlighted(0);
|
||||
}).catch(() => setResults([]));
|
||||
searchFn("")
|
||||
.then((opts) => {
|
||||
setResults(opts.filter((o) => !value.includes(o.id)));
|
||||
setHighlighted(0);
|
||||
})
|
||||
.catch(() => setResults([]));
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
searchFn(q).then((opts) => {
|
||||
setResults(opts.filter((o) => !value.includes(o.id)));
|
||||
setHighlighted(0);
|
||||
}).catch(() => setResults([]));
|
||||
searchFn(q)
|
||||
.then((opts) => {
|
||||
setResults(opts.filter((o) => !value.includes(o.id)));
|
||||
setHighlighted(0);
|
||||
})
|
||||
.catch(() => setResults([]));
|
||||
}, 200);
|
||||
},
|
||||
[searchFn, value],
|
||||
@@ -53,17 +65,20 @@ export function TagInput({ value, onChange, placeholder, searchFn }: TagInputPro
|
||||
// Close on click outside
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, []);
|
||||
|
||||
const select = (id: string) => {
|
||||
onChange([...value, id]);
|
||||
setQuery('');
|
||||
setQuery("");
|
||||
setOpen(false);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
@@ -73,22 +88,22 @@ export function TagInput({ value, onChange, placeholder, searchFn }: TagInputPro
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Backspace' && query === '' && value.length > 0) {
|
||||
if (e.key === "Backspace" && query === "" && value.length > 0) {
|
||||
onChange(value.slice(0, -1));
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === "Escape") {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
if (!open || results.length === 0) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setHighlighted((h) => (h + 1) % results.length);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setHighlighted((h) => (h - 1 + results.length) % results.length);
|
||||
} else if (e.key === 'Enter') {
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (results[highlighted]) select(results[highlighted].id);
|
||||
}
|
||||
@@ -99,19 +114,19 @@ export function TagInput({ value, onChange, placeholder, searchFn }: TagInputPro
|
||||
for (const r of results) labelMap.current.set(r.id, r.label);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={{ position: 'relative' }}>
|
||||
<div ref={containerRef} style={{ position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'var(--ctp-base)',
|
||||
border: '1px solid var(--ctp-surface1)',
|
||||
borderRadius: '0.3rem',
|
||||
cursor: 'text',
|
||||
minHeight: '1.8rem',
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
backgroundColor: "var(--ctp-base)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.375rem",
|
||||
cursor: "text",
|
||||
minHeight: "1.8rem",
|
||||
}}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
@@ -119,14 +134,14 @@ export function TagInput({ value, onChange, placeholder, searchFn }: TagInputPro
|
||||
<span
|
||||
key={id}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
padding: '0.1rem 0.5rem',
|
||||
borderRadius: '1rem',
|
||||
backgroundColor: 'rgba(203,166,247,0.15)',
|
||||
color: 'var(--ctp-mauve)',
|
||||
fontSize: '0.75rem',
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
padding: "0.15rem 0.5rem",
|
||||
borderRadius: "1rem",
|
||||
backgroundColor: "rgba(203,166,247,0.15)",
|
||||
color: "var(--ctp-mauve)",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{labelMap.current.get(id) ?? id}
|
||||
@@ -137,16 +152,16 @@ export function TagInput({ value, onChange, placeholder, searchFn }: TagInputPro
|
||||
remove(id);
|
||||
}}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--ctp-mauve)',
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "var(--ctp-mauve)",
|
||||
padding: 0,
|
||||
fontSize: '0.8rem',
|
||||
lineHeight: 1,
|
||||
display: "inline-flex",
|
||||
}}
|
||||
>
|
||||
×
|
||||
<X size={14} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
@@ -166,30 +181,30 @@ export function TagInput({ value, onChange, placeholder, searchFn }: TagInputPro
|
||||
placeholder={value.length === 0 ? placeholder : undefined}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: '4rem',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
background: 'transparent',
|
||||
color: 'var(--ctp-text)',
|
||||
fontSize: '0.85rem',
|
||||
padding: '0.1rem 0',
|
||||
minWidth: "4rem",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
background: "transparent",
|
||||
color: "var(--ctp-text)",
|
||||
fontSize: "var(--font-body)",
|
||||
padding: "0.15rem 0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{open && results.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
marginTop: '0.2rem',
|
||||
backgroundColor: 'var(--ctp-surface0)',
|
||||
border: '1px solid var(--ctp-surface1)',
|
||||
borderRadius: '0.3rem',
|
||||
maxHeight: '160px',
|
||||
overflowY: 'auto',
|
||||
marginTop: "0.25rem",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.375rem",
|
||||
maxHeight: "160px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{results.map((opt, i) => (
|
||||
@@ -201,15 +216,15 @@ export function TagInput({ value, onChange, placeholder, searchFn }: TagInputPro
|
||||
}}
|
||||
onMouseEnter={() => setHighlighted(i)}
|
||||
style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
height: '28px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.8rem',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--ctp-text)',
|
||||
padding: "0.25rem 0.5rem",
|
||||
height: "28px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontSize: "var(--font-table)",
|
||||
cursor: "pointer",
|
||||
color: "var(--ctp-text)",
|
||||
backgroundColor:
|
||||
i === highlighted ? 'var(--ctp-surface1)' : 'transparent',
|
||||
i === highlighted ? "var(--ctp-surface1)" : "transparent",
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
|
||||
@@ -210,7 +210,7 @@ export function AuditDetailPanel({
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
color: "var(--ctp-peach)",
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
{audit.part_number}
|
||||
@@ -263,7 +263,7 @@ export function AuditDetailPanel({
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
color: "var(--ctp-red)",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
@@ -274,7 +274,7 @@ export function AuditDetailPanel({
|
||||
<div
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
color: "var(--ctp-subtext1)",
|
||||
borderBottom: "1px solid var(--ctp-surface0)",
|
||||
flexShrink: 0,
|
||||
@@ -361,8 +361,8 @@ function FieldGroup({
|
||||
<div style={{ marginBottom: "0.75rem" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "0.3rem 1rem",
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.25rem 1rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
@@ -424,7 +424,7 @@ function FieldRow({
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "0.3rem 1rem",
|
||||
padding: "0.25rem 1rem",
|
||||
borderLeft: `3px solid ${borderColor}`,
|
||||
marginLeft: "0.5rem",
|
||||
gap: "0.5rem",
|
||||
@@ -434,7 +434,7 @@ function FieldRow({
|
||||
style={{
|
||||
width: 140,
|
||||
flexShrink: 0,
|
||||
fontSize: "0.78rem",
|
||||
fontSize: "var(--font-table)",
|
||||
color: "var(--ctp-subtext1)",
|
||||
}}
|
||||
title={`Weight: ${field.weight}`}
|
||||
@@ -445,7 +445,7 @@ function FieldRow({
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
color: "var(--ctp-red)",
|
||||
fontSize: "0.65rem",
|
||||
fontSize: "var(--font-xs)",
|
||||
}}
|
||||
>
|
||||
*
|
||||
@@ -456,7 +456,7 @@ function FieldRow({
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
color: field.filled ? "var(--ctp-text)" : "var(--ctp-subtext0)",
|
||||
fontStyle: field.filled ? "normal" : "italic",
|
||||
}}
|
||||
@@ -477,10 +477,10 @@ function FieldRow({
|
||||
placeholder="---"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "0.2rem 0.4rem",
|
||||
fontSize: "0.8rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
color: "var(--ctp-text)",
|
||||
outline: "none",
|
||||
@@ -492,10 +492,10 @@ function FieldRow({
|
||||
}
|
||||
|
||||
const closeBtnStyle: React.CSSProperties = {
|
||||
padding: "0.2rem 0.5rem",
|
||||
fontSize: "0.8rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "none",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "var(--ctp-surface1)",
|
||||
color: "var(--ctp-subtext1)",
|
||||
cursor: "pointer",
|
||||
|
||||
@@ -51,7 +51,7 @@ export function AuditSummaryBar({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
fontWeight: 600,
|
||||
color: "var(--ctp-crust)",
|
||||
transition: "all 0.15s ease",
|
||||
@@ -71,7 +71,7 @@ export function AuditSummaryBar({
|
||||
display: "flex",
|
||||
gap: "1.5rem",
|
||||
marginTop: "0.4rem",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
color: "var(--ctp-subtext0)",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function AuditTable({
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Plus, Download } from "lucide-react";
|
||||
import { get, post, put, del } from "../../api/client";
|
||||
import type { BOMEntry } from "../../api/types";
|
||||
|
||||
@@ -117,11 +118,11 @@ export function BOMTab({ partNumber, isEditor }: BOMTabProps) {
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
padding: "0.2rem 0.4rem",
|
||||
fontSize: "0.8rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
fontSize: "var(--font-table)",
|
||||
backgroundColor: "var(--ctp-base)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
color: "var(--ctp-text)",
|
||||
width: "100%",
|
||||
};
|
||||
@@ -225,7 +226,9 @@ export function BOMTab({ partNumber, isEditor }: BOMTabProps) {
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: "0.85rem", color: "var(--ctp-subtext1)" }}>
|
||||
<span
|
||||
style={{ fontSize: "var(--font-body)", color: "var(--ctp-subtext1)" }}
|
||||
>
|
||||
{entries.length} entries
|
||||
</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
@@ -233,9 +236,14 @@ export function BOMTab({ partNumber, isEditor }: BOMTabProps) {
|
||||
onClick={() => {
|
||||
window.location.href = `/api/items/${encodeURIComponent(partNumber)}/bom/export.csv`;
|
||||
}}
|
||||
style={toolBtnStyle}
|
||||
style={{
|
||||
...toolBtnStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
>
|
||||
Export CSV
|
||||
<Download size={14} /> Export CSV
|
||||
</button>
|
||||
{isEditor && (
|
||||
<button
|
||||
@@ -244,9 +252,14 @@ export function BOMTab({ partNumber, isEditor }: BOMTabProps) {
|
||||
setEditIdx(null);
|
||||
setForm(emptyForm);
|
||||
}}
|
||||
style={toolBtnStyle}
|
||||
style={{
|
||||
...toolBtnStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
>
|
||||
+ Add
|
||||
<Plus size={14} /> Add
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -254,9 +267,9 @@ export function BOMTab({ partNumber, isEditor }: BOMTabProps) {
|
||||
{isEditor && assemblyCount > 0 && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0.35rem 0.6rem",
|
||||
padding: "0.35rem 0.5rem",
|
||||
marginBottom: "0.5rem",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "rgba(148,226,213,0.1)",
|
||||
border: "1px solid rgba(148,226,213,0.3)",
|
||||
fontSize: "0.75rem",
|
||||
@@ -274,7 +287,7 @@ export function BOMTab({ partNumber, isEditor }: BOMTabProps) {
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
@@ -403,12 +416,12 @@ export function BOMTab({ partNumber, isEditor }: BOMTabProps) {
|
||||
}
|
||||
|
||||
const thStyle: React.CSSProperties = {
|
||||
padding: "0.3rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
textAlign: "left",
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
color: "var(--ctp-overlay1)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
whiteSpace: "nowrap",
|
||||
@@ -438,12 +451,12 @@ const actionBtnStyle: React.CSSProperties = {
|
||||
cursor: "pointer",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "0.1rem 0.3rem",
|
||||
padding: "0.15rem 0.25rem",
|
||||
borderRadius: "0.375rem",
|
||||
};
|
||||
|
||||
const saveBtnStyle: React.CSSProperties = {
|
||||
padding: "0.2rem 0.4rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
border: "none",
|
||||
@@ -455,9 +468,9 @@ const saveBtnStyle: React.CSSProperties = {
|
||||
};
|
||||
|
||||
const sourceBadgeBase: React.CSSProperties = {
|
||||
padding: "0.1rem 0.4rem",
|
||||
padding: "0.15rem 0.4rem",
|
||||
borderRadius: "1rem",
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
fontWeight: 500,
|
||||
};
|
||||
|
||||
@@ -474,7 +487,7 @@ const manualBadge: React.CSSProperties = {
|
||||
};
|
||||
|
||||
const cancelBtnStyle: React.CSSProperties = {
|
||||
padding: "0.2rem 0.4rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
border: "none",
|
||||
|
||||
@@ -95,7 +95,7 @@ export function CategoryPicker({
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: "0.2rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
border: "none",
|
||||
@@ -134,7 +134,7 @@ export function CategoryPicker({
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.4rem 0.5rem",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "none",
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
backgroundColor: "var(--ctp-mantle)",
|
||||
@@ -152,7 +152,7 @@ export function CategoryPicker({
|
||||
padding: "0.75rem",
|
||||
textAlign: "center",
|
||||
color: "var(--ctp-subtext0)",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
Select a domain to see categories
|
||||
@@ -163,7 +163,7 @@ export function CategoryPicker({
|
||||
padding: "0.75rem",
|
||||
textAlign: "center",
|
||||
color: "var(--ctp-subtext0)",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
No categories found
|
||||
@@ -180,9 +180,9 @@ export function CategoryPicker({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
padding: "0.3rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
backgroundColor: isSelected
|
||||
? "rgba(203,166,247,0.12)"
|
||||
: "transparent",
|
||||
@@ -228,7 +228,7 @@ export function CategoryPicker({
|
||||
{value && categories[value] && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0.3rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "0.75rem",
|
||||
color: "var(--ctp-subtext0)",
|
||||
borderTop: "1px solid var(--ctp-surface0)",
|
||||
|
||||
@@ -263,7 +263,7 @@ export function CreateItemPane({ onCreated, onCancel }: CreateItemPaneProps) {
|
||||
style={{
|
||||
color: "var(--ctp-green)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
New Item
|
||||
@@ -400,13 +400,19 @@ export function CreateItemPane({ onCreated, onCancel }: CreateItemPaneProps) {
|
||||
/>
|
||||
) : thumbnailFile?.uploadStatus === "uploading" ? (
|
||||
<span
|
||||
style={{ fontSize: "0.8rem", color: "var(--ctp-subtext0)" }}
|
||||
style={{
|
||||
fontSize: "var(--font-table)",
|
||||
color: "var(--ctp-subtext0)",
|
||||
}}
|
||||
>
|
||||
Uploading... {thumbnailFile.uploadProgress}%
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
style={{ fontSize: "0.8rem", color: "var(--ctp-subtext0)" }}
|
||||
style={{
|
||||
fontSize: "var(--font-table)",
|
||||
color: "var(--ctp-subtext0)",
|
||||
}}
|
||||
>
|
||||
Click to upload
|
||||
</span>
|
||||
@@ -571,7 +577,7 @@ function SectionHeader({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
@@ -608,7 +614,7 @@ function SidebarSection({
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
@@ -629,7 +635,7 @@ function MetaRow({ label, value }: { label: string; value: string }) {
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
padding: "0.15rem 0",
|
||||
}}
|
||||
>
|
||||
@@ -647,13 +653,13 @@ function FormGroup({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginBottom: "0.6rem" }}>
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
<label
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: "0.75rem",
|
||||
color: "var(--ctp-subtext0)",
|
||||
marginBottom: "0.2rem",
|
||||
marginBottom: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
@@ -676,7 +682,7 @@ const headerStyle: React.CSSProperties = {
|
||||
};
|
||||
|
||||
const actionBtnStyle: React.CSSProperties = {
|
||||
padding: "0.3rem 0.75rem",
|
||||
padding: "0.25rem 0.75rem",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
border: "none",
|
||||
@@ -692,17 +698,17 @@ const cancelBtnStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "0.2rem 0.4rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
borderRadius: "0.375rem",
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
padding: "0.35rem 0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
backgroundColor: "var(--ctp-base)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
color: "var(--ctp-text)",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
@@ -717,7 +723,7 @@ const errorStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-red)",
|
||||
backgroundColor: "rgba(243,139,168,0.1)",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
marginBottom: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export function DeleteItemPane({
|
||||
style={{
|
||||
color: "var(--ctp-red)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
Delete Item
|
||||
@@ -73,8 +73,8 @@ export function DeleteItemPane({
|
||||
color: "var(--ctp-red)",
|
||||
backgroundColor: "rgba(243,139,168,0.1)",
|
||||
padding: "0.5rem 1rem",
|
||||
borderRadius: "0.3rem",
|
||||
fontSize: "0.85rem",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "var(--font-body)",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
@@ -86,7 +86,7 @@ export function DeleteItemPane({
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
color: "var(--ctp-text)",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
@@ -97,7 +97,7 @@ export function DeleteItemPane({
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
color: "var(--ctp-peach)",
|
||||
fontSize: "1.1rem",
|
||||
fontSize: "var(--font-title)",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
@@ -108,7 +108,7 @@ export function DeleteItemPane({
|
||||
<p
|
||||
style={{
|
||||
color: "var(--ctp-subtext0)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
textAlign: "center",
|
||||
maxWidth: 300,
|
||||
}}
|
||||
@@ -163,6 +163,6 @@ const headerBtnStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "0.2rem 0.4rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
borderRadius: "0.375rem",
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export function EditItemPane({
|
||||
style={{
|
||||
color: "var(--ctp-blue)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
Edit {partNumber}
|
||||
@@ -89,7 +89,7 @@ export function EditItemPane({
|
||||
onClick={() => void handleSave()}
|
||||
disabled={saving}
|
||||
style={{
|
||||
padding: "0.3rem 0.75rem",
|
||||
padding: "0.25rem 0.75rem",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
border: "none",
|
||||
@@ -114,9 +114,9 @@ export function EditItemPane({
|
||||
color: "var(--ctp-red)",
|
||||
backgroundColor: "rgba(243,139,168,0.1)",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
marginBottom: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
@@ -190,13 +190,13 @@ function FormGroup({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginBottom: "0.6rem" }}>
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
<label
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: "0.75rem",
|
||||
color: "var(--ctp-subtext0)",
|
||||
marginBottom: "0.2rem",
|
||||
marginBottom: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
@@ -209,10 +209,10 @@ function FormGroup({
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
padding: "0.35rem 0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
backgroundColor: "var(--ctp-base)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
color: "var(--ctp-text)",
|
||||
};
|
||||
|
||||
@@ -223,6 +223,6 @@ const headerBtnStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "0.2rem 0.4rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
borderRadius: "0.375rem",
|
||||
};
|
||||
|
||||
@@ -76,7 +76,9 @@ export function FileDropZone({
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: "0.85rem", color: "var(--ctp-subtext1)" }}>
|
||||
<div
|
||||
style={{ fontSize: "var(--font-body)", color: "var(--ctp-subtext1)" }}
|
||||
>
|
||||
Drop files here or{" "}
|
||||
<span style={{ color: "var(--ctp-mauve)", fontWeight: 600 }}>
|
||||
browse
|
||||
@@ -85,7 +87,7 @@ export function FileDropZone({
|
||||
{accept && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
color: "var(--ctp-overlay0)",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
@@ -141,8 +143,8 @@ function FileRow({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
padding: "0.3rem 0.4rem",
|
||||
borderRadius: "0.3rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
borderRadius: "0.375rem",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
@@ -151,13 +153,13 @@ function FileRow({
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: color,
|
||||
opacity: 0.8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "0.6rem",
|
||||
fontSize: "var(--font-xs)",
|
||||
fontWeight: 700,
|
||||
color: "var(--ctp-crust)",
|
||||
flexShrink: 0,
|
||||
@@ -170,7 +172,7 @@ function FileRow({
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
color: "var(--ctp-text)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
@@ -179,7 +181,9 @@ function FileRow({
|
||||
>
|
||||
{attachment.file.name}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.7rem", color: "var(--ctp-overlay0)" }}>
|
||||
<div
|
||||
style={{ fontSize: "var(--font-sm)", color: "var(--ctp-overlay0)" }}
|
||||
>
|
||||
{formatSize(attachment.file.size)}
|
||||
{attachment.uploadStatus === "error" && (
|
||||
<span style={{ color: "var(--ctp-red)", marginLeft: "0.5rem" }}>
|
||||
@@ -215,7 +219,7 @@ function FileRow({
|
||||
{attachment.uploadStatus === "complete" ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
color: "var(--ctp-green)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
@@ -233,7 +237,7 @@ function FileRow({
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
color: hovered ? "var(--ctp-red)" : "var(--ctp-overlay0)",
|
||||
padding: "0 0.2rem",
|
||||
flexShrink: 0,
|
||||
|
||||
@@ -72,7 +72,7 @@ export function ImportItemsPane({
|
||||
style={{
|
||||
color: "var(--ctp-yellow)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
Import Items (CSV)
|
||||
@@ -90,9 +90,9 @@ export function ImportItemsPane({
|
||||
color: "var(--ctp-red)",
|
||||
backgroundColor: "rgba(243,139,168,0.1)",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
marginBottom: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
@@ -102,7 +102,7 @@ export function ImportItemsPane({
|
||||
{/* Instructions */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
color: "var(--ctp-subtext0)",
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
@@ -120,7 +120,10 @@ export function ImportItemsPane({
|
||||
</p>
|
||||
<a
|
||||
href="/api/items/template.csv"
|
||||
style={{ color: "var(--ctp-sapphire)", fontSize: "0.8rem" }}
|
||||
style={{
|
||||
color: "var(--ctp-sapphire)",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
Download CSV template
|
||||
</a>
|
||||
@@ -149,7 +152,7 @@ export function ImportItemsPane({
|
||||
color: "var(--ctp-subtext1)",
|
||||
cursor: "pointer",
|
||||
width: "100%",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
{file ? file.name : "Choose CSV file..."}
|
||||
@@ -162,7 +165,7 @@ export function ImportItemsPane({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.4rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
color: "var(--ctp-subtext1)",
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
@@ -225,7 +228,7 @@ export function ImportItemsPane({
|
||||
padding: "0.5rem",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
borderRadius: "0.4rem",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
@@ -259,7 +262,7 @@ export function ImportItemsPane({
|
||||
style={{
|
||||
color: "var(--ctp-red)",
|
||||
fontSize: "0.75rem",
|
||||
padding: "0.1rem 0",
|
||||
padding: "0.15rem 0",
|
||||
}}
|
||||
>
|
||||
Row {err.row}
|
||||
@@ -293,6 +296,6 @@ const headerBtnStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "0.2rem 0.4rem",
|
||||
padding: "0.25rem 0.4rem",
|
||||
borderRadius: "0.375rem",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { get } from "../../api/client";
|
||||
import type { Item } from "../../api/types";
|
||||
import { MainTab } from "./MainTab";
|
||||
@@ -95,16 +96,16 @@ export function ItemDetail({
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
color: "var(--ctp-peach)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
{item.part_number}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
padding: "0.1rem 0.5rem",
|
||||
padding: "0.15rem 0.5rem",
|
||||
borderRadius: "1rem",
|
||||
fontSize: "0.7rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
fontWeight: 500,
|
||||
backgroundColor: tc.bg,
|
||||
color: tc.color,
|
||||
@@ -131,9 +132,13 @@ export function ItemDetail({
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{ ...headerBtnStyle, fontSize: "1rem" }}
|
||||
style={{
|
||||
...headerBtnStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
×
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -153,7 +158,7 @@ export function ItemDetail({
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
style={{
|
||||
padding: "0.4rem 0.75rem",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "none",
|
||||
borderBottom:
|
||||
activeTab === tab.key
|
||||
@@ -199,6 +204,6 @@ const headerBtnStyle: React.CSSProperties = {
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.8rem",
|
||||
padding: "0.2rem 0.4rem",
|
||||
fontSize: "var(--font-table)",
|
||||
padding: "0.25rem 0.4rem",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { ChevronUp, ChevronDown } from "lucide-react";
|
||||
import type { Item } from "../../api/types";
|
||||
import { ContextMenu, type ContextMenuItem } from "../ContextMenu";
|
||||
|
||||
@@ -191,8 +192,18 @@ export function ItemTable({
|
||||
>
|
||||
{col.label}
|
||||
{sortKey === col.key && (
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
{sortDir === "asc" ? "▲" : "▼"}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
display: "inline-flex",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
>
|
||||
{sortDir === "asc" ? (
|
||||
<ChevronUp size={14} />
|
||||
) : (
|
||||
<ChevronDown size={14} />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
@@ -257,7 +268,7 @@ export function ItemTable({
|
||||
<td key={col.key} style={tdStyle}>
|
||||
<span
|
||||
style={{
|
||||
padding: "0.1rem 0.5rem",
|
||||
padding: "0.15rem 0.5rem",
|
||||
borderRadius: "1rem",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Columns2, Rows2, Plus, Download, Upload } from "lucide-react";
|
||||
import { get } from "../../api/client";
|
||||
import type { Project } from "../../api/types";
|
||||
import type { ItemFilters } from "../../hooks/useItems";
|
||||
@@ -127,20 +128,42 @@ export function ItemsToolbar({
|
||||
onLayoutChange(layout === "horizontal" ? "vertical" : "horizontal")
|
||||
}
|
||||
title={`Switch to ${layout === "horizontal" ? "vertical" : "horizontal"} layout`}
|
||||
style={toolBtnStyle}
|
||||
style={{
|
||||
...toolBtnStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{layout === "horizontal" ? "⬌" : "⬍"}
|
||||
{layout === "horizontal" ? <Columns2 size={14} /> : <Rows2 size={14} />}
|
||||
</button>
|
||||
|
||||
{/* Export */}
|
||||
<button onClick={onExport} style={toolBtnStyle} title="Export CSV">
|
||||
Export
|
||||
<button
|
||||
onClick={onExport}
|
||||
style={{
|
||||
...toolBtnStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
title="Export CSV"
|
||||
>
|
||||
<Download size={14} /> Export
|
||||
</button>
|
||||
|
||||
{/* Import (editor only) */}
|
||||
{isEditor && (
|
||||
<button onClick={onImport} style={toolBtnStyle} title="Import CSV">
|
||||
Import
|
||||
<button
|
||||
onClick={onImport}
|
||||
style={{
|
||||
...toolBtnStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
title="Import CSV"
|
||||
>
|
||||
<Upload size={14} /> Import
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -152,9 +175,12 @@ export function ItemsToolbar({
|
||||
...toolBtnStyle,
|
||||
backgroundColor: "var(--ctp-mauve)",
|
||||
color: "var(--ctp-crust)",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
>
|
||||
+ New
|
||||
<Plus size={14} /> New
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { get, post, del } from "../../api/client";
|
||||
import type { Item, Project, Revision } from "../../api/types";
|
||||
|
||||
@@ -83,8 +84,8 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1rem",
|
||||
padding: "0.3rem 0",
|
||||
fontSize: "0.85rem",
|
||||
padding: "0.25rem 0",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 120, flexShrink: 0, color: "var(--ctp-subtext0)" }}>
|
||||
@@ -134,7 +135,7 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
padding: "0.5rem",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
borderRadius: "0.4rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -176,7 +177,7 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
padding: "0.1rem 0.5rem",
|
||||
padding: "0.15rem 0.5rem",
|
||||
borderRadius: "1rem",
|
||||
backgroundColor: "rgba(203,166,247,0.15)",
|
||||
color: "var(--ctp-mauve)",
|
||||
@@ -192,11 +193,11 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
border: "none",
|
||||
color: "var(--ctp-overlay0)",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.8rem",
|
||||
padding: 0,
|
||||
display: "inline-flex",
|
||||
}}
|
||||
>
|
||||
×
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
@@ -207,11 +208,11 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
value={addProject}
|
||||
onChange={(e) => setAddProject(e.target.value)}
|
||||
style={{
|
||||
padding: "0.1rem 0.3rem",
|
||||
padding: "0.15rem 0.25rem",
|
||||
fontSize: "0.75rem",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
color: "var(--ctp-text)",
|
||||
}}
|
||||
>
|
||||
@@ -228,12 +229,12 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
<button
|
||||
onClick={() => void handleAddProject()}
|
||||
style={{
|
||||
padding: "0.1rem 0.4rem",
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.15rem 0.4rem",
|
||||
fontSize: "var(--font-sm)",
|
||||
border: "none",
|
||||
backgroundColor: "var(--ctp-mauve)",
|
||||
color: "var(--ctp-crust)",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
@@ -269,7 +270,7 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
{latestRev.file_size != null && (
|
||||
@@ -292,12 +293,12 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
|
||||
window.location.href = `/api/items/${encodeURIComponent(item.part_number)}/file/${latestRev.revision_number}`;
|
||||
}}
|
||||
style={{
|
||||
padding: "0.2rem 0.5rem",
|
||||
fontSize: "0.8rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "none",
|
||||
backgroundColor: "var(--ctp-surface1)",
|
||||
color: "var(--ctp-text)",
|
||||
borderRadius: "0.3rem",
|
||||
borderRadius: "0.375rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { post } from '../../api/client';
|
||||
import type { Item } from '../../api/types';
|
||||
import { useState } from "react";
|
||||
import { X, Plus } from "lucide-react";
|
||||
import { post } from "../../api/client";
|
||||
import type { Item } from "../../api/types";
|
||||
|
||||
interface PropertiesTabProps {
|
||||
item: Item;
|
||||
@@ -8,24 +9,24 @@ interface PropertiesTabProps {
|
||||
isEditor: boolean;
|
||||
}
|
||||
|
||||
type Mode = 'form' | 'json';
|
||||
type Mode = "form" | "json";
|
||||
|
||||
interface PropRow {
|
||||
key: string;
|
||||
value: string;
|
||||
type: 'string' | 'number' | 'boolean';
|
||||
type: "string" | "number" | "boolean";
|
||||
}
|
||||
|
||||
function detectType(v: unknown): PropRow['type'] {
|
||||
if (typeof v === 'number') return 'number';
|
||||
if (typeof v === 'boolean') return 'boolean';
|
||||
return 'string';
|
||||
function detectType(v: unknown): PropRow["type"] {
|
||||
if (typeof v === "number") return "number";
|
||||
if (typeof v === "boolean") return "boolean";
|
||||
return "string";
|
||||
}
|
||||
|
||||
function toRows(props: Record<string, unknown>): PropRow[] {
|
||||
return Object.entries(props).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value ?? ''),
|
||||
value: String(value ?? ""),
|
||||
type: detectType(value),
|
||||
}));
|
||||
}
|
||||
@@ -35,17 +36,26 @@ function fromRows(rows: PropRow[]): Record<string, unknown> {
|
||||
for (const row of rows) {
|
||||
if (!row.key.trim()) continue;
|
||||
switch (row.type) {
|
||||
case 'number': obj[row.key] = Number(row.value) || 0; break;
|
||||
case 'boolean': obj[row.key] = row.value === 'true'; break;
|
||||
default: obj[row.key] = row.value;
|
||||
case "number":
|
||||
obj[row.key] = Number(row.value) || 0;
|
||||
break;
|
||||
case "boolean":
|
||||
obj[row.key] = row.value === "true";
|
||||
break;
|
||||
default:
|
||||
obj[row.key] = row.value;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function PropertiesTab({ item, onReload, isEditor }: PropertiesTabProps) {
|
||||
export function PropertiesTab({
|
||||
item,
|
||||
onReload,
|
||||
isEditor,
|
||||
}: PropertiesTabProps) {
|
||||
const props = item.properties ?? {};
|
||||
const [mode, setMode] = useState<Mode>('form');
|
||||
const [mode, setMode] = useState<Mode>("form");
|
||||
const [rows, setRows] = useState<PropRow[]>(toRows(props));
|
||||
const [jsonText, setJsonText] = useState(JSON.stringify(props, null, 2));
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
@@ -62,18 +72,20 @@ export function PropertiesTab({ item, onReload, isEditor }: PropertiesTabProps)
|
||||
setRows(toRows(parsed));
|
||||
setJsonError(null);
|
||||
} catch (e) {
|
||||
setJsonError(e instanceof Error ? e.message : 'Invalid JSON');
|
||||
setJsonError(e instanceof Error ? e.message : "Invalid JSON");
|
||||
}
|
||||
};
|
||||
|
||||
const switchMode = (m: Mode) => {
|
||||
if (m === 'json') syncFormToJson();
|
||||
if (m === "json") syncFormToJson();
|
||||
else syncJsonToForm();
|
||||
setMode(m);
|
||||
};
|
||||
|
||||
const updateRow = (idx: number, field: keyof PropRow, value: string) => {
|
||||
setRows((prev) => prev.map((r, i) => i === idx ? { ...r, [field]: value } : r));
|
||||
setRows((prev) =>
|
||||
prev.map((r, i) => (i === idx ? { ...r, [field]: value } : r)),
|
||||
);
|
||||
};
|
||||
|
||||
const removeRow = (idx: number) => {
|
||||
@@ -81,72 +93,112 @@ export function PropertiesTab({ item, onReload, isEditor }: PropertiesTabProps)
|
||||
};
|
||||
|
||||
const addRow = () => {
|
||||
setRows((prev) => [...prev, { key: '', value: '', type: 'string' }]);
|
||||
setRows((prev) => [...prev, { key: "", value: "", type: "string" }]);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
let properties: Record<string, unknown>;
|
||||
if (mode === 'json') {
|
||||
if (mode === "json") {
|
||||
try {
|
||||
properties = JSON.parse(jsonText) as Record<string, unknown>;
|
||||
} catch {
|
||||
setJsonError('Invalid JSON');
|
||||
setJsonError("Invalid JSON");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
properties = fromRows(rows);
|
||||
}
|
||||
|
||||
const comment = prompt('Revision comment (optional):') ?? '';
|
||||
const comment = prompt("Revision comment (optional):") ?? "";
|
||||
setSaving(true);
|
||||
try {
|
||||
await post(`/api/items/${encodeURIComponent(item.part_number)}/revisions`, { properties, comment });
|
||||
await post(
|
||||
`/api/items/${encodeURIComponent(item.part_number)}/revisions`,
|
||||
{ properties, comment },
|
||||
);
|
||||
onReload();
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Failed to save properties');
|
||||
alert(e instanceof Error ? e.message : "Failed to save properties");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
padding: '0.25rem 0.4rem', fontSize: '0.8rem',
|
||||
backgroundColor: 'var(--ctp-base)', border: '1px solid var(--ctp-surface1)',
|
||||
borderRadius: '0.3rem', color: 'var(--ctp-text)',
|
||||
padding: "0.25rem 0.4rem",
|
||||
fontSize: "var(--font-table)",
|
||||
backgroundColor: "var(--ctp-base)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.375rem",
|
||||
color: "var(--ctp-text)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Mode toggle */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem', alignItems: 'center' }}>
|
||||
<button onClick={() => switchMode('form')} style={mode === 'form' ? activeTabBtn : tabBtn}>Form</button>
|
||||
<button onClick={() => switchMode('json')} style={mode === 'json' ? activeTabBtn : tabBtn}>JSON</button>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
marginBottom: "0.5rem",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => switchMode("form")}
|
||||
style={mode === "form" ? activeTabBtn : tabBtn}
|
||||
>
|
||||
Form
|
||||
</button>
|
||||
<button
|
||||
onClick={() => switchMode("json")}
|
||||
style={mode === "json" ? activeTabBtn : tabBtn}
|
||||
>
|
||||
JSON
|
||||
</button>
|
||||
<span style={{ flex: 1 }} />
|
||||
{isEditor && (
|
||||
<button onClick={() => void handleSave()} disabled={saving} style={{
|
||||
padding: '0.3rem 0.75rem', fontSize: '0.8rem', border: 'none', borderRadius: '0.3rem',
|
||||
backgroundColor: 'var(--ctp-mauve)', color: 'var(--ctp-crust)', cursor: 'pointer',
|
||||
opacity: saving ? 0.6 : 1,
|
||||
}}>
|
||||
{saving ? 'Saving...' : 'Save (New Revision)'}
|
||||
<button
|
||||
onClick={() => void handleSave()}
|
||||
disabled={saving}
|
||||
style={{
|
||||
padding: "0.25rem 0.75rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "none",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "var(--ctp-mauve)",
|
||||
color: "var(--ctp-crust)",
|
||||
cursor: "pointer",
|
||||
opacity: saving ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{saving ? "Saving..." : "Save (New Revision)"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mode === 'form' ? (
|
||||
{mode === "form" ? (
|
||||
<div>
|
||||
{rows.map((row, idx) => (
|
||||
<div key={idx} style={{ display: 'flex', gap: '0.3rem', marginBottom: '0.25rem', alignItems: 'center' }}>
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.25rem",
|
||||
marginBottom: "0.25rem",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
value={row.key}
|
||||
onChange={(e) => updateRow(idx, 'key', e.target.value)}
|
||||
onChange={(e) => updateRow(idx, "key", e.target.value)}
|
||||
placeholder="Key"
|
||||
style={{ ...inputStyle, width: 140 }}
|
||||
disabled={!isEditor}
|
||||
/>
|
||||
<select
|
||||
value={row.type}
|
||||
onChange={(e) => updateRow(idx, 'type', e.target.value)}
|
||||
onChange={(e) => updateRow(idx, "type", e.target.value)}
|
||||
style={{ ...inputStyle, width: 80 }}
|
||||
disabled={!isEditor}
|
||||
>
|
||||
@@ -154,44 +206,90 @@ export function PropertiesTab({ item, onReload, isEditor }: PropertiesTabProps)
|
||||
<option value="number">num</option>
|
||||
<option value="boolean">bool</option>
|
||||
</select>
|
||||
{row.type === 'boolean' ? (
|
||||
<select value={row.value} onChange={(e) => updateRow(idx, 'value', e.target.value)} style={{ ...inputStyle, flex: 1 }} disabled={!isEditor}>
|
||||
{row.type === "boolean" ? (
|
||||
<select
|
||||
value={row.value}
|
||||
onChange={(e) => updateRow(idx, "value", e.target.value)}
|
||||
style={{ ...inputStyle, flex: 1 }}
|
||||
disabled={!isEditor}
|
||||
>
|
||||
<option value="true">true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type={row.type === 'number' ? 'number' : 'text'}
|
||||
type={row.type === "number" ? "number" : "text"}
|
||||
value={row.value}
|
||||
onChange={(e) => updateRow(idx, 'value', e.target.value)}
|
||||
onChange={(e) => updateRow(idx, "value", e.target.value)}
|
||||
placeholder="Value"
|
||||
style={{ ...inputStyle, flex: 1 }}
|
||||
disabled={!isEditor}
|
||||
/>
|
||||
)}
|
||||
{isEditor && (
|
||||
<button onClick={() => removeRow(idx)} style={{ background: 'none', border: 'none', color: 'var(--ctp-red)', cursor: 'pointer', fontSize: '0.9rem' }}>×</button>
|
||||
<button
|
||||
onClick={() => removeRow(idx)}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--ctp-red)",
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isEditor && (
|
||||
<button onClick={addRow} style={{ ...tabBtn, marginTop: '0.25rem' }}>+ Add Property</button>
|
||||
<button
|
||||
onClick={addRow}
|
||||
style={{
|
||||
...tabBtn,
|
||||
marginTop: "0.25rem",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
>
|
||||
<Plus size={14} /> Add Property
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<textarea
|
||||
value={jsonText}
|
||||
onChange={(e) => { setJsonText(e.target.value); setJsonError(null); }}
|
||||
onChange={(e) => {
|
||||
setJsonText(e.target.value);
|
||||
setJsonError(null);
|
||||
}}
|
||||
disabled={!isEditor}
|
||||
style={{
|
||||
width: '100%', minHeight: 200, padding: '0.5rem',
|
||||
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.8rem',
|
||||
backgroundColor: 'var(--ctp-base)', border: '1px solid var(--ctp-surface1)',
|
||||
borderRadius: '0.4rem', color: 'var(--ctp-text)', resize: 'vertical',
|
||||
width: "100%",
|
||||
minHeight: 200,
|
||||
padding: "0.5rem",
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: "var(--font-table)",
|
||||
backgroundColor: "var(--ctp-base)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.4rem",
|
||||
color: "var(--ctp-text)",
|
||||
resize: "vertical",
|
||||
}}
|
||||
/>
|
||||
{jsonError && <div style={{ color: 'var(--ctp-red)', fontSize: '0.8rem', marginTop: '0.25rem' }}>{jsonError}</div>}
|
||||
{jsonError && (
|
||||
<div
|
||||
style={{
|
||||
color: "var(--ctp-red)",
|
||||
fontSize: "var(--font-table)",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{jsonError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -199,11 +297,17 @@ export function PropertiesTab({ item, onReload, isEditor }: PropertiesTabProps)
|
||||
}
|
||||
|
||||
const tabBtn: React.CSSProperties = {
|
||||
padding: '0.25rem 0.5rem', fontSize: '0.8rem', border: 'none', borderRadius: '0.3rem',
|
||||
backgroundColor: 'var(--ctp-surface0)', color: 'var(--ctp-subtext1)', cursor: 'pointer',
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "none",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
color: "var(--ctp-subtext1)",
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const activeTabBtn: React.CSSProperties = {
|
||||
...tabBtn,
|
||||
backgroundColor: 'var(--ctp-surface1)', color: 'var(--ctp-mauve)',
|
||||
backgroundColor: "var(--ctp-surface1)",
|
||||
color: "var(--ctp-mauve)",
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { get, post } from '../../api/client';
|
||||
import type { Revision, RevisionComparison } from '../../api/types';
|
||||
import { useState, useEffect } from "react";
|
||||
import { Download } from "lucide-react";
|
||||
import { get, post } from "../../api/client";
|
||||
import type { Revision, RevisionComparison } from "../../api/types";
|
||||
|
||||
interface RevisionsTabProps {
|
||||
partNumber: string;
|
||||
@@ -8,28 +9,35 @@ interface RevisionsTabProps {
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
draft: 'var(--ctp-overlay1)',
|
||||
review: 'var(--ctp-yellow)',
|
||||
released: 'var(--ctp-green)',
|
||||
obsolete: 'var(--ctp-red)',
|
||||
draft: "var(--ctp-overlay1)",
|
||||
review: "var(--ctp-yellow)",
|
||||
released: "var(--ctp-green)",
|
||||
obsolete: "var(--ctp-red)",
|
||||
};
|
||||
|
||||
function formatDate(s: string) {
|
||||
if (!s) return '';
|
||||
return new Date(s).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
if (!s) return "";
|
||||
return new Date(s).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export function RevisionsTab({ partNumber, isEditor }: RevisionsTabProps) {
|
||||
const [revisions, setRevisions] = useState<Revision[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [compareFrom, setCompareFrom] = useState('');
|
||||
const [compareTo, setCompareTo] = useState('');
|
||||
const [compareFrom, setCompareFrom] = useState("");
|
||||
const [compareTo, setCompareTo] = useState("");
|
||||
const [comparison, setComparison] = useState<RevisionComparison | null>(null);
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
get<Revision[]>(`/api/items/${encodeURIComponent(partNumber)}/revisions`)
|
||||
.then((r) => { setRevisions(r); setLoading(false); })
|
||||
.then((r) => {
|
||||
setRevisions(r);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
};
|
||||
|
||||
@@ -39,97 +47,177 @@ export function RevisionsTab({ partNumber, isEditor }: RevisionsTabProps) {
|
||||
if (!compareFrom || !compareTo) return;
|
||||
try {
|
||||
const result = await get<RevisionComparison>(
|
||||
`/api/items/${encodeURIComponent(partNumber)}/revisions/compare?from=${compareFrom}&to=${compareTo}`
|
||||
`/api/items/${encodeURIComponent(partNumber)}/revisions/compare?from=${compareFrom}&to=${compareTo}`,
|
||||
);
|
||||
setComparison(result);
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Compare failed');
|
||||
alert(e instanceof Error ? e.message : "Compare failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (rev: number, status: string) => {
|
||||
try {
|
||||
await fetch(`/api/items/${encodeURIComponent(partNumber)}/revisions/${rev}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
await fetch(
|
||||
`/api/items/${encodeURIComponent(partNumber)}/revisions/${rev}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ status }),
|
||||
},
|
||||
);
|
||||
load();
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Status update failed');
|
||||
alert(e instanceof Error ? e.message : "Status update failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRollback = async (rev: number) => {
|
||||
if (!confirm(`Rollback to revision ${rev}? This creates a new revision with data from rev ${rev}.`)) return;
|
||||
const comment = prompt('Rollback comment:') ?? `Rollback to rev ${rev}`;
|
||||
if (
|
||||
!confirm(
|
||||
`Rollback to revision ${rev}? This creates a new revision with data from rev ${rev}.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
const comment = prompt("Rollback comment:") ?? `Rollback to rev ${rev}`;
|
||||
try {
|
||||
await post(`/api/items/${encodeURIComponent(partNumber)}/revisions/${rev}/rollback`, { comment });
|
||||
await post(
|
||||
`/api/items/${encodeURIComponent(partNumber)}/revisions/${rev}/rollback`,
|
||||
{ comment },
|
||||
);
|
||||
load();
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Rollback failed');
|
||||
alert(e instanceof Error ? e.message : "Rollback failed");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ color: 'var(--ctp-subtext0)' }}>Loading revisions...</div>;
|
||||
if (loading)
|
||||
return (
|
||||
<div style={{ color: "var(--ctp-subtext0)" }}>Loading revisions...</div>
|
||||
);
|
||||
|
||||
const selectStyle: React.CSSProperties = {
|
||||
padding: '0.25rem 0.4rem', fontSize: '0.8rem',
|
||||
backgroundColor: 'var(--ctp-surface0)', border: '1px solid var(--ctp-surface1)',
|
||||
borderRadius: '0.3rem', color: 'var(--ctp-text)',
|
||||
padding: "0.25rem 0.4rem",
|
||||
fontSize: "var(--font-table)",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.375rem",
|
||||
color: "var(--ctp-text)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Compare controls */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', marginBottom: '0.75rem' }}>
|
||||
<select value={compareFrom} onChange={(e) => setCompareFrom(e.target.value)} style={selectStyle}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
alignItems: "center",
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
>
|
||||
<select
|
||||
value={compareFrom}
|
||||
onChange={(e) => setCompareFrom(e.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">From rev...</option>
|
||||
{revisions.map((r) => <option key={r.id} value={r.revision_number}>Rev {r.revision_number}</option>)}
|
||||
{revisions.map((r) => (
|
||||
<option key={r.id} value={r.revision_number}>
|
||||
Rev {r.revision_number}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={compareTo} onChange={(e) => setCompareTo(e.target.value)} style={selectStyle}>
|
||||
<select
|
||||
value={compareTo}
|
||||
onChange={(e) => setCompareTo(e.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">To rev...</option>
|
||||
{revisions.map((r) => <option key={r.id} value={r.revision_number}>Rev {r.revision_number}</option>)}
|
||||
{revisions.map((r) => (
|
||||
<option key={r.id} value={r.revision_number}>
|
||||
Rev {r.revision_number}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={() => void handleCompare()} disabled={!compareFrom || !compareTo} style={{
|
||||
padding: '0.25rem 0.5rem', fontSize: '0.8rem', border: 'none', borderRadius: '0.3rem',
|
||||
backgroundColor: 'var(--ctp-mauve)', color: 'var(--ctp-crust)', cursor: 'pointer',
|
||||
opacity: (!compareFrom || !compareTo) ? 0.5 : 1,
|
||||
}}>
|
||||
<button
|
||||
onClick={() => void handleCompare()}
|
||||
disabled={!compareFrom || !compareTo}
|
||||
style={{
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "var(--font-table)",
|
||||
border: "none",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "var(--ctp-mauve)",
|
||||
color: "var(--ctp-crust)",
|
||||
cursor: "pointer",
|
||||
opacity: !compareFrom || !compareTo ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
Compare
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Compare results */}
|
||||
{comparison && (
|
||||
<div style={{
|
||||
padding: '0.5rem', backgroundColor: 'var(--ctp-surface0)', borderRadius: '0.4rem',
|
||||
fontSize: '0.8rem', marginBottom: '0.75rem', fontFamily: "'JetBrains Mono', monospace",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
padding: "0.5rem",
|
||||
backgroundColor: "var(--ctp-surface0)",
|
||||
borderRadius: "0.4rem",
|
||||
fontSize: "var(--font-table)",
|
||||
marginBottom: "0.75rem",
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
}}
|
||||
>
|
||||
{comparison.status_changed && (
|
||||
<div>Status: <span style={{ color: 'var(--ctp-red)' }}>{comparison.status_changed.from}</span> → <span style={{ color: 'var(--ctp-green)' }}>{comparison.status_changed.to}</span></div>
|
||||
<div>
|
||||
Status:{" "}
|
||||
<span style={{ color: "var(--ctp-red)" }}>
|
||||
{comparison.status_changed.from}
|
||||
</span>{" "}
|
||||
→{" "}
|
||||
<span style={{ color: "var(--ctp-green)" }}>
|
||||
{comparison.status_changed.to}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{comparison.file_changed && (
|
||||
<div style={{ color: "var(--ctp-yellow)" }}>File changed</div>
|
||||
)}
|
||||
{comparison.file_changed && <div style={{ color: 'var(--ctp-yellow)' }}>File changed</div>}
|
||||
{Object.entries(comparison.added).map(([k, v]) => (
|
||||
<div key={k} style={{ color: 'var(--ctp-green)' }}>+ {k}: {String(v)}</div>
|
||||
<div key={k} style={{ color: "var(--ctp-green)" }}>
|
||||
+ {k}: {String(v)}
|
||||
</div>
|
||||
))}
|
||||
{Object.entries(comparison.removed).map(([k, v]) => (
|
||||
<div key={k} style={{ color: 'var(--ctp-red)' }}>- {k}: {String(v)}</div>
|
||||
<div key={k} style={{ color: "var(--ctp-red)" }}>
|
||||
- {k}: {String(v)}
|
||||
</div>
|
||||
))}
|
||||
{Object.entries(comparison.changed).map(([k, c]) => (
|
||||
<div key={k} style={{ color: 'var(--ctp-yellow)' }}>~ {k}: {String(c.from)} → {String(c.to)}</div>
|
||||
<div key={k} style={{ color: "var(--ctp-yellow)" }}>
|
||||
~ {k}: {String(c.from)} → {String(c.to)}
|
||||
</div>
|
||||
))}
|
||||
{!comparison.status_changed && !comparison.file_changed &&
|
||||
Object.keys(comparison.added).length === 0 && Object.keys(comparison.removed).length === 0 &&
|
||||
Object.keys(comparison.changed).length === 0 && (
|
||||
<div style={{ color: 'var(--ctp-subtext0)' }}>No differences</div>
|
||||
)}
|
||||
{!comparison.status_changed &&
|
||||
!comparison.file_changed &&
|
||||
Object.keys(comparison.added).length === 0 &&
|
||||
Object.keys(comparison.removed).length === 0 &&
|
||||
Object.keys(comparison.changed).length === 0 && (
|
||||
<div style={{ color: "var(--ctp-subtext0)" }}>No differences</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Revisions table */}
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={thStyle}>Rev</th>
|
||||
@@ -143,17 +231,32 @@ export function RevisionsTab({ partNumber, isEditor }: RevisionsTabProps) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{revisions.map((rev, idx) => (
|
||||
<tr key={rev.id} style={{ backgroundColor: idx % 2 === 0 ? 'var(--ctp-base)' : 'var(--ctp-surface0)' }}>
|
||||
<tr
|
||||
key={rev.id}
|
||||
style={{
|
||||
backgroundColor:
|
||||
idx % 2 === 0 ? "var(--ctp-base)" : "var(--ctp-surface0)",
|
||||
}}
|
||||
>
|
||||
<td style={tdStyle}>{rev.revision_number}</td>
|
||||
<td style={tdStyle}>
|
||||
{isEditor ? (
|
||||
<select
|
||||
value={rev.status}
|
||||
onChange={(e) => void handleStatusChange(rev.revision_number, e.target.value)}
|
||||
onChange={(e) =>
|
||||
void handleStatusChange(
|
||||
rev.revision_number,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
style={{
|
||||
padding: '0.1rem 0.3rem', fontSize: '0.75rem', border: 'none', borderRadius: '0.3rem',
|
||||
backgroundColor: 'transparent', color: statusColors[rev.status] ?? 'var(--ctp-text)',
|
||||
cursor: 'pointer',
|
||||
padding: "0.15rem 0.25rem",
|
||||
fontSize: "0.75rem",
|
||||
border: "none",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "transparent",
|
||||
color: statusColors[rev.status] ?? "var(--ctp-text)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<option value="draft">draft</option>
|
||||
@@ -162,27 +265,58 @@ export function RevisionsTab({ partNumber, isEditor }: RevisionsTabProps) {
|
||||
<option value="obsolete">obsolete</option>
|
||||
</select>
|
||||
) : (
|
||||
<span style={{ color: statusColors[rev.status] ?? 'var(--ctp-text)' }}>{rev.status}</span>
|
||||
<span
|
||||
style={{
|
||||
color: statusColors[rev.status] ?? "var(--ctp-text)",
|
||||
}}
|
||||
>
|
||||
{rev.status}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={tdStyle}>{formatDate(rev.created_at)}</td>
|
||||
<td style={tdStyle}>{rev.created_by ?? '—'}</td>
|
||||
<td style={{ ...tdStyle, maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis' }}>{rev.comment ?? ''}</td>
|
||||
<td style={tdStyle}>{rev.created_by ?? "—"}</td>
|
||||
<td
|
||||
style={{
|
||||
...tdStyle,
|
||||
maxWidth: 150,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{rev.comment ?? ""}
|
||||
</td>
|
||||
<td style={tdStyle}>
|
||||
{rev.file_key ? (
|
||||
<button
|
||||
onClick={() => { window.location.href = `/api/items/${encodeURIComponent(partNumber)}/file/${rev.revision_number}`; }}
|
||||
style={{ background: 'none', border: 'none', color: 'var(--ctp-sapphire)', cursor: 'pointer', fontSize: '0.8rem' }}
|
||||
onClick={() => {
|
||||
window.location.href = `/api/items/${encodeURIComponent(partNumber)}/file/${rev.revision_number}`;
|
||||
}}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--ctp-sapphire)",
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
}}
|
||||
>
|
||||
↓
|
||||
<Download size={14} />
|
||||
</button>
|
||||
) : '—'}
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
{isEditor && (
|
||||
<td style={tdStyle}>
|
||||
<button
|
||||
onClick={() => void handleRollback(rev.revision_number)}
|
||||
style={{ background: 'none', border: 'none', color: 'var(--ctp-peach)', cursor: 'pointer', fontSize: '0.75rem' }}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--ctp-peach)",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
title="Rollback to this revision"
|
||||
>
|
||||
Rollback
|
||||
@@ -198,10 +332,18 @@ export function RevisionsTab({ partNumber, isEditor }: RevisionsTabProps) {
|
||||
}
|
||||
|
||||
const thStyle: React.CSSProperties = {
|
||||
padding: '0.3rem 0.5rem', textAlign: 'left', borderBottom: '1px solid var(--ctp-surface1)',
|
||||
color: 'var(--ctp-subtext1)', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
padding: "0.25rem 0.5rem",
|
||||
textAlign: "left",
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontWeight: 600,
|
||||
fontSize: "var(--font-sm)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
};
|
||||
|
||||
const tdStyle: React.CSSProperties = {
|
||||
padding: '0.25rem 0.5rem', borderBottom: '1px solid var(--ctp-surface0)', whiteSpace: 'nowrap',
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderBottom: "1px solid var(--ctp-surface0)",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { get } from '../../api/client';
|
||||
import type { WhereUsedEntry } from '../../api/types';
|
||||
import { useState, useEffect } from "react";
|
||||
import { get } from "../../api/client";
|
||||
import type { WhereUsedEntry } from "../../api/types";
|
||||
|
||||
interface WhereUsedTabProps {
|
||||
partNumber: string;
|
||||
@@ -12,20 +12,35 @@ export function WhereUsedTab({ partNumber }: WhereUsedTabProps) {
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
get<WhereUsedEntry[]>(`/api/items/${encodeURIComponent(partNumber)}/bom/where-used`)
|
||||
get<WhereUsedEntry[]>(
|
||||
`/api/items/${encodeURIComponent(partNumber)}/bom/where-used`,
|
||||
)
|
||||
.then(setEntries)
|
||||
.catch(() => setEntries([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, [partNumber]);
|
||||
|
||||
if (loading) return <div style={{ color: 'var(--ctp-subtext0)' }}>Loading where-used...</div>;
|
||||
if (loading)
|
||||
return (
|
||||
<div style={{ color: "var(--ctp-subtext0)" }}>Loading where-used...</div>
|
||||
);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return <div style={{ color: 'var(--ctp-subtext0)', padding: '1rem' }}>Not used in any assemblies.</div>;
|
||||
return (
|
||||
<div style={{ color: "var(--ctp-subtext0)", padding: "1rem" }}>
|
||||
Not used in any assemblies.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
fontSize: "var(--font-table)",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={thStyle}>Parent PN</th>
|
||||
@@ -36,13 +51,25 @@ export function WhereUsedTab({ partNumber }: WhereUsedTabProps) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((e, idx) => (
|
||||
<tr key={e.id} style={{ backgroundColor: idx % 2 === 0 ? 'var(--ctp-base)' : 'var(--ctp-surface0)' }}>
|
||||
<td style={{ ...tdStyle, fontFamily: "'JetBrains Mono', monospace", color: 'var(--ctp-peach)' }}>
|
||||
<tr
|
||||
key={e.id}
|
||||
style={{
|
||||
backgroundColor:
|
||||
idx % 2 === 0 ? "var(--ctp-base)" : "var(--ctp-surface0)",
|
||||
}}
|
||||
>
|
||||
<td
|
||||
style={{
|
||||
...tdStyle,
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
color: "var(--ctp-peach)",
|
||||
}}
|
||||
>
|
||||
{e.parent_part_number}
|
||||
</td>
|
||||
<td style={tdStyle}>{e.parent_description}</td>
|
||||
<td style={tdStyle}>{e.rel_type}</td>
|
||||
<td style={tdStyle}>{e.quantity ?? '—'}</td>
|
||||
<td style={tdStyle}>{e.quantity ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -51,10 +78,18 @@ export function WhereUsedTab({ partNumber }: WhereUsedTabProps) {
|
||||
}
|
||||
|
||||
const thStyle: React.CSSProperties = {
|
||||
padding: '0.3rem 0.5rem', textAlign: 'left', borderBottom: '1px solid var(--ctp-surface1)',
|
||||
color: 'var(--ctp-subtext1)', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
padding: "0.25rem 0.5rem",
|
||||
textAlign: "left",
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontWeight: 600,
|
||||
fontSize: "var(--font-sm)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
};
|
||||
|
||||
const tdStyle: React.CSSProperties = {
|
||||
padding: '0.25rem 0.5rem', borderBottom: '1px solid var(--ctp-surface0)', whiteSpace: 'nowrap',
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderBottom: "1px solid var(--ctp-surface0)",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
@@ -57,7 +57,7 @@ export function AuditPage() {
|
||||
style={{
|
||||
color: "var(--ctp-red)",
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
Error: {error}
|
||||
|
||||
@@ -179,7 +179,7 @@ export function ItemsPage() {
|
||||
style={{
|
||||
color: "var(--ctp-red)",
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
Error: {error}
|
||||
|
||||
@@ -78,7 +78,7 @@ export function LoginPage() {
|
||||
style={{
|
||||
padding: "0 1rem",
|
||||
color: "var(--ctp-overlay0)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
or
|
||||
@@ -123,7 +123,7 @@ const titleStyle: React.CSSProperties = {
|
||||
const subtitleStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-subtext0)",
|
||||
textAlign: "center",
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
marginBottom: "2rem",
|
||||
};
|
||||
|
||||
@@ -134,7 +134,7 @@ const errorStyle: React.CSSProperties = {
|
||||
padding: "0.75rem 1rem",
|
||||
borderRadius: "0.5rem",
|
||||
marginBottom: "1rem",
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const formGroupStyle: React.CSSProperties = {
|
||||
@@ -146,7 +146,7 @@ const labelStyle: React.CSSProperties = {
|
||||
marginBottom: "0.5rem",
|
||||
fontWeight: 500,
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
@@ -156,7 +156,7 @@ const inputStyle: React.CSSProperties = {
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.5rem",
|
||||
color: "var(--ctp-text)",
|
||||
fontSize: "1rem",
|
||||
fontSize: "var(--font-body)",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState, useCallback, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Plus, ChevronUp, ChevronDown } from "lucide-react";
|
||||
import { get, post, put, del } from "../api/client";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import type {
|
||||
@@ -180,7 +181,17 @@ export function ProjectsPage() {
|
||||
};
|
||||
|
||||
const sortArrow = (key: typeof sortKey) =>
|
||||
sortKey === key ? (sortAsc ? " \u25B2" : " \u25BC") : "";
|
||||
sortKey === key ? (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
display: "inline-flex",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
>
|
||||
{sortAsc ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (loading)
|
||||
return <p style={{ color: "var(--ctp-subtext0)" }}>Loading projects...</p>;
|
||||
@@ -199,8 +210,16 @@ export function ProjectsPage() {
|
||||
>
|
||||
<h2>Projects ({projects.length})</h2>
|
||||
{isEditor && mode === "list" && (
|
||||
<button onClick={openCreate} style={btnPrimaryStyle}>
|
||||
+ New Project
|
||||
<button
|
||||
onClick={openCreate}
|
||||
style={{
|
||||
...btnPrimaryStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
>
|
||||
<Plus size={14} /> New Project
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -319,7 +338,7 @@ export function ProjectsPage() {
|
||||
style={{
|
||||
color: "var(--ctp-red)",
|
||||
marginTop: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
This action cannot be undone.
|
||||
@@ -478,7 +497,7 @@ const btnDangerStyle: React.CSSProperties = {
|
||||
};
|
||||
|
||||
const btnSmallStyle: React.CSSProperties = {
|
||||
padding: "0.3rem 0.6rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.375rem",
|
||||
border: "none",
|
||||
backgroundColor: "var(--ctp-surface1)",
|
||||
@@ -501,7 +520,7 @@ const formHeaderStyle: React.CSSProperties = {
|
||||
alignItems: "center",
|
||||
padding: "0.5rem 1rem",
|
||||
color: "var(--ctp-crust)",
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const formCloseStyle: React.CSSProperties = {
|
||||
@@ -521,7 +540,7 @@ const errorBannerStyle: React.CSSProperties = {
|
||||
padding: "0.5rem 0.75rem",
|
||||
borderRadius: "0.4rem",
|
||||
marginBottom: "0.75rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const fieldStyle: React.CSSProperties = {
|
||||
@@ -533,7 +552,7 @@ const labelStyle: React.CSSProperties = {
|
||||
marginBottom: "0.35rem",
|
||||
fontWeight: 500,
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
@@ -543,7 +562,7 @@ const inputStyle: React.CSSProperties = {
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.4rem",
|
||||
color: "var(--ctp-text)",
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
@@ -560,7 +579,7 @@ const thStyle: React.CSSProperties = {
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
color: "var(--ctp-overlay1)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
cursor: "pointer",
|
||||
@@ -570,5 +589,5 @@ const thStyle: React.CSSProperties = {
|
||||
const tdStyle: React.CSSProperties = {
|
||||
padding: "0.35rem 0.75rem",
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, type FormEvent } from "react";
|
||||
import { ChevronRight, ChevronDown, Plus } from "lucide-react";
|
||||
import { get, post, put, del } from "../api/client";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import type { Schema, SchemaSegment } from "../api/types";
|
||||
@@ -282,10 +283,13 @@ function SchemaCard({
|
||||
color: "var(--ctp-sapphire)",
|
||||
userSelect: "none",
|
||||
marginTop: "1rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{isExpanded ? "\u25BC" : "\u25B6"} View Segments (
|
||||
{schema.segments.length})
|
||||
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}{" "}
|
||||
View Segments ({schema.segments.length})
|
||||
</div>
|
||||
|
||||
{isExpanded &&
|
||||
@@ -381,7 +385,7 @@ function SegmentBlock({
|
||||
style={{
|
||||
color: "var(--ctp-subtext0)",
|
||||
marginBottom: "0.5rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
{segment.description}
|
||||
@@ -415,7 +419,9 @@ function SegmentBlock({
|
||||
return (
|
||||
<tr key={code}>
|
||||
<td style={tdStyle}>
|
||||
<code style={{ fontSize: "0.85rem" }}>{code}</code>
|
||||
<code style={{ fontSize: "var(--font-body)" }}>
|
||||
{code}
|
||||
</code>
|
||||
</td>
|
||||
<td style={tdStyle}>
|
||||
<form
|
||||
@@ -479,13 +485,15 @@ function SegmentBlock({
|
||||
style={{ backgroundColor: "rgba(243, 139, 168, 0.1)" }}
|
||||
>
|
||||
<td style={tdStyle}>
|
||||
<code style={{ fontSize: "0.85rem" }}>{code}</code>
|
||||
<code style={{ fontSize: "var(--font-body)" }}>
|
||||
{code}
|
||||
</code>
|
||||
</td>
|
||||
<td style={tdStyle}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--ctp-red)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
Delete this value?
|
||||
@@ -527,7 +535,9 @@ function SegmentBlock({
|
||||
return (
|
||||
<tr key={code}>
|
||||
<td style={tdStyle}>
|
||||
<code style={{ fontSize: "0.85rem" }}>{code}</code>
|
||||
<code style={{ fontSize: "var(--font-body)" }}>
|
||||
{code}
|
||||
</code>
|
||||
</td>
|
||||
<td style={tdStyle}>{desc}</td>
|
||||
{isEditor && (
|
||||
@@ -642,9 +652,15 @@ function SegmentBlock({
|
||||
!(isThisSegment(editState) && editState!.mode === "add") && (
|
||||
<button
|
||||
onClick={() => onStartAdd(schemaName, segment.name)}
|
||||
style={{ ...btnTinyPrimaryStyle, marginTop: "0.5rem" }}
|
||||
style={{
|
||||
...btnTinyPrimaryStyle,
|
||||
marginTop: "0.5rem",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.35rem",
|
||||
}}
|
||||
>
|
||||
+ Add Value
|
||||
<Plus size={14} /> Add Value
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -664,7 +680,7 @@ const codeStyle: React.CSSProperties = {
|
||||
background: "var(--ctp-surface1)",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.25rem",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const segmentStyle: React.CSSProperties = {
|
||||
@@ -696,19 +712,19 @@ const thStyle: React.CSSProperties = {
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
color: "var(--ctp-overlay1)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
};
|
||||
|
||||
const tdStyle: React.CSSProperties = {
|
||||
padding: "0.3rem 0.75rem",
|
||||
padding: "0.25rem 0.75rem",
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const btnTinyStyle: React.CSSProperties = {
|
||||
padding: "0.2rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.375rem",
|
||||
border: "none",
|
||||
backgroundColor: "var(--ctp-surface1)",
|
||||
@@ -719,7 +735,7 @@ const btnTinyStyle: React.CSSProperties = {
|
||||
};
|
||||
|
||||
const btnTinyPrimaryStyle: React.CSSProperties = {
|
||||
padding: "0.2rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.375rem",
|
||||
border: "none",
|
||||
backgroundColor: "var(--ctp-mauve)",
|
||||
@@ -735,7 +751,7 @@ const inlineInputStyle: React.CSSProperties = {
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.25rem",
|
||||
color: "var(--ctp-text)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ export function SettingsPage() {
|
||||
display: "inline-block",
|
||||
padding: "0.15rem 0.5rem",
|
||||
borderRadius: "1rem",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
fontWeight: 600,
|
||||
...roleBadgeStyles[user.role],
|
||||
}}
|
||||
@@ -137,7 +137,7 @@ export function SettingsPage() {
|
||||
style={{
|
||||
color: "var(--ctp-subtext0)",
|
||||
marginBottom: "1.25rem",
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
}}
|
||||
>
|
||||
API tokens allow the FreeCAD plugin and scripts to authenticate with
|
||||
@@ -175,7 +175,7 @@ export function SettingsPage() {
|
||||
<p
|
||||
style={{
|
||||
color: "var(--ctp-subtext0)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
@@ -212,7 +212,7 @@ export function SettingsPage() {
|
||||
{tokensLoading ? (
|
||||
<p style={mutedStyle}>Loading tokens...</p>
|
||||
) : tokensError ? (
|
||||
<p style={{ color: "var(--ctp-red)", fontSize: "0.85rem" }}>
|
||||
<p style={{ color: "var(--ctp-red)", fontSize: "var(--font-body)" }}>
|
||||
{tokensError}
|
||||
</p>
|
||||
) : (
|
||||
@@ -332,7 +332,7 @@ const cardStyle: React.CSSProperties = {
|
||||
|
||||
const cardTitleStyle: React.CSSProperties = {
|
||||
marginBottom: "1rem",
|
||||
fontSize: "1.1rem",
|
||||
fontSize: "var(--font-title)",
|
||||
};
|
||||
|
||||
const dlStyle: React.CSSProperties = {
|
||||
@@ -344,12 +344,12 @@ const dlStyle: React.CSSProperties = {
|
||||
const dtStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-subtext0)",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const ddStyle: React.CSSProperties = {
|
||||
margin: 0,
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const mutedStyle: React.CSSProperties = {
|
||||
@@ -371,7 +371,7 @@ const tokenDisplayStyle: React.CSSProperties = {
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.5rem",
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
color: "var(--ctp-peach)",
|
||||
wordBreak: "break-all",
|
||||
};
|
||||
@@ -389,7 +389,7 @@ const labelStyle: React.CSSProperties = {
|
||||
marginBottom: "0.35rem",
|
||||
fontWeight: 500,
|
||||
color: "var(--ctp-subtext1)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
@@ -399,7 +399,7 @@ const inputStyle: React.CSSProperties = {
|
||||
border: "1px solid var(--ctp-surface1)",
|
||||
borderRadius: "0.4rem",
|
||||
color: "var(--ctp-text)",
|
||||
fontSize: "0.9rem",
|
||||
fontSize: "var(--font-body)",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
@@ -441,7 +441,7 @@ const btnDangerStyle: React.CSSProperties = {
|
||||
background: "rgba(243, 139, 168, 0.15)",
|
||||
color: "var(--ctp-red)",
|
||||
border: "none",
|
||||
padding: "0.3rem 0.6rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.375rem",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.75rem",
|
||||
@@ -452,7 +452,7 @@ const btnRevokeConfirmStyle: React.CSSProperties = {
|
||||
background: "var(--ctp-red)",
|
||||
color: "var(--ctp-crust)",
|
||||
border: "none",
|
||||
padding: "0.2rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.375rem",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.75rem",
|
||||
@@ -460,7 +460,7 @@ const btnRevokeConfirmStyle: React.CSSProperties = {
|
||||
};
|
||||
|
||||
const btnTinyStyle: React.CSSProperties = {
|
||||
padding: "0.2rem 0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.375rem",
|
||||
border: "none",
|
||||
backgroundColor: "var(--ctp-surface1)",
|
||||
@@ -472,7 +472,7 @@ const btnTinyStyle: React.CSSProperties = {
|
||||
|
||||
const errorStyle: React.CSSProperties = {
|
||||
color: "var(--ctp-red)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
marginTop: "0.25rem",
|
||||
};
|
||||
|
||||
@@ -482,7 +482,7 @@ const thStyle: React.CSSProperties = {
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
color: "var(--ctp-overlay1)",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "var(--font-table)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
};
|
||||
@@ -490,5 +490,5 @@ const thStyle: React.CSSProperties = {
|
||||
const tdStyle: React.CSSProperties = {
|
||||
padding: "0.4rem 0.75rem",
|
||||
borderBottom: "1px solid var(--ctp-surface1)",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "var(--font-body)",
|
||||
};
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
--ctp-crust: #11111b;
|
||||
}
|
||||
|
||||
/* ── Font scale ── */
|
||||
:root {
|
||||
--font-title: 1.1rem; /* page titles */
|
||||
--font-body: 0.8125rem; /* 13px — body text, breadcrumbs */
|
||||
--font-table: 0.75rem; /* 12px — table cells, inputs, buttons */
|
||||
--font-sm: 0.6875rem; /* 11px — section headers, labels, captions */
|
||||
--font-xs: 0.625rem; /* 10px — badges (minimum) */
|
||||
}
|
||||
|
||||
/* ── Density: comfortable (default) ── */
|
||||
[data-density="comfortable"],
|
||||
:root {
|
||||
@@ -39,24 +48,24 @@
|
||||
--d-nav-px: 0.75rem;
|
||||
--d-nav-radius: 0.4rem;
|
||||
--d-user-gap: 0.6rem;
|
||||
--d-user-font: 0.85rem;
|
||||
--d-user-font: var(--font-body);
|
||||
|
||||
--d-th-py: 0.35rem;
|
||||
--d-th-px: 0.75rem;
|
||||
--d-th-font: 0.75rem;
|
||||
--d-th-font: var(--font-table);
|
||||
--d-td-py: 0.25rem;
|
||||
--d-td-px: 0.75rem;
|
||||
--d-td-font: 0.85rem;
|
||||
--d-td-font: var(--font-body);
|
||||
|
||||
--d-toolbar-gap: 0.5rem;
|
||||
--d-toolbar-py: 0.5rem;
|
||||
--d-toolbar-mb: 0.35rem;
|
||||
--d-input-py: 0.35rem;
|
||||
--d-input-px: 0.6rem;
|
||||
--d-input-font: 0.85rem;
|
||||
--d-input-font: var(--font-body);
|
||||
|
||||
--d-footer-h: 28px;
|
||||
--d-footer-font: 0.75rem;
|
||||
--d-footer-font: var(--font-table);
|
||||
--d-footer-px: 2rem;
|
||||
}
|
||||
|
||||
@@ -70,23 +79,23 @@
|
||||
--d-nav-px: 0.5rem;
|
||||
--d-nav-radius: 0.3rem;
|
||||
--d-user-gap: 0.35rem;
|
||||
--d-user-font: 0.8rem;
|
||||
--d-user-font: var(--font-table);
|
||||
|
||||
--d-th-py: 0.2rem;
|
||||
--d-th-px: 0.5rem;
|
||||
--d-th-font: 0.7rem;
|
||||
--d-th-font: var(--font-sm);
|
||||
--d-td-py: 0.125rem;
|
||||
--d-td-px: 0.5rem;
|
||||
--d-td-font: 0.8rem;
|
||||
--d-td-font: var(--font-table);
|
||||
|
||||
--d-toolbar-gap: 0.35rem;
|
||||
--d-toolbar-py: 0.25rem;
|
||||
--d-toolbar-mb: 0.15rem;
|
||||
--d-input-py: 0.2rem;
|
||||
--d-input-px: 0.4rem;
|
||||
--d-input-font: 0.8rem;
|
||||
--d-input-font: var(--font-table);
|
||||
|
||||
--d-footer-h: 24px;
|
||||
--d-footer-font: 0.7rem;
|
||||
--d-footer-font: var(--font-sm);
|
||||
--d-footer-px: 1.25rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user