Files
silo/internal/api/templates/base.html
Zoe Forbes 8c0689991e feat: Infor-style split-panel layout, projects page, fuzzy search, Odoo scaffold
Web UI - Infor CloudSuite-style split-panel layout (items.html rewrite):
- Replace modal-based item detail with inline split-panel workspace
- Horizontal mode: item list on left, tabbed detail panel on right
- Vertical mode: detail panel on top, item list below
- Detail tabs: Main, Properties, Revisions, BOM, Where Used
- Ctrl+F opens in-page filter overlay with fuzzy search
- Column config gear icon with per-layout-mode persistence
- Search scope toggle pills (All / Part Number / Description)
- Selected row highlight with accent border
- Responsive breakpoint forces vertical below 900px
- Create/Edit/Delete remain as modal dialogs

Web UI - Projects page:
- New projects.html template with full CRUD
- Project table: Code, Name, Description, Item count, Created, Actions
- Create/Edit/Delete modals
- Click project code navigates to items filtered by project
- 3-tab navigation in base.html: Items, Projects, Schemas

Fuzzy search:
- Add sahilm/fuzzy dependency for ranked text matching
- New internal/api/search.go with SearchableItems fuzzy.Source
- GET /api/items/search endpoint with field scope and type/project filters
- Frontend routes to fuzzy endpoint when search input is non-empty

Odoo ERP integration scaffold:
- Migration 008: integrations and sync_log tables
- internal/odoo/ package: types, client stubs, sync stubs
- internal/db/integrations.go: IntegrationRepository
- internal/config/config.go: OdooConfig struct
- 6 API endpoints for config CRUD, sync log, test, push, pull
- All sync operations return stub responses

Documentation:
- docs/REPOSITORY_STATUS.md: comprehensive repository state report
  with architecture overview, API surface, feature stubs, and
  potential issues analysis
2026-01-31 09:20:27 -06:00

