Parts inventory: catalog, search, and stock tracking
FastAPI + SQLite behind a single-page frontend, deployed as a container on mainserver behind Caddy. One flexible parts table plus key/value specs so components, filament, fasteners and tooling share a schema. Categories and locations are nestable trees whose filters and sidebar counts both roll up through descendants. Category spec templates pre-fill the properties worth recording for each kind of part, which is what makes manual entry tolerable. Every quantity change is logged. Search is FTS5 with prefix matching across names, MPNs, spec values, tags, category and location. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+239
@@ -0,0 +1,239 @@
|
||||
:root {
|
||||
--bg: #12141a;
|
||||
--panel: #1a1d26;
|
||||
--panel-2: #222634;
|
||||
--line: #2e3342;
|
||||
--text: #e6e8ef;
|
||||
--dim: #949bb0;
|
||||
--accent: #5b9dff;
|
||||
--good: #4ec98a;
|
||||
--warn: #f0a848;
|
||||
--bad: #f2685f;
|
||||
--radius: 10px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 15px/1.5 ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
button, input, select, textarea { font: inherit; color: inherit; }
|
||||
|
||||
button {
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
min-height: 38px;
|
||||
}
|
||||
button:hover { border-color: #414861; }
|
||||
button.primary { background: var(--accent); border-color: var(--accent); color: #08101f; font-weight: 600; }
|
||||
button.danger { color: var(--bad); }
|
||||
button.ghost { background: transparent; }
|
||||
button.small { padding: 4px 9px; min-height: 30px; font-size: 13px; }
|
||||
|
||||
input, select, textarea {
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 9px 11px;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
|
||||
textarea { resize: vertical; min-height: 70px; }
|
||||
label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 4px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
|
||||
.hidden { display: none !important; }
|
||||
.mobile-only { display: none; }
|
||||
|
||||
/* --- login --- */
|
||||
#login {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
#login form {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 28px;
|
||||
width: min(360px, 100%);
|
||||
}
|
||||
#login h1 { margin: 0 0 6px; font-size: 20px; }
|
||||
#login p { margin: 0 0 18px; color: var(--dim); font-size: 13px; }
|
||||
|
||||
/* --- shell --- */
|
||||
header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: rgba(18,20,26,.94);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
header .brand { font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
|
||||
header .search { flex: 1 1 240px; min-width: 160px; }
|
||||
header .stat { color: var(--dim); font-size: 13px; white-space: nowrap; }
|
||||
|
||||
.layout { display: flex; align-items: flex-start; gap: 18px; padding: 16px; max-width: 1400px; margin: 0 auto; }
|
||||
|
||||
aside {
|
||||
width: 250px;
|
||||
flex: 0 0 250px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
position: sticky;
|
||||
top: 68px;
|
||||
max-height: calc(100dvh - 84px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
aside h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .07em; color: var(--dim); margin: 14px 0 6px; }
|
||||
aside h3:first-child { margin-top: 0; }
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.filter:hover { background: var(--panel-2); }
|
||||
.filter.active { background: var(--accent); color: #08101f; font-weight: 600; }
|
||||
.filter .count { color: var(--dim); font-size: 12px; }
|
||||
.filter.active .count { color: #08101f; }
|
||||
.filter.child { padding-left: 20px; font-size: 13px; }
|
||||
|
||||
main { flex: 1; min-width: 0; }
|
||||
|
||||
.toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.toolbar .grow { flex: 1; }
|
||||
.toolbar select { width: auto; min-width: 150px; }
|
||||
|
||||
/* --- part rows --- */
|
||||
.part {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 9px;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.part:hover { border-color: #3c445e; }
|
||||
.part .body { flex: 1 1 260px; min-width: 0; cursor: pointer; }
|
||||
.part .name { font-weight: 600; }
|
||||
.part .meta { color: var(--dim); font-size: 13px; margin-top: 2px; word-break: break-word; }
|
||||
.part .chips { margin-top: 6px; display: flex; gap: 5px; flex-wrap: wrap; }
|
||||
.chip {
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 1px 9px;
|
||||
font-size: 12px;
|
||||
color: var(--dim);
|
||||
}
|
||||
.chip.spec { color: var(--text); }
|
||||
.chip.tag { color: var(--accent); border-color: #2c4470; }
|
||||
|
||||
.qty { display: flex; align-items: center; gap: 6px; }
|
||||
.qty .value { min-width: 84px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
|
||||
.qty .value .unit { color: var(--dim); font-weight: 400; font-size: 13px; margin-left: 3px; }
|
||||
.qty.low .value { color: var(--warn); }
|
||||
.qty.zero .value { color: var(--bad); }
|
||||
|
||||
.empty { text-align: center; color: var(--dim); padding: 60px 20px; }
|
||||
|
||||
/* --- modal --- */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(6,8,13,.72);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
z-index: 50;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.modal {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
width: min(720px, 100%);
|
||||
max-height: calc(100dvh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
}
|
||||
.modal header { position: static; background: none; backdrop-filter: none; justify-content: space-between; }
|
||||
.modal .content { padding: 16px; overflow-y: auto; }
|
||||
.modal footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--line);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.modal footer .grow { flex: 1; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.grid .full { grid-column: 1 / -1; }
|
||||
|
||||
.spec-row { display: flex; gap: 8px; margin-bottom: 7px; align-items: center; }
|
||||
.spec-row input:first-child { flex: 0 0 38%; }
|
||||
|
||||
.history { font-size: 13px; color: var(--dim); }
|
||||
.history div { padding: 4px 0; border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; gap: 10px; }
|
||||
.pos { color: var(--good); }
|
||||
.neg { color: var(--bad); }
|
||||
|
||||
.node-row { display: flex; gap: 8px; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--line); }
|
||||
.node-row .path { flex: 1; min-width: 0; word-break: break-word; }
|
||||
.node-row .count { color: var(--dim); font-size: 12px; }
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 10px 16px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,.5);
|
||||
}
|
||||
.toast.bad { border-color: var(--bad); color: var(--bad); }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.layout { flex-direction: column; padding: 12px; gap: 12px; }
|
||||
/* On a phone the filter list is longer than the screen, so it starts closed:
|
||||
search and results are what you came for, filters are a tap away. */
|
||||
aside { width: 100%; flex: none; position: static; max-height: none; display: none; }
|
||||
aside.open { display: block; }
|
||||
.mobile-only { display: inline-flex; align-items: center; }
|
||||
#filters-btn.active { background: var(--accent); border-color: var(--accent); color: #08101f; }
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
header { padding: 8px 12px; }
|
||||
header .stat { display: none; }
|
||||
}
|
||||
Reference in New Issue
Block a user