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>
This commit is contained in:
gsinghpal
2026-05-23 21:38:11 -04:00
parent fd2b2908f3
commit efc420b4ce

View File

@@ -0,0 +1,527 @@
# 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.