Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md
gsinghpal efc420b4ce docs(shopfloor): tablet lock-screen redesign spec
Hybrid Industrial Bold + Premium Glassmorphism direction approved
during brainstorming. Adds company branding (logo from
res.company.logo with letter-mark fallback), real-time clock, tighter
3-column tile grid for ~10-15 operator small shops, dual dark/light
mode via compile-time $o-webclient-color-scheme branch, 7-animation
catalogue gated by prefers-reduced-motion.

Backend touch: extend /fp/tablet/tiles payload with company block +
per-tile initials/avatar_gradient/has_photo. Two small helper
functions in tablet_controller. No DB migration.

Frontend touch: new _tablet_lock_tokens.scss (loads first), full
rewrite of tablet_lock.scss, extend XML + JS for clock + company.

Mockup: .superpowers/brainstorm/1983-1779585812/content/lock-final.html
(in-repo since the brainstorm session used --project-dir).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:38:11 -04:00

528 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tablet Lock Screen Redesign
**Date:** 2026-05-24
**Status:** Design — approved through brainstorming, awaiting plan
**Affects:** `fusion_plating_shopfloor` (FpTabletLock OWL component + tablet_controller)
**Scope:** Visual + interaction redesign only. PIN gate, unlock RPC, lockout timer, idle warning all unchanged.
---
## 1. Problem
The current FpTabletLock tile screen looks like a placeholder. Two operators per row stretch their tiles across 900px max-width; the screen is mostly empty whitespace; there's no branding; the "Tap your name to unlock" prompt is the only header; no animations; no clock/date. Functionally correct but feels unfinished on a wall-mounted shop-floor tablet.
User feedback after live testing (2026-05-24):
> "i want company logo and other nice customization, add some animation, reduce the card width so its just enough, there may be many employees, i do not want a lot of scrolling but not cramped at the same time"
Target: tablet that looks like a deliberately-designed shop terminal, fits ~10-15 operators per screen without scrolling, brands the device with the company logo, and has subtle motion that signals "alive."
---
## 2. Goals & non-goals
### Goals
1. **Brand the screen** — pull the company logo from `res.company.logo`, surface the company name + tagline.
2. **Tighter tile grid** — 3 columns max-width 480px, ~140px tile width. Fits 6 tiles per visible row; small shops (10-15 ops) show everything without scroll.
3. **Real-time clock + date** — operators glance at the lock screen for the time; big tabular-nums clock front-and-center.
4. **Subtle motion** — staggered entrance, hover lift, clocked-in pulse. Doesn't distract; signals freshness.
5. **Dark + light mode parity** — single SCSS source, branches at compile time via `$o-webclient-color-scheme`. No JS-side theme code.
6. **Accessibility**`prefers-reduced-motion` respected, touch targets ≥ 44px, contrast WCAG AA in both modes.
### Non-goals
- **Replacing the PIN gate.** The 4-digit PIN flow (FpPinPad component, hash + lockout, /fp/tablet/unlock endpoint) stays identical.
- **Multi-tenant theming.** Each company sees its own logo via `res.company.logo`; we don't build a theme editor for accent colors. The amber accent is a hardcoded brand token in this design.
- **Search box on the lock screen.** For ~10-15 operators, scanning the grid is faster than typing. Search returns as a Phase 2 enhancement if a customer scales to 25+ ops.
- **Custom tile sort.** Existing rule stays: clocked-in operators first, then alphabetical.
- **Welcome animations / video / mascot.** Subtle motion only.
---
## 3. Decisions locked during brainstorming
| # | Decision |
|---|---|
| D1 | **Hybrid A+B vibe** — Industrial Bold structure (dark gradient bg, bold tabular clock, amber accent) wearing Premium Glassmorphism finish (frosted-glass tiles with backdrop-filter, smooth cubic-bezier hover). |
| D2 | **Company logo** sourced from `res.company.logo` (Odoo's standard company logo binary field) via `/web/image/res.company/<id>/logo`. Letter-mark fallback when no logo is uploaded — built from `res.company.name` initials. |
| D3 | **Company name + tagline** below the logo. Name = `res.company.name`. Tagline = `res.company.report_header` (existing field, also drives invoice letterheads — natural reuse) with fallback "Shop Floor Terminal" if empty. |
| D4 | **3-column tile grid**, max-width 480px on the grid container. Tile ~140px wide. Avatar 52px circular with status pulse-dot overlay. |
| D5 | **Dark + light mode parity.** Same OWL component + same XML; SCSS branches at compile time on `$o-webclient-color-scheme`. No runtime theme code. |
| D6 | **Animation catalogue** (full list in §6) — entrance stagger, hover lift, click scale, pulse on clocked-in dot, real-time clock update. `prefers-reduced-motion` disables all of these. |
| D7 | **Sort order unchanged** — clocked-in operators first, then alphabetical by name. |
| D8 | **No search box** for MVP — scoped for the ~10-15-operator small-shop case. |
---
## 4. Layout
The screen is a full-viewport flex column, centered, with this vertical sequence:
```
┌──────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ │ ← logo frame (84×84)
│ │ LOGO │ (rounded 20, glass) │ glassmorphic
│ └─────────┘ │
│ Company Name │ ← logo-text (19px, 700)
│ PLATING · ESTD 1985 │ ← logo-sub (11px upper)
│ │
│ 21:09 │ ← clock (40px, 800, tabular)
│ SATURDAY · MAY 23 │ ← clock-date (12px upper)
│ │
│ [ 🔒 TAP YOUR NAME ] │ ← prompt pill
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ GS ● │ │ JM │ │ CV ● │ │
│ │Garry │ │Johnny│ │Carlos│ │ ← 3-column tile grid,
│ │CIN │ │PIN │ │CIN │ │ max-width 480px
│ └──────┘ └──────┘ └──────┘ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ LB ● │ │ RB ● │ │ KP ● │ │
│ │ Lisa │ │ Ravi │ │ Kris │ │
│ │ CIN │ │ CIN │ │ CIN │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
└──────────────────────────────────────────────────┘
```
Spacing between sections: 22px gap. Logo block top margin: 28px. Outer padding: 28px 20px.
### 4.1 Logo block
```html
<div class="o_fp_lock_logo_block">
<div class="o_fp_lock_logo_frame">
<img t-att-src="logoUrl" t-att-alt="companyName" t-if="logoUrl"/>
<div t-else="" class="o_fp_lock_logo_placeholder" t-esc="companyInitials"/>
</div>
<div class="o_fp_lock_logo_text" t-esc="companyName"/>
<div class="o_fp_lock_logo_sub" t-esc="companyTagline"/>
</div>
```
- `logoUrl`: `/web/image/res.company/<id>/logo` — Odoo serves the binary directly. Always 200 if the field is populated (even 1×1 transparent on empty record), so probe the field server-side before emitting the URL.
- `companyInitials`: first 1-2 letters of `res.company.name` (e.g. "EN" for "EN Technologies", "ABC" capped to 2 chars). Computed server-side, sent in the tiles-endpoint payload.
- `companyTagline`: from `res.company.report_header` field; defaults to "Shop Floor Terminal" when empty.
The logo frame is a 84×84 rounded-20 glassmorphic container — same frosted treatment as the tiles. Looks great whether the logo is a sharp PNG, transparent SVG, or the letter-mark fallback.
### 4.2 Clock block
```html
<div class="o_fp_lock_clock_block">
<div class="o_fp_lock_clock" t-esc="state.clockText"/>
<div class="o_fp_lock_clock_date" t-esc="state.dateText"/>
</div>
```
- `state.clockText`: `HH:MM` (24h, configurable via `intl.DateTimeFormat`). Updates every minute via `setInterval` in `tablet_lock.js`.
- `state.dateText`: `WEEKDAY · MMM D` uppercase (e.g. "SATURDAY · MAY 23"). Recomputed on date change.
- Tabular numbers so digits don't jitter when changing.
- Initial render uses `new Date()` synchronously so there's no flash of empty content.
### 4.3 Prompt
A small pill, not a header:
```html
<div class="o_fp_lock_prompt">🔒 Tap your name</div>
```
Amber-tinted background (matches brand accent), uppercase with 0.18em letter-spacing. Sits between the clock and the tile grid as a visual anchor.
### 4.4 Tile grid
```html
<div class="o_fp_lock_tiles">
<t t-foreach="state.tiles" t-as="tile" t-key="tile.user_id">
<button class="o_fp_lock_tile"
t-att-style="'animation-delay: ' + tile.animDelay + 'ms'"
t-on-click="() => this.onTileClick(tile.user_id)">
<div t-att-class="tile.is_clocked_in ? 'o_fp_lock_avatar is-clocked' : 'o_fp_lock_avatar'"
t-att-style="'background: ' + tile.avatar_gradient">
<img t-if="tile.has_photo" t-att-src="tile.avatar_url" t-att-alt="tile.name"/>
<span t-else="" t-esc="tile.initials"/>
</div>
<div class="o_fp_lock_name" t-esc="tile.name"/>
<div t-if="tile.is_clocked_in" class="o_fp_lock_status status-clocked">Clocked in</div>
<div t-elif="!tile.has_pin" class="o_fp_lock_status status-pin">PIN required</div>
</button>
</t>
</div>
```
- Grid: `grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 480px`.
- Animation delay computed JS-side per tile (50ms × index, capped at 300ms) so the stagger ripples without dragging.
- Avatar gradient (per-tile color): server-computed as `user.id % len(_AVATAR_GRADIENTS)` (8 colors). Deterministic — same operator gets the same color across sessions, so operators learn their own tile color. See §7.3 for the gradient list.
- `has_photo` is true when `res.users.image_128` is non-empty. Falls back to initials when empty.
---
## 5. Color system
All colors live in `_tablet_lock_tokens.scss` (new file, loaded before `tablet_lock.scss`). Same pattern as the plant-view tokens shipped earlier.
### Light-mode defaults
| Token | Hex | Purpose |
|---|---|---|
| `$_lock-bg-top` | `#fafafa` | Gradient top |
| `$_lock-bg-bottom` | `#f0f0f3` | Gradient bottom |
| `$_lock-accent` | `rgba(240,165,0,0.12)` | Top-radial ambient glow |
| `$_lock-accent-2` | `rgba(99,102,241,0.06)` | Bottom-radial ambient glow |
| `$_lock-text` | `#1d1f1e` | Primary text |
| `$_lock-muted` | `#71717a` | Secondary text |
| `$_lock-prompt` | `#b45309` | Prompt text |
| `$_lock-prompt-bg` | `rgba(240,165,0,0.10)` | Prompt pill bg |
| `$_lock-prompt-border` | `rgba(240,165,0,0.25)` | Prompt pill border |
| `$_lock-tile-bg` | `rgba(255,255,255,0.7)` | Tile bg (frosted) |
| `$_lock-tile-border` | `rgba(0,0,0,0.05)` | Tile border |
| `$_lock-tile-hover-bg` | `rgba(255,255,255,0.95)` | Tile hover bg |
| `$_lock-tile-hover-border` | `rgba(240,165,0,0.5)` | Tile hover border |
| `$_lock-tile-hover-shadow` | `0 12px 24px rgba(240,165,0,0.18)` | Tile hover shadow |
| `$_lock-frame-bg` | `rgba(255,255,255,0.85)` | Logo frame bg |
| `$_lock-status-clocked` | `#16a34a` | Clocked-in green |
| `$_lock-status-pin` | `#d97706` | PIN required amber |
| `$_lock-pulse-dot-border` | `#fff` | Pulse-dot ring |
### Dark-mode overrides
| Token | Hex |
|---|---|
| `$_lock-bg-top` | `#1a1d21` (gradient base) |
| `$_lock-bg-bottom` | `#2d3138` |
| `$_lock-accent` | `rgba(240,165,0,0.08)` |
| `$_lock-accent-2` | `rgba(99,102,241,0.06)` |
| `$_lock-text` | `#f5f5f7` |
| `$_lock-muted` | `#adb5bd` |
| `$_lock-prompt` | `#f0a500` |
| `$_lock-prompt-bg` | `rgba(240,165,0,0.08)` |
| `$_lock-prompt-border` | `rgba(240,165,0,0.20)` |
| `$_lock-tile-bg` | `rgba(255,255,255,0.06)` |
| `$_lock-tile-border` | `rgba(255,255,255,0.08)` |
| `$_lock-tile-hover-bg` | `rgba(240,165,0,0.10)` |
| `$_lock-tile-hover-border` | `rgba(240,165,0,0.4)` |
| `$_lock-tile-hover-shadow` | `0 12px 24px rgba(240,165,0,0.15), 0 0 0 1px rgba(240,165,0,0.2)` |
| `$_lock-frame-bg` | `rgba(255,255,255,0.08)` |
| `$_lock-status-clocked` | `#34c759` (brighter — needs to pop on dark) |
| `$_lock-status-pin` | `#ff9f0a` |
| `$_lock-pulse-dot-border` | `#2d3138` (so the dot reads as overlapping the dark tile, not floating) |
The full-screen background is a stack of two radial gradients (the ambient accent glows) over a linear gradient (the base), per `lock-final.html` from brainstorm:
```scss
background:
radial-gradient(ellipse at top, $_lock-accent, transparent 50%),
radial-gradient(ellipse at bottom, $_lock-accent-2, transparent 50%),
linear-gradient(135deg, $_lock-bg-top 0%, $_lock-bg-bottom 100%);
```
---
## 6. Animation catalogue
All animations use `cubic-bezier(0.4, 0, 0.2, 1)` for consistency (the "standard easing" curve). Every animation is gated by `@media (prefers-reduced-motion: no-preference)` — operators who set reduced motion in OS preferences see the same screen with no movement.
| # | Name | What it does | Duration | Trigger |
|---|---|---|---|---|
| 1 | `lockLogoEnter` | Logo block fades down + slides in | 500ms | onMount |
| 2 | `lockClockEnter` | Clock + prompt fade up | 500ms (100ms delay) | onMount |
| 3 | `lockTileEnter` | Each tile fades + slides up + scales from 0.96 | 400ms (50ms staggered per index, max 6) | onMount |
| 4 | `lockTileHover` | Lift translateY(-3px) + colored shadow + border glow | 250ms | hover/focus |
| 5 | `lockTilePress` | Quick scale(0.97) | 50ms | active/click |
| 6 | `lockPulseDot` | Green clocked-in dot pulses (ring expands + fades) | 2s loop | clocked-in state present |
| 7 | `lockClockTick` | (no animation — just text content update each minute) | — | `setInterval(60000)` |
### Reduced-motion override
```scss
@media (prefers-reduced-motion: reduce) {
.o_fp_lock_logo_block,
.o_fp_lock_clock_block,
.o_fp_lock_prompt,
.o_fp_lock_tile,
.o_fp_lock_avatar.is-clocked::after {
animation: none !important;
transition: none !important;
}
}
```
### Stagger cap
For very large operator counts the per-tile delay caps at 300ms (6 tiles × 50ms) so the screen doesn't take 3 seconds to settle. Compute `animDelay = Math.min(index * 50, 300)` JS-side.
---
## 7. Backend changes
### 7.1 Extend `/fp/tablet/tiles` payload
Currently returns:
```json
{"ok": true, "tiles": [{user_id, name, avatar_url, is_clocked_in, has_pin}, ...]}
```
After redesign:
```json
{
"ok": true,
"company": {
"id": 1,
"name": "EN Technologies",
"tagline": "Plating & Finishing",
"logo_url": "/web/image/res.company/1/logo",
"has_logo": true,
"initials": "EN"
},
"tiles": [
{
"user_id": 5,
"name": "Garry Singh",
"initials": "GS",
"avatar_url": "/web/image/res.users/5/avatar_128",
"has_photo": true,
"is_clocked_in": true,
"has_pin": true,
"avatar_gradient": "linear-gradient(135deg, #ef4444, #dc2626)"
},
...
]
}
```
New fields per tile:
- `initials`: server-computed from `res.users.name` (first letter of first + last word, capped 2 chars).
- `has_photo`: true when `res.users.image_128` is non-empty (avoids the 1×1 default-image flash).
- `avatar_gradient`: deterministic from hash of user.id. Same gradient across sessions so operators recognize "their" tile color.
The company block is one query: `env.company.id`. Read `name`, `report_header`, check `logo` non-empty.
### 7.2 `_lock_company_payload` helper
A small module-level helper in `tablet_controller.py`:
```python
def _lock_company_payload(env):
"""Returns the company info block for the lock screen."""
co = env.company
return {
'id': co.id,
'name': co.name or '',
'tagline': co.report_header or _('Shop Floor Terminal'),
'logo_url': f'/web/image/res.company/{co.id}/logo',
'has_logo': bool(co.logo),
'initials': _initials_from(co.name),
}
def _initials_from(name):
"""First letter of first + last word, capped at 2 chars uppercase."""
if not name:
return '?'
words = name.strip().split()
if len(words) == 1:
return words[0][:2].upper()
return (words[0][0] + words[-1][0]).upper()
```
### 7.3 `_avatar_gradient_for` helper
```python
_AVATAR_GRADIENTS = [
'linear-gradient(135deg, #ef4444, #dc2626)', # red
'linear-gradient(135deg, #f59e0b, #d97706)', # amber
'linear-gradient(135deg, #10b981, #059669)', # emerald
'linear-gradient(135deg, #3b82f6, #2563eb)', # blue
'linear-gradient(135deg, #8b5cf6, #7c3aed)', # violet
'linear-gradient(135deg, #ec4899, #db2777)', # pink
'linear-gradient(135deg, #14b8a6, #0d9488)', # teal
'linear-gradient(135deg, #f97316, #ea580c)', # orange
]
def _avatar_gradient_for(user_id):
return _AVATAR_GRADIENTS[user_id % len(_AVATAR_GRADIENTS)]
```
8 colors, modulo user_id — same operator gets the same color forever. Sufficient variety for a small shop (10-15 ops have <2 collisions on average).
---
## 8. Frontend changes
### 8.1 Files modified
| File | Change |
|---|---|
| `static/src/scss/_tablet_lock_tokens.scss` | **new** — design tokens (loads first) |
| `static/src/scss/tablet_lock.scss` | full rewrite — gradient bg, logo block, clock block, prompt, tile grid, animations, dark/light branches |
| `static/src/xml/tablet_lock.xml` | wrap existing tile loop with new logo + clock + prompt blocks; add fallback structures |
| `static/src/js/tablet_lock.js` | add `state.clockText` + `state.dateText` + `_tickClock` setInterval; add `state.company`; consume new payload fields |
### 8.2 OWL component reactivity for the clock
The clock updates every 60 seconds:
```javascript
setup() {
// ... existing setup ...
this.state = useState({
// ... existing state ...
clockText: this._formatTime(new Date()),
dateText: this._formatDate(new Date()),
company: null,
});
onMounted(() => {
// ... existing onMounted ...
this._clockInterval = setInterval(() => {
const now = new Date();
this.state.clockText = this._formatTime(now);
this.state.dateText = this._formatDate(now);
}, 60000);
});
onWillUnmount(() => {
// ... existing cleanup ...
if (this._clockInterval) clearInterval(this._clockInterval);
});
}
_formatTime(d) {
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
_formatDate(d) {
return d.toLocaleDateString(undefined, {
weekday: 'long', month: 'short', day: 'numeric'
}).toUpperCase().replace(',', ' ·');
}
```
**Per project rule 20:** all the date/number formatting happens in JS (`_formatTime`, `_formatDate`). The template only renders `state.clockText` / `state.dateText` via `t-esc`. No `String()` / `Number()` / `padStart` calls inside the XML.
### 8.3 Stagger delay computed JS-side
In `_loadTiles`, after fetching, decorate each tile with its `animDelay`:
```javascript
async _loadTiles() {
this.state.loadingTiles = true;
try {
const stationId = parseInt(localStorage.getItem("fp_landing_station_id")) || null;
const res = await rpc("/fp/tablet/tiles", { station_id: stationId });
if (res && res.ok) {
this.state.company = res.company || null;
this.state.tiles = res.tiles.map((tile, idx) => ({
...tile,
animDelay: Math.min(idx * 50, 300), // cap at 300ms
}));
}
} catch (err) {
// Existing quiet fail
} finally {
this.state.loadingTiles = false;
}
}
```
### 8.4 Manifest registration
Adding two SCSS files. Per project rule 8 (SCSS @import forbidden), tokens must register BEFORE the consumer:
```python
# In fusion_plating_shopfloor/__manifest__.py, the lock screen block:
'fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss', # NEW — load first
'fusion_plating_shopfloor/static/src/scss/tablet_lock.scss', # existing — rewritten
'fusion_plating_shopfloor/static/src/xml/tablet_lock.xml', # existing — extended
'fusion_plating_shopfloor/static/src/js/tablet_lock.js', # existing — extended
```
The tokens file lives in `scss/` (not `scss/components/`) because it's session-level — one tokens file for the whole lock-screen experience.
---
## 9. Accessibility
- **Touch targets**: avatar 52px + 14px padding = 80px tile content; tile itself extends to grid cell width ~140px × 110px tall. Both axes well above the 44×44 WCAG minimum.
- **Focus rings**: visible 2px solid amber outline on `:focus-visible`. Distinguishes keyboard navigation from mouse hover.
- **Contrast**:
- Dark mode: white text on `#1a1d21` background = 16.7:1 (AAA).
- Light mode: `#1d1f1e` text on `#fafafa` background = 17.8:1 (AAA).
- Amber prompt text on its tinted bg: 5.2:1 (AA passes).
- **Reduced motion**: full media-query gate documented in §6.
- **Alt text**: logo `<img alt="Company Name">` so screen readers announce the brand on focus.
- **Keyboard navigation**: tab order = logo (skip) → tiles in DOM order → first tile receives initial focus on mount.
---
## 10. Testing strategy
### 10.1 Unit / integration
- `test_tablet_tiles_endpoint_includes_company` — call `/fp/tablet/tiles`, assert response has `company` block with required keys.
- `test_initials_from_helper` — edge cases: empty name, single-word name, multi-word name with hyphens.
- `test_avatar_gradient_deterministic` — same user.id returns same gradient across calls.
### 10.2 Visual snapshot tests
Per state, a Playwright snapshot of the lock screen at `1366×768` (typical tablet) in both light and dark mode. Snapshots checked in; PR diff catches accidental CSS regressions.
### 10.3 Persona walks
- **Cold start** — operator approaches tablet with no recent session. Clock displays current time; tiles fade in; clicking own tile opens PIN pad immediately (no visible loading state).
- **Mid-shift unlock** — operator returns after auto-lock. Same flow; their tile shows the pulsing clocked-in dot.
- **No logo configured** — companies that haven't set `res.company.logo`. Letter-mark renders cleanly; layout unchanged.
- **Reduced motion** — toggle the OS preference; verify all animations disabled, layout still works.
---
## 11. Migration & rollout
No database migration needed — this is a presentation-layer change reusing existing fields (`res.company.logo`, `res.company.report_header`, `res.users.image_128`).
### Rollout sequence
1. Add tokens SCSS + extend tablet_controller payload — backend deploy.
2. Rewrite tablet_lock.scss + extend XML + extend JS — frontend deploy + asset cache bust.
3. Verify on entech: open the tablet lock URL on a real iPad and a desktop browser.
4. Iterate on visual details (logo padding, gradient intensity, accent color) based on shop-floor feedback.
No feature flag — the redesign is a strict visual improvement, no behavioral changes. Reverting is `git revert <commit>` if needed.
---
## 12. Open questions (deferred)
| # | Question | Resolution |
|---|---|---|
| Q1 | Search box for 25+ operator shops? | **Phase 2.** MVP scoped to ~10-15 ops. Re-evaluate when a customer scales. |
| Q2 | Custom accent color per company? | **Phase 2.** Amber is hardcoded in tokens for MVP. Could be a `res.company.x_fc_shopfloor_accent` field later. |
| Q3 | Weather / news widget on lock screen? | **No.** Out of scope; clutters the screen. Operators don't need it. |
| Q4 | Multi-language toggle visible on lock screen? | **No for MVP.** Existing user.lang flow handles this server-side; lock screen renders in the user's language once they're identified post-PIN. |
| Q5 | Operator photo upload UX? | **Existing flow stays** — managers upload via Preferences → My Profile. Lock screen consumes whatever's there. |
| Q6 | Animation when transitioning tile → PIN pad? | **Phase 2 polish.** Currently the existing FpPinPad just appears; could add a crossfade. Subjective; ship clean first. |
---
## 13. Summary
| Question | Answer |
|---|---|
| Layout | Vertical centered flex column: logo (84px) → clock (40px) → prompt pill → 3-column tile grid (max 480px) |
| Card model | One tile per `res.users` with tablet PIN configured (existing rule); deterministic per-user color gradient |
| Card density | 3 columns, ~140px tiles — fits ~9-12 visible without scroll on a 1366×768 tablet |
| Animation | 7 named animations (entrance stagger, hover lift, click press, status pulse) all bezier-eased, all gated by `prefers-reduced-motion` |
| Dark / Light mode | Single SCSS source with compile-time `$o-webclient-color-scheme` branch — same component, two bundles, no JS theme code |
| Backend touch | Extend `/fp/tablet/tiles` payload with `company` block + per-tile `initials`/`avatar_gradient`/`has_photo`. Two small helper functions. |
| Frontend touch | New `_tablet_lock_tokens.scss`. Full rewrite of `tablet_lock.scss`. Extend XML + JS for clock + company block. |
| Rollout | No DB migration. Plain code deploy + asset cache bust. No feature flag. |
The redesign solves the "looks like a placeholder" feel by branding the screen with the company logo, adding a real-time clock, tightening the tile grid for the small-shop case, and layering glassmorphic finishes + cubic-bezier animations on a hybrid Industrial Bold + Premium structure. Dark and light modes share one source.
Implementation plan to follow.