fix(ui): tokenize neutral text colors for dark mode contrast
Replace ~268 hardcoded text-stone-* classes with theme-aware token utilities (text-text, text-text-secondary, text-text-muted) across all templates and JS components. Adjust dark mode token values to meet WCAG AA contrast ratios on dark surfaces.
This commit is contained in:
parent
1377d43935
commit
709adcdd3e
27 changed files with 341 additions and 335 deletions
51
CLAUDE.md
51
CLAUDE.md
|
|
@ -409,6 +409,9 @@ Colors are defined as raw CSS custom properties in `:root` (light) and `[data-th
|
|||
| `--accent-hover` | `#ea580c` | `#f97316` | `bg-accent-hover`, `hover:bg-accent-hover` |
|
||||
| `--accent-subtle` | `#fff7ed` (orange-50) | `rgba(234,88,12,0.1)` | `bg-accent-subtle` |
|
||||
| `--accent-text` | `#ffffff` | `#ffffff` | `text-accent-text` |
|
||||
| `--text` | `#1c1917` (stone-900) | `#e7e5e4` (stone-200) | `text-text` |
|
||||
| `--text-secondary` | `#57534e` (stone-600) | `#d6d3d1` (stone-300) | `text-text-secondary` |
|
||||
| `--text-muted` | `#78716c` (stone-500) | `#a8a29e` (stone-400) | `text-text-muted` |
|
||||
|
||||
A base layer rule sets the default border color so bare `border` / `divide-y` classes use the theme:
|
||||
|
||||
|
|
@ -420,7 +423,7 @@ A base layer rule sets the default border color so bare `border` / `divide-y` cl
|
|||
}
|
||||
```
|
||||
|
||||
Neutral text colors (`text-stone-800`, `text-stone-500`, etc.) are used directly — they are not tokenised since they're static in both themes.
|
||||
Neutral text colors use the tokenised utilities (`text-text`, `text-text-secondary`, `text-text-muted`) which adapt automatically between light and dark themes. Do not use hardcoded `text-stone-*` classes for neutral text — always use the token-based classes.
|
||||
|
||||
#### Dark Mode
|
||||
|
||||
|
|
@ -459,11 +462,11 @@ Extra utilities (e.g., `w-28 justify-center whitespace-nowrap`) can be added alo
|
|||
| Level | Classes | Use |
|
||||
|-------|---------|-----|
|
||||
| Page title | `text-3xl font-semibold` | Top-level `<h1>` on each page |
|
||||
| Section title | `text-lg font-semibold text-stone-800` | `<h2>` / `<h3>` for form card titles |
|
||||
| Subsection title | `text-base font-semibold text-stone-800` | `<h3>` within cards (e.g. "Confirm cafe details") |
|
||||
| Form section label | `text-xs font-semibold text-stone-500 uppercase tracking-wide` | `<h4>` grouping related fields (e.g. "Coffee", "Grinder", "Water") |
|
||||
| Body / description | `text-sm text-stone-600` | Paragraph text below headings |
|
||||
| Muted secondary | `text-xs text-stone-500` | Subtext in option lists, metadata |
|
||||
| Section title | `text-lg font-semibold text-text` | `<h2>` / `<h3>` for form card titles |
|
||||
| Subsection title | `text-base font-semibold text-text` | `<h3>` within cards (e.g. "Confirm cafe details") |
|
||||
| Form section label | `text-xs font-semibold text-text-muted uppercase tracking-wide` | `<h4>` grouping related fields (e.g. "Coffee", "Grinder", "Water") |
|
||||
| Body / description | `text-sm text-text-secondary` | Paragraph text below headings |
|
||||
| Muted secondary | `text-xs text-text-muted` | Subtext in option lists, metadata |
|
||||
| Accent title | `text-2xl font-semibold text-accent` | Login / register page headers |
|
||||
|
||||
Page headers follow a consistent structure — title + constrained description:
|
||||
|
|
@ -471,7 +474,7 @@ Page headers follow a consistent structure — title + constrained description:
|
|||
```html
|
||||
<header class="flex flex-col gap-2">
|
||||
<h1 class="text-3xl font-semibold">Page Title</h1>
|
||||
<p class="max-w-2xl text-sm text-stone-600">Short description of what the page does.</p>
|
||||
<p class="max-w-2xl text-sm text-text-secondary">Short description of what the page does.</p>
|
||||
</header>
|
||||
```
|
||||
|
||||
|
|
@ -485,17 +488,17 @@ Use `w-full` for full-width CTAs. Use `py-3` for larger touch targets on final s
|
|||
|
||||
**Secondary** — cancel, back, alternative actions:
|
||||
```html
|
||||
<button class="rounded-md border px-4 py-2 text-sm font-semibold text-stone-700 transition hover:border-accent hover:text-stone-800">
|
||||
<button class="rounded-md border px-4 py-2 text-sm font-semibold text-text transition hover:border-accent hover:text-text">
|
||||
```
|
||||
|
||||
**Text-only** — minimal actions in summary bars (Change, Back):
|
||||
```html
|
||||
<button class="text-xs text-stone-500 hover:text-stone-700">
|
||||
<button class="text-xs text-text-muted hover:text-text">
|
||||
```
|
||||
|
||||
**Icon-only** — delete, revoke actions in rows/cards:
|
||||
```html
|
||||
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md p-1 text-stone-400 transition hover:text-red-600">
|
||||
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md p-1 text-text-muted transition hover:text-red-600">
|
||||
```
|
||||
|
||||
**Link-style** — inline actions with icon (Brew Again, View all):
|
||||
|
|
@ -520,7 +523,7 @@ Submit buttons are always right-aligned: `<div class="flex items-center justify-
|
|||
|
||||
```html
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Field Name *</span>
|
||||
<span class="text-text">Field Name *</span>
|
||||
<input type="text" name="field" required class="input-field" placeholder="Example" />
|
||||
</label>
|
||||
```
|
||||
|
|
@ -537,7 +540,7 @@ Mark required fields with `*` in the label text. The `input-field` class is defi
|
|||
```html
|
||||
<label class="inline-flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" name="flag" value="true" class="accent-orange-700" />
|
||||
<span class="font-semibold text-stone-800">Label text</span>
|
||||
<span class="font-semibold text-text">Label text</span>
|
||||
</label>
|
||||
```
|
||||
|
||||
|
|
@ -573,8 +576,8 @@ When a form requires a parent entity that doesn't exist yet (e.g. "roasts need a
|
|||
|
||||
```html
|
||||
{% if roaster_options.is_empty() %}
|
||||
<div class="text-sm text-stone-600">
|
||||
<h3 class="text-lg font-semibold text-stone-800">Add a roaster first</h3>
|
||||
<div class="text-sm text-text-secondary">
|
||||
<h3 class="text-lg font-semibold text-text">Add a roaster first</h3>
|
||||
<p class="mt-2">Roasts need a roaster. Add a roaster above to enable this form.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
@ -591,10 +594,10 @@ When a user selects an item (cafe, roast) in a multi-step flow, show a summary b
|
|||
data-show="$_step > 1 && $_cafeName" style="display: none">
|
||||
<div class="flex items-center gap-2">
|
||||
{% call icons::location("h-4 w-4 text-accent shrink-0") %}
|
||||
<span class="font-medium text-stone-800" data-text="$_cafeName"></span>
|
||||
<span class="text-xs text-stone-500" data-show="$_cafeCity" data-text="$_cafeCity" style="display:none"></span>
|
||||
<span class="font-medium text-text" data-text="$_cafeName"></span>
|
||||
<span class="text-xs text-text-muted" data-show="$_cafeCity" data-text="$_cafeCity" style="display:none"></span>
|
||||
</div>
|
||||
<button type="button" class="text-xs text-stone-500 hover:text-stone-700"
|
||||
<button type="button" class="text-xs text-text-muted hover:text-text"
|
||||
data-on:click="$_cafeName = ''; $_step = 1">Change</button>
|
||||
</div>
|
||||
```
|
||||
|
|
@ -609,7 +612,7 @@ Each page template passes `nav_active` to the layout. Nav links use conditional
|
|||
{% if nav_active == "data" %}
|
||||
class="text-accent font-medium"
|
||||
{% else %}
|
||||
class="text-stone-500 hover:text-stone-800 transition"
|
||||
class="text-text-muted hover:text-text transition"
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
|
|
@ -673,7 +676,7 @@ The `<searchable-select>` custom element (defined in `static/js/components/searc
|
|||
{% for roaster in roaster_options %}
|
||||
<button type="button" value="{{ roaster.id }}" data-display="{{ roaster.name }}"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
|
||||
<span class="font-medium text-stone-800">{{ roaster.name }}</span>
|
||||
<span class="font-medium text-text">{{ roaster.name }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</searchable-select>
|
||||
|
|
@ -899,7 +902,7 @@ List partials live in `templates/partials/lists/` (e.g., `roaster_list.html`, `b
|
|||
<div id="{entity}-list" class="mt-6" data-star-scope="{entity}">
|
||||
{% if items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
No {entities} recorded yet. Use the form above to add your first {entity}.
|
||||
|
|
@ -915,7 +918,7 @@ List partials live in `templates/partials/lists/` (e.g., `roaster_list.html`, `b
|
|||
<tbody>...</tbody>
|
||||
</table>
|
||||
{% if items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No {entities} match your search.</div>
|
||||
<div class="p-8 text-center text-text-muted">No {entities} match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(items, navigator, "#{entity}-list") %}
|
||||
{% if items.has_next() %}
|
||||
|
|
@ -953,12 +956,12 @@ Three macros are available:
|
|||
Every table has a sortable "Added" column as its **first column**, sorted by `created-at`. It uses a distinct smaller style to visually separate it from content columns:
|
||||
|
||||
```html
|
||||
<td data-label="Added" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
{{ item.created_at }}
|
||||
</td>
|
||||
```
|
||||
|
||||
The `text-xs font-medium text-stone-600` classes give it a muted, compact appearance compared to the default `text-sm` body text.
|
||||
The `text-xs font-medium text-text-secondary` classes give it a muted, compact appearance compared to the default `text-sm` body text.
|
||||
|
||||
### Actions Column
|
||||
|
||||
|
|
@ -984,7 +987,7 @@ Tables use the `responsive-table` CSS class which converts rows to card-style la
|
|||
```html
|
||||
<td data-label="Coffee" class="px-4 py-3 whitespace-nowrap">
|
||||
<div class="font-medium">{{ brew.roast_name }}</div>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ brew.roaster_name }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ brew.roaster_name }}</div>
|
||||
</td>
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@
|
|||
|
||||
/* Text */
|
||||
--text: #1c1917; /* stone-900 */
|
||||
--text-secondary: #78716c; /* stone-500 */
|
||||
--text-muted: #a8a29e; /* stone-400 */
|
||||
--text-secondary: #57534e; /* stone-600 */
|
||||
--text-muted: #78716c; /* stone-500 */
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
|
|
@ -48,8 +48,8 @@
|
|||
--accent-text: #ffffff;
|
||||
|
||||
--text: #e7e5e4;
|
||||
--text-secondary: #a8a29e;
|
||||
--text-muted: #78716c;
|
||||
--text-secondary: #d6d3d1;
|
||||
--text-muted: #a8a29e;
|
||||
}
|
||||
|
||||
/* ── Theme: map raw vars to Tailwind utilities ─────────────────── */
|
||||
|
|
@ -62,6 +62,9 @@
|
|||
--color-accent-hover: var(--accent-hover);
|
||||
--color-accent-subtle: var(--accent-subtle);
|
||||
--color-accent-text: var(--accent-text);
|
||||
--color-text: var(--text);
|
||||
--color-text-secondary: var(--text-secondary);
|
||||
--color-text-muted: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Base: default border color ────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ customElements.define("searchable-select", class extends HTMLElement {
|
|||
|
||||
const clear = document.createElement("button");
|
||||
clear.type = "button";
|
||||
clear.className = "absolute right-2 top-1/2 -translate-y-1/2 text-stone-400 hover:text-stone-600 transition";
|
||||
clear.className = "absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-secondary transition";
|
||||
clear.innerHTML = '<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" /></svg>';
|
||||
|
||||
const selectedWrap = document.createElement("div");
|
||||
|
|
|
|||
|
|
@ -97,11 +97,11 @@
|
|||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-page text-stone-900">
|
||||
<body class="min-h-screen bg-page text-text">
|
||||
<main class="mx-auto flex w-full max-w-5xl flex-col gap-8 px-6 py-10">
|
||||
{% include "partials/nav.html" %} {% block content %}{% endblock %}
|
||||
<footer class="mt-8 text-center text-xs tracking-widest text-stone-400" style="font-variant: small-caps">
|
||||
<p>B{rew}log by <a href="https://jnsgr.uk" class="text-stone-500 hover:text-accent transition" target="_blank" rel="noreferrer noopener">jnsgruk</a> · {{ version_info.version }} · <a href="https://github.com/jnsgruk/brewlog/commit/{{ version_info.commit }}" class="text-stone-500 hover:text-accent transition" target="_blank" rel="noreferrer noopener">{{ version_info.commit }}</a></p>
|
||||
<footer class="mt-8 text-center text-xs tracking-widest text-text-muted" style="font-variant: small-caps">
|
||||
<p>B{rew}log by <a href="https://jnsgr.uk" class="text-text-muted hover:text-accent transition" target="_blank" rel="noreferrer noopener">jnsgruk</a> · {{ version_info.version }} · <a href="https://github.com/jnsgruk/brewlog/commit/{{ version_info.commit }}" class="text-text-muted hover:text-accent transition" target="_blank" rel="noreferrer noopener">{{ version_info.commit }}</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@
|
|||
<div class="rounded-lg border bg-surface p-4 flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<span class="block font-semibold text-stone-800">{% call icons::key("inline h-4 w-4 mr-1 text-accent") %}{{ passkey.name }}</span>
|
||||
<span class="block font-semibold text-text">{% call icons::key("inline h-4 w-4 mr-1 text-accent") %}{{ passkey.name }}</span>
|
||||
</div>
|
||||
{% if passkeys.len() > 1 %}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md p-1 text-stone-400 transition hover:text-red-600"
|
||||
class="shrink-0 rounded-md p-1 text-text-muted transition hover:text-red-600"
|
||||
onclick="deletePasskey({{ passkey.id }}, '{{ passkey.name }}')"
|
||||
aria-label="Delete passkey"
|
||||
>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-2 space-y-0.5 text-sm text-stone-500">
|
||||
<div class="mt-2 space-y-0.5 text-sm text-text-muted">
|
||||
<p>Added {{ passkey.created_at }}</p>
|
||||
{% if let Some(last_used) = passkey.last_used_at %}
|
||||
<p>Last used {{ last_used }}</p>
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
</div>
|
||||
|
||||
{% if passkeys.len() <= 1 %}
|
||||
<p class="mt-2 text-xs text-stone-400">Add another passkey before removing your only one.</p>
|
||||
<p class="mt-2 text-xs text-text-muted">Add another passkey before removing your only one.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Add passkey form (hidden by default) -->
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
<div id="add-passkey-error" class="mb-3 hidden rounded-md bg-red-100 border border-red-300 p-2 text-sm text-red-800"></div>
|
||||
<div class="flex items-end gap-3">
|
||||
<label class="flex-1 flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Passkey Name</span>
|
||||
<span class="text-text">Passkey Name</span>
|
||||
<input
|
||||
type="text"
|
||||
id="passkey-name"
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick="hidePasskeyForm()"
|
||||
class="shrink-0 rounded-md px-3 py-2 text-sm font-medium text-stone-500 transition hover:text-stone-800"
|
||||
class="shrink-0 rounded-md px-3 py-2 text-sm font-medium text-text-muted transition hover:text-text"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
@ -93,25 +93,25 @@
|
|||
<h2 class="text-lg font-semibold text-accent mb-3">API Tokens</h2>
|
||||
|
||||
{% if tokens.is_empty() %}
|
||||
<p class="text-sm text-stone-500">No active tokens. Create one to use the CLI or API.</p>
|
||||
<p class="text-sm text-text-muted">No active tokens. Create one to use the CLI or API.</p>
|
||||
{% else %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{% for token in tokens %}
|
||||
<div class="rounded-lg border bg-surface p-4 flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<span class="block font-semibold text-stone-800">{{ token.name }}</span>
|
||||
<span class="block font-semibold text-text">{{ token.name }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md p-1 text-stone-400 transition hover:text-red-600"
|
||||
class="shrink-0 rounded-md p-1 text-text-muted transition hover:text-red-600"
|
||||
onclick="revokeToken({{ token.id }}, '{{ token.name }}')"
|
||||
aria-label="Revoke token"
|
||||
>
|
||||
{% call icons::delete("h-4 w-4") %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 space-y-0.5 text-sm text-stone-500">
|
||||
<div class="mt-2 space-y-0.5 text-sm text-text-muted">
|
||||
<p>Created {{ token.created_at }}</p>
|
||||
{% if let Some(last_used) = token.last_used_at %}
|
||||
<p>Last used {{ last_used }}</p>
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
<div id="create-token-error" class="mb-3 hidden rounded-md bg-red-100 border border-red-300 p-2 text-sm text-red-800"></div>
|
||||
<div class="flex items-end gap-3">
|
||||
<label class="flex-1 flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Token Name</span>
|
||||
<span class="text-text">Token Name</span>
|
||||
<input
|
||||
type="text"
|
||||
id="token-name"
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick="hideTokenForm()"
|
||||
class="shrink-0 rounded-md px-3 py-2 text-sm font-medium text-stone-500 transition hover:text-stone-800"
|
||||
class="shrink-0 rounded-md px-3 py-2 text-sm font-medium text-text-muted transition hover:text-text"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
@ -160,7 +160,7 @@
|
|||
<div id="token-created" class="mt-3 hidden rounded-lg border border-green-300 bg-green-50 p-5">
|
||||
<p class="text-sm font-medium text-green-800">Token created! Copy it now — you won't see it again.</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<code id="token-value" class="flex-1 rounded bg-surface px-3 py-2 text-sm font-mono text-stone-800 border border-green-200 break-all select-all"></code>
|
||||
<code id="token-value" class="flex-1 rounded bg-surface px-3 py-2 text-sm font-mono text-text border border-green-200 break-all select-all"></code>
|
||||
<button
|
||||
type="button"
|
||||
onclick="copyToken(this)"
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick="window.location.reload()"
|
||||
class="mt-2 text-sm text-stone-500 hover:text-stone-700"
|
||||
class="mt-2 text-sm text-text-muted hover:text-text"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
|
|
@ -191,7 +191,7 @@
|
|||
<!-- Data -->
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-accent mb-3">Data</h2>
|
||||
<p class="text-sm text-stone-500 mb-3">Export all coffee data as JSON, or restore from a previous backup.</p>
|
||||
<p class="text-sm text-text-muted mb-3">Export all coffee data as JSON, or restore from a previous backup.</p>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border bg-surface px-4 py-2 text-sm font-medium text-stone-600 transition hover:bg-surface-alt hover:text-stone-800"
|
||||
class="rounded-md border bg-surface px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-surface-alt hover:text-text"
|
||||
onclick="document.getElementById('restore-file-input').click()"
|
||||
>
|
||||
Restore from Backup
|
||||
|
|
@ -223,16 +223,16 @@
|
|||
<h2 class="text-lg font-semibold text-accent mb-3">OpenRouter Usage</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="rounded-lg border bg-surface p-4">
|
||||
<span class="block text-sm text-stone-500">Total Cost</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-stone-800">{{ usage.cost }}</span>
|
||||
<span class="block text-sm text-text-muted">Total Cost</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-text">{{ usage.cost }}</span>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-surface p-4">
|
||||
<span class="block text-sm text-stone-500">API Calls</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-stone-800">{{ usage.calls }}</span>
|
||||
<span class="block text-sm text-text-muted">API Calls</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-text">{{ usage.calls }}</span>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-surface p-4">
|
||||
<span class="block text-sm text-stone-500">Total Tokens</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-stone-800">{{ usage.tokens }}</span>
|
||||
<span class="block text-sm text-text-muted">Total Tokens</span>
|
||||
<span class="mt-1 block text-lg font-semibold text-text">{{ usage.tokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
data-signals:_qn-under-extracted="false"
|
||||
data-signals:_qn-over-extracted="false"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-stone-800 mb-3">Manual Add</h2>
|
||||
<h2 class="text-lg font-semibold text-text mb-3">Manual Add</h2>
|
||||
|
||||
<!-- Entity type selector -->
|
||||
<div class="rounded-lg border bg-surface mb-4">
|
||||
|
|
@ -98,26 +98,26 @@
|
|||
<!-- ========== ROASTER FORM ========== -->
|
||||
<div data-show="$_addType === 'roaster'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-stone-800">New Roaster</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Provide the core details and Brewlog will keep track of everything for you.</p>
|
||||
<h3 class="text-lg font-semibold text-text">New Roaster</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Provide the core details and Brewlog will keep track of everything for you.</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/api/v1/roasters" class="mt-4 flex flex-col gap-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Name *</span>
|
||||
<span class="text-text">Name *</span>
|
||||
<input type="text" name="name" required class="input-field" placeholder="Example Coffee Roasters" data-bind:_add-roaster-name />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Country *</span>
|
||||
<span class="text-text">Country *</span>
|
||||
<input type="text" name="country" required class="input-field" placeholder="United States" data-bind:_add-roaster-country />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">City</span>
|
||||
<span class="text-text">City</span>
|
||||
<input type="text" name="city" class="input-field" placeholder="Portland" data-bind:_add-roaster-city />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Homepage</span>
|
||||
<span class="text-text">Homepage</span>
|
||||
<input type="url" name="homepage" class="input-field" placeholder="https://example.coffee" data-bind:_add-roaster-homepage />
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -132,51 +132,51 @@
|
|||
<!-- ========== ROAST FORM ========== -->
|
||||
<div data-show="$_addType === 'roast'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||
{% if roaster_options.is_empty() %}
|
||||
<div class="text-sm text-stone-600">
|
||||
<h3 class="text-lg font-semibold text-stone-800">Add a roaster first</h3>
|
||||
<div class="text-sm text-text-secondary">
|
||||
<h3 class="text-lg font-semibold text-text">Add a roaster first</h3>
|
||||
<p class="mt-2">Roasts need a roaster. Add a roaster above to enable this form.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-stone-800">New Roast</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Select a roaster and fill in the details.</p>
|
||||
<h3 class="text-lg font-semibold text-text">New Roast</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Select a roaster and fill in the details.</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/api/v1/roasts" class="mt-4 flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Roaster *</span>
|
||||
<span class="text-text">Roaster *</span>
|
||||
<searchable-select name="roaster_id" placeholder="Type to search roasters…">
|
||||
{% for roaster in roaster_options %}
|
||||
<button type="button" value="{{ roaster.id }}" data-display="{{ roaster.name }}"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
|
||||
<span class="font-medium text-stone-800">{{ roaster.name }}</span>
|
||||
<span class="font-medium text-text">{{ roaster.name }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</searchable-select>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Roast Name *</span>
|
||||
<span class="text-text">Roast Name *</span>
|
||||
<input type="text" name="name" required class="input-field" placeholder="Ethiopia Yirgacheffe" data-bind:_add-roast-name />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Origin *</span>
|
||||
<span class="text-text">Origin *</span>
|
||||
<input type="text" name="origin" required class="input-field" placeholder="Ethiopia" data-bind:_add-origin />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Region *</span>
|
||||
<span class="text-text">Region *</span>
|
||||
<input type="text" name="region" required class="input-field" placeholder="Guji" data-bind:_add-region />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Producer *</span>
|
||||
<span class="text-text">Producer *</span>
|
||||
<input type="text" name="producer" required class="input-field" placeholder="Chelbesa Cooperative" data-bind:_add-producer />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Process *</span>
|
||||
<span class="text-text">Process *</span>
|
||||
<input type="text" name="process" required class="input-field" placeholder="Washed" data-bind:_add-process />
|
||||
</label>
|
||||
<label class="sm:col-span-2 flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Tasting Notes * (comma or newline separated)</span>
|
||||
<span class="text-text">Tasting Notes * (comma or newline separated)</span>
|
||||
<textarea name="tasting_notes" rows="2" required class="input-field" placeholder="Blueberry, Jasmine" data-bind:_add-tasting-notes></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -192,35 +192,35 @@
|
|||
<!-- ========== BAG FORM ========== -->
|
||||
<div data-show="$_addType === 'bag'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||
{% if roast_options.is_empty() %}
|
||||
<div class="text-sm text-stone-600">
|
||||
<h3 class="text-lg font-semibold text-stone-800">Add a roast first</h3>
|
||||
<div class="text-sm text-text-secondary">
|
||||
<h3 class="text-lg font-semibold text-text">Add a roast first</h3>
|
||||
<p class="mt-2">Bags need a roast. Add a roaster and roast to enable this form.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-stone-800">New Bag</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Select a roast and enter the details.</p>
|
||||
<h3 class="text-lg font-semibold text-text">New Bag</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Select a roast and enter the details.</p>
|
||||
</div>
|
||||
<form method="post" action="/api/v1/bags" class="mt-4 flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Roast *</span>
|
||||
<span class="text-text">Roast *</span>
|
||||
<searchable-select name="roast_id" placeholder="Type to search roasts…">
|
||||
{% for roast in roast_options %}
|
||||
<button type="button" value="{{ roast.id }}" data-display="{{ roast.label }}"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
|
||||
<span class="font-medium text-stone-800">{{ roast.name }}</span>
|
||||
<span class="ml-2 text-xs text-stone-500">{{ roast.roaster_name }}</span>
|
||||
<span class="font-medium text-text">{{ roast.name }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted">{{ roast.roaster_name }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</searchable-select>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Roast Date</span>
|
||||
<span class="text-text">Roast Date</span>
|
||||
<input type="date" name="roast_date" class="input-field" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Amount (g) *</span>
|
||||
<span class="text-text">Amount (g) *</span>
|
||||
<input type="number" name="amount" step="0.1" required class="input-field" placeholder="250" />
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -236,39 +236,39 @@
|
|||
<!-- ========== BREW FORM ========== -->
|
||||
<div data-show="$_addType === 'brew'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||
{% if bag_options.is_empty() %}
|
||||
<div class="text-sm text-stone-600">
|
||||
<h3 class="text-lg font-semibold text-stone-800">Open a bag first</h3>
|
||||
<div class="text-sm text-text-secondary">
|
||||
<h3 class="text-lg font-semibold text-text">Open a bag first</h3>
|
||||
<p class="mt-2">Brews need an open bag of coffee. Add a bag to enable this form.</p>
|
||||
</div>
|
||||
{% else if grinder_options.is_empty() || brewer_options.is_empty() %}
|
||||
<div class="text-sm text-stone-600">
|
||||
<h3 class="text-lg font-semibold text-stone-800">Add your gear first</h3>
|
||||
<div class="text-sm text-text-secondary">
|
||||
<h3 class="text-lg font-semibold text-text">Add your gear first</h3>
|
||||
<p class="mt-2">Brews need a grinder and brewer. Add your gear to enable this form.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-stone-800">New Brew</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Log a cup of coffee.</p>
|
||||
<h3 class="text-lg font-semibold text-text">New Brew</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Log a cup of coffee.</p>
|
||||
</div>
|
||||
<form method="post" action="/api/v1/brews" class="mt-4 flex flex-col gap-6 overflow-hidden">
|
||||
<!-- Coffee -->
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-3">Coffee</h4>
|
||||
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Coffee</h4>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Bag *</span>
|
||||
<span class="text-text">Bag *</span>
|
||||
<searchable-select name="bag_id" placeholder="Type to search bags…">
|
||||
{% for bag in bag_options %}
|
||||
<button type="button" value="{{ bag.id }}" data-display="{{ bag.roast_name }}"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
|
||||
<span class="font-medium text-stone-800">{{ bag.roast_name }}</span>
|
||||
<span class="ml-2 text-xs text-stone-500">{{ bag.roaster_name }} · {{ bag.remaining }}</span>
|
||||
<span class="font-medium text-text">{{ bag.roast_name }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted">{{ bag.roaster_name }} · {{ bag.remaining }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</searchable-select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Weight (g) *</span>
|
||||
<span class="text-text">Weight (g) *</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-adjust" data-on:click="$_brewWeight = Math.max(1, $_brewWeight - 0.5)">-</button>
|
||||
<input type="number" name="coffee_weight" step="any" min="1" required class="input-field flex-1 text-center" data-attr:value="$_brewWeight" data-on:input="$_brewWeight = this.valueAsNumber" />
|
||||
|
|
@ -280,10 +280,10 @@
|
|||
|
||||
<!-- Grinder -->
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-3">Grinder</h4>
|
||||
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Grinder</h4>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Grinder *</span>
|
||||
<span class="text-text">Grinder *</span>
|
||||
<select name="grinder_id" required class="input-field">
|
||||
{% for grinder in grinder_options %}
|
||||
<option value="{{ grinder.id }}"{% if grinder.id == defaults.grinder_id %} selected{% endif %}>{{ grinder.label }}</option>
|
||||
|
|
@ -291,7 +291,7 @@
|
|||
</select>
|
||||
</label>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Grind Setting *</span>
|
||||
<span class="text-text">Grind Setting *</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-adjust" data-on:click="$_brewGrind = Math.max(0, $_brewGrind - 0.5)">-</button>
|
||||
<input type="number" name="grind_setting" step="any" min="0" required class="input-field flex-1 text-center" data-attr:value="$_brewGrind" data-on:input="$_brewGrind = this.valueAsNumber" />
|
||||
|
|
@ -303,10 +303,10 @@
|
|||
|
||||
<!-- Brewer -->
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-3">Brewer</h4>
|
||||
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Brewer</h4>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Brewer *</span>
|
||||
<span class="text-text">Brewer *</span>
|
||||
<select name="brewer_id" required class="input-field">
|
||||
{% for brewer in brewer_options %}
|
||||
<option value="{{ brewer.id }}"{% if brewer.id == defaults.brewer_id %} selected{% endif %}>{{ brewer.label }}</option>
|
||||
|
|
@ -314,7 +314,7 @@
|
|||
</select>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Filter Paper</span>
|
||||
<span class="text-text">Filter Paper</span>
|
||||
<select name="filter_paper_id" class="input-field">
|
||||
<option value="">None</option>
|
||||
{% for fp in filter_paper_options %}
|
||||
|
|
@ -327,10 +327,10 @@
|
|||
|
||||
<!-- Water -->
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-3">Water</h4>
|
||||
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Water</h4>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Volume (ml) *</span>
|
||||
<span class="text-text">Volume (ml) *</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-adjust" data-on:click="$_brewVolume = Math.max(10, $_brewVolume - 10)">-</button>
|
||||
<input type="number" name="water_volume" step="any" min="10" required class="input-field flex-1 text-center" data-attr:value="$_brewVolume" data-on:input="$_brewVolume = this.valueAsNumber" />
|
||||
|
|
@ -338,7 +338,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Temp (°C) *</span>
|
||||
<span class="text-text">Temp (°C) *</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-adjust" data-on:click="$_brewTemp = Math.max(0, $_brewTemp - 0.5)">-</button>
|
||||
<input type="number" name="water_temp" step="any" min="0" max="100" required class="input-field flex-1 text-center" data-attr:value="$_brewTemp" data-on:input="$_brewTemp = this.valueAsNumber" />
|
||||
|
|
@ -349,7 +349,7 @@
|
|||
</div>
|
||||
<!-- Quick Notes -->
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-3">Quick Notes</h4>
|
||||
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Quick Notes</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="button"
|
||||
data-on:click="$_qnGood = !$_qnGood"
|
||||
|
|
@ -385,13 +385,13 @@
|
|||
<!-- ========== GEAR FORM ========== -->
|
||||
<div data-show="$_addType === 'gear'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-stone-800">New Gear</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Add brewing equipment to your collection.</p>
|
||||
<h3 class="text-lg font-semibold text-text">New Gear</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Add brewing equipment to your collection.</p>
|
||||
</div>
|
||||
<form method="post" action="/api/v1/gear" class="mt-4 flex flex-col gap-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Category *</span>
|
||||
<span class="text-text">Category *</span>
|
||||
<select name="category" required class="input-field">
|
||||
<option value="">Select a category</option>
|
||||
<option value="grinder">Grinder</option>
|
||||
|
|
@ -400,11 +400,11 @@
|
|||
</select>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Make *</span>
|
||||
<span class="text-text">Make *</span>
|
||||
<input type="text" name="make" required class="input-field" placeholder="Baratza" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Model *</span>
|
||||
<span class="text-text">Model *</span>
|
||||
<input type="text" name="model" required class="input-field" placeholder="Encore" />
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -419,8 +419,8 @@
|
|||
<!-- ========== CAFE FORM ========== -->
|
||||
<div data-show="$_addType === 'cafe'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-stone-800">New Cafe</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Search for a nearby cafe or enter details manually.</p>
|
||||
<h3 class="text-lg font-semibold text-text">New Cafe</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Search for a nearby cafe or enter details manually.</p>
|
||||
</div>
|
||||
|
||||
<!-- Location search -->
|
||||
|
|
@ -433,11 +433,11 @@
|
|||
<div class="mt-4 border-b pb-4 flex items-center justify-between" data-show="$_reviewingCafe" style="display:none">
|
||||
<div class="flex items-center gap-2">
|
||||
{% call icons::location("h-4 w-4 text-accent shrink-0") %}
|
||||
<span class="text-sm font-medium text-stone-800" data-text="$_cafeName"></span>
|
||||
<span class="text-xs text-stone-500" data-show="$_cafeCity" data-text="$_cafeCity" style="display:none"></span>
|
||||
<span class="text-sm font-medium text-text" data-text="$_cafeName"></span>
|
||||
<span class="text-xs text-text-muted" data-show="$_cafeCity" data-text="$_cafeCity" style="display:none"></span>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="text-xs text-stone-500 hover:text-stone-700"
|
||||
class="text-xs text-text-muted hover:text-text"
|
||||
data-on:click="$_reviewingCafe = false; $_cafeName = ''; $_cafeCity = ''; $_cafeCountry = ''; $_cafeLat = ''; $_cafeLng = ''; $_cafeWebsite = ''"
|
||||
>Back</button>
|
||||
</div>
|
||||
|
|
@ -445,27 +445,27 @@
|
|||
<form id="cafe-form" method="post" action="/api/v1/cafes" class="mt-4 flex flex-col gap-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Name *</span>
|
||||
<span class="text-text">Name *</span>
|
||||
<input type="text" name="name" required class="input-field" placeholder="Blue Bottle Coffee" data-bind:_cafe-name />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">City *</span>
|
||||
<span class="text-text">City *</span>
|
||||
<input type="text" name="city" required class="input-field" placeholder="San Francisco" data-bind:_cafe-city />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Country *</span>
|
||||
<span class="text-text">Country *</span>
|
||||
<input type="text" name="country" required class="input-field" placeholder="United States" data-bind:_cafe-country />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Website</span>
|
||||
<span class="text-text">Website</span>
|
||||
<input type="url" name="website" class="input-field" placeholder="https://bluebottlecoffee.com" data-bind:_cafe-website />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Latitude *</span>
|
||||
<span class="text-text">Latitude *</span>
|
||||
<input type="number" name="latitude" step="any" required class="input-field" placeholder="37.7749" data-bind:_cafe-lat />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Longitude *</span>
|
||||
<span class="text-text">Longitude *</span>
|
||||
<input type="number" name="longitude" step="any" required class="input-field" placeholder="-122.4194" data-bind:_cafe-lng />
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -480,37 +480,37 @@
|
|||
<!-- ========== CUP FORM ========== -->
|
||||
<div data-show="$_addType === 'cup'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||
{% if roast_options.is_empty() || cafe_options.is_empty() %}
|
||||
<div class="text-sm text-stone-600">
|
||||
<h3 class="text-lg font-semibold text-stone-800">Add a roast and cafe first</h3>
|
||||
<div class="text-sm text-text-secondary">
|
||||
<h3 class="text-lg font-semibold text-text">Add a roast and cafe first</h3>
|
||||
<p class="mt-2">Cups need both a roast and a cafe. Add them above to enable this form.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-stone-800">New Cup</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Record a coffee you had at a cafe.</p>
|
||||
<h3 class="text-lg font-semibold text-text">New Cup</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Record a coffee you had at a cafe.</p>
|
||||
</div>
|
||||
<form method="post" action="/api/v1/cups" class="mt-4 flex flex-col gap-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Coffee *</span>
|
||||
<span class="text-text">Coffee *</span>
|
||||
<searchable-select name="roast_id" placeholder="Type to search roasts…">
|
||||
{% for roast in roast_options %}
|
||||
<button type="button" value="{{ roast.id }}" data-display="{{ roast.name }}"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
|
||||
<span class="font-medium text-stone-800">{{ roast.name }}</span>
|
||||
<span class="ml-2 text-xs text-stone-500">{{ roast.roaster_name }}</span>
|
||||
<span class="font-medium text-text">{{ roast.name }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted">{{ roast.roaster_name }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</searchable-select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Cafe *</span>
|
||||
<span class="text-text">Cafe *</span>
|
||||
<searchable-select name="cafe_id" placeholder="Type to search cafes…">
|
||||
{% for cafe in cafe_options %}
|
||||
<button type="button" value="{{ cafe.id }}" data-display="{{ cafe.name }}"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
|
||||
<span class="font-medium text-stone-800">{{ cafe.name }}</span>
|
||||
<span class="ml-2 text-xs text-stone-500">{{ cafe.city }}</span>
|
||||
<span class="font-medium text-text">{{ cafe.name }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted">{{ cafe.city }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</searchable-select>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const filterList = (input, listId) => {
|
|||
>
|
||||
<header class="flex flex-col gap-2">
|
||||
<h1 class="text-3xl font-semibold">Check In</h1>
|
||||
<p class="max-w-2xl text-sm text-stone-600">
|
||||
<p class="max-w-2xl text-sm text-text-secondary">
|
||||
Record a cup of coffee at a cafe.
|
||||
</p>
|
||||
</header>
|
||||
|
|
@ -52,7 +52,7 @@ const filterList = (input, listId) => {
|
|||
<!-- Step progress -->
|
||||
<div class="relative mx-auto mt-6" style="width: 80%">
|
||||
<!-- Background track (continuous line through dot centers) -->
|
||||
<div class="absolute bg-stone-300" style="height: 3px; top: 8px; left: 10px; right: 10px"></div>
|
||||
<div class="absolute bg-border" style="height: 3px; top: 8px; left: 10px; right: 10px"></div>
|
||||
<!-- Accent overlay: first segment -->
|
||||
<div class="absolute bg-accent" style="height: 3px; top: 8px; left: 10px; right: 50%; display: none"
|
||||
data-show="$_step > 1"
|
||||
|
|
@ -65,26 +65,26 @@ const filterList = (input, listId) => {
|
|||
<div class="relative flex justify-between">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="h-5 w-5 rounded-full border-[3px] bg-page transition-all"
|
||||
data-class="{'border-accent': $_step >= 1, 'border-stone-300': $_step < 1}"
|
||||
data-class="{'border-accent': $_step >= 1, 'border-border': $_step < 1}"
|
||||
></div>
|
||||
<span class="mt-2 text-xs font-medium transition-colors"
|
||||
data-class="{'text-accent': $_step === 1, 'text-stone-800': $_step > 1}"
|
||||
data-class="{'text-accent': $_step === 1, 'text-text': $_step > 1}"
|
||||
>Cafe</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="h-5 w-5 rounded-full border-[3px] bg-page transition-all"
|
||||
data-class="{'border-accent': $_step >= 2, 'border-stone-300': $_step < 2}"
|
||||
data-class="{'border-accent': $_step >= 2, 'border-border': $_step < 2}"
|
||||
></div>
|
||||
<span class="mt-2 text-xs font-medium transition-colors"
|
||||
data-class="{'text-accent': $_step === 2, 'text-stone-800': $_step > 2, 'text-stone-400': $_step < 2}"
|
||||
data-class="{'text-accent': $_step === 2, 'text-text': $_step > 2, 'text-text-muted': $_step < 2}"
|
||||
>Coffee</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="h-5 w-5 rounded-full border-[3px] bg-page transition-all"
|
||||
data-class="{'border-accent': $_step >= 3, 'border-stone-300': $_step < 3}"
|
||||
data-class="{'border-accent': $_step >= 3, 'border-border': $_step < 3}"
|
||||
></div>
|
||||
<span class="mt-2 text-xs font-medium transition-colors"
|
||||
data-class="{'text-accent': $_step === 3, 'text-stone-400': $_step < 3}"
|
||||
data-class="{'text-accent': $_step === 3, 'text-text-muted': $_step < 3}"
|
||||
>Submit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -98,13 +98,13 @@ const filterList = (input, listId) => {
|
|||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{% call icons::location("h-4 w-4 text-accent shrink-0") %}
|
||||
<span class="font-medium text-stone-800" data-text="$_cafeName"></span>
|
||||
<span class="text-xs text-stone-500" data-show="$_cafeCity" data-text="$_cafeCity" style="display: none"></span>
|
||||
<span class="font-medium text-text" data-text="$_cafeName"></span>
|
||||
<span class="text-xs text-text-muted" data-show="$_cafeCity" data-text="$_cafeCity" style="display: none"></span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-on:click="$_cafeId = ''; $_cafeName = ''; $_step = 1"
|
||||
class="text-xs text-stone-500 hover:text-stone-700"
|
||||
class="text-xs text-text-muted hover:text-text"
|
||||
>Change</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -116,13 +116,13 @@ const filterList = (input, listId) => {
|
|||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{% call icons::bag("h-4 w-4 text-accent shrink-0") %}
|
||||
<span class="font-medium text-stone-800" data-text="$_roastName"></span>
|
||||
<span class="text-xs text-stone-500" data-show="$_roasterName" data-text="$_roasterName" style="display: none"></span>
|
||||
<span class="font-medium text-text" data-text="$_roastName"></span>
|
||||
<span class="text-xs text-text-muted" data-show="$_roasterName" data-text="$_roasterName" style="display: none"></span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-on:click="$_roastId = ''; $_roastName = ''; $_roasterName = ''; $_scanSuccess = ''; $_step = 2"
|
||||
class="text-xs text-stone-500 hover:text-stone-700"
|
||||
class="text-xs text-text-muted hover:text-text"
|
||||
>Change</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -146,30 +146,30 @@ const filterList = (input, listId) => {
|
|||
data-show="$_reviewingCafe"
|
||||
style="display: none"
|
||||
>
|
||||
<h3 class="text-base font-semibold text-stone-800 mb-1">Confirm cafe details</h3>
|
||||
<p class="text-sm text-stone-600 mb-3">Review and edit the details before saving.</p>
|
||||
<h3 class="text-base font-semibold text-text mb-1">Confirm cafe details</h3>
|
||||
<p class="text-sm text-text-secondary mb-3">Review and edit the details before saving.</p>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Name *</span>
|
||||
<span class="text-text">Name *</span>
|
||||
<input type="text" data-bind:_cafe-name required class="input-field" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">City</span>
|
||||
<span class="text-text">City</span>
|
||||
<input type="text" data-bind:_cafe-city class="input-field" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Country</span>
|
||||
<span class="text-text">Country</span>
|
||||
<input type="text" data-bind:_cafe-country class="input-field" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Website</span>
|
||||
<span class="text-text">Website</span>
|
||||
<input type="url" data-bind:_cafe-website class="input-field" placeholder="https://…" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border px-4 py-2 text-sm font-semibold text-stone-700 transition hover:border-accent hover:text-stone-800"
|
||||
class="rounded-md border px-4 py-2 text-sm font-semibold text-text transition hover:border-accent hover:text-text"
|
||||
data-on:click="$_reviewingCafe = false; $_cafeName = ''; $_cafeCity = ''; $_cafeCountry = ''; $_cafeLat = 0; $_cafeLng = 0; $_cafeWebsite = ''"
|
||||
>
|
||||
Back
|
||||
|
|
@ -187,7 +187,7 @@ const filterList = (input, listId) => {
|
|||
<!-- Saved cafes -->
|
||||
{% if !cafe_options.is_empty() %}
|
||||
<div class="mt-3 border-t pt-3" data-show="!$_reviewingCafe">
|
||||
<p class="mb-2 text-xs text-stone-500">Or choose a saved cafe:</p>
|
||||
<p class="mb-2 text-xs text-text-muted">Or choose a saved cafe:</p>
|
||||
<input
|
||||
type="text"
|
||||
class="input-field w-full text-sm"
|
||||
|
|
@ -203,8 +203,8 @@ const filterList = (input, listId) => {
|
|||
data-name="{{ cafe.name }}"
|
||||
data-city="{{ cafe.city }}"
|
||||
>
|
||||
<span class="font-medium text-stone-800">{{ cafe.name }}</span>
|
||||
<span class="ml-2 text-xs text-stone-500">{{ cafe.city }}</span>
|
||||
<span class="font-medium text-text">{{ cafe.name }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted">{{ cafe.city }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
@ -216,10 +216,10 @@ const filterList = (input, listId) => {
|
|||
<!-- Step 2: Roast identification -->
|
||||
<div class="mt-4" data-show="$_step === 2" style="display: none">
|
||||
<div class="rounded-lg border bg-surface p-5">
|
||||
<h3 class="text-base font-semibold text-stone-800 mb-3">What are you drinking?</h3>
|
||||
<h3 class="text-base font-semibold text-text mb-3">What are you drinking?</h3>
|
||||
|
||||
<div data-show="!$_scanSuccess" class="mb-4">
|
||||
<p class="text-sm text-stone-600 mb-3" data-show="!$_scanWaiting">Scan a bag to identify the coffee, or select from your existing roasts below.</p>
|
||||
<p class="text-sm text-text-secondary mb-3" data-show="!$_scanWaiting">Scan a bag to identify the coffee, or select from your existing roasts below.</p>
|
||||
|
||||
<form id="checkin-scan-form"
|
||||
data-on:submit="$_scanWaiting = true; $_scanError = ''; @post('/api/v1/scan', {contentType: 'form'})"
|
||||
|
|
@ -239,7 +239,7 @@ const filterList = (input, listId) => {
|
|||
|
||||
{% if !roast_options.is_empty() %}
|
||||
<div class="border-t pt-3">
|
||||
<p class="text-xs text-stone-500 mb-2">Or select an existing roast:</p>
|
||||
<p class="text-xs text-text-muted mb-2">Or select an existing roast:</p>
|
||||
<input
|
||||
type="text"
|
||||
class="input-field w-full text-sm"
|
||||
|
|
@ -255,8 +255,8 @@ const filterList = (input, listId) => {
|
|||
data-name="{{ roast.name }}"
|
||||
data-roaster="{{ roast.roaster_name }}"
|
||||
>
|
||||
<span class="font-medium text-stone-800">{{ roast.name }}</span>
|
||||
<span class="ml-2 text-xs text-stone-500">{{ roast.roaster_name }}</span>
|
||||
<span class="font-medium text-text">{{ roast.name }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted">{{ roast.roaster_name }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
@ -268,15 +268,15 @@ const filterList = (input, listId) => {
|
|||
<!-- Step 3: Review + submit -->
|
||||
<div class="mt-4" data-show="$_step === 3" style="display: none">
|
||||
<div class="rounded-lg border bg-surface p-5">
|
||||
<h3 class="text-base font-semibold text-stone-800 mb-4">Review & Submit</h3>
|
||||
<h3 class="text-base font-semibold text-text mb-4">Review & Submit</h3>
|
||||
<dl class="flex flex-col gap-3 text-sm mb-6">
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="font-medium text-stone-500">Cafe</dt>
|
||||
<dd class="text-right text-stone-800" data-text="$_cafeCity ? $_cafeName + ', ' + $_cafeCity : $_cafeName"></dd>
|
||||
<dt class="font-medium text-text-muted">Cafe</dt>
|
||||
<dd class="text-right text-text" data-text="$_cafeCity ? $_cafeName + ', ' + $_cafeCity : $_cafeName"></dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="font-medium text-stone-500">Coffee</dt>
|
||||
<dd class="text-right text-stone-800" data-text="$_roasterName ? $_roastName + ' (' + $_roasterName + ')' : $_roastName"></dd>
|
||||
<dt class="font-medium text-text-muted">Coffee</dt>
|
||||
<dd class="text-right text-text" data-text="$_roasterName ? $_roastName + ' (' + $_roasterName + ')' : $_roastName"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="rounded-lg border bg-surface p-6">
|
||||
{% if token.is_some() %}
|
||||
<h1 class="text-2xl font-semibold text-green-700">Authenticated</h1>
|
||||
<p class="mt-2 text-sm text-stone-600">
|
||||
<p class="mt-2 text-sm text-text-secondary">
|
||||
Your CLI has been authenticated. You can close this window.
|
||||
</p>
|
||||
{% else if error.is_some() %}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<h1 class="text-2xl font-semibold text-accent">CLI Authentication</h1>
|
||||
<p class="mt-2 text-sm text-stone-600">
|
||||
<p class="mt-2 text-sm text-text-secondary">
|
||||
Processing authentication...
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<header class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-3xl font-semibold">Data</h1>
|
||||
<p class="max-w-2xl text-sm text-stone-600">Browse all your coffee data.</p>
|
||||
<p class="max-w-2xl text-sm text-text-secondary">Browse all your coffee data.</p>
|
||||
</div>
|
||||
{% if is_authenticated %}
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
{% if !recent_brews.is_empty() %}
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold text-stone-800">Recent Brews</h2>
|
||||
<h2 class="text-lg font-semibold text-text">Recent Brews</h2>
|
||||
<a href="/data?type=brews" class="text-sm text-accent hover:text-accent-hover font-medium">View all brews →</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 items-start gap-4">
|
||||
|
|
@ -84,15 +84,15 @@
|
|||
<div class="rounded-lg border bg-surface p-4 min-w-0">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<span class="block truncate font-semibold text-stone-800">{{ brew.roast_name }}</span>
|
||||
<p class="truncate text-sm text-stone-500">{{ brew.roaster_name }}</p>
|
||||
<span class="block truncate font-semibold text-text">{{ brew.roast_name }}</span>
|
||||
<p class="truncate text-sm text-text-muted">{{ brew.roaster_name }}</p>
|
||||
</div>
|
||||
<time class="text-xs text-stone-500 whitespace-nowrap shrink-0">{{ brew.relative_date_label }}</time>
|
||||
<time class="text-xs text-text-muted whitespace-nowrap shrink-0">{{ brew.relative_date_label }}</time>
|
||||
</div>
|
||||
<div class="mt-2 space-y-0.5 text-sm">
|
||||
<p class="truncate"><span class="font-medium text-stone-800">{{ brew.coffee_weight }} / {{ brew.water_volume }}</span> <span class="text-stone-500">· {{ brew.ratio }} · {{ brew.water_temp }}</span></p>
|
||||
<p class="truncate"><span class="font-medium text-stone-800">{{ brew.grind_setting }}</span> <span class="text-stone-500">· {{ brew.grinder_name }}</span></p>
|
||||
<p class="truncate"><span class="font-medium text-stone-800">{{ brew.brewer_name }}</span>{% if let Some(fp) = brew.filter_paper_name %} <span class="text-stone-500">· {{ fp }}</span>{% endif %}</p>
|
||||
<p class="truncate"><span class="font-medium text-text">{{ brew.coffee_weight }} / {{ brew.water_volume }}</span> <span class="text-text-muted">· {{ brew.ratio }} · {{ brew.water_temp }}</span></p>
|
||||
<p class="truncate"><span class="font-medium text-text">{{ brew.grind_setting }}</span> <span class="text-text-muted">· {{ brew.grinder_name }}</span></p>
|
||||
<p class="truncate"><span class="font-medium text-text">{{ brew.brewer_name }}</span>{% if let Some(fp) = brew.filter_paper_name %} <span class="text-text-muted">· {{ fp }}</span>{% endif %}</p>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t flex items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
|
|
@ -139,7 +139,7 @@
|
|||
{% if !open_bags.is_empty() %}
|
||||
<section id="open-bags-section">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold text-stone-800">Open Bags</h2>
|
||||
<h2 class="text-lg font-semibold text-text">Open Bags</h2>
|
||||
<a href="/data?type=bags" class="text-sm text-accent hover:text-accent-hover font-medium">View all bags →</a>
|
||||
</div>
|
||||
<div id="open-bags-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
{% if !recent_events.is_empty() %}
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold text-stone-800">Recent Activity</h2>
|
||||
<h2 class="text-lg font-semibold text-text">Recent Activity</h2>
|
||||
<a href="/timeline" class="text-sm text-accent hover:text-accent-hover font-medium">View full timeline →</a>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
|
|
@ -162,9 +162,9 @@
|
|||
<div class="rounded-lg border bg-surface px-4 py-3 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="pill pill-muted w-28 shrink-0 justify-center whitespace-nowrap">{{ event.kind_label }}</span>
|
||||
<p class="min-w-0 truncate text-sm"><a href="{{ event.link }}" class="font-medium text-stone-700 hover:text-accent">{{ event.title }}</a>{% if let Some(sub) = event.subtitle %} <span class="text-xs text-stone-400 italic">· {{ sub }}</span>{% endif %}</p>
|
||||
<p class="min-w-0 truncate text-sm"><a href="{{ event.link }}" class="font-medium text-text hover:text-accent">{{ event.title }}</a>{% if let Some(sub) = event.subtitle %} <span class="text-xs text-text-muted italic">· {{ sub }}</span>{% endif %}</p>
|
||||
</div>
|
||||
<time class="text-xs text-stone-500 whitespace-nowrap shrink-0">{{ event.relative_date_label }}</time>
|
||||
<time class="text-xs text-text-muted whitespace-nowrap shrink-0">{{ event.relative_date_label }}</time>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
@ -174,43 +174,43 @@
|
|||
<!-- Stats -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-lg font-semibold text-stone-800">Stats</h2>
|
||||
<h2 class="text-lg font-semibold text-text">Stats</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-7">
|
||||
<a href="/data?type=brews" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
|
||||
{% call icons::beaker("h-6 w-6 text-accent") %}
|
||||
<span class="text-lg font-bold text-stone-800 group-hover:text-accent">{{ stats.brews }}</span>
|
||||
<span class="text-xs text-stone-500">Brews</span>
|
||||
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.brews }}</span>
|
||||
<span class="text-xs text-text-muted">Brews</span>
|
||||
</a>
|
||||
<a href="/data?type=roasts" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
|
||||
{% call icons::fire("h-6 w-6 text-accent") %}
|
||||
<span class="text-lg font-bold text-stone-800 group-hover:text-accent">{{ stats.roasts }}</span>
|
||||
<span class="text-xs text-stone-500">Roasts</span>
|
||||
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.roasts }}</span>
|
||||
<span class="text-xs text-text-muted">Roasts</span>
|
||||
</a>
|
||||
<a href="/data?type=roasters" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
|
||||
{% call icons::building("h-6 w-6 text-accent") %}
|
||||
<span class="text-lg font-bold text-stone-800 group-hover:text-accent">{{ stats.roasters }}</span>
|
||||
<span class="text-xs text-stone-500">Roasters</span>
|
||||
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.roasters }}</span>
|
||||
<span class="text-xs text-text-muted">Roasters</span>
|
||||
</a>
|
||||
<a href="/data?type=bags" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
|
||||
{% call icons::bag("h-6 w-6 text-accent") %}
|
||||
<span class="text-lg font-bold text-stone-800 group-hover:text-accent">{{ stats.bags }}</span>
|
||||
<span class="text-xs text-stone-500">Bags</span>
|
||||
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.bags }}</span>
|
||||
<span class="text-xs text-text-muted">Bags</span>
|
||||
</a>
|
||||
<a href="/data?type=cups" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
|
||||
{% call icons::cup("h-6 w-6 text-accent") %}
|
||||
<span class="text-lg font-bold text-stone-800 group-hover:text-accent">{{ stats.cups }}</span>
|
||||
<span class="text-xs text-stone-500">Cups</span>
|
||||
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.cups }}</span>
|
||||
<span class="text-xs text-text-muted">Cups</span>
|
||||
</a>
|
||||
<a href="/data?type=cafes" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
|
||||
{% call icons::location("h-6 w-6 text-accent") %}
|
||||
<span class="text-lg font-bold text-stone-800 group-hover:text-accent">{{ stats.cafes }}</span>
|
||||
<span class="text-xs text-stone-500">Cafes</span>
|
||||
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.cafes }}</span>
|
||||
<span class="text-xs text-text-muted">Cafes</span>
|
||||
</a>
|
||||
<a href="/data?type=gear" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
|
||||
{% call icons::wrench("h-6 w-6 text-accent") %}
|
||||
<span class="text-lg font-bold text-stone-800 group-hover:text-accent">{{ stats.gear }}</span>
|
||||
<span class="text-xs text-stone-500">Gear</span>
|
||||
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.gear }}</span>
|
||||
<span class="text-xs text-text-muted">Gear</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<div class="mx-auto max-w-md">
|
||||
<div class="rounded-lg border bg-surface p-6">
|
||||
<h1 class="text-2xl font-semibold text-accent">Login</h1>
|
||||
<p class="mt-2 text-sm text-stone-600">
|
||||
<p class="mt-2 text-sm text-text-secondary">
|
||||
Sign in with your passkey to manage your coffee log.
|
||||
</p>
|
||||
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
Sign in with Passkey
|
||||
</button>
|
||||
|
||||
<div id="login-loading" class="mt-4 hidden text-center text-sm text-stone-500">
|
||||
<div id="login-loading" class="mt-4 hidden text-center text-sm text-text-muted">
|
||||
<p>Waiting for passkey...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<div class="mx-auto max-w-md">
|
||||
<div class="rounded-lg border bg-surface p-6">
|
||||
<h1 class="text-2xl font-semibold text-accent">Register</h1>
|
||||
<p class="mt-2 text-sm text-stone-600">
|
||||
<p class="mt-2 text-sm text-text-secondary">
|
||||
Create your account by registering a passkey. This will be used to sign in going forward.
|
||||
</p>
|
||||
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<div id="register-form" class="mt-6 flex flex-col gap-4">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Display Name</span>
|
||||
<span class="text-text">Display Name</span>
|
||||
<input
|
||||
type="text"
|
||||
id="display-name"
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
</label>
|
||||
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Passkey Name</span>
|
||||
<span class="text-text">Passkey Name</span>
|
||||
<input
|
||||
type="text"
|
||||
id="passkey-name"
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
Register Passkey
|
||||
</button>
|
||||
|
||||
<div id="register-loading" class="mt-2 hidden text-center text-sm text-stone-500">
|
||||
<div id="register-loading" class="mt-2 hidden text-center text-sm text-text-muted">
|
||||
<p>Follow the prompts from your browser or device...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
{% if months.is_empty() %}
|
||||
<p
|
||||
class="rounded-lg border border-dashed p-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed p-6 text-sm text-text-secondary"
|
||||
data-role="timeline-empty-state"
|
||||
>
|
||||
No events yet.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
{% else %}
|
||||
<div class="timeline-list relative" id="timeline-items">
|
||||
{# Single central timeline line for all months #}
|
||||
<div class="timeline-line absolute top-0 bottom-0 w-1 bg-stone-300 dark:bg-stone-600" aria-hidden="true"></div>
|
||||
<div class="timeline-line absolute top-0 bottom-0 w-1 bg-border" aria-hidden="true"></div>
|
||||
{% for month in months %} {% include "partials/timeline_month.html" %} {% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -35,8 +35,8 @@
|
|||
<span aria-hidden="true">↓</span>
|
||||
Load more
|
||||
</button>
|
||||
<p id="timeline-status" class="text-xs text-stone-500" hidden>Loading…</p>
|
||||
<p id="timeline-end" class="text-sm text-stone-500" style="{% if events.has_next() || months.is_empty() %}display: none{% endif %}">No more events.</p>
|
||||
<p id="timeline-status" class="text-xs text-text-muted" hidden>Loading…</p>
|
||||
<p id="timeline-end" class="text-sm text-text-muted" style="{% if events.has_next() || months.is_empty() %}display: none{% endif %}">No more events.</p>
|
||||
<p id="timeline-error" class="text-sm text-red-600" hidden></p>
|
||||
</div>
|
||||
<div id="timeline-sentinel" class="h-1"></div>
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
container.className = "timeline-list relative"
|
||||
container.id = "timeline-items"
|
||||
const line = document.createElement("div")
|
||||
line.className = "timeline-line absolute top-0 bottom-0 w-1 bg-stone-300 dark:bg-stone-600"
|
||||
line.className = "timeline-line absolute top-0 bottom-0 w-1 bg-border"
|
||||
line.setAttribute("aria-hidden", "true")
|
||||
container.appendChild(line)
|
||||
section.insertBefore(container, loader)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
{% macro card(bag, is_authenticated) %}
|
||||
<div id="bag-card-{{ bag.id }}" class="rounded-lg border bg-surface p-4">
|
||||
<div class="min-w-0">
|
||||
<span class="block truncate font-semibold text-stone-800">{{ bag.roast_name }}</span>
|
||||
<p class="truncate text-sm text-stone-500">{{ bag.roaster_name }}</p>
|
||||
<span class="block truncate font-semibold text-text">{{ bag.roast_name }}</span>
|
||||
<p class="truncate text-sm text-text-muted">{{ bag.roaster_name }}</p>
|
||||
</div>
|
||||
<p class="mt-2 truncate text-sm text-stone-600">
|
||||
<p class="mt-2 truncate text-sm text-text-secondary">
|
||||
<span class="font-medium">{{ bag.remaining }}g</span>
|
||||
<span class="text-stone-400">of {{ bag.amount }}g remaining</span>
|
||||
<span class="text-text-muted">of {{ bag.amount }}g remaining</span>
|
||||
</p>
|
||||
{% if is_authenticated %}
|
||||
<div class="mt-3 pt-3 border-t flex gap-3">
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-stone-700"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-text"
|
||||
onclick="closeBag('{{ bag.id }}', document.getElementById('bag-card-{{ bag.id }}'))"
|
||||
>
|
||||
{% call icons::x_circle("h-4 w-4") %}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,51 @@
|
|||
<div>
|
||||
<h3 class="text-base font-semibold text-stone-800">Roaster</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">If this roaster already exists, it will be matched automatically.</p>
|
||||
<h3 class="text-base font-semibold text-text">Roaster</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">If this roaster already exists, it will be matched automatically.</p>
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Name *</span>
|
||||
<span class="text-text">Name *</span>
|
||||
<input type="text" name="roaster_name" required class="input-field" placeholder="Example Coffee Roasters" data-bind:_roaster-name />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Country *</span>
|
||||
<span class="text-text">Country *</span>
|
||||
<input type="text" name="roaster_country" required class="input-field" placeholder="United States" data-bind:_roaster-country />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">City</span>
|
||||
<span class="text-text">City</span>
|
||||
<input type="text" name="roaster_city" class="input-field" placeholder="Portland" data-bind:_roaster-city />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Homepage</span>
|
||||
<span class="text-text">Homepage</span>
|
||||
<input type="url" name="roaster_homepage" class="input-field" placeholder="https://example.coffee" data-bind:_roaster-homepage />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-stone-800">Roast</h3>
|
||||
<p class="mt-1 text-sm text-stone-600">Details about this specific coffee.</p>
|
||||
<h3 class="text-base font-semibold text-text">Roast</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">Details about this specific coffee.</p>
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Roast Name *</span>
|
||||
<span class="text-text">Roast Name *</span>
|
||||
<input type="text" name="roast_name" required class="input-field" placeholder="Ethiopia Yirgacheffe" data-bind:_roast-name />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Origin *</span>
|
||||
<span class="text-text">Origin *</span>
|
||||
<input type="text" name="origin" required class="input-field" placeholder="Ethiopia" data-bind:_origin />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Region *</span>
|
||||
<span class="text-text">Region *</span>
|
||||
<input type="text" name="region" required class="input-field" placeholder="Guji" data-bind:_region />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Producer *</span>
|
||||
<span class="text-text">Producer *</span>
|
||||
<input type="text" name="producer" required class="input-field" placeholder="Chelbesa Cooperative" data-bind:_producer />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Process *</span>
|
||||
<span class="text-text">Process *</span>
|
||||
<input type="text" name="process" required class="input-field" placeholder="Washed" data-bind:_process />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Tasting Notes * (comma separated)</span>
|
||||
<span class="text-text">Tasting Notes * (comma separated)</span>
|
||||
<textarea name="tasting_notes" rows="2" required class="input-field" placeholder="Blueberry, Jasmine" data-bind:_tasting-notes></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -53,11 +53,11 @@
|
|||
<div>
|
||||
<label class="inline-flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" name="open_bag" value="true" class="accent-orange-700" data-bind:_open-bag />
|
||||
<span class="font-semibold text-stone-800">Open a bag of this coffee</span>
|
||||
<span class="font-semibold text-text">Open a bag of this coffee</span>
|
||||
</label>
|
||||
<div data-show="$_openBag" class="mt-3 grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-stone-700">Amount (grams)</span>
|
||||
<span class="text-text">Amount (grams)</span>
|
||||
<input type="number" name="bag_amount" min="1" step="any" class="input-field" placeholder="250" data-bind:_bag-amount />
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<button
|
||||
type="button"
|
||||
data-on:click="$_scanExtracted = false"
|
||||
class="rounded-md border px-4 py-2 text-sm font-semibold text-stone-700 transition hover:bg-surface-alt"
|
||||
class="rounded-md border px-4 py-2 text-sm font-semibold text-text transition hover:bg-surface-alt"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div id="bag-list" class="mt-6" data-star-scope="bags">
|
||||
{% if bags.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
>
|
||||
{% call table::search_header(navigator, "#bag-list") %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-stone-700">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-stone-700">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#bag-list") %}
|
||||
{% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %}
|
||||
|
|
@ -37,11 +37,11 @@
|
|||
<tbody class="divide-y/70">
|
||||
{% for bag in bags.items %}
|
||||
<tr class="transition hover:bg-surface-alt">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ bag.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-stone-400">{{ bag.created_time }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ bag.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap font-medium text-stone-600">
|
||||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap font-medium text-text-secondary">
|
||||
{{ bag.roaster_name }}
|
||||
</td>
|
||||
<td data-label="Roast" class="card-title px-4 py-3 whitespace-nowrap">{{ bag.roast_name }}</td>
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
Closed
|
||||
{% else %}
|
||||
<span class="font-medium text-emerald-600">Open</span>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ bag.remaining }}g left</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ bag.remaining }}g left</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if !bag.closed %}
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
</button>
|
||||
{% endif %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
</button>
|
||||
{% endif %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% if bags.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No bags match your search.</div>
|
||||
<div class="p-8 text-center text-text-muted">No bags match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(bags, navigator, "#bag-list") %}
|
||||
{% if bags.has_next() %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div id="brew-list" class="mt-6" data-star-scope="brews">
|
||||
{% if brews.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -21,8 +21,8 @@
|
|||
{% call table::search_header(navigator, "#brew-list") %}
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-stone-700">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-stone-700">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#brew-list") %}
|
||||
<th scope="col" class="px-4 py-3">Coffee</th>
|
||||
|
|
@ -38,40 +38,40 @@
|
|||
<tbody class="divide-y/70">
|
||||
{% for brew in brews.items %}
|
||||
<tr class="transition hover:bg-surface-alt">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ brew.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-stone-400">{{ brew.created_time }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ brew.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="Coffee" class="card-title px-4 py-3 whitespace-nowrap">
|
||||
<div class="font-medium text-stone-800">{{ brew.roast_name }}</div>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ brew.roaster_name }}</div>
|
||||
<div class="font-medium text-text">{{ brew.roast_name }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ brew.roaster_name }}</div>
|
||||
</td>
|
||||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.roaster_name }}</td>
|
||||
<td data-label="Grind" class="px-4 py-3 whitespace-nowrap">
|
||||
<div>{{ brew.grind_setting }}</div>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ brew.grinder_name }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ brew.grinder_name }}</div>
|
||||
</td>
|
||||
<td data-label="Grinder" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.grinder_name }}</td>
|
||||
<td data-label="Recipe" class="px-4 py-3 whitespace-nowrap">
|
||||
<div>{{ brew.coffee_weight }} / {{ brew.water_volume }}</div>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ brew.ratio }} · {{ brew.water_temp }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ brew.ratio }} · {{ brew.water_temp }}</div>
|
||||
</td>
|
||||
<td data-label="Ratio" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.ratio }}</td>
|
||||
<td data-label="Temp" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.water_temp }}</td>
|
||||
<td data-label="Brewer" class="px-4 py-3 whitespace-nowrap">
|
||||
<div>{{ brew.brewer_name }}</div>
|
||||
{% if let Some(fp_name) = brew.filter_paper_name %}
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ fp_name }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ fp_name }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if let Some(fp_name) = brew.filter_paper_name %}
|
||||
<td data-label="Filter" class="px-4 py-3 whitespace-nowrap md:hidden">{{ fp_name }}</td>
|
||||
{% endif %}
|
||||
<td data-label="Notes" class="px-4 py-3 text-xs text-stone-500">
|
||||
<td data-label="Notes" class="px-4 py-3 text-xs text-text-muted">
|
||||
{% if !brew.quick_notes_label.is_empty() %}
|
||||
{{ brew.quick_notes_label }}
|
||||
{% else %}
|
||||
<span class="text-stone-400">—</span>
|
||||
<span class="text-text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
</button>
|
||||
</form>
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
</button>
|
||||
</form>
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
</div>
|
||||
|
||||
{% if brews.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">
|
||||
<div class="p-8 text-center text-text-muted">
|
||||
No brews match your search.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div id="cafe-list" class="mt-6" data-star-scope="cafes">
|
||||
{% if cafes.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
>
|
||||
{% call table::search_header(navigator, "#cafe-list") %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-stone-700">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-stone-700">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#cafe-list") %}
|
||||
{% call table::sortable_header("Name", "name", navigator, "#cafe-list") %}
|
||||
|
|
@ -39,16 +39,16 @@
|
|||
data-sort-city="{{ cafe.city }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ cafe.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-stone-400">{{ cafe.created_time }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ cafe.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-stone-800">
|
||||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-text">
|
||||
{{ cafe.name }}
|
||||
</td>
|
||||
<td data-label="Location" class="px-4 py-3 whitespace-nowrap">
|
||||
{{ cafe.country }}
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ cafe.city }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ cafe.city }}</div>
|
||||
</td>
|
||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.city }}</td>
|
||||
<td data-label="" class="card-detail hidden">
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% if cafes.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No cafes match your search.</div>
|
||||
<div class="p-8 text-center text-text-muted">No cafes match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(cafes, navigator, "#cafe-list") %}
|
||||
{% if cafes.has_next() %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div id="cup-list" class="mt-6" data-star-scope="cups">
|
||||
{% if cups.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
>
|
||||
{% call table::search_header(navigator, "#cup-list") %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-stone-700">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-stone-700">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#cup-list") %}
|
||||
{% call table::sortable_header("Coffee", "roast", navigator, "#cup-list") %}
|
||||
|
|
@ -37,13 +37,13 @@
|
|||
data-star-key="{{ cup.id }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ cup.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-stone-400">{{ cup.created_time }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ cup.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="Coffee" class="card-title px-4 py-3 whitespace-nowrap">
|
||||
<div class="font-medium text-stone-800">{{ cup.roast_name }}</div>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ cup.roaster_name }}</div>
|
||||
<div class="font-medium text-text">{{ cup.roast_name }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ cup.roaster_name }}</div>
|
||||
</td>
|
||||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cup.roaster_name }}</td>
|
||||
<td data-label="Cafe" class="px-4 py-3 whitespace-nowrap">
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
<td colspan="4" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% if cups.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No cups match your search.</div>
|
||||
<div class="p-8 text-center text-text-muted">No cups match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(cups, navigator, "#cup-list") %}
|
||||
{% if cups.has_next() %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div id="gear-list" class="mt-6" data-star-scope="gear">
|
||||
{% if gear.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -21,8 +21,8 @@
|
|||
{% call table::search_header(navigator, "#gear-list") %}
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-stone-700">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-stone-700">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#gear-list") %}
|
||||
{% call table::sortable_header("Category", "category", navigator, "#gear-list") %}
|
||||
|
|
@ -36,15 +36,15 @@
|
|||
<tbody class="divide-y/70">
|
||||
{% for item in gear.items %}
|
||||
<tr class="transition hover:bg-surface-alt">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ item.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-stone-400">{{ item.created_time }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ item.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="" class="card-title px-4 py-3 whitespace-nowrap md:hidden">{{ item.make }} {{ item.model }}</td>
|
||||
<td data-label="Category" class="px-4 py-3 whitespace-nowrap">
|
||||
{{ item.category_label }}
|
||||
</td>
|
||||
<td data-label="Make" class="mobile-hidden px-4 py-3 whitespace-nowrap font-medium text-stone-600">
|
||||
<td data-label="Make" class="mobile-hidden px-4 py-3 whitespace-nowrap font-medium text-text-secondary">
|
||||
{{ item.make }}
|
||||
</td>
|
||||
<td data-label="Model" class="mobile-hidden px-4 py-3 whitespace-nowrap">
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% if gear.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No gear matches your search.</div>
|
||||
<div class="p-8 text-center text-text-muted">No gear matches your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(gear, navigator, "#gear-list") %}
|
||||
{% if gear.has_next() %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div id="roast-list" class="mt-6" data-star-scope="roasts">
|
||||
{% if roasts.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
>
|
||||
{% call table::search_header(navigator, "#roast-list") %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-stone-700">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-stone-700">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#roast-list") %}
|
||||
{% call table::sortable_header("Roast", "name", navigator, "#roast-list") %}
|
||||
|
|
@ -43,27 +43,27 @@
|
|||
data-sort-producer="{{ roast.producer }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ roast.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-stone-400">{{ roast.created_time }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ roast.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="Roast" class="card-title px-4 py-3">
|
||||
<div class="font-medium text-stone-800">{{ roast.name }}</div>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ roast.roaster_label }}</div>
|
||||
<div class="font-medium text-text">{{ roast.name }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ roast.roaster_label }}</div>
|
||||
</td>
|
||||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roast.roaster_label }}</td>
|
||||
<td data-label="Origin" class="px-4 py-3 whitespace-nowrap">
|
||||
{{ roast.origin }}
|
||||
{% if !roast.producer.is_empty() %}
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ roast.producer }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ roast.producer }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if !roast.producer.is_empty() %}
|
||||
<td data-label="Producer" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roast.producer }}</td>
|
||||
{% endif %}
|
||||
<td data-label="Tasting Notes" class="px-4 py-3 text-xs text-stone-500 text-right md:text-left">
|
||||
<td data-label="Tasting Notes" class="px-4 py-3 text-xs text-text-muted text-right md:text-left">
|
||||
{% if roast.tasting_notes.is_empty() %}
|
||||
<span class="text-stone-400">—</span>
|
||||
<span class="text-text-muted">—</span>
|
||||
{% else %}
|
||||
{{ roast.tasting_notes.join(", ") }}
|
||||
{% endif %}
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% if roasts.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No roasts match your search.</div>
|
||||
<div class="p-8 text-center text-text-muted">No roasts match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(roasts, navigator, "#roast-list") %}
|
||||
{% if roasts.has_next() %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div id="roaster-list" class="mt-6" data-star-scope="roasters">
|
||||
{% if roasters.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-stone-600"
|
||||
class="rounded-lg border border-dashed px-4 py-6 text-sm text-text-secondary"
|
||||
>
|
||||
<p class="text-center">
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
>
|
||||
{% call table::search_header(navigator, "#roaster-list") %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-stone-700">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-stone-700">
|
||||
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
|
||||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#roaster-list") %}
|
||||
{% call table::sortable_header("Name", "name", navigator, "#roaster-list") %}
|
||||
|
|
@ -39,17 +39,17 @@
|
|||
data-sort-city="{{ roaster.city }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ roaster.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-stone-400">{{ roaster.created_time }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ roaster.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-stone-800">
|
||||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-text">
|
||||
{{ roaster.name }}
|
||||
</td>
|
||||
<td data-label="Location" class="px-4 py-3 whitespace-nowrap">
|
||||
{{ roaster.country }}
|
||||
{% if !roaster.city.is_empty() %}
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ roaster.city }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ roaster.city }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if !roaster.city.is_empty() %}
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
{% if roaster.has_homepage || is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
|
||||
{% call icons::delete("h-4 w-4") %} Delete
|
||||
</button>
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% if roasters.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No roasters match your search.</div>
|
||||
<div class="p-8 text-center text-text-muted">No roasters match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(roasters, navigator, "#roaster-list") %}
|
||||
{% if roasters.has_next() %}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
{% macro pagination_header(items, navigator, target_selector) %}
|
||||
<div
|
||||
class="pagination-controls hidden md:flex flex-wrap items-center justify-between gap-x-3 border-t px-4 py-1.5 text-xs leading-none text-stone-600"
|
||||
class="pagination-controls hidden md:flex flex-wrap items-center justify-between gap-x-3 border-t px-4 py-1.5 text-xs leading-none text-text-secondary"
|
||||
>
|
||||
<span>
|
||||
Showing {{ items.start_index() }}–{{ items.end_index() }} of {{ items.total }}
|
||||
|
|
@ -35,9 +35,9 @@
|
|||
<span>Previous</span>
|
||||
</button>
|
||||
{% if items.is_showing_all() %}
|
||||
<span class="font-semibold text-stone-700">Showing all results</span>
|
||||
<span class="font-semibold text-text">Showing all results</span>
|
||||
{% else %}
|
||||
<span class="font-semibold text-stone-700"
|
||||
<span class="font-semibold text-text"
|
||||
>Page {{ items.page }} of {{ items.total_pages() }}</span
|
||||
>
|
||||
{% endif %}
|
||||
|
|
@ -54,10 +54,10 @@
|
|||
<span aria-hidden="true">→</span>
|
||||
</button>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-xs font-semibold text-stone-600">
|
||||
<label class="flex items-center gap-2 text-xs font-semibold text-text-secondary">
|
||||
<span>Rows</span>
|
||||
<select
|
||||
class="rounded-md border bg-surface px-2 py-1 text-xs font-semibold text-stone-700 transition hover:border-accent"
|
||||
class="rounded-md border bg-surface px-2 py-1 text-xs font-semibold text-text transition hover:border-accent"
|
||||
data-on:change="history.pushState(null, '', evt.target.selectedOptions[0].dataset.url); @get(evt.target.selectedOptions[0].dataset.href, {responseOverrides: {selector: '{{ target_selector }}', mode: 'replace'}})"
|
||||
>
|
||||
<option value="5" data-href="{{ navigator.fragment_rows_href("5")|safe }}" data-url="{{ navigator.rows_href("5")|safe }}" {% if items.is_page_size(5) %}selected{% endif %}>5</option>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 text-stone-800 hover:text-accent"
|
||||
class="flex items-center gap-1 text-text hover:text-accent"
|
||||
data-on:click="history.pushState(null, '', '{{ navigator.sort_href(key)|safe }}'); @get('{{ navigator.fragment_sort_href(key)|safe }}', {responseOverrides: {selector: '{{ target_selector }}', mode: 'replace'}})"
|
||||
>
|
||||
<span>{{ label }}</span>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const locateUser = (triggerEl) => {
|
|||
<button type="button"
|
||||
data-on:click="locateUser(el)"
|
||||
class="shrink-0 inline-flex items-center justify-center rounded-md border p-2.5 transition hover:bg-surface-alt"
|
||||
data-class="{'text-accent': $_locationFound, 'text-stone-800': !$_locationFound}"
|
||||
data-class="{'text-accent': $_locationFound, 'text-text': !$_locationFound}"
|
||||
data-attr:disabled="$_locating || $_locationFound"
|
||||
aria-label="Use my location"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold uppercase tracking-[0.25em] text-accent"><a href="/">B{rew}log</a></div>
|
||||
<div class="flex items-center gap-1">
|
||||
<a class="rounded-md p-1.5 transition {% if nav_active == "home" %}text-accent{% else %}text-stone-400 hover:text-stone-600 dark:text-stone-500 dark:hover:text-stone-300{% endif %}" href="/" title="Home" aria-label="Home">
|
||||
<a class="rounded-md p-1.5 transition {% if nav_active == "home" %}text-accent{% else %}text-text-muted hover:text-text-secondary{% endif %}" href="/" title="Home" aria-label="Home">
|
||||
{% call icons::home("h-5 w-5") %}
|
||||
</a>
|
||||
<button type="button" class="rounded-md p-1.5 text-stone-400 transition hover:text-stone-600 dark:text-stone-500 dark:hover:text-stone-300" title="Toggle theme" aria-label="Toggle theme" onclick="toggleTheme()">
|
||||
<button type="button" class="rounded-md p-1.5 text-text-muted transition hover:text-text-secondary" title="Toggle theme" aria-label="Toggle theme" onclick="toggleTheme()">
|
||||
<span class="theme-icon-light">{% call icons::sun("h-5 w-5") %}</span>
|
||||
<span class="theme-icon-dark hidden">{% call icons::moon("h-5 w-5") %}</span>
|
||||
</button>
|
||||
<button class="rounded-md p-1.5 text-stone-400 transition hover:text-stone-600 dark:text-stone-500 dark:hover:text-stone-300" data-on:click="$_navOpen = !$_navOpen" title="Menu" aria-label="Toggle menu">
|
||||
<button class="rounded-md p-1.5 text-text-muted transition hover:text-text-secondary" data-on:click="$_navOpen = !$_navOpen" title="Menu" aria-label="Toggle menu">
|
||||
<span data-show="!$_navOpen">{% call icons::hamburger("h-5 w-5") %}</span>
|
||||
<span data-show="$_navOpen" style="display:none">{% call icons::close("h-5 w-5") %}</span>
|
||||
</button>
|
||||
|
|
@ -18,26 +18,26 @@
|
|||
</div>
|
||||
<!-- Menu -->
|
||||
<div class="flex flex-col gap-2 pt-3 mt-3 border-t md:items-end" data-show="$_navOpen" style="display:none">
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "data" %}text-accent font-medium{% else %}text-stone-500 hover:text-stone-800 dark:hover:text-stone-300{% endif %}" href="/data">
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "data" %}text-accent font-medium{% else %}text-text-muted hover:text-text{% endif %}" href="/data">
|
||||
Data {% call icons::database("h-4 w-4") %}
|
||||
</a>
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "timeline" %}text-accent font-medium{% else %}text-stone-500 hover:text-stone-800 dark:hover:text-stone-300{% endif %}" href="/timeline">
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "timeline" %}text-accent font-medium{% else %}text-text-muted hover:text-text{% endif %}" href="/timeline">
|
||||
Timeline {% call icons::timeline("h-4 w-4") %}
|
||||
</a>
|
||||
{% if is_authenticated %}
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "checkin" %}text-accent font-medium{% else %}text-stone-500 hover:text-stone-800 dark:hover:text-stone-300{% endif %}" href="/check-in">
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "checkin" %}text-accent font-medium{% else %}text-text-muted hover:text-text{% endif %}" href="/check-in">
|
||||
Check In {% call icons::checkin("h-4 w-4") %}
|
||||
</a>
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "account" %}text-accent font-medium{% else %}text-stone-500 hover:text-stone-800 dark:hover:text-stone-300{% endif %}" href="/account">
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 {% if nav_active == "account" %}text-accent font-medium{% else %}text-text-muted hover:text-text{% endif %}" href="/account">
|
||||
Admin {% call icons::cog("h-4 w-4") %}
|
||||
</a>
|
||||
<form method="post" action="/logout" class="flex md:justify-end">
|
||||
<button type="submit" class="py-1 transition inline-flex items-center gap-1.5 text-stone-500 hover:text-stone-800 dark:hover:text-stone-300">
|
||||
<button type="submit" class="py-1 transition inline-flex items-center gap-1.5 text-text-muted hover:text-text">
|
||||
Sign Out {% call icons::logout("h-4 w-4") %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 text-stone-500 hover:text-stone-800 dark:hover:text-stone-300" href="/login">
|
||||
<a class="py-1 transition inline-flex items-center gap-1.5 text-text-muted hover:text-text" href="/login">
|
||||
Login {% call icons::user("h-4 w-4") %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<div id="nearby-results" class="mt-3 max-h-60 overflow-y-auto rounded-lg border bg-surface">
|
||||
{% if cafes.is_empty() %}
|
||||
<p class="px-3 py-2 text-sm text-stone-500">No nearby cafes found.</p>
|
||||
<p class="px-3 py-2 text-sm text-text-muted">No nearby cafes found.</p>
|
||||
{% else %}
|
||||
<h3 class="px-3 py-2 text-xs font-semibold text-stone-500 uppercase tracking-wide">Nearby</h3>
|
||||
<h3 class="px-3 py-2 text-xs font-semibold text-text-muted uppercase tracking-wide">Nearby</h3>
|
||||
{% for cafe in cafes %}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -15,8 +15,8 @@
|
|||
data-cafe-website="{{ cafe.website }}"
|
||||
data-on:click="$_cafeId = ''; $_cafeName = el.dataset.cafeName; $_cafeCity = el.dataset.cafeCity; $_cafeCountry = el.dataset.cafeCountry; $_cafeLat = parseFloat(el.dataset.cafeLat); $_cafeLng = parseFloat(el.dataset.cafeLng); $_cafeWebsite = el.dataset.cafeWebsite; $_reviewingCafe = true"
|
||||
>
|
||||
<span class="font-medium text-stone-800">{{ cafe.name }}</span>
|
||||
<span class="ml-2 text-xs text-stone-500">{{ cafe.location }} · {{ cafe.distance }}</span>
|
||||
<span class="font-medium text-text">{{ cafe.name }}</span>
|
||||
<span class="ml-2 text-xs text-text-muted">{{ cafe.location }} · {{ cafe.distance }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% import "partials/icons.html" as icons %}
|
||||
<h2 id="{{ month.anchor }}" class="timeline-heading scroll-mt-24 mb-6 text-2xl font-semibold text-stone-800" data-timeline-month>
|
||||
<h2 id="{{ month.anchor }}" class="timeline-heading scroll-mt-24 mb-6 text-2xl font-semibold text-text" data-timeline-month>
|
||||
{{ month.heading }}
|
||||
<button type="button" class="timeline-top-btn" onclick="window.scrollTo({top: 0, behavior: 'smooth'})" aria-label="Back to top">
|
||||
{% call icons::arrow_up("h-5 w-5") %}
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
<span class="pill pill-muted">{{ event.kind_label }}</span>
|
||||
<time
|
||||
datetime="{{ event.iso_timestamp }}"
|
||||
class="text-xs uppercase tracking-wide text-stone-500"
|
||||
class="text-xs uppercase tracking-wide text-text-muted"
|
||||
>
|
||||
{{ event.date_label }}{% if let Some(label) = event.time_label %} · {{ label }}{% endif
|
||||
%}
|
||||
</time>
|
||||
</div>
|
||||
<h3 class="mt-3 flex items-center gap-2 text-lg font-semibold text-stone-800">
|
||||
<h3 class="mt-3 flex items-center gap-2 text-lg font-semibold text-text">
|
||||
<a href="{{ event.link }}" class="hover:text-accent">{{ event.title }}</a>
|
||||
{% if let Some(url) = event.external_link %}
|
||||
<a
|
||||
|
|
@ -66,10 +66,10 @@
|
|||
{% endif %}
|
||||
</h3>
|
||||
{% if event.details.len() > 0 %}
|
||||
<dl class="mt-4 flex flex-col gap-2 text-sm text-stone-600">
|
||||
<dl class="mt-4 flex flex-col gap-2 text-sm text-text-secondary">
|
||||
{% for detail in event.details %}
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="font-medium text-stone-500">{{ detail.label }}</dt>
|
||||
<dt class="font-medium text-text-muted">{{ detail.label }}</dt>
|
||||
{% if detail.label.eq_ignore_ascii_case("position") && detail.link.is_some() %}
|
||||
<dd class="text-right flex items-center justify-end gap-1.5">
|
||||
{{ detail.value }}
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
{% if let Some(notes) = event.tasting_notes %}
|
||||
{% if !notes.is_empty() %}
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="shrink-0 font-medium text-stone-500">Tasting Notes</dt>
|
||||
<dt class="shrink-0 font-medium text-text-muted">Tasting Notes</dt>
|
||||
<dd class="text-right">{{ notes.join(", ") }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue