Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions web/apps/desk/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<html lang="en">

<head>
<!-- Applies the stored mode before the first paint (a same origin file: the CSP refuses inline scripts) -->
<script src="/theme-boot.js"></script>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand All @@ -21,7 +23,7 @@
<body>
<style>
body {
background-color: #0A0B10;
background-color: rgb(var(--gb-deep-space, 10 11 16));
margin: 0;
font-family: 'Outfit', sans-serif;
}
Expand All @@ -32,7 +34,7 @@
align-items: center;
justify-content: center;
height: 100vh;
color: white;
color: rgb(var(--gb-white, 255 255 255));
}

.logo-container {
Expand All @@ -47,18 +49,26 @@
inset: 0;
width: 100%;
height: 100%;
color: white;
color: rgb(var(--gb-white, 255 255 255));
}

.spinner {
position: absolute;
inset: -10px;
border: 4px solid rgba(255, 255, 255, 0.05);
border-top-color: #00FFA3;
border: 4px solid rgb(var(--gb-white, 255 255 255) / 0.05);
border-top-color: rgb(var(--gb-gable-green, 0 255 163));
border-radius: 50%;
animation: spin 1s linear infinite;
}

.logo-accent-stroke {
stroke: rgb(var(--gb-gable-green, 0 255 163));
}

.logo-accent-fill {
fill: rgb(var(--gb-gable-green, 0 255 163));
}