507 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{if .Title}}{{.Title}} - {{end}}Silo</title>
<style>
/* Catppuccin Mocha Theme */
:root {
--ctp-rosewater: #f5e0dc;
--ctp-flamingo: #f2cdcd;
--ctp-pink: #f5c2e7;
--ctp-mauve: #cba6f7;
--ctp-red: #f38ba8;
--ctp-maroon: #eba0ac;
--ctp-peach: #fab387;
--ctp-yellow: #f9e2af;
--ctp-green: #a6e3a1;
--ctp-teal: #94e2d5;
--ctp-sky: #89dceb;
--ctp-sapphire: #74c7ec;
--ctp-blue: #89b4fa;
--ctp-lavender: #b4befe;
--ctp-text: #cdd6f4;
--ctp-subtext1: #bac2de;
--ctp-subtext0: #a6adc8;
--ctp-overlay2: #9399b2;
--ctp-overlay1: #7f849c;
--ctp-overlay0: #6c7086;
--ctp-surface2: #585b70;
--ctp-surface1: #45475a;
--ctp-surface0: #313244;
--ctp-base: #1e1e2e;
--ctp-mantle: #181825;
--ctp-crust: #11111b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--ctp-base);
color: var(--ctp-text);
line-height: 1.6;
min-height: 100vh;
}
a {
color: var(--ctp-sapphire);
text-decoration: none;
}
a:hover {
color: var(--ctp-sky);
text-decoration: underline;
}
/* Header */
.header {
background-color: var(--ctp-mantle);
border-bottom: 1px solid var(--ctp-surface0);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header-brand h1 {
font-size: 1.5rem;
font-weight: 600;
color: var(--ctp-mauve);
}
.header-nav {
display: flex;
gap: 1.5rem;
}
.header-nav a {
color: var(--ctp-subtext1);
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s;
}
.header-nav a:hover {
background-color: var(--ctp-surface0);
color: var(--ctp-text);
text-decoration: none;
}
.header-nav a.active {
background-color: var(--ctp-surface1);
color: var(--ctp-mauve);
}
/* Main Content */
.main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Cards */
.card {
background-color: var(--ctp-surface0);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1rem;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--ctp-text);
}
/* Search and Filters */
.search-bar {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 250px;
padding: 0.75rem 1rem;
background-color: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.5rem;
color: var(--ctp-text);
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: var(--ctp-mauve);
box-shadow: 0 0 0 3px rgba(203, 166, 247, 0.2);
}
.search-input::placeholder {
color: var(--ctp-overlay0);
}
select {
padding: 0.75rem 1rem;
background-color: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.5rem;
color: var(--ctp-text);
font-size: 1rem;
cursor: pointer;
}
select:focus {
outline: none;
border-color: var(--ctp-mauve);
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
font-size: 0.95rem;
cursor: pointer;
border: none;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background-color: var(--ctp-mauve);
color: var(--ctp-crust);
}
.btn-primary:hover {
background-color: var(--ctp-lavender);
}
.btn-secondary {
background-color: var(--ctp-surface1);
color: var(--ctp-text);
}
.btn-secondary:hover {
background-color: var(--ctp-surface2);
}
/* Table */
.table-container {
overflow-x: auto;
border-radius: 0.75rem;
border: 1px solid var(--ctp-surface1);
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--ctp-surface1);
}
th {
background-color: var(--ctp-surface0);
color: var(--ctp-subtext1);
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
tr:hover {
background-color: var(--ctp-surface0);
}
tr:last-child td {
border-bottom: none;
}
.part-number {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--ctp-peach);
font-weight: 500;
}
.item-type {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.8rem;
font-weight: 500;
}
.item-type-part {
background-color: rgba(137, 180, 250, 0.2);
color: var(--ctp-blue);
}
.item-type-assembly {
background-color: rgba(166, 227, 161, 0.2);
color: var(--ctp-green);
}
.item-type-document {
background-color: rgba(249, 226, 175, 0.2);
color: var(--ctp-yellow);
}
.item-type-tooling {
background-color: rgba(243, 139, 168, 0.2);
color: var(--ctp-red);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--ctp-subtext0);
}
.empty-state h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: var(--ctp-subtext1);
}
/* Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal {
background-color: var(--ctp-surface0);
border-radius: 1rem;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
}
.modal-close {
background: none;
border: none;
color: var(--ctp-subtext0);
cursor: pointer;
font-size: 1.5rem;
padding: 0.25rem;
}
.modal-close:hover {
color: var(--ctp-text);
}
/* Form */
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--ctp-subtext1);
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
background-color: var(--ctp-base);
border: 1px solid var(--ctp-surface1);
border-radius: 0.5rem;
color: var(--ctp-text);
font-size: 1rem;
}
.form-input:focus {
outline: none;
border-color: var(--ctp-mauve);
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
/* Stats */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: var(--ctp-surface0);
border-radius: 0.75rem;
padding: 1.25rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--ctp-text);
}
.stat-label {
color: var(--ctp-subtext0);
font-size: 0.9rem;
}
/* Pagination */
.pagination {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-top: 2rem;
}
.pagination-btn {
padding: 0.5rem 1rem;
background-color: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.5rem;
color: var(--ctp-text);
cursor: pointer;
}
.pagination-btn:hover {
background-color: var(--ctp-surface1);
}
.pagination-btn.active {
background-color: var(--ctp-mauve);
color: var(--ctp-crust);
border-color: var(--ctp-mauve);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.spinner {
width: 2rem;
height: 2rem;
border: 3px solid var(--ctp-surface1);
border-top-color: var(--ctp-mauve);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 1rem;
}
.search-bar {
flex-direction: column;
}
.search-input {
min-width: 100%;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-brand">
<h1>Silo</h1>
</div>
<nav class="header-nav">
<a href="/" class="{{if eq .Page "items"}}active{{end}}">Items</a>
<a href="/projects" class="{{if eq .Page "projects"}}active{{end}}">Projects</a>
<a href="/schemas" class="{{if eq .Page "schemas"}}active{{end}}">Schemas</a>
</nav>
</header>
<main class="main">
{{if eq .Page "items"}}
{{template "items_content" .}}
{{else if eq .Page "projects"}}
{{template "projects_content" .}}
{{else if eq .Page "schemas"}}
{{template "schemas_content" .}}
{{end}}
</main>
{{if eq .Page "items"}}
{{template "items_scripts" .}}
{{else if eq .Page "projects"}}
{{template "projects_scripts" .}}
{{else if eq .Page "schemas"}}
{{template "schemas_scripts" .}}
{{end}}
</body>
</html>