## 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
250 lines
6.3 KiB
Markdown
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
|