feat(design): apply Monolith Editorial design system to all Fase 1 screens
- Remove all 1px border separators (sidebar border-r, page header border-b, section border-t, member list divide-y, invite card border). Separation is now achieved via background color shifts and whitespace per the "No-Line" rule. - Fix sidebar: bg-surface-raised vs bg-background creates tonal separation. - Correct dark background to #020617 (slate-950) per spec. - Add surface-container-low, text-primary/secondary/muted, destructive tokens. - Fix CSS variable color space: all tokens are RGB triplets; switch Tailwind config and app.css from hsl() to rgb() to prevent yellow/warm color bleed. - Replace emoji nav icons with Lucide icons at 16px / 1.5 stroke-width. - Apply Masthead typography (uppercase Label-MD + Title-LG) to all page headers and section labels across lists, tasks, notes, search, settings, manage. - Destructive action (sign out) uses bg-destructive per spec. - Add `just serve` command: builds and runs the prod Node server at :3000 against the dev Docker stack for production build testing. - Add nav_collective i18n key (en + es). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,7 @@ infra/
|
|||||||
```bash
|
```bash
|
||||||
just setup # first-time (and idempotent) local environment setup
|
just setup # first-time (and idempotent) local environment setup
|
||||||
just dev # start docker-compose.dev.yml + SvelteKit dev server
|
just dev # start docker-compose.dev.yml + SvelteKit dev server
|
||||||
|
just serve # build + start prod Node server at :3000 against dev Docker stack
|
||||||
just stop # stop all services (Docker stack + Vite dev server)
|
just stop # stop all services (Docker stack + Vite dev server)
|
||||||
just dev-stop # stop Docker stack only
|
just dev-stop # stop Docker stack only
|
||||||
just dev-clean # stop Docker stack and remove all volumes (full reset)
|
just dev-clean # stop Docker stack and remove all volumes (full reset)
|
||||||
@@ -173,7 +174,9 @@ Logout is a single call — `supabase.auth.signOut()` — which clears the GoTru
|
|||||||
|
|
||||||
7. **`onAuthStateChange` fires `INITIAL_SESSION` on page load, not `SIGNED_IN`.** `SIGNED_IN` only fires immediately after a login flow. Load collective data whenever the session is present, regardless of event type — otherwise collectives are never loaded on page refresh. Pattern in root `+layout.svelte`: `if (session) { await loadUserCollectives(...) }` covering `INITIAL_SESSION`, `SIGNED_IN`, and `TOKEN_REFRESHED`.
|
7. **`onAuthStateChange` fires `INITIAL_SESSION` on page load, not `SIGNED_IN`.** `SIGNED_IN` only fires immediately after a login flow. Load collective data whenever the session is present, regardless of event type — otherwise collectives are never loaded on page refresh. Pattern in root `+layout.svelte`: `if (session) { await loadUserCollectives(...) }` covering `INITIAL_SESSION`, `SIGNED_IN`, and `TOKEN_REFRESHED`.
|
||||||
|
|
||||||
8. **PWA service worker: do not use `injectManifest` with a file named `service-worker.ts`.** SvelteKit intercepts any file at `src/service-worker.[jt]s` and enforces a hard restriction: only `$service-worker` and `$env/static/public` may be imported — Workbox modules are blocked. With `injectManifest`, `@vite-pwa/sveltekit` tries to find the compiled SW output at `.svelte-kit/output/client/service-worker.js`, but SvelteKit never produces it (compilation fails on the blocked imports). Fix for Fase 1–2a: use `generateSW` strategy (plugin auto-generates the SW, no custom source needed). Fix for Fase 2b when a custom SW is needed: name the file `src/sw.ts` — SvelteKit does not recognise it as a service worker — and set `strategies: 'injectManifest'`, `filename: 'sw.ts'` in `vite.config.ts`.
|
9. **CSS custom properties use RGB triplets — always use `rgb()`, never `hsl()`.** All design tokens in `app.css` store values as space-separated RGB triplets (e.g. `51 65 85`). Using `hsl(var(--token))` interprets the first value as a hue degree — `hsl(51 65% 85%)` renders as light yellow, not slate-700. The Tailwind config and any `background-color`/`color` declarations in `app.css` must use `rgb(var(--token) / <alpha>)`.
|
||||||
|
|
||||||
|
10. **PWA service worker: do not use `injectManifest` with a file named `service-worker.ts`.** SvelteKit intercepts any file at `src/service-worker.[jt]s` and enforces a hard restriction: only `$service-worker` and `$env/static/public` may be imported — Workbox modules are blocked. With `injectManifest`, `@vite-pwa/sveltekit` tries to find the compiled SW output at `.svelte-kit/output/client/service-worker.js`, but SvelteKit never produces it (compilation fails on the blocked imports). Fix for Fase 1–2a: use `generateSW` strategy (plugin auto-generates the SW, no custom source needed). Fix for Fase 2b when a custom SW is needed: name the file `src/sw.ts` — SvelteKit does not recognise it as a service worker — and set `strategies: 'injectManifest'`, `filename: 'sw.ts'` in `vite.config.ts`.
|
||||||
|
|
||||||
## Sync Strategy
|
## Sync Strategy
|
||||||
|
|
||||||
|
|||||||
526
DESIGN.md
526
DESIGN.md
@@ -1,442 +1,96 @@
|
|||||||
# DESIGN.md
|
# Design System Specification
|
||||||
|
|
||||||
Design reference for the Colectivo app. Stack: SvelteKit + shadcn-svelte + Tailwind CSS.
|
## 1. Overview & Creative North Star: "The Monolith Editorial"
|
||||||
|
The creative direction for this design system is **The Monolith Editorial**. We are moving away from the "busy" aesthetics of modern SaaS and toward a disciplined, high-end editorial experience. The goal is to create a sense of permanent, quiet authority. By stripping away all accent colors and relying on a monochromatic slate palette, we force the user to focus on content, structure, and intent.
|
||||||
Generated mockups are in `.stitch/designs/`. Stitch project ID: `10316144241804324155`.
|
|
||||||
|
The "Template" look is avoided through **Intentional Negative Space** and **Tonal Depth**. We do not use lines to separate ideas; we use the weight of the typography and the shift of the background. It is a system that feels carved rather than assembled.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Visual Style
|
## 2. Colors & Tonal Architecture
|
||||||
|
The palette is strictly monochromatic, utilizing a "Slate" foundation to provide a cooling, sophisticated atmosphere.
|
||||||
### Direction
|
|
||||||
|
### Core Palette
|
||||||
Clean and minimal. Content is the UI — no decorative elements, no gradients, no shadows unless they carry meaning. Generous whitespace. Every element earns its place.
|
- **Background**: `#f7f9fb` (Light) / `#020617` (Dark)
|
||||||
|
- **Primary (Action)**: `#000000` (Light) / `#ffffff` (Dark) — *Note: Primary actions invert the surface.*
|
||||||
### Color
|
- **Destructive**: `#ba1a1a` (The only color allowed to break the monochrome seal).
|
||||||
|
|
||||||
Monochromatic slate palette. No accent color — contrast and weight carry hierarchy.
|
### The "No-Line" Rule
|
||||||
|
**Explicit Instruction:** Designers are prohibited from using 1px solid borders to section off major UI areas. To define boundaries, you must use background shifts.
|
||||||
```
|
- Use `surface-container-low` for secondary content areas.
|
||||||
--background: slate-50 / slate-950
|
- Use `surface-container-highest` for high-priority nested components.
|
||||||
--surface: white / slate-900
|
- Sectioning is achieved via a 32px–48px vertical gap or a subtle change in the background hex, never a line.
|
||||||
--surface-raised: slate-100 / slate-800
|
|
||||||
--border: slate-200 / slate-700
|
### Surface Hierarchy & Nesting
|
||||||
--text-primary: slate-900 / slate-50
|
Treat the UI as stacked sheets of gallery-grade paper.
|
||||||
--text-secondary: slate-500 / slate-400
|
1. **Base Layer:** `surface` (The foundation).
|
||||||
--text-disabled: slate-300 / slate-600
|
2. **Raised Layer:** `surface-container-low` (For subtle grouping).
|
||||||
--destructive: red-500 / red-400 ← only color in the UI
|
3. **Focus Layer:** `surface-container-lowest` (For cards and interactive modules to "pop" via tonal contrast).
|
||||||
```
|
|
||||||
|
|
||||||
Interactive elements (buttons, checkboxes, active states) use `slate-900` on light and `slate-50` on dark — inverting the surface. The only true color is destructive red.
|
|
||||||
|
|
||||||
### Typography
|
|
||||||
|
|
||||||
Font: **Inter** (or `system-ui` as fallback — acceptable on iOS/macOS).
|
|
||||||
|
|
||||||
| Role | Size | Weight | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Screen title | 20px | 600 | |
|
|
||||||
| Section label | 13px | 600 | Uppercase, `letter-spacing: 0.05em` |
|
|
||||||
| Body / list item | 16px | 400 | |
|
|
||||||
| Meta / secondary | 13px | 400 | `text-slate-500` |
|
|
||||||
| Badge / tag | 11px | 500 | Uppercase, `rounded-full` |
|
|
||||||
| Input | 16px | 400 | Min 16px prevents iOS zoom on focus |
|
|
||||||
|
|
||||||
Line height: 1.5 for body, 1.2 for headings. No text smaller than 11px.
|
|
||||||
|
|
||||||
### Spacing
|
|
||||||
|
|
||||||
Base unit: 4px (Tailwind default). Prefer multiples of 4. Common values:
|
|
||||||
|
|
||||||
- List item padding: `px-4 py-3` (min 48px touch target)
|
|
||||||
- Screen horizontal margin: `px-4` (mobile), `px-6` (desktop)
|
|
||||||
- Section gap: `gap-6`
|
|
||||||
- Between section label and content: `mt-2`
|
|
||||||
- Bottom nav / sticky input safe area: always account for `env(safe-area-inset-bottom)`
|
|
||||||
|
|
||||||
### Iconography
|
|
||||||
|
|
||||||
**Lucide** (ships with shadcn-svelte). Size `20px` inline, `24px` for nav and primary actions. Stroke width `1.5`. Never fill icons.
|
|
||||||
|
|
||||||
### Dark Mode
|
|
||||||
|
|
||||||
Class-based (`dark` on `<html>`). Respect `prefers-color-scheme` on first load, then persist user preference in `localStorage`. shadcn-svelte's CSS variable system handles theming — no additional setup needed per component.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Component Library
|
## 3. Typography: The Inter Hierarchy
|
||||||
|
We use **Inter** exclusively. To achieve an editorial feel, we rely on extreme contrast between `headline-lg` and `label-sm`.
|
||||||
**shadcn-svelte** — copy-paste components built on bits-ui + Tailwind. No runtime dependency, full ownership of component code.
|
|
||||||
|
| Level | Size | Weight | Case | Spacing |
|
||||||
### Conventions
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **Display-LG** | 3.5rem | 600 | Default | -0.02em |
|
||||||
- Copy components into `apps/web/src/lib/components/ui/` (shadcn-svelte default)
|
| **Title-LG** | 20px | 600 | Default | -0.01em |
|
||||||
- App-specific compositions go in `apps/web/src/lib/components/` (e.g. `ItemRow.svelte`, `ListCard.svelte`)
|
| **Header (Label-MD)** | 13px | 600 | Uppercase | +0.05em |
|
||||||
- Never modify shadcn-svelte primitives directly — compose or wrap them
|
| **Body-LG** | 16px | 400 | Default | Normal |
|
||||||
- Use shadcn-svelte's `cn()` utility for conditional classes
|
| **Meta (Label-SM)** | 13px | 400 | Default | Normal |
|
||||||
|
|
||||||
### Key components to install
|
**Editorial Note:** Always pair an Uppercase Label-MD header directly above a Title-LG to create a "Masthead" effect for sections.
|
||||||
|
|
||||||
| Component | Used for |
|
|
||||||
|---|---|
|
|
||||||
| `Button` | All actions |
|
|
||||||
| `Input` | Add item, search, forms |
|
|
||||||
| `Checkbox` | Shopping items, tasks |
|
|
||||||
| `Sheet` | Mobile bottom drawers (item edit, member invite, avatar picker) |
|
|
||||||
| `Dialog` | Confirmations (delete list, dissolve collective) |
|
|
||||||
| `Popover` | Presence avatars tooltip |
|
|
||||||
| `Badge` | Priority tags (TODAY, URGENT), status labels (DRAFT, HIGH PRIORITY) |
|
|
||||||
| `Skeleton` | Loading states |
|
|
||||||
| `Sonner` | Toast notifications (sync errors, member joined, undo delete) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Layout & Navigation
|
## 4. Elevation & Depth: Tonal Layering
|
||||||
|
Traditional drop shadows are largely forbidden. Hierarchy must be achieved through **Tonal Layering**.
|
||||||
### App Shell
|
|
||||||
|
- **The Layering Principle:** Instead of a shadow, place a `surface-container-lowest` card on a `surface-container-low` background. The slight shift in slate value creates a "natural" lift.
|
||||||
Two-column on desktop, single-column on mobile.
|
- **Ambient Shadows:** For floating elements like Sheets or Dialogs, use a "Ghost Shadow": `0px 20px 40px rgba(15, 23, 42, 0.06)`. It should feel like a soft glow, not a dark stain.
|
||||||
|
- **Glassmorphism:** For the "Sticky Bottom Input," use a backdrop-blur of `12px` and a semi-transparent `surface` color (80% opacity). This ensures the content "flows" under the interface, maintaining a sense of depth.
|
||||||
```
|
|
||||||
Mobile Desktop (lg+)
|
|
||||||
┌─────────────────────┐ ┌──────────┬──────────────────────┐
|
|
||||||
│ Header │ │ │ Breadcrumb / Title │
|
|
||||||
├─────────────────────┤ │ Sidebar │──────────────────────┤
|
|
||||||
│ │ │ │ │
|
|
||||||
│ Content │ │ │ Content │
|
|
||||||
│ │ │ │ │
|
|
||||||
├─────────────────────┤ │ User │ │
|
|
||||||
│ Bottom Nav │ │ Info │ │
|
|
||||||
└─────────────────────┘ └──────────┴──────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sidebar (desktop)
|
|
||||||
|
|
||||||
Fixed left sidebar, `surface-raised` background (vs `surface` main area — tonal shift, no border line).
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐
|
|
||||||
│ Colectivo │ ← collective name, 14px/600
|
|
||||||
├──────────────┤
|
|
||||||
│ 🛒 Lists │ ← active item: slate-900 bg, white text
|
|
||||||
│ ✓ Tasks │ ← inactive: slate-500 text
|
|
||||||
│ 📄 Notes │
|
|
||||||
│ 🔍 Search │
|
|
||||||
│ │
|
|
||||||
│ (spacer) │
|
|
||||||
│ │
|
|
||||||
│ ┌──┐ Name │ ← user avatar + display name
|
|
||||||
│ │AG│ Role │ ⚙ ← settings gear icon
|
|
||||||
└──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- Collective name at top, 14px/600
|
|
||||||
- 4 nav items: Lists, Tasks, Notes, Search
|
|
||||||
- User avatar + display name + settings gear at bottom
|
|
||||||
- Width: `w-56` (`224px`)
|
|
||||||
|
|
||||||
### Navigation (mobile)
|
|
||||||
|
|
||||||
Fixed bottom tab bar, 4 tabs. Icons only — no text labels. Active icon: `slate-900`. Inactive: `slate-400`. Glassmorphism background: `backdrop-blur-md bg-white/80`. Respects `safe-area-inset-bottom`.
|
|
||||||
|
|
||||||
| Tab | Icon | Route |
|
|
||||||
|---|---|---|
|
|
||||||
| Lists | `shopping-cart` | `/lists` |
|
|
||||||
| Tasks | `check-square` | `/tasks` |
|
|
||||||
| Notes | `file-text` | `/notes` |
|
|
||||||
| Search | `search` | `/search` |
|
|
||||||
|
|
||||||
### Page Headers
|
|
||||||
|
|
||||||
Desktop: breadcrumb-style context path above the title, in uppercase 11px/500 `text-slate-400`:
|
|
||||||
|
|
||||||
```
|
|
||||||
WORKSPACE OVERVIEW
|
|
||||||
Shared Lists
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
PROJECT ATLAS / Tasks
|
|
||||||
```
|
|
||||||
|
|
||||||
Mobile: plain title with optional back arrow and overflow `⋯` menu on the right.
|
|
||||||
|
|
||||||
### Responsive Breakpoints
|
|
||||||
|
|
||||||
Tailwind defaults: `sm` (640), `md` (768), `lg` (1024). Sidebar appears at `lg`. Content max-width: `max-w-2xl` centered on large screens.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Screen Patterns
|
## 5. Components
|
||||||
|
All components follow a 4px base grid and a `rounded-md` (0.375rem) corner radius.
|
||||||
### Lists View (`/lists`)
|
|
||||||
|
### Buttons (Inversion Logic)
|
||||||
Featured card for the primary/active list (full width, larger), secondary lists in a 2-column grid below.
|
- **Primary:** `bg-slate-900 text-slate-50` (Light Mode). This "Inversion" is the signature interactive state.
|
||||||
|
- **Destructive:** `bg-red-500 text-white`. Reserved only for irreversible actions.
|
||||||
```
|
- **Ghost:** No background, `text-slate-500`. On hover, shift to `surface-raised`.
|
||||||
WORKSPACE OVERVIEW
|
|
||||||
Shared Lists
|
### Input Fields & Sticky Input
|
||||||
┌─────────────────────────────────────┐
|
- **The Monolith Input:** A sticky bottom container with `backdrop-blur`. No border; only a subtle `surface-raised` background.
|
||||||
│ Home 👤👤 │ ← featured card
|
- **Chips:** `rounded-full`, `bg-slate-200`. When active, they invert to `bg-slate-900 text-white`.
|
||||||
│ 12 items · Updated 2h ago │
|
|
||||||
│ [GROCERIES] [HOUSEHOLD] │ ← category tags, rounded-full
|
### Lists & Swipe Actions
|
||||||
└─────────────────────────────────────┘
|
- **Forbid Dividers:** Do not use lines between list items. Use 12px of vertical padding (`py-3`) to create separation through whitespace.
|
||||||
┌──────────────┐ ┌──────────────┐
|
- **Swipe-to-Delete:** The "Red Zone" emerges from behind the item. As the user swipes, the background transitions into `red-500` with a white Lucide `trash-2` icon.
|
||||||
│ Office │ │ Party │
|
|
||||||
│ 5 items │ │ 26 items │
|
### Shopping Mode Components
|
||||||
│ 🗂 │ │ HIGH PRTY │ ← badge
|
- **Large Tap Targets:** Increase minimum hit area to 56px.
|
||||||
└──────────────┘ └──────────────┘
|
- **Presence Avatars:** 24px circles with a 2px `surface` border, overlapped horizontally to show collaboration.
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ + Create a new shared list... [Create] │ ← sticky bottom input
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- Category tags on list cards: `rounded-full text-xs font-medium bg-slate-100 px-2 py-0.5`
|
|
||||||
- Presence avatars overlap on cards with active members
|
|
||||||
|
|
||||||
### List Detail (`/lists/[id]`)
|
|
||||||
|
|
||||||
Normal (non-session) view. Items with quantity controls.
|
|
||||||
|
|
||||||
```
|
|
||||||
ACTIVE LIST
|
|
||||||
Grocery List ⋯
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ ☐ Honeycrisp Apples 6 + │
|
|
||||||
│ ☐ Organic Baby Spinach 2 + │
|
|
||||||
│ ☐ Whole milk 1L 2 + │
|
|
||||||
│ ☐ Salted Butter 1 + │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
|
|
||||||
[MILK] [BREAD] [EGGS] [COFFEE] [APPLES] ← frequency chips
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ Add item (e.g. Greek Yogurt 500g) [+] │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- Quantity controls: `−` count `+` inline at the right of each row
|
|
||||||
- Frequency chips always visible above the input (not just on focus); active chip: `bg-slate-900 text-white`; inactive: `bg-slate-100 text-slate-700`
|
|
||||||
|
|
||||||
### Shopping Session Mode (`/lists/[id]/session`)
|
|
||||||
|
|
||||||
Full-screen takeover. No sidebar, no bottom nav.
|
|
||||||
|
|
||||||
```
|
|
||||||
✕ Compra semanal 👤👤
|
|
||||||
─────────────────────────────────────
|
|
||||||
TO BUY
|
|
||||||
☐ Whole milk 1L
|
|
||||||
☐ Bread
|
|
||||||
☐ Eggs 12 uds
|
|
||||||
☐ Avocados
|
|
||||||
|
|
||||||
CHECKED
|
|
||||||
☑ Butter 250g (muted, struck through)
|
|
||||||
☑ Olive Oil 1L
|
|
||||||
|
|
||||||
─────────────────────────────────────
|
|
||||||
[ Finish shopping ● ]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `TO BUY` and `CHECKED` are uppercase section labels, not divider lines
|
|
||||||
- Items slide from TO BUY to CHECKED on tap — no page scroll
|
|
||||||
- Minimum tap target: `min-h-[56px]`
|
|
||||||
- Presence avatars: `size-6 rounded-full`, overlapping, top-right of header
|
|
||||||
- "Finish shopping" button: full-width, `bg-slate-900 text-white`, with a subtle pulse indicator dot (●)
|
|
||||||
|
|
||||||
### Tasks View (`/tasks`)
|
|
||||||
|
|
||||||
Tasks grouped into named sections. Filter pills at the bottom.
|
|
||||||
|
|
||||||
```
|
|
||||||
PROJECT ATLAS / Tasks 👤👤
|
|
||||||
─────────────────────────────────────
|
|
||||||
HIGH PRIORITY
|
|
||||||
☐ Finalize Q4 strategy deck TODAY
|
|
||||||
☐ Coordinate with design on monolith tokens URGENT
|
|
||||||
|
|
||||||
PRODUCT DEVELOPMENT
|
|
||||||
☐ Core API Integration
|
|
||||||
☐ Review documentation for Auth endpoints (indented sub-item)
|
|
||||||
☑ Map database schema to editorial structure (completed)
|
|
||||||
☐ User testing: Editorial workflow
|
|
||||||
|
|
||||||
BACKLOG
|
|
||||||
☐ Update dependency libraries
|
|
||||||
|
|
||||||
─────────────────────────────────────
|
|
||||||
[DAILY] [WEEKLY] [URGENT] ← filter pills
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ Add task to Tasks... [Save] │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- Section names are user-defined (not system-fixed)
|
|
||||||
- Priority badges on individual tasks: `text-xs uppercase text-slate-400` (TODAY, URGENT)
|
|
||||||
- Filter pills: same style as frequency chips; filter the visible tasks
|
|
||||||
|
|
||||||
### Notes View (`/notes`)
|
|
||||||
|
|
||||||
Notes displayed as cards in a masonry-style grid, grouped by user-defined categories.
|
|
||||||
|
|
||||||
```
|
|
||||||
WORKSPACE / NOTES 🔔 ⊞ 👤
|
|
||||||
─────────────────────────────────────
|
|
||||||
CATEGORY
|
|
||||||
Meeting Notes
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Q4 Product │ │ Sprint Retro: │
|
|
||||||
│ Roadmap... │ │ Project Apollo │
|
|
||||||
│ Oct 24, 2023 │ │ Oct 22, 2023 │
|
|
||||||
└─────────────────┘ └─────────────────┘
|
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
Project Ideas
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Minimalist │
|
|
||||||
│ Portfolio... │
|
|
||||||
└─────────────────┘
|
|
||||||
|
|
||||||
─────────────────────────────────────
|
|
||||||
│ Capture a new thought... # @ [SAVE] │
|
|
||||||
```
|
|
||||||
|
|
||||||
- Cards: `bg-white rounded-md` on `surface-raised` background (tonal lift, no shadow)
|
|
||||||
- Category labels: uppercase section labels above each group
|
|
||||||
- Inline new-note input at the bottom (not a separate screen)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## UX Patterns
|
## 6. Do's and Don'ts
|
||||||
|
|
||||||
### Optimistic Updates
|
### Do
|
||||||
|
- **Do** use uppercase labels with letter-spacing for all category headers.
|
||||||
All mutations update the local store immediately before the server responds. On error, revert and show a toast. Never block the UI waiting for a network response.
|
- **Do** use `slate-500` for meta-text to create a visual "recession" of less important data.
|
||||||
|
- **Do** maximize the use of the `2xl` max-width container on desktop to ensure the "Editorial" line lengths remain readable.
|
||||||
### Offline Indicator
|
- **Do** use Lucide icons at 1.5 stroke width to match the weight of Inter's medium-weight characters.
|
||||||
|
|
||||||
A slim bar at the top of the content area (not the header) when offline:
|
### Don't
|
||||||
|
- **Don't** use a 1px border to separate the sidebar from the main content. Use a background color shift.
|
||||||
```
|
- **Don't** use any colors outside of the Slate and Red-500 palette. No "Success Green" or "Warning Orange." Use icons and text to convey state.
|
||||||
No connection · Saving locally
|
- **Don't** use standard "drop shadows" on cards. If it doesn't pop via tonal shift, the layout needs more whitespace, not more shadow.
|
||||||
```
|
- **Don't** use "Slate-900" for body text. Use "Slate-900" only for titles; use "Slate-800" or "Slate-700" for long-form reading to reduce eye strain.
|
||||||
|
|
||||||
`bg-slate-800 text-slate-100`, 36px tall. Mounts/unmounts immediately — no animation.
|
|
||||||
|
|
||||||
### Frequency Chips (Add Item Input)
|
|
||||||
|
|
||||||
Sticky input area at the bottom of list and task screens. Frequency chips are **always visible** above the input, not conditional on focus.
|
|
||||||
|
|
||||||
```
|
|
||||||
[MILK] [BREAD ●] [EGGS] [COFFEE] [APPLES] ← scrollable row
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ Add item (e.g. Greek Yogurt 500g) [+] │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- Chips: `rounded-full text-xs font-medium px-3 py-1 bg-slate-100` (inactive) / `bg-slate-900 text-white` (active/selected)
|
|
||||||
- Tapping a chip fills the input; user can still edit before confirming
|
|
||||||
- While typing, chips switch to prefix-filtered suggestions (max 10, same pill style)
|
|
||||||
- Confirm with `+` button or `Enter`
|
|
||||||
|
|
||||||
### Swipe to Delete
|
|
||||||
|
|
||||||
On list items and tasks. Swipe left reveals a red destructive zone:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────────────────────────────────┐
|
|
||||||
│ ☐ Whole milk 1L ░░░░░░ │ ← item sliding left
|
|
||||||
│ [🗑] │ ← red-500 zone, trash icon only
|
|
||||||
└────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
Full swipe: immediate deletion + Sonner toast "Deleted · Undo" (4 seconds). Partial swipe: snaps back. No confirmation dialog for single items.
|
|
||||||
|
|
||||||
### Drag & Drop Reordering
|
|
||||||
|
|
||||||
Handle (`grip-vertical` Lucide icon) on the left — always visible on desktop, revealed on long-press on mobile. Use `svelte-dnd-action`. During drag: lifted item has `shadow-md bg-white`, others compress to show insertion point.
|
|
||||||
|
|
||||||
### Confirmations
|
|
||||||
|
|
||||||
**Destructive, hard-to-reverse actions** (delete list, remove member, dissolve collective): `Dialog` with explicit description text. No shortcut.
|
|
||||||
|
|
||||||
**Soft deletions** (single item, single task): swipe + Sonner undo. No dialog.
|
|
||||||
|
|
||||||
### Avatar Component
|
|
||||||
|
|
||||||
Three mutually exclusive states, all `rounded-full`.
|
|
||||||
|
|
||||||
| State | Appearance | Size tokens |
|
|
||||||
|---|---|---|
|
|
||||||
| `initials` | Initials on `slate-700` bg, white 600-weight text | `size-6` presence, `size-8` lists, `size-16` settings |
|
|
||||||
| `emoji` | Emoji centered on `slate-700` bg | same |
|
|
||||||
| `upload` | `<img object-cover>`, fallback to initials on error | same |
|
|
||||||
|
|
||||||
Initials rule: first letter of first + last word in `display_name` ("Ana García" → "AG", "Borja" → "B").
|
|
||||||
|
|
||||||
### User Settings Screen (`/settings`)
|
|
||||||
|
|
||||||
In the desktop sidebar shell. Content area:
|
|
||||||
|
|
||||||
```
|
|
||||||
← Settings
|
|
||||||
|
|
||||||
┌──────┐
|
|
||||||
│ AG │ ← size-16 avatar
|
|
||||||
└──────┘
|
|
||||||
Change avatar ›
|
|
||||||
|
|
||||||
DISPLAY NAME
|
|
||||||
┌────────────────────────────────┐
|
|
||||||
│ Ana García │ ← auto-save, 500ms debounce
|
|
||||||
└────────────────────────────────┘
|
|
||||||
Changes saved automatically ← slate-400 hint
|
|
||||||
|
|
||||||
LANGUAGE
|
|
||||||
[ English ] [ Español ] ← pill segmented control
|
|
||||||
English selected: slate-900/white. Instant effect, no reload.
|
|
||||||
|
|
||||||
ACCOUNT ← background shift (no line)
|
|
||||||
Email & password → ← external link to Keycloak
|
|
||||||
Managed in Keycloak ← slate-400
|
|
||||||
|
|
||||||
DANGER ZONE ← section label in red-500
|
|
||||||
[ Delete account ] ← ghost red button
|
|
||||||
```
|
|
||||||
|
|
||||||
**Avatar sheet** — `Sheet` (bottom on mobile, dialog on desktop):
|
|
||||||
|
|
||||||
- Segmented tabs: Initials | Emoji | Photo
|
|
||||||
- Emoji grid: 8 columns, ~40 emoji (🐸 🐼 🦊 🐨 🦁 🐯 🐻 🦋 🌵 🌻 🍀 🌈 ⭐ 🔥 💎 🎯 🎸 🎹 🎨 📚 ✈️ 🏔️ 🌊 🎭 🍕 🍜 🍣 ☕ 🍎 🥑 🍓 🎂 🏠 🚲 🛹 ⚽ 🏄 🧘 🎮 🤿)
|
|
||||||
- Photo upload: file picker → 1:1 crop in-browser → preview → confirm. Max 2MB. JPG/PNG/WebP.
|
|
||||||
|
|
||||||
### Empty States
|
|
||||||
|
|
||||||
Text-only, no illustrations. `text-slate-500`, centered in content area.
|
|
||||||
|
|
||||||
```
|
|
||||||
No lists yet.
|
|
||||||
Tap + to create one.
|
|
||||||
```
|
|
||||||
|
|
||||||
Two lines max.
|
|
||||||
|
|
||||||
### Loading States
|
|
||||||
|
|
||||||
`Skeleton` components matching the shape of the content (list rows, card grids). Spinner only for full-screen blocking operations (login, initial data fetch).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Stitch Mockup Inventory
|
## 7. Layout Philosophy
|
||||||
|
- **Desktop (lg+):** The left sidebar is a fixed Monolith. It stays `surface-raised` while the main content area remains `surface`.
|
||||||
Screens in `.stitch/designs/` — project `10316144241804324155`:
|
- **Mobile:** The bottom tab bar utilizes Glassmorphism. Navigation icons should be `slate-400`, switching to `slate-900` when active. No text labels on mobile tabs; the icons must be clear enough to stand alone.
|
||||||
|
|
||||||
| File | Screen | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| `01-lists-view.png` | Lists View | ✅ |
|
|
||||||
| `02-shopping-session.png` | Shopping Session | ✅ |
|
|
||||||
| `03-notes-view.png` | Notes View | ✅ |
|
|
||||||
| `04-tasks-view.png` | Tasks View | ✅ |
|
|
||||||
| `05-list-detail.png` | List Detail with frequency chips | ✅ |
|
|
||||||
| — | User Settings | ⏳ pending generation |
|
|
||||||
| — | Collective Management | ⏳ pending generation |
|
|
||||||
5
Justfile
5
Justfile
@@ -78,6 +78,11 @@ kc-open:
|
|||||||
build:
|
build:
|
||||||
pnpm turbo run build
|
pnpm turbo run build
|
||||||
|
|
||||||
|
# Run the production build locally against the dev Docker stack (port 3000)
|
||||||
|
serve: build
|
||||||
|
{{dc_dev}} up -d
|
||||||
|
PORT=3000 node apps/web/build/index.js
|
||||||
|
|
||||||
# Type-check all packages
|
# Type-check all packages
|
||||||
check:
|
check:
|
||||||
pnpm turbo run check
|
pnpm turbo run check
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ colectivo/
|
|||||||
```bash
|
```bash
|
||||||
just setup # preparación del entorno local (idempotente, seguro relanzar)
|
just setup # preparación del entorno local (idempotente, seguro relanzar)
|
||||||
just dev # levanta docker-compose.dev.yml + SvelteKit dev server
|
just dev # levanta docker-compose.dev.yml + SvelteKit dev server
|
||||||
|
just serve # build + servidor Node prod en :3000 contra el stack Docker dev
|
||||||
just stop # para todos los servicios (Docker stack + servidor Vite)
|
just stop # para todos los servicios (Docker stack + servidor Vite)
|
||||||
just dev-stop # para solo el stack de Docker
|
just dev-stop # para solo el stack de Docker
|
||||||
just dev-clean # para Docker y elimina todos los volúmenes (reset completo)
|
just dev-clean # para Docker y elimina todos los volúmenes (reset completo)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"nav_notes": "Notes",
|
"nav_notes": "Notes",
|
||||||
"nav_search": "Search",
|
"nav_search": "Search",
|
||||||
"nav_settings": "Settings",
|
"nav_settings": "Settings",
|
||||||
|
"nav_collective": "Collective",
|
||||||
"app_name": "Colectivo",
|
"app_name": "Colectivo",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"error_generic": "Something went wrong. Please try again.",
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"nav_notes": "Notas",
|
"nav_notes": "Notas",
|
||||||
"nav_search": "Buscar",
|
"nav_search": "Buscar",
|
||||||
"nav_settings": "Ajustes",
|
"nav_settings": "Ajustes",
|
||||||
|
"nav_collective": "Colectivo",
|
||||||
"app_name": "Colectivo",
|
"app_name": "Colectivo",
|
||||||
"loading": "Cargando…",
|
"loading": "Cargando…",
|
||||||
"error_generic": "Algo salió mal. Por favor, inténtalo de nuevo.",
|
"error_generic": "Algo salió mal. Por favor, inténtalo de nuevo.",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@supabase/supabase-js": "^2.46.2",
|
"@supabase/supabase-js": "^2.46.2",
|
||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"idb": "^8.0.1",
|
"idb": "^8.0.1",
|
||||||
|
"lucide-svelte": "^1.0.1",
|
||||||
"svelte-dnd-action": "^0.9.51"
|
"svelte-dnd-action": "^0.9.51"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -5,26 +5,31 @@
|
|||||||
/* Inter font */
|
/* Inter font */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
/* Design tokens — light mode */
|
/* Design tokens — light mode
|
||||||
|
* Surface hierarchy (stacked sheets of paper):
|
||||||
|
* background → surface-container-low → surface → surface-raised (cards pop up)
|
||||||
|
*/
|
||||||
:root {
|
:root {
|
||||||
--background: 248 250 252; /* slate-50 */
|
--background: 247 249 251; /* #f7f9fb — spec exact value */
|
||||||
--surface: 255 255 255; /* white */
|
--surface-container-low: 241 245 249; /* slate-100 — subtle grouping bg */
|
||||||
--surface-raised: 241 245 249; /* slate-100 */
|
--surface: 255 255 255; /* white — base card surface */
|
||||||
--border: 226 232 240; /* slate-200 */
|
--surface-raised: 248 250 252; /* slate-50 — popped card (sidebar) */
|
||||||
--text-primary: 15 23 42; /* slate-900 */
|
--text-primary: 15 23 42; /* slate-900 — titles only */
|
||||||
--text-secondary: 100 116 139; /* slate-500 */
|
--text-secondary: 100 116 139; /* slate-500 — meta / labels */
|
||||||
--text-muted: 148 163 184; /* slate-400 */
|
--text-muted: 148 163 184; /* slate-400 */
|
||||||
|
--destructive: 185 28 28; /* red-700 ≈ #ba1a1a spec */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Design tokens — dark mode */
|
/* Design tokens — dark mode */
|
||||||
.dark {
|
.dark {
|
||||||
--background: 15 23 42; /* slate-950 */
|
--background: 2 6 23; /* #020617 slate-950 — spec exact value */
|
||||||
--surface: 30 41 59; /* slate-800 → closer to slate-900 card */
|
--surface-container-low: 15 23 42; /* slate-900 — subtle grouping */
|
||||||
--surface-raised: 51 65 85; /* slate-700 */
|
--surface: 30 41 59; /* slate-800 — base card surface */
|
||||||
--border: 51 65 85; /* slate-700 */
|
--surface-raised: 51 65 85; /* slate-700 — popped card (sidebar) */
|
||||||
--text-primary: 248 250 252; /* slate-50 */
|
--text-primary: 248 250 252; /* slate-50 */
|
||||||
--text-secondary: 148 163 184; /* slate-400 */
|
--text-secondary: 148 163 184; /* slate-400 */
|
||||||
--text-muted: 100 116 139; /* slate-500 */
|
--text-muted: 100 116 139; /* slate-500 */
|
||||||
|
--destructive: 239 68 68; /* red-500 — more visible on dark bg */
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -38,8 +43,8 @@ body {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
background-color: hsl(var(--background));
|
background-color: rgb(var(--background));
|
||||||
color: hsl(var(--text-primary));
|
color: rgb(var(--text-primary));
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { logout } from '$lib/auth';
|
import { logout } from '$lib/auth';
|
||||||
import Avatar from '$lib/components/Avatar.svelte';
|
import Avatar from '$lib/components/Avatar.svelte';
|
||||||
import * as m from '$lib/paraglide/messages';
|
import * as m from '$lib/paraglide/messages';
|
||||||
|
import { ShoppingCart, CheckSquare, FileText, Search, Settings } from 'lucide-svelte';
|
||||||
|
|
||||||
// When auth resolves as unauthenticated, redirect to Keycloak.
|
// When auth resolves as unauthenticated, redirect to Keycloak.
|
||||||
// This runs after onAuthStateChange fires (authLoading = false), so there
|
// This runs after onAuthStateChange fires (authLoading = false), so there
|
||||||
@@ -20,10 +21,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/lists', label: () => m.nav_lists(), icon: '🛒' },
|
{ href: '/lists', label: () => m.nav_lists(), icon: ShoppingCart },
|
||||||
{ href: '/tasks', label: () => m.nav_tasks(), icon: '✓' },
|
{ href: '/tasks', label: () => m.nav_tasks(), icon: CheckSquare },
|
||||||
{ href: '/notes', label: () => m.nav_notes(), icon: '📄' },
|
{ href: '/notes', label: () => m.nav_notes(), icon: FileText },
|
||||||
{ href: '/search', label: () => m.nav_search(), icon: '🔍' }
|
{ href: '/search', label: () => m.nav_search(), icon: Search }
|
||||||
];
|
];
|
||||||
|
|
||||||
let collectiveSwitcherOpen = $state(false);
|
let collectiveSwitcherOpen = $state(false);
|
||||||
@@ -46,14 +47,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if $isAuthenticated}
|
{:else if $isAuthenticated}
|
||||||
<div class="flex h-screen overflow-hidden bg-background">
|
<div class="flex h-screen overflow-hidden bg-background">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar — surface-raised bg creates the separation without a border line -->
|
||||||
<aside class="flex w-56 flex-shrink-0 flex-col border-r border-slate-200 bg-surface dark:border-slate-700">
|
<aside class="flex w-56 flex-shrink-0 flex-col bg-surface-raised">
|
||||||
|
|
||||||
<!-- Collective header + switcher -->
|
<!-- Collective header + switcher — bottom gap instead of border -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
class="flex h-14 w-full items-center gap-2 border-b border-slate-200 px-4 text-left
|
class="flex h-14 w-full items-center gap-2 px-4 text-left
|
||||||
hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800"
|
hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
onclick={() => (collectiveSwitcherOpen = !collectiveSwitcherOpen)}
|
onclick={() => (collectiveSwitcherOpen = !collectiveSwitcherOpen)}
|
||||||
aria-expanded={collectiveSwitcherOpen}
|
aria-expanded={collectiveSwitcherOpen}
|
||||||
>
|
>
|
||||||
@@ -68,12 +69,12 @@
|
|||||||
|
|
||||||
{#if collectiveSwitcherOpen && $userCollectives.length > 1}
|
{#if collectiveSwitcherOpen && $userCollectives.length > 1}
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 right-0 top-full z-50 border-b border-slate-200 bg-surface shadow-md dark:border-slate-700"
|
class="absolute left-0 right-0 top-full z-50 bg-surface-raised shadow-[0px_20px_40px_rgba(15,23,42,0.06)]"
|
||||||
>
|
>
|
||||||
{#each $userCollectives as c}
|
{#each $userCollectives as c}
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center gap-2 px-4 py-2.5 text-left text-sm
|
class="flex w-full items-center gap-2 px-4 py-2.5 text-left text-sm
|
||||||
hover:bg-slate-50 dark:hover:bg-slate-800
|
hover:bg-black/5 dark:hover:bg-white/5
|
||||||
{c.id === $currentCollective?.id ? 'font-semibold text-slate-900 dark:text-slate-50' : 'text-slate-600 dark:text-slate-400'}"
|
{c.id === $currentCollective?.id ? 'font-semibold text-slate-900 dark:text-slate-50' : 'text-slate-600 dark:text-slate-400'}"
|
||||||
onclick={() => switchCollective(c.id)}
|
onclick={() => switchCollective(c.id)}
|
||||||
>
|
>
|
||||||
@@ -87,22 +88,22 @@
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="flex-1 overflow-y-auto px-2 py-3">
|
<nav class="flex-1 overflow-y-auto px-2 py-3">
|
||||||
{#each navItems as item}
|
{#each navItems as { href, label, icon: Icon }}
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
{href}
|
||||||
class="mb-0.5 flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors
|
class="mb-0.5 flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors
|
||||||
{activePath.startsWith(item.href)
|
{activePath.startsWith(href)
|
||||||
? 'bg-slate-100 text-slate-900 dark:bg-slate-700 dark:text-slate-50'
|
? 'bg-black/10 text-slate-900 dark:bg-white/10 dark:text-slate-50'
|
||||||
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-50'}"
|
: 'text-slate-500 hover:bg-black/5 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-white/5 dark:hover:text-slate-50'}"
|
||||||
>
|
>
|
||||||
<span class="text-base">{item.icon}</span>
|
<Icon size={16} strokeWidth={1.5} />
|
||||||
{item.label()}
|
{label()}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- User footer -->
|
<!-- User footer — top gap instead of border -->
|
||||||
<div class="border-t border-slate-200 p-3 dark:border-slate-700">
|
<div class="p-3 pt-2">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
<Avatar name={$displayName} size={32} />
|
<Avatar name={$displayName} size={32} />
|
||||||
@@ -112,17 +113,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="/settings"
|
href="/settings"
|
||||||
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-600 dark:hover:bg-slate-700 dark:hover:text-slate-300"
|
class="rounded-md p-1.5 text-slate-400 hover:bg-black/5 hover:text-slate-600
|
||||||
|
dark:hover:bg-white/5 dark:hover:text-slate-300"
|
||||||
aria-label={m.nav_settings()}
|
aria-label={m.nav_settings()}
|
||||||
>
|
>
|
||||||
⚙
|
<Settings size={16} strokeWidth={1.5} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content — bg-background creates the tonal separation from sidebar -->
|
||||||
<main class="flex flex-1 flex-col overflow-hidden">
|
<main class="flex flex-1 flex-col overflow-hidden bg-background">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -136,25 +136,32 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
<h1 class="mb-6 text-xl font-bold text-slate-900 dark:text-slate-50">
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="px-8 pb-4 pt-8">
|
||||||
|
<p class="mb-1 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
{m.nav_collective()}
|
||||||
|
</p>
|
||||||
|
<h1 class="text-[20px] font-semibold tracking-[-0.01em] text-slate-900 dark:text-slate-50">
|
||||||
{m.manage_title()}
|
{m.manage_title()}
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="px-8 py-6">
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="mb-4 text-sm text-red-600">{error}</p>
|
<p class="mb-4 text-sm text-red-600">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Members list -->
|
<!-- Members list — no dividers, vertical padding creates separation -->
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-text-secondary">
|
<h2 class="mb-4 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
{m.manage_members()}
|
{m.manage_members()}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="text-sm text-text-secondary">{m.loading()}</p>
|
<p class="text-sm text-text-secondary">{m.loading()}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="divide-y divide-slate-100 dark:divide-slate-700">
|
<ul>
|
||||||
{#each members as member}
|
{#each members as member}
|
||||||
<li class="flex items-center gap-3 py-3">
|
<li class="flex items-center gap-3 py-3">
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -202,10 +209,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Invite link generator (admins only) -->
|
<!-- Invite link generator — background shift instead of border -->
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<section class="rounded-lg border border-slate-200 p-4 dark:border-slate-700">
|
<section class="rounded-lg bg-surface-container-low p-4 dark:bg-surface-raised">
|
||||||
<h2 class="mb-3 text-sm font-semibold text-slate-900 dark:text-slate-50">
|
<h2 class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
{m.manage_invite_title()}
|
{m.manage_invite_title()}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -232,8 +239,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if generatedLink}
|
{#if generatedLink}
|
||||||
<div class="flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 p-2
|
<div class="flex items-center gap-2 rounded-lg bg-surface p-2 dark:bg-surface-raised">
|
||||||
dark:border-slate-700 dark:bg-slate-800">
|
|
||||||
<code class="min-w-0 flex-1 truncate text-xs text-slate-600 dark:text-slate-400">
|
<code class="min-w-0 flex-1 truncate text-xs text-slate-600 dark:text-slate-400">
|
||||||
{generatedLink}
|
{generatedLink}
|
||||||
</code>
|
</code>
|
||||||
@@ -250,3 +256,5 @@
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col overflow-hidden">
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
<header class="flex h-14 items-center border-b border-slate-200 px-6 dark:border-slate-700">
|
<header class="px-8 pb-4 pt-8">
|
||||||
<h1 class="text-base font-semibold text-slate-900 dark:text-slate-50">{m.lists_title()}</h1>
|
<p class="mb-1 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
{m.nav_lists()}
|
||||||
|
</p>
|
||||||
|
<h1 class="text-[20px] font-semibold tracking-[-0.01em] text-slate-900 dark:text-slate-50">
|
||||||
|
{m.lists_title()}
|
||||||
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex-1 overflow-y-auto px-8 py-6">
|
||||||
<!-- Fase 2a: shopping lists content -->
|
<!-- Fase 2a: shopping lists content -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col overflow-hidden">
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
<header class="flex h-14 items-center border-b border-slate-200 px-6 dark:border-slate-700">
|
<header class="px-8 pb-4 pt-8">
|
||||||
<h1 class="text-base font-semibold text-slate-900 dark:text-slate-50">{m.notes_title()}</h1>
|
<p class="mb-1 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
{m.nav_notes()}
|
||||||
|
</p>
|
||||||
|
<h1 class="text-[20px] font-semibold tracking-[-0.01em] text-slate-900 dark:text-slate-50">
|
||||||
|
{m.notes_title()}
|
||||||
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex-1 overflow-y-auto px-8 py-6">
|
||||||
<!-- Fase 3: notes content -->
|
<!-- Fase 3: notes content -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col overflow-hidden">
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
<header class="flex h-14 items-center border-b border-slate-200 px-6 dark:border-slate-700">
|
<header class="px-8 pb-4 pt-8">
|
||||||
<h1 class="text-base font-semibold text-slate-900 dark:text-slate-50">{m.search_title()}</h1>
|
<p class="mb-1 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
{m.nav_search()}
|
||||||
|
</p>
|
||||||
|
<h1 class="text-[20px] font-semibold tracking-[-0.01em] text-slate-900 dark:text-slate-50">
|
||||||
|
{m.search_title()}
|
||||||
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex-1 overflow-y-auto px-8 py-6">
|
||||||
<!-- Fase 4: search content -->
|
<!-- Fase 4: search content -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -143,11 +143,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col overflow-hidden">
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
<header class="flex h-14 items-center border-b border-slate-200 px-6 dark:border-slate-700">
|
<header class="px-8 pb-4 pt-8">
|
||||||
<h1 class="text-base font-semibold text-slate-900 dark:text-slate-50">{m.settings_title()}</h1>
|
<p class="mb-1 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
{m.nav_settings()}
|
||||||
|
</p>
|
||||||
|
<h1 class="text-[20px] font-semibold tracking-[-0.01em] text-slate-900 dark:text-slate-50">
|
||||||
|
{m.settings_title()}
|
||||||
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex-1 overflow-y-auto px-8 py-6">
|
||||||
<div class="mx-auto max-w-md space-y-8">
|
<div class="mx-auto max-w-md space-y-8">
|
||||||
|
|
||||||
<!-- Avatar preview -->
|
<!-- Avatar preview -->
|
||||||
@@ -163,7 +168,7 @@
|
|||||||
|
|
||||||
<!-- Display name -->
|
<!-- Display name -->
|
||||||
<section>
|
<section>
|
||||||
<label for="display-name" class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label for="display-name" class="mb-1 block text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
{m.settings_display_name()}
|
{m.settings_display_name()}
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -185,7 +190,7 @@
|
|||||||
|
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<section>
|
<section>
|
||||||
<p class="mb-3 text-sm font-medium text-slate-700 dark:text-slate-300">
|
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
{m.settings_avatar()}
|
{m.settings_avatar()}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -229,7 +234,7 @@
|
|||||||
|
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
<section>
|
<section>
|
||||||
<p class="mb-3 text-sm font-medium text-slate-700 dark:text-slate-300">
|
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
{m.settings_language()}
|
{m.settings_language()}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -247,9 +252,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Account -->
|
<!-- Account — spacing creates the separation, not a border line -->
|
||||||
<section class="border-t border-slate-200 pt-6 dark:border-slate-700">
|
<section class="pt-8">
|
||||||
<p class="mb-3 text-sm font-medium text-slate-700 dark:text-slate-300">
|
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
{m.settings_account()}
|
{m.settings_account()}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
@@ -262,8 +267,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
onclick={logout}
|
onclick={logout}
|
||||||
class="rounded-lg border border-red-200 px-4 py-2 text-sm font-medium text-red-600
|
class="rounded-md bg-destructive px-4 py-2 text-sm font-semibold text-white hover:opacity-90"
|
||||||
hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
|
|
||||||
>
|
>
|
||||||
{m.settings_logout()}
|
{m.settings_logout()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col overflow-hidden">
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
<header class="flex h-14 items-center border-b border-slate-200 px-6 dark:border-slate-700">
|
<header class="px-8 pb-4 pt-8">
|
||||||
<h1 class="text-base font-semibold text-slate-900 dark:text-slate-50">{m.tasks_title()}</h1>
|
<p class="mb-1 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
{m.nav_tasks()}
|
||||||
|
</p>
|
||||||
|
<h1 class="text-[20px] font-semibold tracking-[-0.01em] text-slate-900 dark:text-slate-50">
|
||||||
|
{m.tasks_title()}
|
||||||
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex-1 overflow-y-auto px-8 py-6">
|
||||||
<!-- Fase 3: tasks content -->
|
<!-- Fase 3: tasks content -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,10 +9,18 @@ export default {
|
|||||||
sans: ['Inter', 'system-ui', 'sans-serif']
|
sans: ['Inter', 'system-ui', 'sans-serif']
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
// Semantic aliases over Tailwind slate
|
// Semantic aliases — "The Monolith Editorial" surface hierarchy.
|
||||||
background: 'hsl(var(--background) / <alpha-value>)',
|
// Use background shifts (never 1px borders) to separate major UI areas.
|
||||||
surface: 'hsl(var(--surface) / <alpha-value>)',
|
background: 'rgb(var(--background) / <alpha-value>)',
|
||||||
'surface-raised': 'hsl(var(--surface-raised) / <alpha-value>)'
|
'surface-container-low': 'rgb(var(--surface-container-low) / <alpha-value>)',
|
||||||
|
surface: 'rgb(var(--surface) / <alpha-value>)',
|
||||||
|
'surface-raised': 'rgb(var(--surface-raised) / <alpha-value>)',
|
||||||
|
// Text hierarchy
|
||||||
|
'text-primary': 'rgb(var(--text-primary) / <alpha-value>)',
|
||||||
|
'text-secondary': 'rgb(var(--text-secondary) / <alpha-value>)',
|
||||||
|
'text-muted': 'rgb(var(--text-muted) / <alpha-value>)',
|
||||||
|
// Only non-slate color allowed in the system
|
||||||
|
destructive: 'rgb(var(--destructive) / <alpha-value>)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
idb:
|
idb:
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
|
lucide-svelte:
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1(svelte@5.55.3)
|
||||||
svelte-dnd-action:
|
svelte-dnd-action:
|
||||||
specifier: ^0.9.51
|
specifier: ^0.9.51
|
||||||
version: 0.9.69(svelte@5.55.3)
|
version: 0.9.69(svelte@5.55.3)
|
||||||
@@ -2383,6 +2386,11 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
lucide-svelte@1.0.1:
|
||||||
|
resolution: {integrity: sha512-WvzZgk0pqzgda+AErLvgWxHkfg/+GgUwqKMRHvzt0IqyMdmyEDzDCk3Z+Wo/3y753oIgx8u9Q4eUbWkghFa8Jg==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||||
|
|
||||||
magic-string@0.25.9:
|
magic-string@0.25.9:
|
||||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||||
|
|
||||||
@@ -5844,6 +5852,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
lucide-svelte@1.0.1(svelte@5.55.3):
|
||||||
|
dependencies:
|
||||||
|
svelte: 5.55.3
|
||||||
|
|
||||||
magic-string@0.25.9:
|
magic-string@0.25.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
sourcemap-codec: 1.4.8
|
sourcemap-codec: 1.4.8
|
||||||
|
|||||||
Reference in New Issue
Block a user