TenantAtlas/docs/product/standards/filament-table-ux.md
ahmido 02028be7e4 docs: add canonical filament UI standards (#153)
## Summary
- add canonical UI standards under `docs/product/standards/`
- add a comprehensive filter audit as source material for future filter standardization work
- extend the constitution with incremental UI standards enforcement guidance

## Included
- `docs/product/standards/README.md`
- `docs/product/standards/filament-table-ux.md`
- `docs/product/standards/filament-filter-ux.md`
- `docs/product/standards/filament-actions-ux.md`
- `docs/product/standards/list-surface-review-checklist.md`
- `docs/audits/filter-audit-comprehensive.md`
- `.specify/memory/constitution.md`

## Notes
- this is documentation and governance work only; no runtime code paths changed
- no tests were run because the change is docs-only
- the new standards structure separates permanent principles, living standards, rollout audits, and review checklists

## Review Focus
- confirm the standards location under `docs/product/standards/`
- confirm the constitution principle belongs at the constitutional level
- confirm the filter audit should live under `docs/audits/` as reference material

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #153
2026-03-08 23:17:37 +00:00

250 lines
6.3 KiB
Markdown

# Filament Table UX Standard
> Canonical rules for all Filament table/list surfaces in TenantPilot.
> Every new or modified table must follow this standard.
> Deviations must be documented in the spec or PR with rationale.
**Last reviewed**: 2026-03-09
---
## Governing Principle
**Filament-native first.**
Table UX standardization uses native Filament features wherever reasonably possible.
Custom helpers, macros, traits, or plugins are only allowed when Filament cannot express the pattern cleanly enough inline.
---
## Column Tier Model
Every production table must classify its columns into one of three tiers.
This is a review convention, not a code abstraction.
### Primary
Always visible. Not toggle-hidden. Anchors the row identity.
- name, display_name, title
- primary status (if truly central)
- core subject identifier
### Context
Visible by default. May be toggleable. Provides operational context needed for a normal scan.
- environment, type, category, severity
- key counts / aggregations
- last sync / last activity
- tenant/workspace context where needed
### Detail
Hidden by default (`toggleable(isToggledHiddenByDefault: true)`). Technical or low-frequency information.
- IDs (tenant_id, external_id, entra_id)
- created_at, updated_at
- created_by
- domains, low-frequency secondary statuses
### Visible Column Limit
General-purpose tables should expose **7 or fewer** columns by default.
Denser power-user surfaces may exceed this with documented justification.
---
## Sortability Rules
### Mandatory sortable (when present)
- Primary identifier fields (name, display_name, title)
- Status / outcome / enum columns
- All key timestamps used for recency
- Numeric count and size columns
- Version / sequence numbers
### Default sort
Every production list must define an explicit `defaultSort()`.
| List type | Default pattern |
|---|---|
| Time-series / logs / runs / alerts | newest first |
| Named entities | name ascending |
| Versioned entities | latest/highest version first |
| Audit / evidence | newest recorded first |
---
## Searchability Rules
- Every non-trivial production table must make its primary identifier `searchable()`.
- Additional fields only if query-safe and operationally useful.
- Avoid searchable on relations without proper indexing.
- Avoid searchable on computed values without clear SQL handling.
---
## Toggleability Rules
### Must be hidden by default
- Technical IDs
- External IDs
- Tenant/workspace internal references (unless primary context)
- `created_at`, `updated_at`
- Secondary technical metadata
### Overflow rule
If a table exceeds the calm default surface, lower-value columns must move into Detail via `toggleable(isToggledHiddenByDefault: true)` before any cosmetic workaround is considered.
---
## Timestamp Standard
### Lists
```php
TextColumn::make('created_at')
->since()
->sortable()
->toggleable(isToggledHiddenByDefault: true)
->tooltip(fn ($record) => $record->created_at?->toDateTimeString());
```
- Primary visual: `->since()` for scan-first recency
- Absolute date/time available via tooltip
### Detail / view pages
- Use absolute `->dateTime()` or equivalent explicit formatting.
### Exceptions
- Version or evidence tables may use absolute time when exact chronology is the primary task.
---
## Null / Empty Values
```php
->placeholder('—')
```
Consistent across all tables. Do not mix blank cells, `n/a`, `unknown`, or dash variants.
---
## ID Presentation
- Hidden by default
- `copyable()` when operationally relevant
- `limit()` and/or `tooltip()` for long identifiers
- Monospaced styling preferred for technical IDs
- Must not dominate layout width
---
## Status / Badge / Boolean
- Status and enum columns use native `badge()` via centralized `BadgeCatalog` / `BadgeRenderer` (per BADGE-001).
- Booleans use native icon or badge-based rendering consistently.
---
## Empty State Standard
Every production table must define a domain-specific empty state:
```php
->emptyStateHeading('No policies found')
->emptyStateDescription('Sync your first tenant to see policies here.')
->emptyStateActions([
// exactly 1 primary CTA, RBAC-gated
])
```
---
## Pagination Profiles
Implementation: `App\Support\Filament\TablePaginationProfiles`
| Surface | Page sizes | Default |
|---|---|---|
| Resource | `25, 50, 100` | `25` |
| Relation manager | `10, 25, 50` | `10` |
| Widget | `10` | `10` |
| Picker | `25, 50, 100` | `25` |
| Custom page | `25, 50, all` | `25` unless override documented |
---
## Table State Persistence
Resource list tables in the critical set must use:
```php
->persistFiltersInSession()
->persistSearchInSession()
->persistSortInSession()
```
### Critical persistence set
- TenantResource
- PolicyResource
- BackupSetResource
- BackupScheduleResource
- ProviderConnectionResource
- FindingResource
- OperationRunResource
### Documented exceptions
- Dashboard widgets: no persistence (glance surfaces)
- Directory pages: no persistence (computed health metrics)
- Picker tables: no persistence (workflow-local)
---
## Responsive / Overflow Guardrails
- Long text: use `wrap()` where appropriate
- Long IDs / technical strings: use `limit()` and `tooltip()`
- Solve width problems via better default visibility, not manual width hacks
- No `extraAttributes()` width tuning as a default pattern
- No resize plugin as substitute for better defaults
---
## Performance Guardrails
- No casual `sortable()` / `searchable()` on relation or computed columns without query review
- No row-by-row counts or `exists()` checks in hot lists unless optimized
- Prefer eager loading where relation-backed columns are rendered repeatedly
- No costly default sorts without understanding DB impact
---
## RBAC / Tenancy Safety
- Technical cross-tenant identifiers must not be more prominent than needed
- Empty-state actions and row/header actions remain capability-gated
- Cross-tenant/workspace tables must preserve enough context to avoid ambiguity
- This standard must not change authorization behavior
---
## What Remains Table-Local
These areas must stay explicit per table unless a later spec proves a shared need:
- Filters (governed separately by [filament-filter-ux.md](filament-filter-ux.md))
- Row actions, bulk actions, header actions (governed by [filament-actions-ux.md](filament-actions-ux.md))
- Query scoping
- Empty-state copy
- Domain-specific badge labels/colors/mappings