@keyframes spin {
from {
transform: rotate(0deg);
Expand All @@ -79,7 +89,7 @@
}

.accent {
color: #00FFA3;
color: rgb(var(--gb-gable-green, 0 255 163));
font-weight: 300;
letter-spacing: 0.2em;
}
Expand All @@ -92,10 +102,10 @@
<svg class="logo-svg" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 36 L32 8 L60 36" stroke="currentColor" stroke-width="6" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M32 16 V28" stroke="#00FFA3" stroke-width="4" stroke-linecap="round" />
<path d="M22 28 C22 28 22 46 32 46 C42 46 42 28 42 28" stroke="#00FFA3" stroke-width="4"
<path d="M32 16 V28" class="logo-accent-stroke" stroke-width="4" stroke-linecap="round" />
<path d="M22 28 C22 28 22 46 32 46 C42 46 42 28 42 28" class="logo-accent-stroke" stroke-width="4"
stroke-linecap="round" fill="none" />
<rect x="28" y="46" width="8" height="6" rx="1" fill="#00FFA3" />
<rect x="28" y="46" width="8" height="6" rx="1" class="logo-accent-fill" />
</svg>
</div>
<div class="brand">Gable<span class="accent">LBM</span></div>
Expand Down
19 changes: 19 additions & 0 deletions web/apps/desk/public/theme-boot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* SPDX-License-Identifier: LicenseRef-OpenLBM-Surface-1.0 */
/* SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors */

/* Boot theme: applies the person's stored light or dark choice before the
first paint, so the loader never flashes the wrong ground. A classic,
same origin script because the page's content security policy
(script-src 'self') refuses an inline one. The desk's nginx root serves it
for the front door too. The last used value (gable_theme) is always the
current person's choice, so it is the only key read here; the app's own
entry applies the per person value again before its first render. */
(function () {
try {
if (localStorage.getItem('gable_theme') === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
}
} catch (e) {
/* a hostile storage context: Industrial Dark, the default */
}
})();
16 changes: 8 additions & 8 deletions web/apps/desk/src/components/BarcodeScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ export class GableBarcodeScanner extends LitElement {
render() {
return html`
<div
class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
class="fixed inset-0 z-50 bg-backdrop/80 backdrop-blur-sm flex items-center justify-center p-4"
@click=${this._handleBackdropClick}
role="dialog"
aria-modal="true"
aria-labelledby="barcode-scanner-title"
>
<div class="bg-[#12131A] border border-white/10 rounded-xl overflow-hidden w-full max-w-md shadow-2xl flex flex-col relative animate-scale-up">
<div class="bg-well border border-white/10 rounded-xl overflow-hidden w-full max-w-md shadow-2xl flex flex-col relative animate-scale-up">
<!-- Header -->
<div class="px-4 py-3 border-b border-white/10 flex justify-between items-center bg-[#0A0B10]">
<div class="px-4 py-3 border-b border-white/10 flex justify-between items-center bg-deep-space">
<h3 id="barcode-scanner-title" class="text-white font-medium">Scan Barcode</h3>
<button
@click=${this._close}
Expand All @@ -109,7 +109,7 @@ export class GableBarcodeScanner extends LitElement {
</div>

<!-- Body -->
<div class="p-4 bg-black/40 min-h-[300px] flex items-center justify-center">
<div class="p-4 bg-shade/40 min-h-[300px] flex items-center justify-center">
${this._error ? html`
<div class="text-center p-4 text-rose-400 bg-rose-500/10 rounded-lg border border-rose-500/20">
<p class="mb-2">${this._error}</p>
Expand All @@ -122,11 +122,11 @@ export class GableBarcodeScanner extends LitElement {
</div>
` : html`
<div class="w-full relative">
<div id="reader-container" class="w-full overflow-hidden rounded-lg bg-black border border-white/10 [&>div]:!border-none [&>div>video]:!rounded-lg [&_button]:!bg-[#00FFA3] [&_button]:!text-black [&_button]:!border-none [&_button]:!rounded [&_button]:!px-3 [&_button]:!py-1.5 [&_button]:!font-medium [&_button]:!text-sm hover:[&_button]:!bg-[#00E593] [&_select]:!bg-[#12131A] [&_select]:!text-white [&_select]:!border-white/20 [&_select]:!rounded [&_select]:!px-2 [&_select]:!py-1 [&_select]:!text-sm [&_a]:hidden pb-2"></div>
<div id="reader-container" class="w-full overflow-hidden rounded-lg bg-backdrop border border-white/10 [&>div]:!border-none [&>div>video]:!rounded-lg [&_button]:!bg-gable-green [&_button]:!text-black [&_button]:!border-none [&_button]:!rounded [&_button]:!px-3 [&_button]:!py-1.5 [&_button]:!font-medium [&_button]:!text-sm hover:[&_button]:!bg-gable-green-hover [&_select]:!bg-well [&_select]:!text-white [&_select]:!border-white/20 [&_select]:!rounded [&_select]:!px-2 [&_select]:!py-1 [&_select]:!text-sm [&_a]:hidden pb-2"></div>

${this._isScanning ? html`
<div class="absolute top-4 right-4 flex items-center gap-2 bg-black/60 backdrop-blur px-2 py-1 rounded text-xs text-[#00FFA3] font-mono border border-[#00FFA3]/20">
<span class="w-2 h-2 rounded-full bg-[#00FFA3] animate-pulse"></span>
<div class="absolute top-4 right-4 flex items-center gap-2 bg-chip-ground/60 backdrop-blur px-2 py-1 rounded text-xs text-gable-green font-mono border border-gable-green/20">
<span class="w-2 h-2 rounded-full bg-gable-green animate-pulse"></span>
Scanning
</div>
` : nothing}
Expand All @@ -135,7 +135,7 @@ export class GableBarcodeScanner extends LitElement {
</div>

<!-- Footer hint -->
<div class="px-4 py-3 bg-[#0A0B10] border-t border-white/10 text-center">
<div class="px-4 py-3 bg-deep-space border-t border-white/10 text-center">
<p class="text-xs text-zinc-400">Position the barcode or QR code inside the frame</p>
</div>
</div>
Expand Down
32 changes: 16 additions & 16 deletions web/apps/desk/src/components/crm/LogActivityModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,26 @@ export class GableLogActivityModal extends LitElement {

render() {
return html`
<div class="fixed inset-0 bg-gray-600 bg-opacity-75 flex justify-center items-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50">
<h3 class="text-lg font-bold text-gray-900">Log Activity</h3>
<button @click=${this._close} class="text-gray-400 hover:text-gray-600">
<div class="fixed inset-0 bg-backdrop/75 flex justify-center items-center z-50">
<div class="bg-slate-steel rounded-lg shadow-xl w-full max-w-lg overflow-hidden">
<div class="px-6 py-4 border-b border-white/10 flex justify-between items-center bg-white/5">
<h3 class="text-lg font-bold text-white">Log Activity</h3>
<button @click=${this._close} class="text-gray-400 hover:text-zinc-300">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>

<form @submit=${this._handleSubmit} class="px-6 py-4">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Activity Type</label>
<label class="block text-sm font-medium text-zinc-300 mb-1">Activity Type</label>
<div class="grid grid-cols-4 gap-2">
${ACTIVITY_TYPES.map(type => html`
<button
type="button"
@click=${() => this._activityType = type}
class="px-3 py-2 text-sm font-medium rounded-md border ${this._activityType === type
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
? 'bg-blue-500/10 border-blue-500 text-blue-400'
: 'bg-slate-steel border-white/20 text-zinc-300 hover:bg-white/5'
}"
>
${type.charAt(0).toUpperCase() + type.slice(1)}
Expand All @@ -76,24 +76,24 @@ export class GableLogActivityModal extends LitElement {
</div>

<div class="mb-4">
<label for="log-activity-date" class="block text-sm font-medium text-gray-700 mb-1">Date & Time</label>
<label for="log-activity-date" class="block text-sm font-medium text-zinc-300 mb-1">Date & Time</label>
<input
id="log-activity-date"
type="datetime-local"
required
.value=${this._activityDate}
@input=${(e: InputEvent) => this._activityDate = (e.target as HTMLInputElement).value}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
class="bg-zinc-800 text-white w-full px-3 py-2 border border-white/20 rounded-md focus:ring-blue-500 focus:border-blue-500"
/>
</div>

<div class="mb-4">
<label for="log-activity-contact" class="block text-sm font-medium text-gray-700 mb-1">Contact (Optional)</label>
<label for="log-activity-contact" class="block text-sm font-medium text-zinc-300 mb-1">Contact (Optional)</label>
<select
id="log-activity-contact"
.value=${this._contactId}
@change=${(e: Event) => this._contactId = (e.target as HTMLSelectElement).value}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
class="bg-zinc-800 text-white w-full px-3 py-2 border border-white/20 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
<option value="">-- No Specific Contact --</option>
${this.contacts.map(c => html`
Expand All @@ -103,25 +103,25 @@ export class GableLogActivityModal extends LitElement {
</div>

<div class="mb-6">
<label for="log-activity-description" class="block text-sm font-medium text-gray-700 mb-1">Description / Notes</label>
<label for="log-activity-description" class="block text-sm font-medium text-zinc-300 mb-1">Description / Notes</label>
<textarea
id="log-activity-description"
required
rows="4"
.value=${this._description}
@input=${(e: InputEvent) => this._description = (e.target as HTMLTextAreaElement).value}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
class="bg-zinc-800 text-white w-full px-3 py-2 border border-white/20 rounded-md focus:ring-blue-500 focus:border-blue-500"
placeholder="Details about the ${this._activityType.toLowerCase()}..."
></textarea>
</div>

<div class="flex justify-end gap-3">
<button type="button" @click=${this._close}
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
class="px-4 py-2 border border-white/20 rounded-md text-sm font-medium text-zinc-300 bg-slate-steel hover:bg-white/5">
Cancel
</button>
<button type="submit" ?disabled=${this._loading}
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50">
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-plate bg-blue-600 hover:bg-blue-700 disabled:opacity-50">
${this._loading ? 'Saving...' : 'Log Activity'}
</button>
</div>
Expand Down
8 changes: 4 additions & 4 deletions web/apps/desk/src/components/customers/CustomerSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class GableCustomerSelect extends LitElement {
<div class="relative">
<div
@click=${() => this._isOpen = !this._isOpen}
class="flex items-center w-full px-4 py-2 bg-[#161821] border border-white/10 rounded-md cursor-pointer hover:border-[#00FFA3] transition-colors"
class="flex items-center w-full px-4 py-2 bg-slate-steel border border-white/10 rounded-md cursor-pointer hover:border-gable-green transition-colors"
>
${icon(Search, 16, 'text-gray-400 mr-2')}
<input
Expand All @@ -79,7 +79,7 @@ export class GableCustomerSelect extends LitElement {
</div>

${this._isOpen ? html`
<div class="absolute z-50 w-full mt-1 bg-[#161821] border border-white/10 rounded-md shadow-xl max-h-60 overflow-auto">
<div class="absolute z-50 w-full mt-1 bg-slate-steel border border-white/10 rounded-md shadow-xl max-h-60 overflow-auto">
${this._loading ? html`
<div class="p-4 text-center text-gray-500 text-sm">Loading...</div>
` : nothing}
Expand All @@ -90,11 +90,11 @@ export class GableCustomerSelect extends LitElement {

${!this._loading ? this._filteredCustomers.map(customer => html`
<div
class="px-4 py-2 hover:bg-[#00FFA3]/10 cursor-pointer flex justify-between items-center group"
class="px-4 py-2 hover:bg-gable-green/10 cursor-pointer flex justify-between items-center group"
@click=${() => this._selectCustomer(customer)}
>
<div>
<div class="text-white font-medium group-hover:text-[#00FFA3] transition-colors">${customer.name}</div>
<div class="text-white font-medium group-hover:text-gable-green transition-colors">${customer.name}</div>
<div class="text-xs text-gray-500">#${customer.account_number}</div>
</div>
<div class="text-xs text-right text-gray-500">
Expand Down
28 changes: 21 additions & 7 deletions web/apps/desk/src/components/dashboard/OrderStatusChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import Chart from 'chart.js/auto';
import { onThemeChange, themeColor } from '@gable/design-system';

const COLORS = ['#00FFA3', '#38BDF8', '#818cf8', '#fbbf24', '#f43f5e', '#a1a1aa'];
/**
* The series, by token, so they follow Industrial Dark and Gable light. The
* third (the fulfilled share, nearly the whole ring) is indigo 400 in dark and
* the deep sage in light, so the donut stays in the warm family.
*/
const SERIES = ['gable-green', 'blueprint-blue', 'chart-fulfilled', 'amber-400', 'rose-500', 'zinc-400'];

/** Helper to map status to readable names */
function formatStatus(status: string): string {
Expand All @@ -22,6 +28,12 @@ export class GableOrderStatusChart extends LitElement {
@state() private _totalOrders = 0;

private _chart: Chart<'doughnut'> | null = null;
private _offTheme: (() => void) | null = null;

connectedCallback() {
super.connectedCallback();
this._offTheme = onThemeChange(() => { if (!this.loading) this._renderChart(); });
}

updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
Expand All @@ -34,6 +46,8 @@ export class GableOrderStatusChart extends LitElement {

disconnectedCallback() {
super.disconnectedCallback();
this._offTheme?.();
this._offTheme = null;
this._destroyChart();
}

Expand Down Expand Up @@ -69,7 +83,7 @@ export class GableOrderStatusChart extends LitElement {
labels: data.map(d => d.name),
datasets: [{
data: data.map(d => d.value),
backgroundColor: data.map((_, i) => COLORS[i % COLORS.length]),
backgroundColor: data.map((_, i) => themeColor(SERIES[i % SERIES.length])),
borderWidth: 0,
spacing: 4,
}],
Expand All @@ -82,19 +96,19 @@ export class GableOrderStatusChart extends LitElement {
legend: {
position: 'bottom',
labels: {
color: '#a1a1aa',
color: themeColor('zinc-400'),
usePointStyle: true,
pointStyle: 'circle',
padding: 16,
font: { size: 11 },
},
},
tooltip: {
backgroundColor: 'rgba(22, 24, 33, 0.9)',
titleColor: '#a1a1aa',
bodyColor: '#ffffff',
backgroundColor: themeColor('slate-steel', 0.9),
titleColor: themeColor('zinc-400'),
bodyColor: themeColor('white'),
bodyFont: { family: 'JetBrains Mono, monospace', weight: 'bold' },
borderColor: 'rgba(255,255,255,0.1)',
borderColor: themeColor('white', 0.1),
borderWidth: 1,
cornerRadius: 8,
padding: 8,
Expand Down
Loading
Loading