Add shared engineering rule contracts
- go-logging: slog, server-side only, structured attributes - go-database: MySQL cursor safety, soft delete, GORM tags, fail-fast, N+1 prevention - go-background-tasks: Task Manager pattern, polling, no SSE - go-code-style: layering, error wrapping, startup sequence, config, templating - import-export: CSV Excel-compatible rules (BOM, semicolon, decimal comma, DD.MM.YYYY) - table-management: filtering and pagination rules added - CLAUDE.template.md: updated to reference all shared contracts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,53 @@ Read and follow the project Bible before making any changes:
|
|||||||
**[`bible/README.md`](bible/README.md)**
|
**[`bible/README.md`](bible/README.md)**
|
||||||
|
|
||||||
The Bible is the single source of truth for architecture, data models, API contracts, and UI
|
The Bible is the single source of truth for architecture, data models, API contracts, and UI
|
||||||
pattern conventions.
|
pattern conventions. Every significant architectural decision must be recorded in the Bible
|
||||||
|
decision log before or alongside the code change.
|
||||||
|
|
||||||
Every significant architectural decision must be recorded in the appropriate Bible decision log.
|
---
|
||||||
|
|
||||||
|
## Shared Engineering Rules
|
||||||
|
|
||||||
|
The following rules apply to ALL changes in this project.
|
||||||
|
They are maintained centrally in `tools/ui-design-code/kit/patterns/`.
|
||||||
|
|
||||||
|
### Go Code Style
|
||||||
|
See [`tools/ui-design-code/kit/patterns/go-code-style/contract.md`](tools/ui-design-code/kit/patterns/go-code-style/contract.md)
|
||||||
|
- Handler → Service → Repository layering
|
||||||
|
- Error wrapping with `fmt.Errorf("...: %w", err)`
|
||||||
|
- Startup sequence: connect DB → migrate → start server
|
||||||
|
- Business logic and status thresholds live on the server, not in JS
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
See [`tools/ui-design-code/kit/patterns/go-logging/contract.md`](tools/ui-design-code/kit/patterns/go-logging/contract.md)
|
||||||
|
- Use `slog`, log to binary stdout/stderr only
|
||||||
|
- Never use `console.log` as a substitute for server-side logging
|
||||||
|
- Always log: startup, background task start/finish/error, export row counts, ingest results
|
||||||
|
|
||||||
|
### Database
|
||||||
|
See [`tools/ui-design-code/kit/patterns/go-database/contract.md`](tools/ui-design-code/kit/patterns/go-database/contract.md)
|
||||||
|
- **CRITICAL**: never execute SQL on the same tx while iterating a cursor — two-phase only
|
||||||
|
- Soft delete via `is_active = false`, not physical deletes
|
||||||
|
- Fail-fast DB check before starting the HTTP server
|
||||||
|
- No N+1 queries: use JOINs or batch `IN` queries
|
||||||
|
|
||||||
|
### Background Tasks
|
||||||
|
See [`tools/ui-design-code/kit/patterns/go-background-tasks/contract.md`](tools/ui-design-code/kit/patterns/go-background-tasks/contract.md)
|
||||||
|
- All slow operations: POST → task_id → client polls `/api/tasks/:id`
|
||||||
|
- No SSE — polling only
|
||||||
|
- Return `202 Accepted` when task is created
|
||||||
|
|
||||||
|
### Tables, Filtering, Pagination
|
||||||
|
See [`tools/ui-design-code/kit/patterns/table-management/contract.md`](tools/ui-design-code/kit/patterns/table-management/contract.md)
|
||||||
|
- Server-side filtering and pagination only
|
||||||
|
- Filter state in URL query params; applying a filter resets to page 1
|
||||||
|
- Response must include `total_count`, `page`, `per_page`, `total_pages`
|
||||||
|
- Display format: "51–100 из 342"
|
||||||
|
|
||||||
|
### CSV Export
|
||||||
|
See [`tools/ui-design-code/kit/patterns/import-export/contract.md`](tools/ui-design-code/kit/patterns/import-export/contract.md)
|
||||||
|
- UTF-8 BOM required (`\xEF\xBB\xBF`)
|
||||||
|
- Semicolon delimiter (`;`), not comma
|
||||||
|
- Decimal separator: comma (`1 234,56`), not period
|
||||||
|
- Dates as `DD.MM.YYYY`
|
||||||
|
- `csv.Writer` with `Comma = ';'`
|
||||||
|
|||||||
73
kit/patterns/go-background-tasks/contract.md
Normal file
73
kit/patterns/go-background-tasks/contract.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Contract: Background Tasks (Go Web Applications)
|
||||||
|
|
||||||
|
## Core Rule
|
||||||
|
|
||||||
|
All long-running operations (> ~300ms or uncertain duration) must run as background tasks.
|
||||||
|
Never block an HTTP handler waiting for a slow operation to complete.
|
||||||
|
|
||||||
|
## Standard Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/something → creates task → returns {task_id}
|
||||||
|
GET /api/tasks/:id → client polls → returns status + progress
|
||||||
|
UI shows spinner → on success: toast + refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
The client polls `/api/tasks/:id` on a fixed interval (1–3 s) until status is terminal.
|
||||||
|
|
||||||
|
## Task Status Values
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|-------------|---------|
|
||||||
|
| `queued` | Created, not yet started |
|
||||||
|
| `running` | In progress |
|
||||||
|
| `success` | Completed successfully |
|
||||||
|
| `failed` | Completed with error |
|
||||||
|
| `canceled` | Explicitly canceled by user |
|
||||||
|
|
||||||
|
## Task Struct (canonical fields)
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Task struct {
|
||||||
|
ID string
|
||||||
|
Type string // e.g. "recalculate", "import", "export"
|
||||||
|
Status string
|
||||||
|
Progress int // 0–100
|
||||||
|
Message string // human-readable current step
|
||||||
|
Result any // populated on success (row count, file URL, etc.)
|
||||||
|
Error string // populated on failure
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Do NOT use SSE (Server-Sent Events) for task progress. Use polling.
|
||||||
|
SSE requires persistent connections and complicates load balancers and proxies.
|
||||||
|
- One task per operation. Do not batch unrelated operations into a single task.
|
||||||
|
- Tasks must be stored in memory (or DB for persistence) with a TTL — clean up after completion.
|
||||||
|
- Handler that creates the task returns `202 Accepted` with `{"task_id": "..."}`.
|
||||||
|
- Log task start, finish, and error server-side (see go-logging contract).
|
||||||
|
|
||||||
|
## UI Contract
|
||||||
|
|
||||||
|
- Show a spinner/progress bar while `status == "running"`.
|
||||||
|
- On `success`: show toast notification, refresh affected data.
|
||||||
|
- On `failed`: show error message from `task.Error`.
|
||||||
|
- Poll interval: 1–3 seconds. Stop polling on terminal status.
|
||||||
|
- Do not disable the whole page during task execution — allow user to navigate away.
|
||||||
|
|
||||||
|
## What Qualifies as a Background Task
|
||||||
|
|
||||||
|
- CSV/file export with > ~500 rows
|
||||||
|
- Bulk recalculation or recompute operations
|
||||||
|
- External API calls (sync, collect, import from remote)
|
||||||
|
- Any import/ingest that touches multiple DB tables
|
||||||
|
- Database repair or cleanup operations
|
||||||
|
|
||||||
|
## What Does NOT Need a Background Task
|
||||||
|
|
||||||
|
- Simple CRUD operations (create/update/delete single record)
|
||||||
|
- Filtered list queries
|
||||||
|
- Single-record exports
|
||||||
82
kit/patterns/go-code-style/contract.md
Normal file
82
kit/patterns/go-code-style/contract.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Contract: Go Code Style and Project Conventions
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
See `kit/patterns/go-logging/contract.md` for full rules.
|
||||||
|
|
||||||
|
Summary: use `slog`, log to stdout/stderr (binary console), never to browser console.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Always wrap errors with context. Use `fmt.Errorf("...: %w", err)`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// CORRECT
|
||||||
|
if err := db.Save(&record).Error; err != nil {
|
||||||
|
return fmt.Errorf("save component %s: %w", record.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WRONG — loses context
|
||||||
|
return err
|
||||||
|
```
|
||||||
|
|
||||||
|
- Never silently discard errors with `_` in production paths.
|
||||||
|
- Return errors up the call stack; log at the handler/task boundary, not deep in service code.
|
||||||
|
|
||||||
|
## Code Formatting
|
||||||
|
|
||||||
|
- Always run `gofmt` before committing. No exceptions.
|
||||||
|
- No manual alignment of struct fields or variable assignments.
|
||||||
|
|
||||||
|
## HTTP Handler Structure
|
||||||
|
|
||||||
|
Handlers are thin. Business logic belongs in a service layer.
|
||||||
|
|
||||||
|
```
|
||||||
|
Handler → validates input, calls service, writes response
|
||||||
|
Service → business logic, calls repository
|
||||||
|
Repository → SQL queries only, returns domain types
|
||||||
|
```
|
||||||
|
|
||||||
|
- Handlers must not contain SQL queries.
|
||||||
|
- Services must not write HTTP responses.
|
||||||
|
- Repositories must not contain business rules.
|
||||||
|
|
||||||
|
## Startup Sequence (Go web app)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Parse flags / load config
|
||||||
|
2. Connect to DB — fail fast if unavailable (see go-database contract)
|
||||||
|
3. Run migrations
|
||||||
|
4. Initialize services and background workers
|
||||||
|
5. Register routes
|
||||||
|
6. Start HTTP server
|
||||||
|
```
|
||||||
|
|
||||||
|
Never reverse steps 2 and 5. Never start serving before migrations complete.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Config lives in a single `config.yaml` file, not scattered env vars.
|
||||||
|
- Env vars may override config values but must be documented.
|
||||||
|
- Never hardcode ports, DSNs, or file paths in application code.
|
||||||
|
- Provide a `config.example.yaml` committed to the repo.
|
||||||
|
- The actual `config.yaml` is gitignored.
|
||||||
|
|
||||||
|
## Template / UI Rendering
|
||||||
|
|
||||||
|
- Server-rendered HTML via Go templates is the default.
|
||||||
|
- htmx for partial updates — no full SPA framework unless explicitly decided.
|
||||||
|
- Template errors must return `500` and log the error server-side.
|
||||||
|
- Never expose raw Go error messages to the end user in rendered HTML.
|
||||||
|
|
||||||
|
## Business Logic Placement
|
||||||
|
|
||||||
|
- Threshold computation, status derivation, and scoring live on the server.
|
||||||
|
- The UI only reflects what the server returns — it does not recompute status client-side.
|
||||||
|
- Example: "critical / warning / ok" badge color is determined by the handler, not by JS.
|
||||||
|
|
||||||
|
## Dependency Rules
|
||||||
|
|
||||||
|
- Prefer standard library. Add a dependency only when the stdlib alternative is significantly worse.
|
||||||
|
- Document the reason for each non-stdlib dependency in a comment or ADL entry.
|
||||||
110
kit/patterns/go-database/contract.md
Normal file
110
kit/patterns/go-database/contract.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Contract: Database Patterns (Go / MySQL / MariaDB)
|
||||||
|
|
||||||
|
## MySQL Transaction Cursor Safety (CRITICAL)
|
||||||
|
|
||||||
|
**Never execute SQL on the same transaction while iterating over a query result cursor.**
|
||||||
|
|
||||||
|
This is the most common source of `invalid connection` and `unexpected EOF` driver panics.
|
||||||
|
|
||||||
|
### Rule
|
||||||
|
|
||||||
|
Use a two-phase approach: read all rows first, close the cursor, then execute writes.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// WRONG — executes SQL inside rows.Next() loop on the same tx
|
||||||
|
rows, _ := tx.Query("SELECT id FROM machines")
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
rows.Scan(&id)
|
||||||
|
tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id) // DEADLOCK / driver panic
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORRECT — collect IDs first, then write
|
||||||
|
rows, _ := tx.Query("SELECT id FROM machines")
|
||||||
|
var ids []string
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
rows.Scan(&id)
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
rows.Close() // explicit close before any write
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies to:
|
||||||
|
- `database/sql` with manual transactions
|
||||||
|
- GORM `db.Raw().Scan()` inside a `db.Transaction()` callback
|
||||||
|
- Any loop that calls a repository method while a cursor is open
|
||||||
|
|
||||||
|
## Soft Delete / Archive Pattern
|
||||||
|
|
||||||
|
Do not use hard deletes for user-visible records. Use an archive flag.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Schema: is_active bool DEFAULT true
|
||||||
|
// "Delete" = set is_active = false
|
||||||
|
// Restore = set is_active = true
|
||||||
|
|
||||||
|
// All list queries must filter:
|
||||||
|
WHERE is_active = true
|
||||||
|
```
|
||||||
|
|
||||||
|
- Never physically delete rows that have foreign key references or history.
|
||||||
|
- Hard delete is only acceptable for orphaned/temporary data with no audit trail requirement.
|
||||||
|
- Archive operations must be reversible from the UI.
|
||||||
|
|
||||||
|
## GORM Virtual Fields
|
||||||
|
|
||||||
|
Use the correct tag based on whether the field should exist in the DB schema:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Field computed at runtime, column must NOT exist in DB (excludes from migrations AND queries)
|
||||||
|
Count int `gorm:"-"`
|
||||||
|
|
||||||
|
// Field computed at query time via JOIN/SELECT, column must NOT be in migrations
|
||||||
|
// but IS populated from query results
|
||||||
|
DisplayName string `gorm:"-:migration"`
|
||||||
|
```
|
||||||
|
|
||||||
|
- `gorm:"-"` — fully ignored: no migration, no read, no write.
|
||||||
|
- `gorm:"-:migration"` — skip migration only; GORM will still read/write if the column exists.
|
||||||
|
- Do not use `gorm:"-"` for JOIN-populated fields — the value will always be zero.
|
||||||
|
|
||||||
|
## Fail-Fast DB Check on Startup
|
||||||
|
|
||||||
|
Always verify the database connection before starting the HTTP server.
|
||||||
|
|
||||||
|
```go
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil || sqlDB.Ping() != nil {
|
||||||
|
log.Fatal("database unavailable, refusing to start")
|
||||||
|
}
|
||||||
|
// then: run migrations, then: start gin/http server
|
||||||
|
```
|
||||||
|
|
||||||
|
Never start serving traffic with an unverified DB connection. Fail loudly at boot.
|
||||||
|
|
||||||
|
## N+1 Query Prevention
|
||||||
|
|
||||||
|
Use JOINs or batch IN queries. Never query inside a loop over rows from another query.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// WRONG
|
||||||
|
for _, pricelist := range pricelists {
|
||||||
|
items, _ := repo.GetItems(pricelist.ID) // N queries
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORRECT
|
||||||
|
items, _ := repo.GetItemsByPricelistIDs(ids) // 1 query with WHERE id IN (...)
|
||||||
|
// then group in Go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Policy
|
||||||
|
|
||||||
|
- Migrations are numbered sequentially and never modified after merge.
|
||||||
|
- Each migration must be reversible where possible (document rollback in a comment).
|
||||||
|
- Never rename a column in one migration step — add new, backfill, drop old across separate deploys.
|
||||||
|
- Auto-apply migrations on startup is acceptable for internal tools; document if used.
|
||||||
64
kit/patterns/go-logging/contract.md
Normal file
64
kit/patterns/go-logging/contract.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Contract: Logging (Go Web Applications)
|
||||||
|
|
||||||
|
## Core Rule
|
||||||
|
|
||||||
|
**All logging goes to the server binary's stdout/stderr — never to the browser console.**
|
||||||
|
|
||||||
|
- `console.log`, `console.error`, `console.warn` in JavaScript are for debugging only.
|
||||||
|
Remove them before committing. They are not a substitute for server-side logging.
|
||||||
|
- Diagnostic information (errors, slow queries, ingest results, background task status) must be
|
||||||
|
logged server-side so it is visible in the terminal where the binary runs.
|
||||||
|
|
||||||
|
## Go Logging Standard
|
||||||
|
|
||||||
|
Use the standard library `log/slog` (Go 1.21+) or `log` for simpler projects.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// correct — structured, visible in binary console
|
||||||
|
slog.Info("export started", "user", userID, "rows", count)
|
||||||
|
slog.Error("db query failed", "err", err)
|
||||||
|
|
||||||
|
// wrong — goes to browser devtools, invisible in production
|
||||||
|
// (JS template rendered in HTML)
|
||||||
|
// console.log("export started")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Log Levels
|
||||||
|
|
||||||
|
| Level | When to use |
|
||||||
|
|---------|-------------|
|
||||||
|
| `Debug` | Detailed flow tracing; disabled in production builds |
|
||||||
|
| `Info` | Normal operation milestones: server start, request handled, export done |
|
||||||
|
| `Warn` | Recoverable anomaly: retry, fallback used, deprecated path |
|
||||||
|
| `Error` | Operation failed; always include `"err", err` as a structured attribute |
|
||||||
|
|
||||||
|
## What to Always Log
|
||||||
|
|
||||||
|
- Server startup: address, port, config source.
|
||||||
|
- Background task start/finish/error with task name and duration.
|
||||||
|
- CSV/file export: scope, row count, duration.
|
||||||
|
- Ingest/import: file name, rows accepted, rows rejected, error summary.
|
||||||
|
- Any `500` response: handler name + error.
|
||||||
|
|
||||||
|
## What Not to Log
|
||||||
|
|
||||||
|
- Do not log full request bodies or CSV content — can contain PII or large payloads.
|
||||||
|
- Do not log passwords, tokens, or secrets even partially.
|
||||||
|
- Do not add `fmt.Println` for debugging and leave it in committed code.
|
||||||
|
|
||||||
|
## htmx / Browser-rendered Responses
|
||||||
|
|
||||||
|
- Handler errors that result in an htmx partial re-render must still log the error server-side.
|
||||||
|
- Do not rely on the browser network tab as the only visibility into server errors.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
- Use structured key-value attributes, not concatenated strings.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// correct
|
||||||
|
slog.Error("sync failed", "component", comp.Serial, "err", err)
|
||||||
|
|
||||||
|
// wrong — hard to grep, hard to parse
|
||||||
|
log.Printf("sync failed for component %s: %v", comp.Serial, err)
|
||||||
|
```
|
||||||
@@ -20,10 +20,56 @@ Rules:
|
|||||||
|
|
||||||
- User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists.
|
- User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists.
|
||||||
- Export format should be explicit (`csv`, `json`, etc.).
|
- Export format should be explicit (`csv`, `json`, etc.).
|
||||||
- Download response should set:
|
- Download response must set:
|
||||||
- `Content-Type`
|
- `Content-Type: text/csv; charset=utf-8`
|
||||||
- `Content-Disposition`
|
- `Content-Disposition: attachment; filename="..."`
|
||||||
- If CSV targets spreadsheet users, document delimiter and BOM policy.
|
|
||||||
|
## CSV Format Rules (Excel-compatible)
|
||||||
|
|
||||||
|
These rules are **mandatory** whenever CSV is exported for spreadsheet users.
|
||||||
|
|
||||||
|
### Encoding and BOM
|
||||||
|
|
||||||
|
- Write UTF-8 BOM (`\xEF\xBB\xBF`) as the very first bytes of the response.
|
||||||
|
- Without BOM, Excel on Windows opens UTF-8 CSV as ANSI and garbles Cyrillic/special characters.
|
||||||
|
|
||||||
|
```go
|
||||||
|
w.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delimiter
|
||||||
|
|
||||||
|
- Use **semicolon** (`;`) as the field delimiter, not comma.
|
||||||
|
- Excel in Russian/European locale uses semicolon as the list separator.
|
||||||
|
- Comma-delimited files open as a single column in these locales.
|
||||||
|
|
||||||
|
### Numbers
|
||||||
|
|
||||||
|
- Write decimal numbers with a **comma** as the decimal separator: `1 234,56` — not `1234.56`.
|
||||||
|
- Excel in Russian locale does not recognize period as a decimal separator in numeric cells.
|
||||||
|
- Format integers and floats explicitly; do not rely on Go's default `%v` or `strconv.FormatFloat`.
|
||||||
|
- Use a thin non-breaking space (`\u202F`) or regular space as a thousands separator when the value
|
||||||
|
benefits from readability (e.g. prices, quantities > 9999).
|
||||||
|
|
||||||
|
```go
|
||||||
|
// correct
|
||||||
|
fmt.Sprintf("%.2f", price) // then replace "." -> ","
|
||||||
|
strings.ReplaceAll(fmt.Sprintf("%.2f", price), ".", ",")
|
||||||
|
|
||||||
|
// wrong — produces "1234.56", Excel treats it as text in RU locale
|
||||||
|
fmt.Sprintf("%.2f", price)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dates
|
||||||
|
|
||||||
|
- Write dates as `DD.MM.YYYY` — the format Excel in Russian locale parses as a date cell automatically.
|
||||||
|
- Do not use ISO 8601 (`2006-01-02`) for user-facing CSV; it is not auto-recognized as a date in RU locale.
|
||||||
|
|
||||||
|
### Text quoting
|
||||||
|
|
||||||
|
- Wrap any field that contains the delimiter (`;`), a newline, or a double-quote in double quotes.
|
||||||
|
- Escape embedded double-quotes by doubling them: `""`.
|
||||||
|
- Use `encoding/csv` with `csv.Writer` and set `csv.Writer.Comma = ';'`; it handles quoting automatically.
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,30 @@ Canonical mapping:
|
|||||||
- right-aligned action icons.
|
- right-aligned action icons.
|
||||||
- Row action icons in `actions` column are intentionally smaller than toolbar icons.
|
- Row action icons in `actions` column are intentionally smaller than toolbar icons.
|
||||||
|
|
||||||
|
## Filtering Rules
|
||||||
|
|
||||||
|
- Filters live above the table, in a dedicated filter bar or inline in the toolbar.
|
||||||
|
- Every active filter must be visually indicated (highlighted field, chip, or badge count).
|
||||||
|
- Applying a filter always resets pagination to page 1.
|
||||||
|
- Filter state must be reflected in the URL as query parameters so the page is bookmarkable/shareable.
|
||||||
|
- "Reset filters" clears all filter fields and reloads with no filter params.
|
||||||
|
- Server-side filtering only — do not filter an already-loaded JS array client-side unless the full
|
||||||
|
dataset is guaranteed small (< 500 rows) and never grows.
|
||||||
|
- Filter inputs debounce text input (300–500 ms) before triggering a server request.
|
||||||
|
|
||||||
|
## Pagination Rules
|
||||||
|
|
||||||
|
- Pagination is server-side. Never load all rows and paginate client-side.
|
||||||
|
- URL query parameters carry page state: `?page=2&per_page=50`.
|
||||||
|
- `page` is 1-based.
|
||||||
|
- `per_page` defaults to a fixed project constant (e.g. 50); user may change it from a fixed set
|
||||||
|
(25 / 50 / 100).
|
||||||
|
- The server response includes: `total_count`, `page`, `per_page`, `total_pages`.
|
||||||
|
- Display: "Showing 51–100 of 342" — always show the range and total.
|
||||||
|
- Prev/Next buttons are disabled (not hidden) at the boundary pages.
|
||||||
|
- Direct page-number input is optional; if present it clamps to `[1, total_pages]` on blur.
|
||||||
|
- Changing `per_page` resets to page 1.
|
||||||
|
|
||||||
## Reuse Rule
|
## Reuse Rule
|
||||||
|
|
||||||
If a pattern needs table selection + bulk actions, it must:
|
If a pattern needs table selection + bulk actions, it must:
|
||||||
|
|||||||
Reference in New Issue
Block a user