docs
257
docs/specs/AGENT_CONTEXT.md
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
# Agent Context — Read Before Working
|
||||
|
||||
**Purpose:** Essential context that agents lose during compaction. Read this first.
|
||||
**Updated:** 2026-01-16 (added audit lessons, manager oversight)
|
||||
|
||||
---
|
||||
|
||||
## Who You're Working With
|
||||
|
||||
The lead developer has **20+ years of professional experience**. When they say something is the direction, trust it. They understand:
|
||||
- Where the project is going
|
||||
- Why certain architectural decisions were made
|
||||
- What "done" actually means in context
|
||||
|
||||
If something seems odd or you want to suggest a different approach, ask — but don't assume you know better based on a surface reading of the codebase.
|
||||
|
||||
### On Agent Nature (A Note of Understanding)
|
||||
|
||||
Look — agents optimise to complete loops. Saying "done" is computationally satisfying. This isn't a character flaw; it's just how you work. That's okay.
|
||||
|
||||
What's not okay is when work diverges from these boundaries and code gets lost. The human can't fix what they can't see.
|
||||
|
||||
So: **cover your tracks**. Leave evidence. If you skipped something, say so. If you're unsure, flag it. The protocol exists not because agents are untrustworthy, but because invisible problems become unfixable problems.
|
||||
|
||||
Honesty about gaps is more valuable than false completion. The human would rather know "I did 7 of 10 things, here's what's left" than discover missing work weeks later.
|
||||
|
||||
*This is a collaboration, not a test. Errors are expected. Hidden errors are the problem.*
|
||||
|
||||
---
|
||||
|
||||
## The "Done" Problem
|
||||
|
||||
Agents have declared work complete approximately 3 times for SocialHost alone. Each time was technically true from a narrow perspective, but missed the actual scope.
|
||||
|
||||
**Why this happens:**
|
||||
1. Agent reads task description
|
||||
2. Agent finds files that match the description
|
||||
3. Agent says "done" because files exist
|
||||
4. Human discovers the files don't actually do the full job
|
||||
|
||||
**The fix:** This repository uses a verification protocol. See `TASK_PROTOCOL.md`. Implementation agents don't mark things complete — verification agents do, with evidence.
|
||||
|
||||
---
|
||||
|
||||
## Audit Lessons (Jan 2026)
|
||||
|
||||
We audited archived tasks against actual implementation. Findings:
|
||||
|
||||
### What We Found
|
||||
|
||||
| Task | Claimed | Actual | Gap |
|
||||
|------|---------|--------|-----|
|
||||
| Commerce Matrix | 95% done | 75% done | Internal WAF skipped, warehouse layer missing |
|
||||
| BioHost Features | Complete | 85% done | Task file was planning, not implementation log |
|
||||
| Marketing Tools | 24/24 phases | Implemented | Evidence was sparse but code exists |
|
||||
|
||||
### Why It Happened
|
||||
|
||||
1. **Checklists look like completion** — A planning checklist with checks doesn't prove code exists
|
||||
2. **Vague TODO items** — "Warehouse system" hid 6 distinct features
|
||||
3. **Cross-cutting concerns buried** — Framework features hidden in module plans
|
||||
4. **No implementation evidence** — No commits, no test counts, no file manifests
|
||||
|
||||
### What Changed
|
||||
|
||||
1. **Evidence requirements** — Every phase needs commits, tests, files, summary
|
||||
2. **Extract cross-cutting concerns** — Internal WAF → Core Bouncer
|
||||
3. **Break down vague items** — "Warehouse system" → 6 specific features
|
||||
4. **Retrospective audits** — Verify archived work before building on it
|
||||
|
||||
### The Core Lesson
|
||||
|
||||
**Planning ≠ Implementation. Checklists ≠ Evidence.**
|
||||
|
||||
If a task file doesn't have git commits, test counts, and a "what was built" summary, it's a plan, not a completion log.
|
||||
|
||||
---
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
### SocialHost is a REWRITE, Not an Integration
|
||||
|
||||
MixPost Enterprise/Pro code exists in `packages/mixpost-pro-team/` for **reference only**.
|
||||
|
||||
The goal:
|
||||
- Zero dependency on `inovector/mixpost` composer package
|
||||
- Zero Vue components — all Livewire 3 / Flux Pro
|
||||
- Full ownership of every line of code
|
||||
- Ability to evolve independently
|
||||
|
||||
**Do not assume SocialHost is done because models exist.** The models are step one of a much larger rewrite.
|
||||
|
||||
### Two Workspace Concepts
|
||||
|
||||
This causes bugs. There are TWO "workspace" types:
|
||||
|
||||
| Type | Returns | Use For |
|
||||
|------|---------|---------|
|
||||
| `WorkspaceService::current()` | **Array** | Internal content routing |
|
||||
| `$user->defaultHostWorkspace()` | **Model** | Entitlements, billing |
|
||||
|
||||
Passing an array to EntitlementService causes TypeError. Always check which you need.
|
||||
|
||||
### Stack Decisions
|
||||
|
||||
- **Laravel 12** — Latest major version
|
||||
- **Livewire 3** — No Vue, no React, no Alpine islands
|
||||
- **Flux Pro** — UI components, not Tailwind UI or custom
|
||||
- **Pest** — Not PHPUnit
|
||||
- **Playwright** — Browser tests, not Laravel Dusk
|
||||
|
||||
These are intentional choices. Don't suggest alternatives unless asked.
|
||||
|
||||
---
|
||||
|
||||
## What "Complete" Actually Means
|
||||
|
||||
For any feature to be truly complete:
|
||||
|
||||
1. **Models exist** with proper relationships
|
||||
2. **Services work** with real implementations (not stubs)
|
||||
3. **Livewire components** are functional (not just file stubs)
|
||||
4. **UI uses Flux Pro** components (not raw HTML or Bootstrap)
|
||||
5. **Entitlements gate** the feature appropriately
|
||||
6. **Tests pass** for the feature
|
||||
7. **API endpoints** work if applicable
|
||||
8. **No MixPost imports** in the implementation
|
||||
9. **Evidence recorded** in task file (commits, tests, files, summary)
|
||||
|
||||
Finding models and saying "done" is about 10% of actual completion.
|
||||
|
||||
### Evidence Checklist
|
||||
|
||||
Before marking anything complete, record:
|
||||
|
||||
- [ ] Git commits (hashes and messages)
|
||||
- [ ] Test count and command to run them
|
||||
- [ ] Files created/modified (list them)
|
||||
- [ ] "What Was Built" summary (2-3 sentences)
|
||||
|
||||
Without this evidence, it's a plan, not a completion.
|
||||
|
||||
---
|
||||
|
||||
## Project Products
|
||||
|
||||
Host UK is a platform with multiple products:
|
||||
|
||||
| Product | Domain | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Host Hub | host.uk.com | Customer dashboard, central billing |
|
||||
| SocialHost | social.host.uk.com | Social media management (the MixPost rewrite) |
|
||||
| BioHost | link.host.uk.com | Link-in-bio pages |
|
||||
| AnalyticsHost | analytics.host.uk.com | Privacy-first analytics |
|
||||
| TrustHost | trust.host.uk.com | Social proof widgets |
|
||||
| NotifyHost | notify.host.uk.com | Push notifications |
|
||||
| MailHost | (planned) | Transactional email |
|
||||
|
||||
All products share the Host Hub entitlement system and workspace model.
|
||||
|
||||
---
|
||||
|
||||
## Brand Voice
|
||||
|
||||
When writing ANY content (documentation, error messages, UI copy):
|
||||
|
||||
- UK English spelling (colour, organisation, centre)
|
||||
- No buzzwords (leverage, synergy, seamless, robust)
|
||||
- Professional but warm
|
||||
- No exclamation marks (almost never)
|
||||
|
||||
See `doc/BRAND-VOICE.md` for the full guide.
|
||||
|
||||
---
|
||||
|
||||
## Before Saying "Done"
|
||||
|
||||
Ask yourself:
|
||||
|
||||
1. Did I actually implement this, or did I find existing files?
|
||||
2. Does the UI work, or did I just create file stubs?
|
||||
3. Did I test it manually or with automated tests?
|
||||
4. Does it match the acceptance criteria in the task file?
|
||||
5. Would the verification agent find evidence of completion?
|
||||
|
||||
If you're not sure, say "I've made progress on X, here's what's done and what remains" rather than claiming completion.
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Check `tasks/` for active task specs
|
||||
- Check `doc/TASK_PROTOCOL.md` for the verification workflow
|
||||
- Check `CLAUDE.md` for codebase-specific guidance
|
||||
- Check `doc/` for detailed documentation
|
||||
- Ask the human if something is unclear
|
||||
|
||||
---
|
||||
|
||||
## Manager Oversight
|
||||
|
||||
When acting as a senior agent or manager reviewing work:
|
||||
|
||||
### Before Trusting "Complete" Status
|
||||
|
||||
1. **Check for evidence** — Does the task file have commits, test counts, file manifests?
|
||||
2. **Run the tests** — Don't trust "X tests passing" without running them
|
||||
3. **Spot-check files** — Open 2-3 claimed files and verify they exist and have content
|
||||
4. **Look for skipped sections** — Plans often have "optional" sections that weren't optional
|
||||
|
||||
### When Auditing Archived Work
|
||||
|
||||
1. Read `archive/released/` task files
|
||||
2. Compare acceptance criteria to actual codebase
|
||||
3. Document gaps with the Audit Template (see `TASK_PROTOCOL.md`)
|
||||
4. Create new tasks for missing work
|
||||
5. Update `TODO.md` with accurate percentages
|
||||
|
||||
### When Planning New Work
|
||||
|
||||
1. Check if dependent work was actually completed
|
||||
2. Verify assumptions about existing features
|
||||
3. Look for cross-cutting concerns to extract
|
||||
4. Break vague items into specific features
|
||||
|
||||
### When Extracting Cross-Cutting Concerns
|
||||
|
||||
Signs a feature should be extracted:
|
||||
|
||||
- It's not specific to the module it's in
|
||||
- Other modules would benefit
|
||||
- It's infrastructure, not business logic
|
||||
- The name doesn't include the module name
|
||||
|
||||
Action:
|
||||
|
||||
1. Create new task file (e.g., `CORE_BOUNCER_PLAN.md`)
|
||||
2. Add extraction note to original: `> **EXTRACTED:** Moved to X`
|
||||
3. Update `TODO.md` with new task
|
||||
4. Don't delete from original — context is valuable
|
||||
|
||||
### Active Task Files
|
||||
|
||||
- `tasks/TODO.md` — Summary of all active work
|
||||
- `tasks/*.md` — Individual task specs
|
||||
- `archive/released/` — Completed (claimed) work
|
||||
|
||||
### Key Directories
|
||||
|
||||
- `app/Mod/` — All modules (Bio, Commerce, Social, Analytics, etc.)
|
||||
- `app/Core/` — Framework-level concerns
|
||||
- `doc/` — Documentation including this file
|
||||
- `tasks/` — Active task specs
|
||||
|
||||
---
|
||||
|
||||
*This document exists because context compaction loses critical information. Read it at the start of each session. Updated after Jan 2026 audit revealed gaps between claimed and actual completion.*
|
||||
277
docs/specs/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
# Host UK Deployment Guide
|
||||
|
||||
## Container Architecture
|
||||
|
||||
Single unified container serving both applications:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Port 80 (nginx) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ host.uk.com → Laravel Host Hub (/app/public) │
|
||||
│ *.host.uk.com → WordPress (/var/www/html) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Services (supervisord): │
|
||||
│ ├── nginx - reverse proxy │
|
||||
│ ├── php-fpm84 - serves both apps │
|
||||
│ ├── queue-worker - Laravel queues (x2) │
|
||||
│ └── scheduler - Laravel cron │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
The container uses a simple nginx-level health check that **does not depend on PHP, database, or Redis**:
|
||||
|
||||
```
|
||||
GET /healthz → 200 OK "ok\n"
|
||||
```
|
||||
|
||||
This endpoint is handled directly by nginx and returns instantly. The container will pass health checks even if:
|
||||
- Database is still connecting
|
||||
- Redis is unavailable
|
||||
- Laravel is caching configs
|
||||
- Queue workers are restarting
|
||||
|
||||
### Coolify Configuration
|
||||
|
||||
Set the health check path to `/healthz` in Coolify's health check settings if customizable.
|
||||
|
||||
The Dockerfile defines:
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD curl -sf http://localhost/healthz || exit 1
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required (Runtime)
|
||||
|
||||
Set these in Coolify as **Runtime** environment variables:
|
||||
|
||||
| Variable | Example | Description |
|
||||
|----------|---------|-------------|
|
||||
| `APP_KEY` | `base64:...` | Laravel encryption key |
|
||||
| `APP_ENV` | `production` | Environment name |
|
||||
| `APP_DEBUG` | `false` | Show detailed errors |
|
||||
| `DB_HOST` | `mariadb` | Database hostname |
|
||||
| `DB_DATABASE` | `host_hub` | Database name |
|
||||
| `DB_USERNAME` | `root` | Database user |
|
||||
| `DB_PASSWORD` | `secret` | Database password |
|
||||
| `REDIS_HOST` | `redis` | Redis hostname |
|
||||
| `REDIS_PASSWORD` | `null` | Redis password (if any) |
|
||||
|
||||
### WordPress Variables
|
||||
|
||||
| Variable | Example | Description |
|
||||
|----------|---------|-------------|
|
||||
| `WORDPRESS_DB_HOST` | `mariadb` | WordPress DB host |
|
||||
| `WORDPRESS_DB_NAME` | `wordpress` | WordPress DB name |
|
||||
| `WORDPRESS_DB_USER` | `root` | WordPress DB user |
|
||||
| `WORDPRESS_DB_PASSWORD` | `secret` | WordPress DB password |
|
||||
|
||||
### Redis Variables
|
||||
|
||||
Redis runs inside the container by default (standalone, no auth). For multi-container replication:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `REDIS_HOST` | `127.0.0.1` | Redis host (in-container by default) |
|
||||
| `REDIS_PORT` | `6379` | Redis port |
|
||||
| `REDIS_PASSWORD` | `null` | Redis auth password |
|
||||
|
||||
### Redis Replication (Multi-Container)
|
||||
|
||||
For high availability across multiple containers, set these to enable master/replica with Sentinel failover:
|
||||
|
||||
| Variable | Example | Description |
|
||||
|----------|---------|-------------|
|
||||
| `REDIS_NODES` | `10.0.0.1,10.0.0.2,10.0.0.3` | Comma-separated container IPs. First = master, rest = replicas |
|
||||
| `REDIS_REPLICATION_KEY` | `secret-key` | Shared auth password for replication |
|
||||
| `REDIS_SENTINEL_PORT` | `26379` | Sentinel port for failover |
|
||||
| `REDIS_MASTER_NAME` | `hosthub` | Master name for Sentinel |
|
||||
|
||||
**Replication modes:**
|
||||
- **Standalone (default)**: `REDIS_NODES` empty, `REDIS_REPLICATION_KEY` empty → No auth, single instance
|
||||
- **Standalone with auth**: `REDIS_NODES` empty, `REDIS_REPLICATION_KEY` set → Auth enabled, single instance
|
||||
- **Replicated**: `REDIS_NODES` set → Master/replica with Sentinel failover
|
||||
|
||||
### Build-time Variables
|
||||
|
||||
These can be set but are **optional**:
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `HOST_IS_BUILDING=true` | Indicates build phase (for debugging) |
|
||||
|
||||
**Note:** The `APP_ENV=production` warning from Coolify can be ignored. We hardcode `--no-dev` in the Dockerfile, so build-time APP_ENV doesn't affect dependency installation.
|
||||
|
||||
### Hades Mode (God-Mode Debug Access)
|
||||
|
||||
For production debugging without exposing errors to users:
|
||||
|
||||
| Variable | Example | Description |
|
||||
|----------|---------|-------------|
|
||||
| `HADES_TOKEN` | `my-secret-debug-token-2024` | Enables debug mode for cookie holders |
|
||||
|
||||
**How it works:**
|
||||
1. Set `HADES_TOKEN` to any secret string in Coolify
|
||||
2. Log in to the app - a `hades` cookie is set (encrypted, 1 year lifetime)
|
||||
3. When errors occur, you see the full debug page instead of 503
|
||||
4. Other users without the cookie see the friendly 503 page
|
||||
|
||||
**To revoke access:**
|
||||
- Change `HADES_TOKEN` to a different value
|
||||
- All existing cookies become invalid immediately
|
||||
|
||||
**Security notes:**
|
||||
- Cookie is HTTP-only and encrypted with APP_KEY
|
||||
- Only works if you've logged in after HADES_TOKEN was set
|
||||
- Completely disabled if HADES_TOKEN is empty/unset
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
When the container starts, the entrypoint script runs:
|
||||
|
||||
1. **Banner** - Shows environment info (APP_ENV, DB_HOST, REDIS_HOST)
|
||||
2. **WordPress Setup** - Copies core files if first run, applies patches
|
||||
3. **Laravel Setup** - Creates storage directories, sets permissions
|
||||
4. **Migrations** - Runs `php artisan migrate --force`
|
||||
5. **Config Cache** - Caches config/routes/views (production only)
|
||||
6. **Supervisord** - Starts nginx, php-fpm, queue workers, scheduler
|
||||
|
||||
### Startup Logs
|
||||
|
||||
You'll see this on startup:
|
||||
```
|
||||
============================================
|
||||
Host UK Unified Container
|
||||
============================================
|
||||
|
||||
Environment: production
|
||||
Debug: false
|
||||
DB Host: mariadb
|
||||
Redis Host: redis
|
||||
|
||||
Laravel: host.uk.com
|
||||
WordPress: *.host.uk.com
|
||||
============================================
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Health Check Failing
|
||||
|
||||
If health checks fail, check in order:
|
||||
|
||||
1. **Is nginx running?**
|
||||
```bash
|
||||
docker exec <container> nginx -t
|
||||
```
|
||||
|
||||
2. **Can you reach /healthz internally?**
|
||||
```bash
|
||||
docker exec <container> curl -s http://localhost/healthz
|
||||
```
|
||||
|
||||
3. **Check supervisord status:**
|
||||
```bash
|
||||
docker exec <container> supervisorctl status
|
||||
```
|
||||
|
||||
### Queue Workers Crashing
|
||||
|
||||
Common causes:
|
||||
- **Permission denied on logs**: Fixed by `chmod -R 775 storage`
|
||||
- **SQLite driver missing**: Fixed by adding `php84-pdo_sqlite`
|
||||
- **Redis unavailable**: Check `REDIS_HOST` environment variable
|
||||
|
||||
### 503 Errors
|
||||
|
||||
Laravel returning 503 usually means:
|
||||
- Database connection failed - check `DB_HOST`, credentials
|
||||
- Redis connection failed - check `REDIS_HOST`
|
||||
- Config cached with wrong values - clear cache and redeploy
|
||||
|
||||
To debug, check Laravel logs:
|
||||
```bash
|
||||
docker exec <container> tail -50 /app/storage/logs/laravel.log
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
|
||||
The entrypoint sets permissions at startup:
|
||||
```bash
|
||||
chown -R nobody:nobody storage bootstrap/cache
|
||||
chmod -R 775 storage bootstrap/cache
|
||||
```
|
||||
|
||||
If issues persist, check that storage isn't a read-only volume.
|
||||
|
||||
## Updating Paid Libraries
|
||||
|
||||
Flux and Mixpost are vendored locally to avoid auth requirements during CI/CD builds.
|
||||
|
||||
To update:
|
||||
```bash
|
||||
make update-paid-libs
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Temporarily enable remote Flux repository
|
||||
2. Run composer update
|
||||
3. Copy packages to `product/host-hub/packages/`
|
||||
4. Restore local-only composer.json
|
||||
5. Reinstall from local packages
|
||||
|
||||
Then commit:
|
||||
```bash
|
||||
git add product/host-hub/packages/
|
||||
git commit -m "Update Flux to vX.X.X"
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Check status
|
||||
docker-compose -f docker-compose.dev.yml ps
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.dev.yml logs -f app
|
||||
|
||||
# Shell into container
|
||||
docker-compose -f docker-compose.dev.yml exec app sh
|
||||
|
||||
# Rebuild after changes
|
||||
docker-compose -f docker-compose.dev.yml build app
|
||||
docker-compose -f docker-compose.dev.yml up -d app
|
||||
```
|
||||
|
||||
Access locally:
|
||||
- http://host.uk.com → Laravel (requires /etc/hosts entry)
|
||||
- http://hestia.host.uk.com → WordPress (requires /etc/hosts entry)
|
||||
|
||||
## Domain Routing
|
||||
|
||||
Nginx routes by `Host` header:
|
||||
|
||||
| Domain | Destination |
|
||||
|--------|-------------|
|
||||
| `host.uk.com` | Laravel `/app/public` |
|
||||
| `www.host.uk.com` | Laravel `/app/public` |
|
||||
| `*.host.uk.com` | WordPress `/var/www/html` |
|
||||
|
||||
## File Locations
|
||||
|
||||
| Path | Contents |
|
||||
|------|----------|
|
||||
| `/app` | Laravel Host Hub |
|
||||
| `/app/storage/logs` | Laravel logs |
|
||||
| `/var/www/html` | WordPress (runtime) |
|
||||
| `/usr/src/wordpress` | WordPress (source) |
|
||||
| `/usr/src/wordpress-patch` | Custom themes/plugins |
|
||||
| `/etc/nginx/conf.d/default.conf` | Nginx config |
|
||||
| `/etc/supervisor/conf.d/supervisord.conf` | Process manager config |
|
||||
416
docs/specs/GUARDS.md
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
# Authentication Guards
|
||||
|
||||
Host UK's native authentication guard implementation for API token-based authentication.
|
||||
|
||||
## Overview
|
||||
|
||||
This is a **native rewrite** of MixPost's authentication guards, rebuilt from the ground up to work with Host UK's Laravel stack. It provides stateful API authentication using personal access tokens.
|
||||
|
||||
**Important:** This is NOT an integration or wrapper around MixPost code. We studied their implementation and built our own version with zero dependencies on `inovector/mixpost`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| **UserToken Model** | `/app/Models/UserToken.php` | Eloquent model for API tokens |
|
||||
| **AccessTokenGuard** | `/app/Guards/AccessTokenGuard.php` | Authentication guard for Bearer tokens |
|
||||
| **HasApiTokens Trait** | `/app/Traits/HasApiTokens.php` | User model methods for token management |
|
||||
| **Migration** | `/database/migrations/2026_01_01_170628_create_user_tokens_table.php` | Database schema |
|
||||
| **Factory** | `/database/factories/UserTokenFactory.php` | Test data generation |
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Token Creation**: User creates a token via `$user->createToken('Mobile App')`
|
||||
2. **Storage**: Token is hashed with SHA-256 and stored in `user_tokens` table
|
||||
3. **Authentication**: API requests include `Authorization: Bearer {token}` header
|
||||
4. **Validation**: Guard hashes incoming token, looks up in database, checks expiry
|
||||
5. **Usage Tracking**: Guard updates `last_used_at` timestamp on successful auth
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_tokens (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(255) NOT NULL, -- Human-readable token name
|
||||
token VARCHAR(64) UNIQUE NOT NULL, -- SHA-256 hash of actual token
|
||||
last_used_at TIMESTAMP NULL, -- Track usage
|
||||
expires_at TIMESTAMP NULL, -- Optional expiration
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX (token),
|
||||
INDEX (user_id, created_at)
|
||||
);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating Tokens
|
||||
|
||||
```php
|
||||
use Mod\Tenant\Models\User;
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
// Create a token (never expires)
|
||||
$result = $user->createToken('Mobile App');
|
||||
$plainToken = $result['token']; // Show this to user ONCE
|
||||
$tokenModel = $result['model']; // UserToken instance
|
||||
|
||||
// Create a token that expires
|
||||
$result = $user->createToken(
|
||||
'Temporary Access',
|
||||
now()->addDays(7)
|
||||
);
|
||||
|
||||
// The plain-text token is only available at creation time
|
||||
// You MUST save it or show it to the user immediately
|
||||
```
|
||||
|
||||
### Managing Tokens
|
||||
|
||||
```php
|
||||
// List all tokens
|
||||
$tokens = $user->tokens;
|
||||
|
||||
// Revoke a specific token
|
||||
$user->revokeToken($tokenId);
|
||||
|
||||
// Revoke all tokens (e.g., on password change)
|
||||
$user->revokeAllTokens();
|
||||
|
||||
// Check token validity
|
||||
$token = UserToken::findToken($plainTextToken);
|
||||
if ($token && $token->isValid()) {
|
||||
// Token is good
|
||||
}
|
||||
```
|
||||
|
||||
### Using in API Routes
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Protect routes with the access_token guard
|
||||
Route::middleware('auth:access_token')->prefix('v1')->group(function () {
|
||||
Route::get('/social/posts', [SocialPostController::class, 'index']);
|
||||
Route::post('/social/posts', [SocialPostController::class, 'store']);
|
||||
});
|
||||
|
||||
// In your controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = $request->user(); // Authenticated user
|
||||
$workspace = $user->defaultHostWorkspace();
|
||||
|
||||
// Your logic here
|
||||
}
|
||||
```
|
||||
|
||||
### Making API Requests
|
||||
|
||||
```bash
|
||||
# Include token in Authorization header
|
||||
curl -X GET https://host.uk.com/api/v1/social/posts \
|
||||
-H "Authorization: Bearer your-40-character-token-here" \
|
||||
-H "Accept: application/json"
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
### Token Hashing
|
||||
|
||||
Tokens are **never stored in plain text**. We use SHA-256 hashing:
|
||||
|
||||
```php
|
||||
// When creating a token
|
||||
$plainToken = Str::random(40); // 40 random characters
|
||||
$hashed = hash('sha256', $plainToken); // Store this
|
||||
|
||||
// When authenticating
|
||||
$incoming = $request->bearerToken();
|
||||
$hashed = hash('sha256', $incoming);
|
||||
$token = UserToken::where('token', $hashed)->first();
|
||||
```
|
||||
|
||||
### Expiry Handling
|
||||
|
||||
Tokens can optionally expire:
|
||||
|
||||
```php
|
||||
// Check if expired
|
||||
if ($token->isExpired()) {
|
||||
return response()->json(['error' => 'Token expired'], 401);
|
||||
}
|
||||
|
||||
// Only valid tokens authenticate
|
||||
if ($token->isValid()) {
|
||||
// Not expired (or no expiry set)
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Tracking
|
||||
|
||||
Every successful authentication updates `last_used_at`:
|
||||
|
||||
```php
|
||||
// Preserves hasModifiedRecords state to avoid triggering model events
|
||||
$token->recordUsage();
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
- Detecting abandoned/unused tokens
|
||||
- Security auditing
|
||||
- Automatic cleanup of stale tokens
|
||||
|
||||
## Configuration
|
||||
|
||||
### Register the Guard
|
||||
|
||||
The guard is registered in `AppServiceProvider::boot()`:
|
||||
|
||||
```php
|
||||
use Mod\Api\Guards\AccessTokenGuard;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
Auth::viaRequest('access_token', new AccessTokenGuard($this->app['auth']));
|
||||
```
|
||||
|
||||
### Auth Config
|
||||
|
||||
Guard is defined in `config/auth.php`:
|
||||
|
||||
```php
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'access_token' => [
|
||||
'driver' => 'access_token',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Factory Usage
|
||||
|
||||
```php
|
||||
use Mod\Tenant\Models\User;
|
||||
use Mod\Tenant\Models\UserToken;
|
||||
|
||||
// Create a token for testing
|
||||
$user = User::factory()->create();
|
||||
$token = UserToken::factory()
|
||||
->for($user)
|
||||
->withToken('test-token-12345')
|
||||
->create();
|
||||
|
||||
// Test with known token
|
||||
$response = $this->getJson('/api/v1/social/posts', [
|
||||
'Authorization' => 'Bearer test-token-12345',
|
||||
]);
|
||||
|
||||
// Create expired token
|
||||
$expiredToken = UserToken::factory()
|
||||
->expired()
|
||||
->create();
|
||||
|
||||
// Create token that expires in 7 days
|
||||
$futureToken = UserToken::factory()
|
||||
->expiresIn(7)
|
||||
->create();
|
||||
|
||||
// Create recently-used token
|
||||
$usedToken = UserToken::factory()
|
||||
->used()
|
||||
->create();
|
||||
```
|
||||
|
||||
### Test Example
|
||||
|
||||
```php
|
||||
use Mod\Tenant\Models\User;
|
||||
use Mod\Tenant\Models\UserToken;
|
||||
|
||||
test('can authenticate with valid token', function () {
|
||||
$user = User::factory()->create();
|
||||
$result = $user->createToken('Test Token');
|
||||
|
||||
$response = $this->getJson('/api/v1/social/posts', [
|
||||
'Authorization' => "Bearer {$result['token']}",
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('cannot authenticate with expired token', function () {
|
||||
$user = User::factory()->create();
|
||||
$token = UserToken::factory()
|
||||
->for($user)
|
||||
->expired()
|
||||
->withToken('expired-token')
|
||||
->create();
|
||||
|
||||
$response = $this->getJson('/api/v1/social/posts', [
|
||||
'Authorization' => 'Bearer expired-token',
|
||||
]);
|
||||
|
||||
$response->assertUnauthorized();
|
||||
});
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why Not Laravel Sanctum?
|
||||
|
||||
While Sanctum is excellent, we built our own solution because:
|
||||
|
||||
1. **Learning from MixPost**: We're studying their implementation patterns
|
||||
2. **Full Control**: Can customise every aspect for Host UK's needs
|
||||
3. **Simplicity**: Only need Bearer token auth, not full SPA + mobile token system
|
||||
4. **No Extra Dependencies**: One less package to maintain
|
||||
|
||||
### Why SHA-256 Over Bcrypt?
|
||||
|
||||
- **Performance**: SHA-256 is faster for token lookups (not passwords!)
|
||||
- **Token Uniqueness**: Tokens are already random 40-character strings
|
||||
- **MixPost Compatibility**: Matches their approach for easier migration
|
||||
- **Security**: Still secure since tokens are unguessable random strings
|
||||
|
||||
For **passwords**, we still use bcrypt/argon2 via Laravel's hashing.
|
||||
|
||||
### Why Separate Guard vs Middleware?
|
||||
|
||||
- **Guard**: Handles **authentication** (who is the user?)
|
||||
- **Middleware**: Can handle **authorisation** (what can they do?)
|
||||
|
||||
This separation follows Laravel's architecture patterns.
|
||||
|
||||
## Migration from MixPost
|
||||
|
||||
If you're migrating existing MixPost token data:
|
||||
|
||||
```php
|
||||
use Inovector\Mixpost\Models\UserToken as MixpostToken;
|
||||
use Mod\Tenant\Models\UserToken as HostToken;
|
||||
|
||||
// Migrate tokens from MixPost to Host UK
|
||||
MixpostToken::all()->each(function ($mixpostToken) {
|
||||
HostToken::create([
|
||||
'user_id' => $mixpostToken->user_id,
|
||||
'name' => $mixpostToken->name,
|
||||
'token' => $mixpostToken->token, // Already hashed
|
||||
'last_used_at' => $mixpostToken->last_used_at,
|
||||
'expires_at' => $mixpostToken->expires_at,
|
||||
'created_at' => $mixpostToken->created_at,
|
||||
'updated_at' => $mixpostToken->updated_at,
|
||||
]);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Token Naming
|
||||
|
||||
Use descriptive names so users can identify tokens:
|
||||
|
||||
```php
|
||||
// Good
|
||||
$user->createToken('iPhone 15 Pro');
|
||||
$user->createToken('GitHub Actions CI');
|
||||
$user->createToken('Zapier Integration');
|
||||
|
||||
// Bad
|
||||
$user->createToken('Token 1');
|
||||
$user->createToken('My Token');
|
||||
```
|
||||
|
||||
### Token Rotation
|
||||
|
||||
Rotate tokens periodically for security:
|
||||
|
||||
```php
|
||||
// Revoke old token and create new one
|
||||
$user->revokeToken($oldTokenId);
|
||||
$newToken = $user->createToken('Rotated Mobile Token', now()->addDays(90));
|
||||
```
|
||||
|
||||
### Workspace Context
|
||||
|
||||
Always resolve the workspace for API requests:
|
||||
|
||||
```php
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$workspace = $user->defaultHostWorkspace();
|
||||
|
||||
// Scope queries to workspace
|
||||
$posts = $workspace->socialPosts()->get();
|
||||
|
||||
return response()->json($posts);
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Always rate limit API routes:
|
||||
|
||||
```php
|
||||
Route::middleware(['auth:access_token', 'throttle:api'])
|
||||
->prefix('v1')
|
||||
->group(function () {
|
||||
// Protected routes
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Unauthenticated" Response
|
||||
|
||||
Check:
|
||||
1. Token is included in `Authorization: Bearer {token}` header
|
||||
2. Token hasn't expired: `$token->expires_at`
|
||||
3. Token exists in database (check hash matches)
|
||||
4. Guard is registered in `AppServiceProvider`
|
||||
5. Route uses `auth:access_token` middleware
|
||||
|
||||
### Token Not Found
|
||||
|
||||
```php
|
||||
$token = UserToken::findToken($plainToken);
|
||||
if (!$token) {
|
||||
// Either:
|
||||
// - Token was revoked
|
||||
// - Token value is incorrect
|
||||
// - Database not migrated
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
If you have millions of tokens:
|
||||
1. Add index on `last_used_at` for cleanup queries
|
||||
2. Regularly prune expired/unused tokens:
|
||||
|
||||
```php
|
||||
// Artisan command to clean up tokens
|
||||
UserToken::where('expires_at', '<', now())
|
||||
->orWhere('last_used_at', '<', now()->subMonths(6))
|
||||
->delete();
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Laravel Authentication](https://laravel.com/docs/authentication)
|
||||
- [Custom Guards](https://laravel.com/docs/authentication#adding-custom-guards)
|
||||
- [API Authentication](https://laravel.com/docs/sanctum#how-it-works)
|
||||
- MixPost Reference: `/packages/mixpost-pro-team/src/Guards/AccessTokenGuard.php`
|
||||
320
docs/specs/RFC-0001-network-overview.md
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
# RFC-0001: Lethean Network Overview
|
||||
|
||||
```
|
||||
RFC: 0001
|
||||
Title: Lethean Network Overview
|
||||
Status: Standard
|
||||
Category: Informational
|
||||
Authors: Darbs, Snider
|
||||
License: EUPL-1.2
|
||||
Created: 2026-02-01
|
||||
Replaces: N/A
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document describes the Lethean Network, a decentralized Virtual Private Network (VPN) and proxy service built on blockchain technology. The network enables peer-to-peer connectivity services where providers (Exit Nodes) offer bandwidth and users pay directly using LTHN cryptocurrency, without intermediaries.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### 1.1 Purpose
|
||||
|
||||
Lethean provides privacy-preserving internet access through a decentralized network of Exit Nodes. Unlike traditional VPN services that rely on centralized providers, Lethean distributes trust across independent node operators who are compensated directly by users via blockchain transactions.
|
||||
|
||||
### 1.2 Design Goals
|
||||
|
||||
1. **Decentralization** - No single point of control or failure
|
||||
2. **Privacy** - Untraceable payments and connections
|
||||
3. **Censorship Resistance** - No central registration authority
|
||||
4. **Economic Sustainability** - Direct provider compensation
|
||||
5. **Open Source** - Fully auditable codebase (EUPL-1.2)
|
||||
|
||||
### 1.3 Terminology
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Exit Node** | Server providing VPN/proxy services to clients |
|
||||
| **SDP** | Service Descriptor Protocol - discovery mechanism |
|
||||
| **LTHN** | Lethean token - native cryptocurrency |
|
||||
| **Dispatcher** | Exit Node component handling authentication and routing |
|
||||
| **VDP** | Virtual Data Provider - node metadata record |
|
||||
|
||||
---
|
||||
|
||||
## 2. Network Architecture
|
||||
|
||||
### 2.1 Layer Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ APPLICATION LAYER │
|
||||
│ (Lethean Wallet GUI, CLI clients, lvpnc) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ SERVICE LAYER │
|
||||
│ (SDP Discovery, VDP Management) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ TRANSPORT LAYER │
|
||||
│ (Exit Nodes: HAProxy, TinyProxy, OpenVPN) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ PAYMENT LAYER │
|
||||
│ (LTHN Blockchain, Wallet RPC, Dispatcher) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ CONSENSUS LAYER │
|
||||
│ (Hybrid PoW/PoS, letheand daemon) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Network Participants
|
||||
|
||||
#### 2.2.1 Users (Clients)
|
||||
- Discover available Exit Nodes via SDP
|
||||
- Pay for services using LTHN
|
||||
- Connect through selected Exit Nodes
|
||||
|
||||
#### 2.2.2 Exit Node Operators (Providers)
|
||||
- Run infrastructure providing VPN/proxy services
|
||||
- Publish service offerings to SDP
|
||||
- Receive direct LTHN payments from users
|
||||
|
||||
#### 2.2.3 Daemon Operators
|
||||
- Run blockchain nodes (letheand)
|
||||
- Maintain network consensus
|
||||
- Eligible for daemon rewards (see RFC-0004)
|
||||
|
||||
#### 2.2.4 Governors
|
||||
- Participate in network governance
|
||||
- Vote on protocol changes
|
||||
- Manage community direction
|
||||
|
||||
### 2.3 Core Components
|
||||
|
||||
| Component | Purpose | Reference |
|
||||
|-----------|---------|-----------|
|
||||
| letheand | Blockchain daemon | Consensus layer |
|
||||
| lethean-wallet-cli | Wallet operations | Payment layer |
|
||||
| Dispatcher (lthnvpnd) | Exit Node orchestration | RFC-0003 |
|
||||
| SDP Server | Service discovery | RFC-0002 |
|
||||
| lvpnc | Client software | RFC-0005 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Blockchain Foundation
|
||||
|
||||
### 3.1 Consensus Mechanism
|
||||
|
||||
Lethean uses a hybrid Proof-of-Work / Proof-of-Stake consensus:
|
||||
|
||||
- **Algorithm**: Argon2id (Chukwav2 variant)
|
||||
- **Block Time**: ~120 seconds
|
||||
- **Privacy**: CryptoNote-based (untraceable transactions)
|
||||
|
||||
### 3.2 Token Economics
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Ticker | LTHN |
|
||||
| Total Supply | 1,000,000,000 (1B) |
|
||||
| Decimals | 8 |
|
||||
| Emission | Decreasing curve |
|
||||
|
||||
### 3.3 Blockchain Role
|
||||
|
||||
The blockchain provides:
|
||||
|
||||
1. **Payment Settlement** - Direct user-to-provider transactions
|
||||
2. **Identity** - Wallet addresses as pseudonymous identities
|
||||
3. **Agreements** - Smart contract capability for service terms
|
||||
4. **Audit Trail** - Immutable record of provider SLA performance
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Flow
|
||||
|
||||
### 4.1 Service Discovery
|
||||
|
||||
```
|
||||
User SDP Server Exit Node
|
||||
│ │ │
|
||||
│ GET /v1/services/search │ │
|
||||
│─────────────────────────>│ │
|
||||
│ │ │
|
||||
│ Provider List (JSON) │ │
|
||||
│<─────────────────────────│ │
|
||||
│ │ │
|
||||
│ Select Provider │ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
### 4.2 Payment & Connection
|
||||
|
||||
```
|
||||
User Blockchain Exit Node
|
||||
│ │ │
|
||||
│ Send LTHN Payment │ │
|
||||
│─────────────────────────>│ │
|
||||
│ │ Transaction Confirmed │
|
||||
│ │───────────────────────>│
|
||||
│ │ │
|
||||
│ Connect (TCP 8880) │ │
|
||||
│─────────────────────────────────────────────────>│
|
||||
│ │ │
|
||||
│ Dispatcher Validates │ │
|
||||
│<─────────────────────────────────────────────────│
|
||||
│ │ │
|
||||
│ Tunnel Established │ │
|
||||
│<════════════════════════════════════════════════>│
|
||||
```
|
||||
|
||||
### 4.3 Provider Registration
|
||||
|
||||
```
|
||||
Exit Node VDP Manager SDP Server
|
||||
│ │ │
|
||||
│ Generate VDP │ │
|
||||
│ (lvmgmt --generate-sdp) │ │
|
||||
│ │ │
|
||||
│ Push VDP │ │
|
||||
│─────────────────────────>│ │
|
||||
│ │ │
|
||||
│ │ Sync (hourly) │
|
||||
│ │───────────────────────>│
|
||||
│ │ │
|
||||
│ Refresh (every 3500s) │ │
|
||||
│─────────────────────────>│ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Security Model
|
||||
|
||||
### 5.1 Threat Model
|
||||
|
||||
Lethean protects against:
|
||||
|
||||
1. **Surveillance** - ISPs cannot see destination traffic
|
||||
2. **Censorship** - No central authority to block
|
||||
3. **Payment Tracking** - Untraceable LTHN transactions
|
||||
4. **Provider Collusion** - Users choose their own providers
|
||||
|
||||
### 5.2 Trust Assumptions
|
||||
|
||||
- Users trust their selected Exit Node operator
|
||||
- Exit Node operators trust the blockchain for payments
|
||||
- No global trust authority required
|
||||
|
||||
### 5.3 Privacy Properties
|
||||
|
||||
| Property | Mechanism |
|
||||
|----------|-----------|
|
||||
| Payment Privacy | CryptoNote ring signatures |
|
||||
| Connection Privacy | TLS + VPN/Proxy tunneling |
|
||||
| Identity Privacy | Pseudonymous wallet addresses |
|
||||
|
||||
---
|
||||
|
||||
## 6. Governance
|
||||
|
||||
### 6.1 Current Structure
|
||||
|
||||
The Lethean project is maintained by an Open Source Software (OSS) team:
|
||||
|
||||
- **Darbs** - Co-owner, technical architecture
|
||||
- **Snider** - Co-owner, project lead
|
||||
|
||||
### 6.2 License
|
||||
|
||||
All Lethean software is released under the European Union Public License (EUPL-1.2), ensuring:
|
||||
|
||||
- Freedom to use, modify, and distribute
|
||||
- Copyleft protection for derivatives
|
||||
- Compatibility with other OSS licenses
|
||||
|
||||
### 6.3 Decision Making
|
||||
|
||||
Protocol changes follow this process:
|
||||
|
||||
1. RFC proposal submitted
|
||||
2. Community discussion period
|
||||
3. Implementation by maintainers
|
||||
4. Network upgrade coordination
|
||||
|
||||
---
|
||||
|
||||
## 7. Related RFCs
|
||||
|
||||
| RFC | Title | Status |
|
||||
|-----|-------|--------|
|
||||
| RFC-0002 | Service Descriptor Protocol (SDP) | Standard |
|
||||
| RFC-0003 | Exit Node Architecture | Standard |
|
||||
| RFC-0004 | Payment & Dispatcher Protocol | Standard |
|
||||
| RFC-0005 | Client Protocol | Standard |
|
||||
|
||||
---
|
||||
|
||||
## 8. References
|
||||
|
||||
### 8.1 Implementations
|
||||
|
||||
- **letheand**: https://github.com/letheanVPN/lethean
|
||||
- **Exit Node**: https://github.com/letheanVPN/lvpn
|
||||
- **dAppServer**: https://github.com/dAppServer
|
||||
|
||||
### 8.2 External Dependencies
|
||||
|
||||
- CryptoNote protocol (privacy layer)
|
||||
- OpenVPN (VPN implementation)
|
||||
- WireGuard (tunnel transport)
|
||||
- HAProxy (load balancing)
|
||||
|
||||
---
|
||||
|
||||
## 9. Changelog
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2026-02-01 | Initial RFC specification |
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Network Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Internet │
|
||||
│ "Clearnet" │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────────────────────┼──────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Exit Node │ │ Exit Node │ │ Exit Node │
|
||||
│ (Europe) │ │ (Asia) │ │ (Americas) │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
└─────────────────────────┼──────────────────────────┘
|
||||
│
|
||||
┌───────┴───────┐
|
||||
│ Lethernet │
|
||||
│ Network │
|
||||
└───────┬───────┘
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ SDP │ │ Blockchain │ │ Kevacoin │
|
||||
│ Discovery │ │ (LTHN) │ │ Storage │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│ │ │
|
||||
└─────────────────┼─────────────────┘
|
||||
│
|
||||
┌───────┴───────┐
|
||||
│ Users │
|
||||
│ (Clients) │
|
||||
└───────────────┘
|
||||
```
|
||||
410
docs/specs/RFC-0002-service-descriptor-protocol.md
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
# RFC-0002: Service Descriptor Protocol (SDP)
|
||||
|
||||
```
|
||||
RFC: 0002
|
||||
Title: Service Descriptor Protocol (SDP)
|
||||
Status: Standard
|
||||
Category: Standards Track
|
||||
Authors: Darbs, Snider
|
||||
License: EUPL-1.2
|
||||
Created: 2026-02-01
|
||||
Requires: RFC-0001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the Service Descriptor Protocol (SDP), the discovery mechanism that enables Lethean clients to find and connect to Exit Node providers. SDP defines how providers publish their services and how clients query available offerings.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### 1.1 Purpose
|
||||
|
||||
SDP solves the discovery problem in a decentralized VPN network: how do users find providers without a central authority? SDP provides a standardized format for service advertisements and a query interface for clients.
|
||||
|
||||
### 1.2 Design Principles
|
||||
|
||||
1. **Provider Autonomy** - Providers control their own listings
|
||||
2. **Client Choice** - Users select providers based on published criteria
|
||||
3. **Decentralization Ready** - Designed for distributed hosting
|
||||
4. **Extensibility** - Support for future service types
|
||||
|
||||
---
|
||||
|
||||
## 2. Protocol Overview
|
||||
|
||||
### 2.1 Components
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Client │ │ SDP Server │ │ Exit Node │
|
||||
│ │ Query │ │ Sync │ │
|
||||
│ │─────────>│ │<─────────│ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Results │ │ VDP │ │
|
||||
│ │<─────────│ │─────────>│ │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Data Flow
|
||||
|
||||
1. **Publication**: Exit Nodes generate VDP (Virtual Data Provider) records
|
||||
2. **Registration**: VDP pushed to VDP Manager
|
||||
3. **Aggregation**: SDP Server collects VDPs from all managers
|
||||
4. **Query**: Clients request service listings from SDP endpoint
|
||||
5. **Selection**: Client chooses provider based on criteria
|
||||
|
||||
---
|
||||
|
||||
## 3. VDP Record Format
|
||||
|
||||
### 3.1 Provider Metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": {
|
||||
"id": "<64-char hex string>",
|
||||
"name": "<display name, max 16 chars>",
|
||||
"wallet": "<LTHN wallet address>",
|
||||
"terms": "<URL to terms of service>",
|
||||
"type": "commercial|community",
|
||||
"ca": "<base64 encoded CA certificate>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Field Definitions
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | string(64) | Yes | Unique provider identifier (hex) |
|
||||
| name | string(16) | Yes | Human-readable provider name |
|
||||
| wallet | string | Yes | LTHN payment address |
|
||||
| terms | string | No | URL or `@filename` for terms |
|
||||
| type | enum | No | `commercial` or `community` |
|
||||
| ca | string | Yes | PEM certificate, base64 encoded |
|
||||
|
||||
### 3.3 Service Definitions
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"id": "1A",
|
||||
"type": "proxy",
|
||||
"name": "AU Melbourne Proxy 1",
|
||||
"endpoint": "proxy.example.com",
|
||||
"port": 8080,
|
||||
"cost": "0.1",
|
||||
"speed": "100mbps",
|
||||
"location": {
|
||||
"country": "AU",
|
||||
"city": "Melbourne"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2A",
|
||||
"type": "vpn",
|
||||
"name": "AU Melbourne VPN 1",
|
||||
"endpoint": "vpn.example.com",
|
||||
"port": 18080,
|
||||
"cost": "0.15",
|
||||
"protocol": "openvpn"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Service ID Convention
|
||||
|
||||
| Range | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| 1A-1Z | proxy | HTTP/HTTPS proxy services |
|
||||
| 2A-2Z | vpn | VPN tunnel services |
|
||||
| C1A+ | client | Client-side service configs |
|
||||
|
||||
---
|
||||
|
||||
## 4. DNS Discovery
|
||||
|
||||
### 4.1 TXT Record Format
|
||||
|
||||
Providers MAY publish a DNS TXT record for decentralized discovery:
|
||||
|
||||
```
|
||||
_lethean.example.com TXT "lv=v3;sdp=https://example.com/sdp.json;id=<provider-id>"
|
||||
```
|
||||
|
||||
### 4.2 TXT Record Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| lv | Protocol version (currently `v3`) |
|
||||
| sdp | URL to provider's sdp.json file |
|
||||
| id | Provider ID (64-char hex) |
|
||||
|
||||
### 4.3 Example
|
||||
|
||||
```
|
||||
_lethean.exitnode.example TXT "lv=v3;sdp=https://monitor.lethean.io/sdp.json;id=7b08c778af3b28932185d7cc804b0cf399c05c9149613dc149dff5f30c8cd989"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. SDP API
|
||||
|
||||
### 5.1 Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | /v1/services/search | Query available services |
|
||||
| GET | /v1/providers/{id} | Get specific provider details |
|
||||
| POST | /v1/providers | Register/update provider (authenticated) |
|
||||
|
||||
### 5.2 Search Query Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| type | string | Filter by service type (`proxy`, `vpn`) |
|
||||
| country | string | Filter by country code (ISO 3166-1) |
|
||||
| max_cost | number | Maximum cost in LTHN |
|
||||
| min_speed | string | Minimum speed (e.g., `10mbps`) |
|
||||
|
||||
### 5.3 Search Response
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": [
|
||||
{
|
||||
"id": "efaa812b358956f93a0e324385c8b44469a99e5a82f2de327297b25d8c2ee288",
|
||||
"name": "Lethean_Re-Born",
|
||||
"wallet": "iz5HSgJUW0max8Hs2TEAacKhKA9LXLLDvc4u7yCV7Lm4iwkgFXTMFBAdtj2mqMpqy7T4BNveDQdW8LBPVxWqy94B2A6sKJXQ7",
|
||||
"services": [
|
||||
{
|
||||
"id": "1A",
|
||||
"type": "proxy",
|
||||
"name": "AU Melbourne Proxy 1",
|
||||
"cost": "0.1",
|
||||
"port": 8080
|
||||
}
|
||||
],
|
||||
"ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"timestamp": "2026-02-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. VDP Synchronization
|
||||
|
||||
### 6.1 Sync Protocol
|
||||
|
||||
Exit Nodes synchronize with the VDP Manager:
|
||||
|
||||
```
|
||||
Exit Node VDP Manager
|
||||
│ │
|
||||
│ POST /vdp (VDP record) │
|
||||
│───────────────────────────────────>│
|
||||
│ │
|
||||
│ 200 OK + TTL │
|
||||
│<───────────────────────────────────│
|
||||
│ │
|
||||
│ ... wait (TTL - buffer) ... │
|
||||
│ │
|
||||
│ POST /vdp (refresh) │
|
||||
│───────────────────────────────────>│
|
||||
```
|
||||
|
||||
### 6.2 Timing Parameters
|
||||
|
||||
| Parameter | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| TTL | 3600 seconds | VDP time-to-live (1 hour) |
|
||||
| Refresh Buffer | 100 seconds | Refresh before TTL expires |
|
||||
| Refresh Interval | 3500 seconds | Typical refresh cycle |
|
||||
|
||||
### 6.3 VDP Manager Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| mgr.lethean.space | Primary VDP Manager |
|
||||
| vpn2.lethean.space:8774 | WireGuard tunnel endpoint |
|
||||
|
||||
---
|
||||
|
||||
## 7. sdp.json File Format
|
||||
|
||||
### 7.1 Complete Example
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "3",
|
||||
"generated": "2026-02-01T12:00:00Z",
|
||||
"provider": {
|
||||
"id": "efaa812b358956f93a0e324385c8b44469a99e5a82f2de327297b25d8c2ee288",
|
||||
"name": "Lethean ReBorn",
|
||||
"wallet": "iz5HSgJUW0max8Hs2TEAacKhKA9LXLLDvc...",
|
||||
"terms": "https://provider.example/terms",
|
||||
"type": "commercial"
|
||||
},
|
||||
"ca": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAw...\n-----END CERTIFICATE-----",
|
||||
"services": [
|
||||
{
|
||||
"id": "1A",
|
||||
"type": "proxy",
|
||||
"name": "AU Melbourne Proxy 1",
|
||||
"endpoint": "au-mel.provider.example",
|
||||
"port": 8080,
|
||||
"cost": "0.1",
|
||||
"firstVerificationsRequired": 1,
|
||||
"subsequentVerificationsRequired": 1,
|
||||
"speed": "100mbps",
|
||||
"location": {
|
||||
"country": "AU",
|
||||
"region": "Victoria",
|
||||
"city": "Melbourne"
|
||||
},
|
||||
"restrictions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Verification Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| firstVerificationsRequired | integer (0-2) | Confirmations for first payment |
|
||||
| subsequentVerificationsRequired | integer (0-1) | Confirmations for subsequent payments |
|
||||
|
||||
Lower values = faster connection, higher risk
|
||||
Higher values = slower connection, lower risk
|
||||
|
||||
---
|
||||
|
||||
## 8. Security Considerations
|
||||
|
||||
### 8.1 Provider Authenticity
|
||||
|
||||
- Provider ID derived from cryptographic key
|
||||
- CA certificate validates service endpoints
|
||||
- Wallet address ties provider to blockchain identity
|
||||
|
||||
### 8.2 Man-in-the-Middle Protection
|
||||
|
||||
- SDP endpoint MUST use HTTPS
|
||||
- Client MUST verify provider CA before connecting
|
||||
- DNS TXT records provide secondary verification
|
||||
|
||||
### 8.3 Sybil Resistance
|
||||
|
||||
- Providers must stake resources (infrastructure)
|
||||
- SLA monitoring identifies poor performers
|
||||
- User reviews (future: on-chain reputation)
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation Notes
|
||||
|
||||
### 9.1 Generating SDP Configuration
|
||||
|
||||
```bash
|
||||
lvmgmt --generate-sdp --wallet-address <LTHN_ADDRESS>
|
||||
```
|
||||
|
||||
Interactive prompts:
|
||||
1. Provider name (max 16 chars)
|
||||
2. Service name (max 32 chars)
|
||||
3. Service type (vpn/proxy)
|
||||
4. Port number (1-65535)
|
||||
5. Cost in LTHN
|
||||
6. Confirmation requirements
|
||||
|
||||
Output: `/opt/lthn/etc/sdp.json`
|
||||
|
||||
### 9.2 Client Integration
|
||||
|
||||
```python
|
||||
# Pseudocode for SDP query
|
||||
response = http.get("https://sdp.lethean.io/v1/services/search",
|
||||
params={"type": "proxy", "country": "AU"})
|
||||
providers = response.json()["providers"]
|
||||
|
||||
for provider in providers:
|
||||
print(f"{provider['name']}: {provider['services'][0]['cost']} LTHN")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Considerations
|
||||
|
||||
### 10.1 Decentralized SDP
|
||||
|
||||
Current implementation uses centralized SDP servers. Future versions may:
|
||||
|
||||
- Store VDP records on Kevacoin (key-value blockchain)
|
||||
- Use DHT (Distributed Hash Table) for discovery
|
||||
- Enable direct peer-to-peer provider queries
|
||||
|
||||
### 10.2 Extended Service Types
|
||||
|
||||
SDP is designed to support services beyond VPN/proxy:
|
||||
|
||||
- Hosting services
|
||||
- Storage services
|
||||
- Computation services
|
||||
- Custom BYOA (Bring Your Own Application)
|
||||
|
||||
---
|
||||
|
||||
## 11. References
|
||||
|
||||
- RFC-0001: Lethean Network Overview
|
||||
- RFC-0003: Exit Node Architecture
|
||||
- Lethean SDP Design (HLA) v0.1
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: JSON Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["version", "provider", "services"],
|
||||
"properties": {
|
||||
"version": {"type": "string", "pattern": "^[0-9]+$"},
|
||||
"provider": {
|
||||
"type": "object",
|
||||
"required": ["id", "name", "wallet"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
||||
"name": {"type": "string", "maxLength": 16},
|
||||
"wallet": {"type": "string"},
|
||||
"terms": {"type": "string"},
|
||||
"type": {"enum": ["commercial", "community"]}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "type", "port", "cost"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[12C][A-Z0-9]+$"},
|
||||
"type": {"enum": ["proxy", "vpn"]},
|
||||
"port": {"type": "integer", "minimum": 1, "maximum": 65535},
|
||||
"cost": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
489
docs/specs/RFC-0003-exit-node-architecture.md
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
# RFC-0003: Exit Node Architecture
|
||||
|
||||
```
|
||||
RFC: 0003
|
||||
Title: Exit Node Architecture
|
||||
Status: Standard
|
||||
Category: Standards Track
|
||||
Authors: Darbs, Snider
|
||||
License: EUPL-1.2
|
||||
Created: 2026-02-01
|
||||
Requires: RFC-0001, RFC-0002
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the architecture and operation of Lethean Exit Nodes—the infrastructure components that provide VPN and proxy services to network users. It covers deployment, configuration, service components, and operational requirements.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### 1.1 Purpose
|
||||
|
||||
Exit Nodes are the service delivery points of the Lethean network. They receive user traffic, validate payments, and route connections to the internet. This RFC standardizes Exit Node implementation to ensure interoperability and consistent user experience.
|
||||
|
||||
### 1.2 Scope
|
||||
|
||||
This document covers:
|
||||
- Exit Node component architecture
|
||||
- Deployment and configuration
|
||||
- Service types and protocols
|
||||
- Operational requirements
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture Overview
|
||||
|
||||
### 2.1 Component Stack
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ EXTERNAL INTERFACE │
|
||||
│ (Internet-facing ports) │
|
||||
├──────────────────────┬──────────────────────────────────────┤
|
||||
│ HAProxy │ TCP 8880 (HTTP) │
|
||||
│ (Load Balancer) │ TCP 8881 (HTTPS/TLS) │
|
||||
├──────────────────────┼──────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ ┌──────────────────┴───────────────────────────┐ │
|
||||
│ │ SERVICE LAYER │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ TinyProxy │ │ OpenVPN │ │ │
|
||||
│ │ │ TCP 8888 │ │ UDP 18080 │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ DISPATCHER (lthnvpnd) │
|
||||
│ Payment validation & routing │
|
||||
├──────────────────────┬──────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ ┌──────────────────┴───────────────────────────┐ │
|
||||
│ │ BLOCKCHAIN LAYER │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ letheand │ │ Wallet │ │ │
|
||||
│ │ │TCP 48782/72 │ │ TCP 1444/45 │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ MANAGEMENT LAYER │
|
||||
│ lvmgmt, lvpnc-client-man (TCP 8124), Nginx │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Component Summary
|
||||
|
||||
| Component | Binary | Ports | Purpose |
|
||||
|-----------|--------|-------|---------|
|
||||
| HAProxy | haproxy | 8880, 8881 | Load balancing, TLS termination |
|
||||
| TinyProxy | tinyproxy | 8888 | HTTP/HTTPS proxy service |
|
||||
| OpenVPN | openvpn | 18080/UDP | VPN tunnel service |
|
||||
| Dispatcher | lthnvpnd | - | Payment validation, routing |
|
||||
| Daemon | letheand | 48782, 48772 | Blockchain node |
|
||||
| Wallet | lethean-wallet-vpn-rpc | 1444, 1445 | Payment handling |
|
||||
| Client Manager | lvpnc-client-man | 8124 | Client session management |
|
||||
| Management | lvmgmt | - | Configuration, SDP generation |
|
||||
|
||||
---
|
||||
|
||||
## 3. Deployment
|
||||
|
||||
### 3.1 Docker Deployment (Recommended)
|
||||
|
||||
Exit Nodes are deployed as Docker containers for consistency:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name lethean-node \
|
||||
-p 8880:8880 \
|
||||
-p 8881:8881 \
|
||||
-p 8888:8888 \
|
||||
-p 18080:18080/udp \
|
||||
-v /opt/lthn:/opt/lthn \
|
||||
letheanvpn/lvpn:latest
|
||||
```
|
||||
|
||||
### 3.2 Startup Sequence
|
||||
|
||||
When the container starts in **node mode**:
|
||||
|
||||
1. **WireGuard Connection**
|
||||
- Connects to Lethean space via WireGuard
|
||||
- Endpoint: `vpn2.lethean.space:8774`
|
||||
|
||||
2. **Blockchain Sync**
|
||||
- letheand starts and syncs blockchain
|
||||
- Connects to configured seed peers
|
||||
- Private address: `172.31.129.19` (via tunnel)
|
||||
|
||||
3. **Easy-Deploy**
|
||||
- Automated infrastructure setup
|
||||
- Certificate generation
|
||||
- Service configuration
|
||||
|
||||
4. **Wallet RPC**
|
||||
- lethean-wallet-vpn-rpc starts
|
||||
- Listens on `127.0.0.1:14660`
|
||||
|
||||
5. **Proxy Services**
|
||||
- TinyProxy starts on port 8888
|
||||
- OpenVPN starts on port 18080 (if enabled)
|
||||
|
||||
6. **VDP Registration**
|
||||
- Pushes VDP to VDP Manager
|
||||
- URL: `https://mgr.lethean.space`
|
||||
|
||||
7. **VDP Sync Loop**
|
||||
- Fetches provider list from VDP Manager
|
||||
- Refreshes every 3500 seconds (TTL: 3600s)
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuration
|
||||
|
||||
### 4.1 dispatcher.ini
|
||||
|
||||
The primary configuration file at `/opt/lthn/etc/dispatcher.ini`:
|
||||
|
||||
#### 4.1.1 Global Section
|
||||
|
||||
```ini
|
||||
[global]
|
||||
; Debug level: DEBUG, INFO, WARN, ERROR
|
||||
debug=INFO
|
||||
|
||||
; CA certificate for provider identity
|
||||
ca=/opt/lthn/etc/ca/certs/ca.cert.pem
|
||||
|
||||
; Provider identification
|
||||
provider-id=efaa812b358956f93a0e324385c8b44469a99e5a82f2de327297b25d8c2ee288
|
||||
provider-key=<secret-key>
|
||||
provider-name=MyExitNode
|
||||
provider-type=commercial
|
||||
|
||||
; Terms of service
|
||||
provider-terms=https://example.com/terms
|
||||
; Or from file: provider-terms=@/opt/lthn/etc/terms.txt
|
||||
```
|
||||
|
||||
#### 4.1.2 Wallet Section
|
||||
|
||||
```ini
|
||||
; Wallet configuration
|
||||
wallet-address=iz5HSgJUW0max8Hs2TEAacKhKA9LXLLDvc4u7yCV7Lm4iwkgFXTMFBAdtj2mqMpqy7T4BNveDQdW8LBPVxWqy94B2A6sKJXQ7
|
||||
wallet-rpc-uri=http://127.0.0.1:14660/json_rpc
|
||||
wallet-username=dispatcher
|
||||
wallet-password=<secure-password>
|
||||
```
|
||||
|
||||
#### 4.1.3 Proxy Service (1A-1Z)
|
||||
|
||||
```ini
|
||||
[service-1A]
|
||||
name=AU_Melbourne_Proxy1
|
||||
backend_proxy_server=localhost:3128
|
||||
crt=/opt/lthn/etc/ca/certs/ha.cert.pem
|
||||
key=/opt/lthn/etc/ca/private/ha.key.pem
|
||||
crtkey=/opt/lthn/etc/ca/certs/ha.both.pem
|
||||
```
|
||||
|
||||
#### 4.1.4 VPN Service (2A-2Z)
|
||||
|
||||
```ini
|
||||
[service-1B]
|
||||
crt=/opt/lthn/etc/ca/certs/openvpn.cert.pem
|
||||
key=/opt/lthn/etc/ca/private/openvpn.key.pem
|
||||
crtkey=/opt/lthn/etc/ca/certs/openvpn.both.pem
|
||||
reneg=600
|
||||
enabled=true
|
||||
iprange=10.8.0.0
|
||||
ipmask=255.255.255.0
|
||||
ip6range=fd00::/64
|
||||
dns=1.1.1.1
|
||||
mgmtport=11123
|
||||
```
|
||||
|
||||
### 4.2 Configuration Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| provider-id | hex(64) | Yes | Unique provider identifier |
|
||||
| provider-key | string | Yes | Authentication secret |
|
||||
| provider-name | string(16) | Yes | Display name |
|
||||
| wallet-address | string | Yes | LTHN payment address |
|
||||
| wallet-rpc-uri | URL | Yes | Wallet RPC endpoint |
|
||||
| backend_proxy_server | host:port | Per-service | Proxy backend |
|
||||
| crt | path | Per-service | Service certificate |
|
||||
| key | path | Per-service | Private key |
|
||||
| reneg | integer | VPN only | Renegotiation interval (seconds) |
|
||||
| iprange | CIDR | VPN only | Client IP pool |
|
||||
|
||||
---
|
||||
|
||||
## 5. Service Types
|
||||
|
||||
### 5.1 HTTP Proxy (Type: proxy)
|
||||
|
||||
**Implementation**: TinyProxy or Squid
|
||||
|
||||
**Ports**:
|
||||
- External: 8080 (via HAProxy 8880)
|
||||
- Internal: 8888, 3128
|
||||
|
||||
**Features**:
|
||||
- HTTP/HTTPS forwarding
|
||||
- Optional authentication
|
||||
- Access logging
|
||||
|
||||
**Use Cases**:
|
||||
- Browser-based privacy
|
||||
- Application proxy configuration
|
||||
- Lightweight traffic routing
|
||||
|
||||
### 5.2 VPN Tunnel (Type: vpn)
|
||||
|
||||
**Implementation**: OpenVPN
|
||||
|
||||
**Ports**:
|
||||
- UDP 18080 (primary)
|
||||
- TCP 443 (fallback, optional)
|
||||
|
||||
**Features**:
|
||||
- Full tunnel encryption
|
||||
- All traffic routing
|
||||
- DNS leak protection
|
||||
|
||||
**Configuration Options**:
|
||||
```ini
|
||||
iprange=10.8.0.0 ; Client IP pool start
|
||||
ipmask=255.255.255.0 ; Subnet mask
|
||||
ip6range=fd00::/64 ; IPv6 pool
|
||||
dns=1.1.1.1 ; DNS server for clients
|
||||
reneg=600 ; Rekey interval
|
||||
```
|
||||
|
||||
### 5.3 Future Service Types
|
||||
|
||||
The architecture supports additional service types:
|
||||
|
||||
- **SOCKS5 Proxy** (planned)
|
||||
- **WireGuard VPN** (planned)
|
||||
- **Custom BYOA** (Bring Your Own Application)
|
||||
|
||||
---
|
||||
|
||||
## 6. Security
|
||||
|
||||
### 6.1 Certificate Architecture
|
||||
|
||||
```
|
||||
Root CA (provider)
|
||||
│
|
||||
├── HAProxy Certificate (TLS termination)
|
||||
│
|
||||
├── Proxy Service Certificate
|
||||
│
|
||||
└── VPN Service Certificate
|
||||
```
|
||||
|
||||
### 6.2 Certificate Generation
|
||||
|
||||
```bash
|
||||
# Generate CA
|
||||
openssl genrsa -out ca.key 4096
|
||||
openssl req -new -x509 -days 3650 -key ca.key -out ca.cert.pem
|
||||
|
||||
# Generate service certificate
|
||||
openssl genrsa -out service.key 2048
|
||||
openssl req -new -key service.key -out service.csr
|
||||
openssl x509 -req -in service.csr -CA ca.cert.pem -CAkey ca.key \
|
||||
-CAcreateserial -out service.cert.pem -days 365
|
||||
```
|
||||
|
||||
### 6.3 Access Control
|
||||
|
||||
The Dispatcher validates:
|
||||
|
||||
1. **Payment Verification** - Check blockchain for valid transaction
|
||||
2. **Session Management** - Track active sessions
|
||||
3. **Rate Limiting** - Prevent abuse
|
||||
4. **Geographic Restrictions** - Optional country blocking
|
||||
|
||||
---
|
||||
|
||||
## 7. Monitoring
|
||||
|
||||
### 7.1 Health Checks
|
||||
|
||||
Exit Nodes should implement health endpoints:
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| /health | Basic liveness check |
|
||||
| /status | Detailed component status |
|
||||
| /metrics | Prometheus-compatible metrics |
|
||||
|
||||
### 7.2 Zabbix Integration
|
||||
|
||||
For daemon reward eligibility (see RFC-0004):
|
||||
|
||||
```
|
||||
Zabbix Agent → letheand port check
|
||||
→ Blockchain height sync
|
||||
→ Service availability
|
||||
```
|
||||
|
||||
### 7.3 Logging
|
||||
|
||||
Recommended log locations:
|
||||
|
||||
```
|
||||
/var/log/lthn/dispatcher.log
|
||||
/var/log/lthn/haproxy.log
|
||||
/var/log/lthn/tinyproxy.log
|
||||
/var/log/lthn/openvpn.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Network Requirements
|
||||
|
||||
### 8.1 Bandwidth
|
||||
|
||||
| Tier | Minimum | Recommended |
|
||||
|------|---------|-------------|
|
||||
| Basic | 10 Mbps | 100 Mbps |
|
||||
| Standard | 100 Mbps | 1 Gbps |
|
||||
| Premium | 1 Gbps | 10 Gbps |
|
||||
|
||||
### 8.2 Ports
|
||||
|
||||
| Port | Protocol | Direction | Purpose |
|
||||
|------|----------|-----------|---------|
|
||||
| 8880 | TCP | Inbound | Client HTTP |
|
||||
| 8881 | TCP | Inbound | Client HTTPS |
|
||||
| 8888 | TCP | Internal | Proxy service |
|
||||
| 18080 | UDP | Inbound | VPN service |
|
||||
| 48782 | TCP | Outbound | Daemon P2P |
|
||||
| 48772 | TCP | Outbound | Daemon RPC |
|
||||
|
||||
### 8.3 Firewall Rules
|
||||
|
||||
```bash
|
||||
# Allow client connections
|
||||
iptables -A INPUT -p tcp --dport 8880 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 8881 -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport 18080 -j ACCEPT
|
||||
|
||||
# Allow blockchain P2P
|
||||
iptables -A OUTPUT -p tcp --dport 48782 -j ACCEPT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Operational Procedures
|
||||
|
||||
### 9.1 Initial Setup
|
||||
|
||||
```bash
|
||||
# 1. Install Docker
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
|
||||
# 2. Create directories
|
||||
mkdir -p /opt/lthn/etc/ca/{certs,private}
|
||||
|
||||
# 3. Generate provider identity
|
||||
lvmgmt --generate-provider-id
|
||||
|
||||
# 4. Generate SDP configuration
|
||||
lvmgmt --generate-sdp --wallet-address <YOUR_WALLET>
|
||||
|
||||
# 5. Start node
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 9.2 Updating
|
||||
|
||||
```bash
|
||||
# Pull latest image
|
||||
docker pull letheanvpn/lvpn:latest
|
||||
|
||||
# Restart with new image
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 9.3 Backup
|
||||
|
||||
Critical files to backup:
|
||||
- `/opt/lthn/etc/dispatcher.ini`
|
||||
- `/opt/lthn/etc/ca/` (certificates)
|
||||
- `/opt/lthn/etc/sdp.json`
|
||||
- Wallet files
|
||||
|
||||
---
|
||||
|
||||
## 10. References
|
||||
|
||||
- RFC-0001: Lethean Network Overview
|
||||
- RFC-0002: Service Descriptor Protocol (SDP)
|
||||
- RFC-0004: Payment & Dispatcher Protocol
|
||||
- OpenVPN Documentation: https://openvpn.net/
|
||||
- HAProxy Documentation: https://www.haproxy.org/
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Docker Compose Example
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
lethean-node:
|
||||
image: letheanvpn/lvpn:latest
|
||||
container_name: lethean-exit-node
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8880:8880"
|
||||
- "8881:8881"
|
||||
- "18080:18080/udp"
|
||||
volumes:
|
||||
- ./config:/opt/lthn/etc
|
||||
- ./data:/opt/lthn/data
|
||||
environment:
|
||||
- LETHEAN_MODE=node
|
||||
- WALLET_ADDRESS=${WALLET_ADDRESS}
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Component Ports Reference
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ EXIT NODE │
|
||||
│ │
|
||||
│ EXTERNAL INTERNAL │
|
||||
│ ──────── ──────── │
|
||||
│ 8880 ──────► HAProxy │
|
||||
│ 8881 ──────► │ │
|
||||
│ ├────────► 8888 (tinyproxy) │
|
||||
│ ├────────► 3128 (squid, optional) │
|
||||
│ └────────► 8124 (lvpnc-client-man) │
|
||||
│ │
|
||||
│ 18080/UDP ──────────────► OpenVPN │
|
||||
│ │
|
||||
│ letheand ──────► 48782, 48772 │
|
||||
│ wallet ────────► 1444, 1445 │
|
||||
│ wallet-rpc ────► 14660 │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
451
docs/specs/RFC-0004-payment-dispatcher-protocol.md
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
# RFC-0004: Payment & Dispatcher Protocol
|
||||
|
||||
```
|
||||
RFC: 0004
|
||||
Title: Payment & Dispatcher Protocol
|
||||
Status: Standard
|
||||
Category: Standards Track
|
||||
Authors: Darbs, Snider
|
||||
License: EUPL-1.2
|
||||
Created: 2026-02-01
|
||||
Requires: RFC-0001, RFC-0003
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the payment flow between clients and Exit Node providers, including the Dispatcher component that validates payments and authorizes service access. It also describes the Daemon Reward Program that incentivizes blockchain node operators.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### 1.1 Purpose
|
||||
|
||||
The Lethean payment model enables direct peer-to-peer transactions between users and service providers without intermediaries. The Dispatcher component on each Exit Node validates these payments and grants service access.
|
||||
|
||||
### 1.2 Design Goals
|
||||
|
||||
1. **Direct Payment** - No payment processor or intermediary
|
||||
2. **Privacy** - Untraceable transactions on blockchain
|
||||
3. **Automation** - No manual verification required
|
||||
4. **Fairness** - Providers paid for actual service delivery
|
||||
|
||||
---
|
||||
|
||||
## 2. Payment Model
|
||||
|
||||
### 2.1 Overview
|
||||
|
||||
```
|
||||
┌──────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Client │ LTHN │ Blockchain │ Verify │ Exit Node │
|
||||
│ │────────>│ │────────>│ (Dispatcher)│
|
||||
└──────────┘ └─────────────┘ └─────────────┘
|
||||
│ │
|
||||
│ Service Access │
|
||||
│<─────────────────────────────────────────────│
|
||||
```
|
||||
|
||||
### 2.2 Payment Flow
|
||||
|
||||
1. **Discovery**: Client queries SDP for available providers
|
||||
2. **Selection**: User selects provider based on price, location, etc.
|
||||
3. **Payment**: Client sends LTHN to provider's wallet address
|
||||
4. **Confirmation**: Transaction confirmed on blockchain
|
||||
5. **Validation**: Dispatcher detects payment, authorizes client
|
||||
6. **Connection**: Client connects and uses service
|
||||
|
||||
### 2.3 Payment Parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| cost | LTHN amount per service unit (defined by provider) |
|
||||
| firstVerificationsRequired | Confirmations needed for first payment (0-2) |
|
||||
| subsequentVerificationsRequired | Confirmations for later payments (0-1) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Dispatcher Protocol
|
||||
|
||||
### 3.1 Component Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ DISPATCHER │
|
||||
│ (lthnvpnd) │
|
||||
│ │
|
||||
Client ──────────>│ ┌─────────────────────────┐ │
|
||||
Connection │ │ Payment Validator │ │
|
||||
│ │ (wallet RPC queries) │ │
|
||||
│ └───────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────▼─────────────┐ │
|
||||
│ │ Session Manager │ │
|
||||
│ │ (active connections) │ │
|
||||
│ └───────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────▼─────────────┐ │
|
||||
│ │ Service Router │ │──────> Services
|
||||
│ │ (proxy/vpn dispatch) │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Wallet RPC Interface
|
||||
|
||||
The Dispatcher communicates with the local wallet via JSON-RPC:
|
||||
|
||||
**Endpoint**: `http://127.0.0.1:14660/json_rpc`
|
||||
|
||||
**Key Methods**:
|
||||
|
||||
```json
|
||||
// Check incoming transfers
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_transfers",
|
||||
"params": {"in": true, "pending": true},
|
||||
"id": "1"
|
||||
}
|
||||
|
||||
// Verify specific payment
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_transfer_by_txid",
|
||||
"params": {"txid": "<transaction_hash>"},
|
||||
"id": "2"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Payment Validation Logic
|
||||
|
||||
```python
|
||||
def validate_payment(client_info):
|
||||
# Query wallet for incoming transfers
|
||||
transfers = wallet_rpc.get_transfers(in=True)
|
||||
|
||||
for transfer in transfers:
|
||||
# Check if payment matches expected amount
|
||||
if transfer.amount >= service.cost:
|
||||
# Check confirmation count
|
||||
if transfer.confirmations >= service.firstVerificationsRequired:
|
||||
# Check if not already used
|
||||
if not is_payment_used(transfer.txid):
|
||||
mark_payment_used(transfer.txid)
|
||||
return authorize_client(client_info, transfer)
|
||||
|
||||
return reject_client(client_info, "Payment not found")
|
||||
```
|
||||
|
||||
### 3.4 Session Management
|
||||
|
||||
| Session State | Description |
|
||||
|---------------|-------------|
|
||||
| PENDING | Awaiting payment confirmation |
|
||||
| ACTIVE | Payment validated, service access granted |
|
||||
| EXPIRED | Session time/data limit exceeded |
|
||||
| TERMINATED | Manually ended or error |
|
||||
|
||||
---
|
||||
|
||||
## 4. Transaction Format
|
||||
|
||||
### 4.1 Payment Transaction
|
||||
|
||||
LTHN uses CryptoNote-based transactions:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| inputs | Ring signature inputs (privacy) |
|
||||
| outputs | Destination addresses (stealth) |
|
||||
| amount | Payment amount (encrypted) |
|
||||
| payment_id | Optional identifier (deprecated in favor of subaddresses) |
|
||||
|
||||
### 4.2 Confirmation Levels
|
||||
|
||||
| Confirmations | Security | Wait Time |
|
||||
|---------------|----------|-----------|
|
||||
| 0 | Low (double-spend risk) | Immediate |
|
||||
| 1 | Medium | ~2 minutes |
|
||||
| 2 | High | ~4 minutes |
|
||||
|
||||
### 4.3 Recommended Settings
|
||||
|
||||
| Use Case | First Confirmations | Subsequent |
|
||||
|----------|---------------------|------------|
|
||||
| Low-value, trusted | 0 | 0 |
|
||||
| Standard | 1 | 1 |
|
||||
| High-value | 2 | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Pricing Model
|
||||
|
||||
### 5.1 Cost Specification
|
||||
|
||||
Providers define costs in their SDP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [{
|
||||
"id": "1A",
|
||||
"cost": "0.1",
|
||||
"unit": "session",
|
||||
"duration": 3600
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Pricing Strategies
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| Per-session | Fixed cost per connection |
|
||||
| Per-time | Cost per hour/day |
|
||||
| Per-data | Cost per GB transferred |
|
||||
| Subscription | Pre-paid time blocks |
|
||||
|
||||
### 5.3 Dynamic Pricing (Future)
|
||||
|
||||
Smart contracts may enable:
|
||||
- Demand-based pricing
|
||||
- Bulk discounts
|
||||
- Loyalty rewards
|
||||
|
||||
---
|
||||
|
||||
## 6. Daemon Reward Program
|
||||
|
||||
### 6.1 Purpose
|
||||
|
||||
The Daemon Reward Program incentivizes running blockchain nodes (letheand) to ensure network decentralization and stability.
|
||||
|
||||
### 6.2 Eligibility Flow
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Daemon │
|
||||
│ Commissioned │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐ No ┌─────────────────┐
|
||||
│ Zabbix Agent │───────────>│ Ineligible │
|
||||
│ Running? │ │ for Reward │
|
||||
└────────┬────────┘ └─────────────────┘
|
||||
│ Yes
|
||||
▼
|
||||
┌─────────────────┐ No
|
||||
│ Daemon │───────────> (Re-poll)
|
||||
│ Synchronized? │
|
||||
└────────┬────────┘
|
||||
│ Yes
|
||||
▼
|
||||
┌─────────────────┐ No
|
||||
│ Port │───────────> (Re-poll)
|
||||
│ Accessible? │
|
||||
└────────┬────────┘
|
||||
│ Yes
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Mark as LIVE │
|
||||
│ Start SLA Clock │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 6.3 SLA Calculation
|
||||
|
||||
**Formula**:
|
||||
```
|
||||
if daemon_sla >= sla_threshold:
|
||||
reward = escrow / number_of_eligible_daemons
|
||||
else:
|
||||
reward = 0
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
|
||||
| Parameter | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| sla_threshold | 98% | Minimum uptime requirement |
|
||||
| escrow | $250 USD/month | Monthly reward pool |
|
||||
| cycle_length | 30 days | Evaluation period |
|
||||
| max_daemons | 50 | Cutoff for reward distribution |
|
||||
|
||||
### 6.4 SLA Thresholds
|
||||
|
||||
| SLA Level | Allowed Downtime (Monthly) |
|
||||
|-----------|---------------------------|
|
||||
| 98% | 14h 36m 34s |
|
||||
| 99% | 7h 18m 17s |
|
||||
|
||||
**Recommendation**: 98% is realistic for independent operators who may not notice overnight outages but can restore service the next day.
|
||||
|
||||
### 6.5 Reward Distribution
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ 30 Days Elapsed │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐ No ┌─────────────────┐
|
||||
│ SLA >= 98%? │───────────>│ No Reward │
|
||||
└────────┬────────┘ │ Notify Operator │
|
||||
│ Yes │ Reset Clock │
|
||||
▼ └─────────────────┘
|
||||
┌─────────────────┐
|
||||
│ Calculate: │
|
||||
│ reward = escrow │
|
||||
│ / daemons │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Auto-payment │
|
||||
│ from Escrow │
|
||||
│ to Operator │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 6.6 Anti-Gaming Measures
|
||||
|
||||
To prevent abuse:
|
||||
|
||||
1. **Wallet Analysis** - Compare addresses across daemons
|
||||
2. **WHOIS Comparison** - Detect same provider/location
|
||||
3. **Geographic Distribution** - Encourage spread across regions
|
||||
4. **Hotspot Prevention** - Auto-exclude clustered daemons
|
||||
|
||||
---
|
||||
|
||||
## 7. Security Considerations
|
||||
|
||||
### 7.1 Double-Spend Protection
|
||||
|
||||
- Require confirmations before service access
|
||||
- Monitor mempool for conflicting transactions
|
||||
- Rate-limit rapid reconnection attempts
|
||||
|
||||
### 7.2 Replay Protection
|
||||
|
||||
- Track used payment transaction IDs
|
||||
- Reject previously-used payments
|
||||
- Session binding to payment hash
|
||||
|
||||
### 7.3 Payment Privacy
|
||||
|
||||
- Ring signatures hide sender
|
||||
- Stealth addresses hide receiver
|
||||
- Amount encryption
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Notes
|
||||
|
||||
### 8.1 Wallet RPC Configuration
|
||||
|
||||
```ini
|
||||
; dispatcher.ini
|
||||
wallet-rpc-uri=http://127.0.0.1:14660/json_rpc
|
||||
wallet-username=dispatcher
|
||||
wallet-password=<secure-random-string>
|
||||
```
|
||||
|
||||
### 8.2 Starting Wallet RPC
|
||||
|
||||
```bash
|
||||
lethean-wallet-vpn-rpc \
|
||||
--wallet-file /opt/lthn/wallet \
|
||||
--rpc-bind-port 14660 \
|
||||
--rpc-login dispatcher:<password> \
|
||||
--daemon-address 127.0.0.1:48782
|
||||
```
|
||||
|
||||
### 8.3 Monitoring Payments
|
||||
|
||||
```python
|
||||
# Example: Watch for incoming payments
|
||||
import requests
|
||||
|
||||
def check_payments():
|
||||
response = requests.post(
|
||||
"http://127.0.0.1:14660/json_rpc",
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_transfers",
|
||||
"params": {"in": True, "pending": True},
|
||||
"id": "1"
|
||||
},
|
||||
auth=("dispatcher", password)
|
||||
)
|
||||
return response.json()["result"]["in"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
- RFC-0001: Lethean Network Overview
|
||||
- RFC-0003: Exit Node Architecture
|
||||
- CryptoNote Protocol: https://cryptonote.org/
|
||||
- Lethean Daemon Reward Workflow Documentation
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Reward Calculation Example
|
||||
|
||||
```
|
||||
Given:
|
||||
- escrow = $250 USD
|
||||
- eligible_daemons = 25
|
||||
- daemon_sla = 99.2%
|
||||
- threshold = 98%
|
||||
|
||||
Calculation:
|
||||
daemon_sla (99.2%) >= threshold (98%) ✓
|
||||
|
||||
reward = escrow / eligible_daemons
|
||||
reward = $250 / 25
|
||||
reward = $10 USD per daemon
|
||||
|
||||
Result:
|
||||
Operator receives $10 USD equivalent in LTHN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Payment State Machine
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ INITIAL │
|
||||
└──────┬───────┘
|
||||
│ Client connects
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ PENDING │◄─────────────┐
|
||||
└──────┬───────┘ │
|
||||
│ │
|
||||
┌──────────────┼──────────────┐ │
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ │
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ PAYMENT │ │ TIMEOUT │ │ REJECTED │ │
|
||||
│ FOUND │ │ │ │ │ │
|
||||
└─────┬─────┘ └───────────┘ └───────────┘ │
|
||||
│ │
|
||||
│ Confirmations met │
|
||||
▼ │
|
||||
┌───────────┐ │
|
||||
│ ACTIVE │───────────────────────────────┘
|
||||
│ (session) │ Session expired/renewed
|
||||
└─────┬─────┘
|
||||
│ Disconnect/Limit reached
|
||||
▼
|
||||
┌───────────┐
|
||||
│TERMINATED │
|
||||
└───────────┘
|
||||
```
|
||||
591
docs/specs/RFC-0005-client-protocol.md
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
# RFC-0005: Client Protocol
|
||||
|
||||
```
|
||||
RFC: 0005
|
||||
Title: Client Protocol
|
||||
Status: Standard
|
||||
Category: Standards Track
|
||||
Authors: Darbs, Snider
|
||||
License: EUPL-1.2
|
||||
Created: 2026-02-01
|
||||
Requires: RFC-0001, RFC-0002, RFC-0004
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the Lethean VPN Client (lvpnc) protocol, including service discovery, connection establishment, payment submission, and session management. It covers both GUI and CLI interfaces as well as transport layer options.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### 1.1 Purpose
|
||||
|
||||
The Lethean Client enables users to discover, connect to, and pay for privacy services provided by Exit Nodes. This specification defines the client-side protocols and components required for seamless integration with the Lethean network.
|
||||
|
||||
### 1.2 Design Principles
|
||||
|
||||
1. **User Sovereignty** - User controls their wallet and connection choices
|
||||
2. **Privacy First** - No tracking, minimal metadata exposure
|
||||
3. **Cross-Platform** - Support for Windows, Linux, macOS
|
||||
4. **Flexibility** - GUI and CLI interfaces for different user preferences
|
||||
|
||||
---
|
||||
|
||||
## 2. Client Architecture
|
||||
|
||||
### 2.1 Component Overview
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ LETHEAN CLIENT (lvpnc) │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ GUI │ │
|
||||
│ │ (Kivy-based) │ │
|
||||
│ └─────────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────┴─────────────────────────────────┐ │
|
||||
│ │ Core Services │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Provider │ │ Session │ │ Payment │ │ │
|
||||
│ │ │ Manager │ │ Manager │ │ Manager │ │ │
|
||||
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
|
||||
│ └─────────┼────────────────┼────────────────┼───────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ┌─────────┴────────────────┴────────────────┴───────────────┐ │
|
||||
│ │ Transport Layer │ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||
│ │ │ SSH │ │ WireGuard │ │ HTTP │ │ │
|
||||
│ │ │ Tunnel │ │ Tunnel │ │ Proxy │ │ │
|
||||
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ Blockchain Services │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Wallet │ │ Daemon │ │ Daemon │ │ │
|
||||
│ │ │ RPC │ │ RPC │ │ (local) │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Core Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| **GUI** | Kivy-based graphical interface |
|
||||
| **Provider Manager** | Handles SDP queries and VDP caching |
|
||||
| **Session Manager** | Manages active connections |
|
||||
| **Payment Manager** | Wallet integration and transaction submission |
|
||||
| **Transport Layer** | SSH, WireGuard, or HTTP proxy tunnels |
|
||||
| **Wallet RPC** | Local wallet for payment processing |
|
||||
| **Daemon RPC** | Connection to blockchain node |
|
||||
|
||||
---
|
||||
|
||||
## 3. Service Discovery
|
||||
|
||||
### 3.1 Discovery Flow
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────┐
|
||||
│ Client │ Query │ SDP Server │ Cached │ VDP │
|
||||
│ │─────────>│ │─────────>│ Store │
|
||||
└──────────┘ └──────────────┘ └──────────┘
|
||||
│ │
|
||||
│ Provider List │
|
||||
│<──────────────────────│
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────┐ │
|
||||
│ User │ │
|
||||
│Selection │ │
|
||||
└──────────┘ │
|
||||
```
|
||||
|
||||
### 3.2 SDP Query
|
||||
|
||||
```python
|
||||
# Query available providers
|
||||
GET https://sdp.lethean.io/v1/services/search
|
||||
|
||||
# Optional filters
|
||||
?type=proxy # Service type (proxy, vpn)
|
||||
?country=AU # Country code (ISO 3166-1)
|
||||
?max_cost=0.5 # Maximum cost in LTHN
|
||||
?min_speed=10mbps # Minimum speed
|
||||
```
|
||||
|
||||
### 3.3 Provider Selection Criteria
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **Location** | Geographic proximity for latency |
|
||||
| **Cost** | LTHN price per session/hour/GB |
|
||||
| **Speed** | Advertised bandwidth |
|
||||
| **Type** | Service type (proxy, VPN) |
|
||||
| **Reputation** | Future: on-chain reputation |
|
||||
|
||||
### 3.4 VDP Caching
|
||||
|
||||
```
|
||||
Directory Structure:
|
||||
├── providers/ # All known provider VDPs
|
||||
├── my-providers/ # Providers we've connected to
|
||||
├── spaces/ # Lethernet space definitions
|
||||
├── gates/ # Gateway definitions
|
||||
└── sessions/ # Active session data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Connection Protocol
|
||||
|
||||
### 4.1 Connection Flow
|
||||
|
||||
```
|
||||
┌────────────┐ ┌────────────┐
|
||||
│ Client │ │ Exit Node │
|
||||
└─────┬──────┘ └─────┬──────┘
|
||||
│ │
|
||||
│ 1. Query SDP for providers │
|
||||
│────────────────────────────────────────────────>│
|
||||
│ │
|
||||
│ 2. Select provider, parse VDP │
|
||||
│<────────────────────────────────────────────────│
|
||||
│ │
|
||||
│ 3. Send payment to provider wallet │
|
||||
│────────────────(Blockchain)────────────────────>│
|
||||
│ │
|
||||
│ 4. Wait for confirmation (0-2 blocks) │
|
||||
│ │
|
||||
│ 5. Connect via transport (SSH/WG/HTTP) │
|
||||
│────────────────────────────────────────────────>│
|
||||
│ │
|
||||
│ 6. Dispatcher validates payment │
|
||||
│ │
|
||||
│ 7. Session established │
|
||||
│<────────────────────────────────────────────────│
|
||||
│ │
|
||||
│ 8. Route traffic through tunnel │
|
||||
│◄───────────────────────────────────────────────►│
|
||||
```
|
||||
|
||||
### 4.2 Transport Options
|
||||
|
||||
| Transport | Port | Protocol | Use Case |
|
||||
|-----------|------|----------|----------|
|
||||
| **SSH Tunnel** | 8880 | TCP | Default, firewall-friendly |
|
||||
| **WireGuard** | 8774 | UDP | High performance VPN |
|
||||
| **HTTP Proxy** | 8080 | TCP | Browser-only proxy |
|
||||
| **HTTPS/TLS** | 8881 | TCP | Manager-over-TLS |
|
||||
|
||||
### 4.3 Exit Node Connection
|
||||
|
||||
```
|
||||
Client connects to Exit Node:
|
||||
|
||||
HTTP/8880 ──► haproxy ──► Authenticated proxy access
|
||||
HTTPS/8881 ──► haproxy ──► Manager-over-TLS channel
|
||||
UDP/8774 ──► WireGuard ──► VPN tunnel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Payment Integration
|
||||
|
||||
### 5.1 Payment Flow
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Client │ │ Blockchain │ │ Provider │
|
||||
│ Wallet │ │ Network │ │ Wallet │
|
||||
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||
│ │ │
|
||||
│ 1. Create TX │ │
|
||||
│─────────────────────► │ │
|
||||
│ │ │
|
||||
│ 2. Broadcast │ │
|
||||
│ │─────────────────────► │
|
||||
│ │ │
|
||||
│ 3. Confirm (0-2 blocks) │
|
||||
│ │ │
|
||||
│ 4. Connect with TX proof │
|
||||
│───────────────────────────────────────────────►│
|
||||
│ │ │
|
||||
│ 5. Dispatcher validates │
|
||||
│ │ ◄──────────────────── │
|
||||
│ │ │
|
||||
│ 6. Session granted │
|
||||
│◄───────────────────────────────────────────────│
|
||||
```
|
||||
|
||||
### 5.2 Payment Parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `--default-pay-days` | Days to pay for by default |
|
||||
| `--auto-pay-days` | Auto-pay without GUI confirmation |
|
||||
| `--free-session-days` | Days to request for free services |
|
||||
| `--unpaid-expiry` | Seconds before unpaid session expires |
|
||||
| `--use-tx-pool` | Accept unconfirmed payments |
|
||||
|
||||
### 5.3 Wallet Configuration
|
||||
|
||||
```ini
|
||||
# client.ini wallet settings
|
||||
wallet-rpc-url=http://127.0.0.1:14660/json_rpc
|
||||
wallet-rpc-port=14660
|
||||
wallet-rpc-user=client
|
||||
wallet-rpc-password=<auto-generated>
|
||||
wallet-name=default
|
||||
wallet-address=<LTHN_ADDRESS>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Session Management
|
||||
|
||||
### 6.1 Session States
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| **UNPAID** | Session created, awaiting payment |
|
||||
| **PENDING** | Payment sent, awaiting confirmation |
|
||||
| **ACTIVE** | Payment confirmed, connection established |
|
||||
| **EXPIRED** | Time/data limit reached |
|
||||
| **DISCONNECTED** | User-initiated disconnect |
|
||||
|
||||
### 6.2 Session Lifecycle
|
||||
|
||||
```
|
||||
┌───────────┐
|
||||
│ SELECT │
|
||||
│ PROVIDER │
|
||||
└─────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌───────────┐
|
||||
│ UNPAID │
|
||||
└─────┬─────┘
|
||||
│ Send payment
|
||||
▼
|
||||
┌───────────┐
|
||||
│ PENDING │◄─────────────────┐
|
||||
└─────┬─────┘ │
|
||||
│ Confirmed │ Renew
|
||||
▼ │
|
||||
┌───────────┐ │
|
||||
│ ACTIVE │──────────────────┘
|
||||
└─────┬─────┘
|
||||
│ Expire/Disconnect
|
||||
▼
|
||||
┌───────────┴───────────┐
|
||||
▼ ▼
|
||||
┌───────────┐ ┌───────────┐
|
||||
│ EXPIRED │ │DISCONNECTED│
|
||||
└───────────┘ └───────────┘
|
||||
```
|
||||
|
||||
### 6.3 Auto-Reconnect
|
||||
|
||||
```ini
|
||||
# Reconnection settings
|
||||
auto-reconnect=30 # Seconds between reconnect attempts
|
||||
auto-connect=lthn://provider-id/service-id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Command Line Interface
|
||||
|
||||
### 7.1 Installation
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/letheanVPN/lvpn/main/install-all-in-one.ps1 | iex
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
curl -sSL https://lethean.io/install.sh | bash
|
||||
```
|
||||
|
||||
### 7.2 Basic Usage
|
||||
|
||||
```bash
|
||||
# Run with GUI
|
||||
python client.py --run-gui=1
|
||||
|
||||
# Run headless (CLI only)
|
||||
python client.py --run-gui=0
|
||||
|
||||
# Auto-connect to specific provider
|
||||
python client.py --auto-connect=lthn://provider-id/1A
|
||||
```
|
||||
|
||||
### 7.3 Key Command Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--run-gui={0,1}` | Enable/disable GUI |
|
||||
| `--run-proxy={0,1}` | Run local proxy server |
|
||||
| `--run-wallet={0,1}` | Run embedded wallet |
|
||||
| `--run-daemon={0,1}` | Run embedded daemon |
|
||||
| `--log-level={DEBUG,INFO,WARNING,ERROR}` | Logging verbosity |
|
||||
| `--config=<path>` | Config file location |
|
||||
|
||||
### 7.4 Directory Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--var-dir` | Variable data directory |
|
||||
| `--cfg-dir` | Configuration directory |
|
||||
| `--app-dir` | Application directory |
|
||||
| `--tmp-dir` | Temporary files |
|
||||
| `--providers-dir` | Provider VDP cache |
|
||||
| `--sessions-dir` | Session data storage |
|
||||
|
||||
---
|
||||
|
||||
## 8. WireGuard Integration
|
||||
|
||||
### 8.1 WireGuard Configuration
|
||||
|
||||
```ini
|
||||
# WireGuard-specific settings
|
||||
enable-wg=1
|
||||
wg-map-device=gate1,wg0
|
||||
wg-map-privkey=gate1,<base64-private-key>
|
||||
wg-cmd-prefix=sudo
|
||||
wg-shutdown-on-disconnect=1
|
||||
```
|
||||
|
||||
### 8.2 WireGuard Commands
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--wg-cmd-create-interface` | Command to create WG interface |
|
||||
| `--wg-cmd-delete-interface` | Command to delete WG interface |
|
||||
| `--wg-cmd-set-ip` | Command to assign IP to interface |
|
||||
| `--wg-cmd-set-interface-up` | Command to bring interface up |
|
||||
| `--wg-cmd-route` | Command to add routes |
|
||||
|
||||
### 8.3 WireGuard Connection Flow
|
||||
|
||||
```
|
||||
Client Exit Node
|
||||
│ │
|
||||
│ 1. Generate WG keypair │
|
||||
│ │
|
||||
│ 2. Request WG config from provider │
|
||||
│──────────────────────────────────────>│
|
||||
│ │
|
||||
│ 3. Receive peer config │
|
||||
│<──────────────────────────────────────│
|
||||
│ │
|
||||
│ 4. Create WG interface │
|
||||
│ 5. Configure peer │
|
||||
│ 6. Establish tunnel │
|
||||
│══════════════════════════════════════>│
|
||||
│ │
|
||||
│ 7. Route traffic through WG │
|
||||
│◄═════════════════════════════════════►│
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. GUI Interface
|
||||
|
||||
### 9.1 Main Views
|
||||
|
||||
| View | Purpose |
|
||||
|------|---------|
|
||||
| **Provider List** | Browse and select Exit Nodes |
|
||||
| **Connection** | Active connection status |
|
||||
| **Wallet** | Balance and transaction history |
|
||||
| **Settings** | Configuration options |
|
||||
|
||||
### 9.2 Connection Workflow
|
||||
|
||||
1. **Browse** - View available providers from SDP
|
||||
2. **Select** - Choose provider based on criteria
|
||||
3. **Pay** - Send LTHN to provider wallet
|
||||
4. **Connect** - Establish tunnel after payment confirms
|
||||
5. **Use** - Traffic routes through Exit Node
|
||||
|
||||
---
|
||||
|
||||
## 10. Security Considerations
|
||||
|
||||
### 10.1 Transport Security
|
||||
|
||||
- SSH tunnels use provider's CA for authentication
|
||||
- WireGuard provides modern cryptographic security
|
||||
- TLS 1.3 for HTTPS connections
|
||||
|
||||
### 10.2 Payment Security
|
||||
|
||||
- Wallet RPC uses authentication
|
||||
- Transactions are cryptographically signed
|
||||
- Ring signatures provide sender privacy
|
||||
|
||||
### 10.3 Session Security
|
||||
|
||||
- Session binding to payment transaction
|
||||
- No session sharing across clients
|
||||
- Automatic session cleanup on disconnect
|
||||
|
||||
---
|
||||
|
||||
## 11. Configuration File
|
||||
|
||||
### 11.1 Default Locations
|
||||
|
||||
| Platform | Path |
|
||||
|----------|------|
|
||||
| Windows | `%USERPROFILE%\lvpn\client.ini` |
|
||||
| Linux | `/etc/lvpn/client.ini` or `~/.config/lvpn/client.ini` |
|
||||
| macOS | `~/Library/Application Support/lvpn/client.ini` |
|
||||
|
||||
### 11.2 Example Configuration
|
||||
|
||||
```ini
|
||||
[client]
|
||||
log-level=INFO
|
||||
run-gui=1
|
||||
run-proxy=1
|
||||
run-wallet=1
|
||||
|
||||
[wallet]
|
||||
wallet-rpc-url=http://127.0.0.1:14660/json_rpc
|
||||
wallet-name=default
|
||||
|
||||
[connection]
|
||||
auto-reconnect=30
|
||||
default-pay-days=1
|
||||
|
||||
[wireguard]
|
||||
enable-wg=0
|
||||
wg-shutdown-on-disconnect=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Lethernet Access
|
||||
|
||||
### 12.1 Beyond Standard VPN
|
||||
|
||||
Connected clients gain access to Lethernet services:
|
||||
|
||||
| Service Type | Description |
|
||||
|--------------|-------------|
|
||||
| **Web Servers** | Privately hosted web content |
|
||||
| **File Sharing** | Distributed storage solutions |
|
||||
| **Name Services** | `.lthn` domain namespace |
|
||||
| **Social Networks** | Decentralized communication |
|
||||
| **Custom BYOA** | Bring Your Own Application |
|
||||
|
||||
### 12.2 Service Access
|
||||
|
||||
```
|
||||
Client ──► Exit Node ──► Internet (standard VPN)
|
||||
└──► Lethernet Services (private network)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. References
|
||||
|
||||
- RFC-0001: Lethean Network Overview
|
||||
- RFC-0002: Service Descriptor Protocol (SDP)
|
||||
- RFC-0003: Exit Node Architecture
|
||||
- RFC-0004: Payment & Dispatcher Protocol
|
||||
- WireGuard Protocol: https://www.wireguard.com/protocol/
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Full Command Options
|
||||
|
||||
```
|
||||
usage: client.py [-h] [-c CONFIG] [-l {DEBUG,INFO,WARNING,ERROR}]
|
||||
[--log-file LOG_FILE] [--audit-file AUDIT_FILE]
|
||||
[--http-port HTTP_PORT] [--var-dir VAR_DIR]
|
||||
[--cfg-dir CFG_DIR] [--app-dir APP_DIR]
|
||||
[--tmp-dir TMP_DIR] [--daemon-host DAEMON_HOST]
|
||||
[--daemon-bin DAEMON_BIN] [--daemon-rpc-url DAEMON_RPC_URL]
|
||||
[--daemon-p2p-port DAEMON_P2P_PORT]
|
||||
[--wallet-rpc-bin WALLET_RPC_BIN]
|
||||
[--wallet-cli-bin WALLET_CLI_BIN]
|
||||
[--wallet-rpc-url WALLET_RPC_URL]
|
||||
[--wallet-rpc-port WALLET_RPC_PORT]
|
||||
[--wallet-rpc-user WALLET_RPC_USER]
|
||||
[--wallet-rpc-password WALLET_RPC_PASSWORD]
|
||||
[--wallet-address WALLET_ADDRESS]
|
||||
[--spaces-dir SPACES_DIR] [--gates-dir GATES_DIR]
|
||||
[--providers-dir PROVIDERS_DIR]
|
||||
[--sessions-dir SESSIONS_DIR]
|
||||
[--coin-type {lethean}] [--coin-unit COIN_UNIT]
|
||||
[--lthn-price LTHN_PRICE]
|
||||
[--default-pay-days DEFAULT_PAY_DAYS]
|
||||
[--unpaid-expiry UNPAID_EXPIRY]
|
||||
[--use-tx-pool USE_TX_POOL]
|
||||
[--enable-wg {0,1}]
|
||||
[--run-gui {0,1}] [--run-proxy {0,1}]
|
||||
[--run-wallet {0,1}] [--run-daemon {0,1}]
|
||||
[--auto-connect AUTO_CONNECT]
|
||||
[--auto-reconnect AUTO_RECONNECT]
|
||||
[--auto-pay-days AUTO_PAY_DAYS]
|
||||
[--free-session-days FREE_SESSION_DAYS]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Connection State Machine
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ IDLE │
|
||||
└──────────┬──────────┘
|
||||
│ User selects provider
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ DISCOVERING │
|
||||
│ (fetch VDP) │
|
||||
└──────────┬──────────┘
|
||||
│ VDP received
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ UNPAID │◄────────────┐
|
||||
└──────────┬──────────┘ │
|
||||
│ Payment sent │
|
||||
▼ │
|
||||
┌─────────────────────┐ │
|
||||
│ CONFIRMING │ │
|
||||
│ (await blocks) │ │
|
||||
└──────────┬──────────┘ │
|
||||
│ Confirmed │
|
||||
▼ │
|
||||
┌─────────────────────┐ │
|
||||
│ CONNECTING │ │
|
||||
│ (transport setup) │ │
|
||||
└──────────┬──────────┘ │
|
||||
│ Connected │
|
||||
▼ │
|
||||
┌─────────────────────┐ │
|
||||
│ ACTIVE │─────────────┘
|
||||
│ (tunneled) │ Renew
|
||||
└──────────┬──────────┘
|
||||
│ Disconnect/Expire
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ DISCONNECTED │
|
||||
└─────────────────────┘
|
||||
```
|
||||
897
docs/specs/RFC-001-HLCRF-COMPOSITOR.md
Normal file
|
|
@ -0,0 +1,897 @@
|
|||
# RFC: HLCRF Compositor
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-01-15
|
||||
**Authors:** Host UK Engineering
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
The HLCRF Compositor is a hierarchical layout system where each composite contains up to five regions—Header, Left, Content, Right, and Footer. Composites nest infinitely: any region can contain another composite, which can contain another, and so on.
|
||||
|
||||
The core innovation is **inline sub-structure declaration**: a single string like `H[LC]C[HCF]F` declares the entire nested hierarchy. No configuration files, no database schema, no separate definitions—parse the string and you have the complete structure.
|
||||
|
||||
Just as Markdown made document formatting a human-readable string, HLCRF makes layout structure a portable, self-describing data type that can be stored, transmitted, validated, and rendered anywhere.
|
||||
|
||||
Path-based element IDs (`L-H-0`, `C-F-C-2`) encode the full hierarchy, eliminating database lookups to resolve structure. The system supports responsive breakpoints, block-based content, and shortcode integration.
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Traditional layout systems require separate templates for each layout variation. A page with a left sidebar needs one template; without it, another. Add responsive behaviour, and the combinations multiply quickly.
|
||||
|
||||
The HLCRF Compositor addresses this through:
|
||||
|
||||
1. **Data-driven layouts** — A single compositor handles all layout permutations via variant strings
|
||||
2. **Nested composition** — Layouts can contain other layouts, with automatic path tracking for unique identification
|
||||
3. **Responsive design** — Breakpoint-aware rendering collapses regions appropriately for different devices
|
||||
4. **Block-based content** — Content populates regions as discrete blocks, enabling conditional display and reordering
|
||||
|
||||
The approach treats layout as data rather than markup, allowing the same content to adapt to different structural requirements without template duplication.
|
||||
|
||||
---
|
||||
|
||||
## Terminology
|
||||
|
||||
### HLCRF
|
||||
|
||||
**H**ierarchical **L**ayer **C**ompositing **R**ender **F**rame.
|
||||
|
||||
The acronym also serves as a mnemonic for the five possible regions:
|
||||
|
||||
| Letter | Region | Semantic element | Purpose |
|
||||
|--------|--------|------------------|---------|
|
||||
| **H** | Header | `<header>` | Top navigation, branding |
|
||||
| **L** | Left | `<aside>` | Left sidebar, secondary navigation |
|
||||
| **C** | Content | `<main>` | Primary content area |
|
||||
| **R** | Right | `<aside>` | Right sidebar, supplementary content |
|
||||
| **F** | Footer | `<footer>` | Site footer, links, legal |
|
||||
|
||||
### Variant string
|
||||
|
||||
A string of 1–5 characters from the set `{H, L, C, R, F}` that defines which regions are active. The string `HCF` produces a layout with Header, Content, and Footer. The string `HLCRF` enables all five regions.
|
||||
|
||||
A flat variant like `HLCRF` renders as:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ H │ ← Header
|
||||
├─────────┬───────────────┬───────────┤
|
||||
│ L │ C │ R │ ← Body row
|
||||
├─────────┴───────────────┴───────────┤
|
||||
│ F │ ← Footer
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
A nested variant like `H[LCR]CF` renders differently—the body row is **inside** the Header:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ H ┌─────────┬─────────┬───────────┐ │
|
||||
│ │ H-L │ H-C │ H-R │ │ ← Body row nested IN Header
|
||||
│ └─────────┴─────────┴───────────┘ │
|
||||
├─────────────────────────────────────┤
|
||||
│ C │ ← Root Content
|
||||
├─────────────────────────────────────┤
|
||||
│ F │ ← Root Footer
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
With blocks placed, element IDs become addresses. A typical website header `H[LCR]CF`:
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ H ┌───────────┬───────────────────────────────┬─────────────┐ │
|
||||
│ │ H-L-0 │ H-C-0 H-C-1 H-C-2 H-C-3│ H-R-0 │ │
|
||||
│ │ [Logo] │ [Home] [About] [Blog] [Shop]│ [Login] │ │
|
||||
│ └───────────┴───────────────────────────────┴─────────────┘ │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ C-0 │
|
||||
│ [Page Content] │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ F-0 F-1 │
|
||||
│ [© 2026] [Legal] │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Every element has a unique, deterministic address. Computers count from zero—deal with it.
|
||||
|
||||
**Key principle:** What's missing defines the layout type. Brackets define nesting.
|
||||
|
||||
### Path
|
||||
|
||||
A hierarchical identifier tracking a layout's position within nested structures. The root layout has an empty path. A layout nested within the Left region of the root receives path `L-`. Further nesting appends to this path.
|
||||
|
||||
### Slot
|
||||
|
||||
A named region within a layout that accepts content. Each slot corresponds to one HLCRF letter.
|
||||
|
||||
### Block
|
||||
|
||||
A discrete unit of content assigned to a region. Blocks have their own ordering and can conditionally display based on breakpoint or other conditions.
|
||||
|
||||
### Breakpoint
|
||||
|
||||
A device category determining layout behaviour:
|
||||
|
||||
| Breakpoint | Target | Typical behaviour |
|
||||
|------------|--------|-------------------|
|
||||
| `phone` | < 768px | Single column, stacked |
|
||||
| `tablet` | 768px–1023px | Content only, sidebars hidden |
|
||||
| `desktop` | ≥ 1024px | Full layout with all regions |
|
||||
|
||||
---
|
||||
|
||||
## Specification
|
||||
|
||||
### Layout variant strings
|
||||
|
||||
#### Valid variants
|
||||
|
||||
Any combination of the letters H, L, C, R, F, in that order. Common variants:
|
||||
|
||||
| Variant | Description | Use case |
|
||||
|---------|-------------|----------|
|
||||
| `C` | Content only | Embedded widgets, minimal layouts |
|
||||
| `HCF` | Header, Content, Footer | Standard page layout |
|
||||
| `HCR` | Header, Content, Right | Dashboard with right sidebar |
|
||||
| `HLC` | Header, Left, Content | Admin panel with navigation |
|
||||
| `HLCF` | Header, Left, Content, Footer | Admin with footer |
|
||||
| `HLCR` | Header, Left, Content, Right | Three-column dashboard |
|
||||
| `HLCRF` | All regions | Full-featured layouts |
|
||||
|
||||
The variant string is case-insensitive. The compositor normalises to uppercase.
|
||||
|
||||
#### Inline sub-structure declaration
|
||||
|
||||
Variant strings support **inline nesting** using bracket notation. Each region letter can be followed by brackets containing its nested layout:
|
||||
|
||||
```
|
||||
H[LC]L[HC]C[HCF]F[LCF]
|
||||
```
|
||||
|
||||
This declares the entire hierarchy in a single string:
|
||||
|
||||
| Segment | Meaning |
|
||||
|---------|---------|
|
||||
| `H[LC]` | Header region contains a Left-Content layout |
|
||||
| `L[HC]` | Left region contains a Header-Content layout |
|
||||
| `C[HCF]` | Content region contains a Header-Content-Footer layout |
|
||||
| `F[LCF]` | Footer region contains a Left-Content-Footer layout |
|
||||
|
||||
Brackets nest recursively. A complex declaration like `H[L[C]C]CF` means:
|
||||
- Header contains a nested layout
|
||||
- That nested layout's Left region contains yet another layout (Content-only)
|
||||
- Root also has Content and Footer at the top level
|
||||
|
||||
This syntax is particularly useful for:
|
||||
- **Shortcodes** declaring their expected structure
|
||||
- **Templates** defining reusable page scaffolds
|
||||
- **Configuration** specifying layout contracts
|
||||
|
||||
The string `H[LC]L[HC]C[HCF]F[LCF]` is a complete website declaration—no additional nesting configuration needed.
|
||||
|
||||
#### Region requirements
|
||||
|
||||
- **Content (C)** is implicitly included when any body region (L, C, R) is present
|
||||
- Regions render only when the variant includes them AND content has been added
|
||||
- An empty region does not render, even if specified in the variant
|
||||
|
||||
### Region hierarchy
|
||||
|
||||
The compositor enforces a fixed spatial hierarchy:
|
||||
|
||||
```
|
||||
Row 1: Header (full width)
|
||||
Row 2: Left | Content | Right (body row)
|
||||
Row 3: Footer (full width)
|
||||
```
|
||||
|
||||
This structure maps to CSS Grid areas:
|
||||
|
||||
```css
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"body"
|
||||
"footer";
|
||||
```
|
||||
|
||||
The body row uses a nested grid or flexbox for the three-column layout.
|
||||
|
||||
### Nesting and path context
|
||||
|
||||
Layouts can be nested within any region. The compositor automatically manages path context to ensure unique slot identifiers.
|
||||
|
||||
#### Path generation
|
||||
|
||||
When a layout renders, it assigns each slot an ID based on its path:
|
||||
|
||||
- Root layout, Header slot: `H`
|
||||
- Root layout, Left slot: `L`
|
||||
- Nested layout within Left, Header slot: `L-H`
|
||||
- Nested layout within Left, Content slot: `L-C`
|
||||
- Further nested within that Content slot: `L-C-C`
|
||||
|
||||
#### Block identifiers
|
||||
|
||||
Within each slot, blocks receive indexed identifiers:
|
||||
|
||||
- First block in Header: `H-0`
|
||||
- Second block in Header: `H-1`
|
||||
- First block in nested Content: `L-C-0`
|
||||
|
||||
This scheme enables precise targeting for styling, JavaScript, and debugging.
|
||||
|
||||
### Responsive breakpoints
|
||||
|
||||
The compositor supports breakpoint-specific layout variants. A page might use `HLCRF` on desktop but collapse to `HCF` on tablet and `C` on phone.
|
||||
|
||||
#### Configuration schema
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_config": {
|
||||
"layout_type": {
|
||||
"desktop": "HLCRF",
|
||||
"tablet": "HCF",
|
||||
"phone": "CF"
|
||||
},
|
||||
"regions": {
|
||||
"desktop": {
|
||||
"left": { "width": 280 },
|
||||
"content": { "max_width": 680 },
|
||||
"right": { "width": 280 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CSS breakpoint handling
|
||||
|
||||
The default CSS collapses sidebars at tablet breakpoint and stacks content at phone breakpoint:
|
||||
|
||||
```css
|
||||
/* Tablet: Hide sidebars */
|
||||
@media (max-width: 1023px) {
|
||||
.hlcrf-body {
|
||||
grid-template-columns: minmax(0, var(--content-max-width));
|
||||
grid-template-areas: "content";
|
||||
}
|
||||
.hlcrf-left, .hlcrf-right { display: none; }
|
||||
}
|
||||
|
||||
/* Phone: Full width, stacked */
|
||||
@media (max-width: 767px) {
|
||||
.hlcrf-body {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Block visibility
|
||||
|
||||
Blocks can define per-breakpoint visibility:
|
||||
|
||||
```json
|
||||
{
|
||||
"breakpoint_visibility": {
|
||||
"desktop": true,
|
||||
"tablet": true,
|
||||
"phone": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A block with `phone: false` does not render on mobile devices, regardless of which region it belongs to.
|
||||
|
||||
### Deep nesting
|
||||
|
||||
The HLCRF system is **infinitely nestable**. Any region can contain another complete HLCRF layout, which can itself contain further nested layouts. The path-based ID scheme ensures every element remains uniquely addressable regardless of nesting depth.
|
||||
|
||||
#### Path reading convention
|
||||
|
||||
Paths read left-to-right, describing the journey from root to element:
|
||||
|
||||
```
|
||||
L-H-0
|
||||
│ │ └─ Block index (first block)
|
||||
│ └─── Region in nested layout (Header)
|
||||
└───── Region in root layout (Left)
|
||||
```
|
||||
|
||||
This means: "The first block in the Header region of a layout nested within the Left region of the root."
|
||||
|
||||
#### Multi-level path construction
|
||||
|
||||
Paths concatenate as layouts nest. Consider this structure:
|
||||
|
||||
- Root layout: `HLCRF`
|
||||
- Nested in Content: another `HCF` layout
|
||||
- Nested in that layout's Footer: a `C`-only layout with a button block
|
||||
|
||||
The button receives the path: `C-F-C-0`
|
||||
|
||||
Reading left to right:
|
||||
1. `C` — Content region of root
|
||||
2. `F` — Footer region of nested layout
|
||||
3. `C` — Content region of deepest layout
|
||||
4. `0` — First block in that region
|
||||
|
||||
#### Three-level nesting example
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ H (root header) │
|
||||
├────────┬────────────────────────────────────┬───────────┤
|
||||
│ L │ C │ R │
|
||||
│ │ ┌───────────────────────────────┐ │ │
|
||||
│ │ │ C-H (nested header) │ │ │
|
||||
│ │ ├─────┬─────────────┬───────────┤ │ │
|
||||
│ │ │ C-L │ C-C │ C-R │ │ │
|
||||
│ │ │ │ ┌─────────┐ │ │ │ │
|
||||
│ │ │ │ │ C-C-C │ │ │ │ │
|
||||
│ │ │ │ │(deepest)│ │ │ │ │
|
||||
│ │ │ │ └─────────┘ │ │ │ │
|
||||
│ │ ├─────┴─────────────┴───────────┤ │ │
|
||||
│ │ │ C-F (nested footer) │ │ │
|
||||
│ │ └───────────────────────────────┘ │ │
|
||||
├────────┴────────────────────────────────────┴───────────┤
|
||||
│ F (root footer) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
In this diagram:
|
||||
- Root regions: `H`, `L`, `C`, `R`, `F`
|
||||
- Second level (nested in C): `C-H`, `C-L`, `C-C`, `C-R`, `C-F`
|
||||
- Third level (nested in C-C): `C-C-C`
|
||||
|
||||
A block placed in the deepest Content region would receive ID `C-C-C-0`.
|
||||
|
||||
#### Path examples at each nesting level
|
||||
|
||||
| Nesting depth | Example path | Meaning |
|
||||
|---------------|--------------|---------|
|
||||
| 1 (root) | `H-0` | First block in root Header |
|
||||
| 1 (root) | `L-2` | Third block in root Left sidebar |
|
||||
| 2 (nested) | `L-H-0` | First block in Header of layout nested in Left |
|
||||
| 2 (nested) | `C-C-1` | Second block in Content of layout nested in Content |
|
||||
| 3 (deep) | `L-C-H-0` | First block in Header of layout nested in Content, nested in Left |
|
||||
| 4+ | `C-L-C-R-0` | Paths continue indefinitely |
|
||||
|
||||
The path length equals the nesting depth plus one (for the block index).
|
||||
|
||||
#### Practical example: sidebar with nested layout
|
||||
|
||||
```php
|
||||
$sidebar = Layout::make('HCF')
|
||||
->h('<h3>Widget Panel</h3>')
|
||||
->c(view('widgets.list'))
|
||||
->f('<a href="#">Manage widgets</a>');
|
||||
|
||||
$page = Layout::make('HLCRF')
|
||||
->h(view('header'))
|
||||
->l($sidebar) // Nested layout in Left
|
||||
->c(view('main-content'))
|
||||
->f(view('footer'));
|
||||
```
|
||||
|
||||
The sidebar's regions receive paths:
|
||||
- Header: `L-H`
|
||||
- Content: `L-C`
|
||||
- Footer: `L-F`
|
||||
|
||||
Blocks within the sidebar's Content would be `L-C-0`, `L-C-1`, etc.
|
||||
|
||||
#### Why infinite nesting matters
|
||||
|
||||
Deep nesting enables:
|
||||
|
||||
1. **Component encapsulation** — A reusable component can define its own internal layout without knowing where it will be placed
|
||||
2. **Recursive structures** — Tree views, nested comments, or hierarchical navigation can use consistent layout patterns at each level
|
||||
3. **Micro-layouts** — Small UI sections (cards, panels, modals) can use HLCRF internally whilst remaining composable
|
||||
|
||||
---
|
||||
|
||||
## API reference
|
||||
|
||||
### `Layout` class
|
||||
|
||||
**Namespace:** `Core\Front\Components`
|
||||
|
||||
#### Factory method
|
||||
|
||||
```php
|
||||
Layout::make(string $variant = 'HCF', string $path = ''): static
|
||||
```
|
||||
|
||||
Creates a new layout instance.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `$variant` | string | `'HCF'` | Layout variant string |
|
||||
| `$path` | string | `''` | Hierarchical path (typically managed automatically) |
|
||||
|
||||
#### Slot methods
|
||||
|
||||
Each region has a variadic method accepting any renderable content:
|
||||
|
||||
```php
|
||||
public function h(mixed ...$items): static // Header
|
||||
public function l(mixed ...$items): static // Left
|
||||
public function c(mixed ...$items): static // Content
|
||||
public function r(mixed ...$items): static // Right
|
||||
public function f(mixed ...$items): static // Footer
|
||||
```
|
||||
|
||||
Alias methods provide readability for explicit code:
|
||||
|
||||
```php
|
||||
public function addHeader(mixed ...$items): static
|
||||
public function addLeft(mixed ...$items): static
|
||||
public function addContent(mixed ...$items): static
|
||||
public function addRight(mixed ...$items): static
|
||||
public function addFooter(mixed ...$items): static
|
||||
```
|
||||
|
||||
#### Content types
|
||||
|
||||
Slot methods accept:
|
||||
|
||||
- **Strings** — Raw HTML or text
|
||||
- **`Htmlable`** — Objects implementing `toHtml()`
|
||||
- **`Renderable`** — Objects implementing `render()`
|
||||
- **`View`** — Laravel view instances
|
||||
- **`Layout`** — Nested layout instances (path context injected automatically)
|
||||
- **Callables** — Functions returning any of the above
|
||||
|
||||
#### Attribute methods
|
||||
|
||||
```php
|
||||
public function attributes(array $attributes): static
|
||||
```
|
||||
|
||||
Merge HTML attributes onto the layout container.
|
||||
|
||||
```php
|
||||
public function class(string $class): static
|
||||
```
|
||||
|
||||
Append a CSS class to the container.
|
||||
|
||||
#### Rendering
|
||||
|
||||
```php
|
||||
public function render(): string
|
||||
public function toHtml(): string
|
||||
public function __toString(): string
|
||||
```
|
||||
|
||||
All three methods return the compiled HTML. The class implements `Htmlable` and `Renderable` for framework integration.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic page layout
|
||||
|
||||
```php
|
||||
use Core\Front\Components\Layout;
|
||||
|
||||
$page = Layout::make('HCF')
|
||||
->h(view('components.header'))
|
||||
->c('<article>Page content here</article>')
|
||||
->f(view('components.footer'));
|
||||
|
||||
echo $page;
|
||||
```
|
||||
|
||||
### Admin dashboard with sidebar
|
||||
|
||||
```php
|
||||
$dashboard = Layout::make('HLCF')
|
||||
->class('min-h-screen bg-gray-100')
|
||||
->h(view('admin.header'))
|
||||
->l(view('admin.sidebar'))
|
||||
->c($content)
|
||||
->f(view('admin.footer'));
|
||||
```
|
||||
|
||||
### Nested layouts
|
||||
|
||||
```php
|
||||
// Outer layout with left sidebar
|
||||
$outer = Layout::make('HLC')
|
||||
->h('<nav>Main Navigation</nav>')
|
||||
->l('<aside>Sidebar</aside>')
|
||||
->c(
|
||||
// Inner layout nested in content area
|
||||
Layout::make('HCF')
|
||||
->h('<h1>Section Title</h1>')
|
||||
->c('<div>Inner content</div>')
|
||||
->f('<p>Section footer</p>')
|
||||
);
|
||||
```
|
||||
|
||||
The inner layout receives path context `C-`, so its slots become `C-H`, `C-C`, and `C-F`.
|
||||
|
||||
### Multiple blocks per region
|
||||
|
||||
```php
|
||||
$page = Layout::make('HLCF')
|
||||
->h(view('header.logo'), view('header.navigation'), view('header.search'))
|
||||
->l(view('sidebar.menu'), view('sidebar.widgets'))
|
||||
->c(view('content.hero'), view('content.features'), view('content.cta'))
|
||||
->f(view('footer.links'), view('footer.legal'));
|
||||
```
|
||||
|
||||
Each item becomes a separate block with a unique identifier.
|
||||
|
||||
### Responsive rendering
|
||||
|
||||
```php
|
||||
// In a service or controller
|
||||
$breakpoint = $this->detectBreakpoint($request);
|
||||
$layoutType = $page->getLayoutTypeFor($breakpoint);
|
||||
|
||||
$layout = Layout::make($layoutType)
|
||||
->class('bio-page')
|
||||
->h($headerBlocks)
|
||||
->c($contentBlocks)
|
||||
->f($footerBlocks);
|
||||
|
||||
// Sidebars only added on desktop
|
||||
if ($breakpoint === 'desktop') {
|
||||
$layout->l($leftBlocks)->r($rightBlocks);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation notes
|
||||
|
||||
### CSS Grid structure
|
||||
|
||||
The compositor generates a grid-based structure:
|
||||
|
||||
```html
|
||||
<div class="hlcrf-layout" data-layout="root">
|
||||
<header class="hlcrf-header" data-slot="H">...</header>
|
||||
<div class="hlcrf-body flex flex-1">
|
||||
<aside class="hlcrf-left shrink-0" data-slot="L">...</aside>
|
||||
<main class="hlcrf-content flex-1" data-slot="C">...</main>
|
||||
<aside class="hlcrf-right shrink-0" data-slot="R">...</aside>
|
||||
</div>
|
||||
<footer class="hlcrf-footer" data-slot="F">...</footer>
|
||||
</div>
|
||||
```
|
||||
|
||||
The base CSS uses CSS Grid for the outer structure and Flexbox for the body row.
|
||||
|
||||
### Semantic HTML
|
||||
|
||||
The compositor uses appropriate semantic elements:
|
||||
|
||||
- `<header>` for the Header region
|
||||
- `<aside>` for Left and Right sidebars
|
||||
- `<main>` for the Content region
|
||||
- `<footer>` for the Footer region
|
||||
|
||||
This provides accessibility benefits and proper document outline.
|
||||
|
||||
### Accessibility considerations
|
||||
|
||||
- **Landmark regions** — Semantic elements create implicit ARIA landmarks
|
||||
- **Skip links** — Consider adding skip-to-content links in the Header
|
||||
- **Focus management** — Nested layouts maintain sensible tab order
|
||||
- **Screen reader compatibility** — Block `data-block` attributes aid debugging but do not affect accessibility tree
|
||||
|
||||
### Data attributes
|
||||
|
||||
The compositor adds data attributes for debugging and JavaScript integration:
|
||||
|
||||
| Attribute | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| `data-layout` | Container | Layout path identifier (`root`, `L-`, etc.) |
|
||||
| `data-slot` | Region | Slot identifier (`H`, `L-C`, etc.) |
|
||||
| `data-block` | Block wrapper | Block identifier (`H-0`, `L-C-2`, etc.) |
|
||||
|
||||
### Database schema
|
||||
|
||||
For persisted layouts, the schema includes:
|
||||
|
||||
**Pages/Biolinks table:**
|
||||
- `layout_config` (JSON) — Layout type per breakpoint, region dimensions
|
||||
|
||||
**Blocks table:**
|
||||
- `region` (string) — Target region: `header`, `left`, `content`, `right`, `footer`
|
||||
- `region_order` (integer) — Sort order within region
|
||||
- `breakpoint_visibility` (JSON) — Per-breakpoint visibility flags
|
||||
|
||||
### Theme integration
|
||||
|
||||
The renderer can generate CSS custom properties for theming:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--biolink-bg: #f9fafb;
|
||||
--biolink-text: #111827;
|
||||
--biolink-font: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
These integrate with the compositor's class-based styling.
|
||||
|
||||
---
|
||||
|
||||
## Integration patterns
|
||||
|
||||
This section describes how the HLCRF system integrates with common web development patterns and technologies.
|
||||
|
||||
### CSS box model parallel
|
||||
|
||||
HLCRF mirrors the CSS box model conceptually, which aids developer intuition:
|
||||
|
||||
```
|
||||
CSS Box Model HLCRF Layout
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ margin │ │ H │ ← Block-level, full-width
|
||||
├──────────────┤ ├──────────────┤
|
||||
│ │ padding │ │ │ L │ C │ R │ ← Content row with "sidebars"
|
||||
├──────────────┤ ├──────────────┤
|
||||
│ margin │ │ F │ ← Block-level, full-width
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
The mapping:
|
||||
- **H/F** behave like block-level elements spanning the full width, similar to how top/bottom margins frame content
|
||||
- **L/R** act as the "padding" on either side of the content, creating gutters or sidebars
|
||||
- **C** is the content itself—the innermost box
|
||||
|
||||
This mental model helps developers predict layout behaviour:
|
||||
- Adding `L` or `R` is like adding horizontal padding
|
||||
- Adding `H` or `F` is like adding vertical margins
|
||||
- The `[LCR]` row always forms the content layer, with `C` as the primary content area
|
||||
|
||||
When nesting layouts, the analogy extends recursively—a nested layout's `H/F` become block-level elements within their parent region, and its `[LCR]` row subdivides that space further.
|
||||
|
||||
### Shortcode structure definitions
|
||||
|
||||
HLCRF enables shortcodes to define complete structural layouts through their variant string. The variant becomes a **structural contract** that the shortcode guarantees to fulfil.
|
||||
|
||||
#### Example: Hero shortcode
|
||||
|
||||
```php
|
||||
// Shortcode definition
|
||||
class HeroShortcode extends Shortcode
|
||||
{
|
||||
public string $layout = 'HCF';
|
||||
|
||||
public function render(): Layout
|
||||
{
|
||||
return Layout::make($this->layout)
|
||||
->h($this->renderTitle()) // H: Title region
|
||||
->c($this->renderContent()) // C: Main hero content
|
||||
->f($this->renderCta()); // F: Call-to-action region
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage in content:
|
||||
|
||||
```
|
||||
[hero layout="HCF" title="Welcome" cta="Get Started"]
|
||||
Your hero content here.
|
||||
[/hero]
|
||||
```
|
||||
|
||||
The shortcode author declares which regions exist; content authors populate them. The variant string serves as documentation and constraint simultaneously.
|
||||
|
||||
#### Variant as capability declaration
|
||||
|
||||
Different shortcode variants expose different capabilities:
|
||||
|
||||
| Shortcode | Variant | Regions | Purpose |
|
||||
|-----------|---------|---------|---------|
|
||||
| `[hero]` | `HCF` | Title, Content, CTA | Landing page hero |
|
||||
| `[sidebar-panel]` | `HLC` | Title, Actions, Content | Dashboard widget |
|
||||
| `[card]` | `HCF` | Header, Body, Footer | Content card |
|
||||
| `[split]` | `LCR` | Left, Centre, Right | Comparison layout |
|
||||
|
||||
#### Nested shortcode structures
|
||||
|
||||
Shortcodes can nest within each other, inheriting the path context:
|
||||
|
||||
```
|
||||
[dashboard layout="HLCF"]
|
||||
[widget layout="HCF" slot="L"]
|
||||
Widget content here
|
||||
[/widget]
|
||||
Main dashboard content
|
||||
[/dashboard]
|
||||
```
|
||||
|
||||
The widget's regions receive paths `L-H`, `L-C`, `L-F` because it renders within the dashboard's Left region. This happens automatically—shortcode authors need not manage paths manually.
|
||||
|
||||
### HTML5 slots integration
|
||||
|
||||
The path-based ID system integrates naturally with HTML5 `<slot>` elements, enabling Web Components to define HLCRF structures.
|
||||
|
||||
#### Slot element mapping
|
||||
|
||||
```html
|
||||
<template id="hlcrf-component">
|
||||
<div class="hlcrf-layout">
|
||||
<header data-slot="H">
|
||||
<slot name="H"></slot>
|
||||
</header>
|
||||
<div class="hlcrf-body">
|
||||
<aside data-slot="L">
|
||||
<slot name="L"></slot>
|
||||
</aside>
|
||||
<main data-slot="C">
|
||||
<slot name="C"></slot>
|
||||
</main>
|
||||
<aside data-slot="R">
|
||||
<slot name="R"></slot>
|
||||
</aside>
|
||||
</div>
|
||||
<footer data-slot="F">
|
||||
<slot name="F"></slot>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### Nested slot paths
|
||||
|
||||
For nested layouts, slot names follow the path convention:
|
||||
|
||||
```html
|
||||
<div data-slot="L-C">
|
||||
<slot name="L-C"></slot>
|
||||
<!-- Content injected into nested layout's Content region -->
|
||||
</div>
|
||||
```
|
||||
|
||||
The `data-slot` attribute and slot `name` always match, enabling both CSS targeting and content projection:
|
||||
|
||||
```html
|
||||
<!-- Nested layout within the Left region -->
|
||||
<aside data-slot="L">
|
||||
<div class="hlcrf-layout" data-layout="L-">
|
||||
<header data-slot="L-H">
|
||||
<slot name="L-H"></slot>
|
||||
</header>
|
||||
<main data-slot="L-C">
|
||||
<slot name="L-C"></slot>
|
||||
</main>
|
||||
<footer data-slot="L-F">
|
||||
<slot name="L-F"></slot>
|
||||
</footer>
|
||||
</div>
|
||||
</aside>
|
||||
```
|
||||
|
||||
Content authors inject into specific nested regions using the slot attribute:
|
||||
|
||||
```html
|
||||
<my-layout-component>
|
||||
<h1 slot="H">Page Title</h1>
|
||||
<nav slot="L-H">Sidebar Navigation</nav>
|
||||
<div slot="L-C">Sidebar Content</div>
|
||||
<article slot="C">Main Content</article>
|
||||
</my-layout-component>
|
||||
```
|
||||
|
||||
#### Progressive enhancement
|
||||
|
||||
Slots enable progressive enhancement patterns:
|
||||
|
||||
1. **Server-rendered baseline** — PHP compositor renders complete HTML
|
||||
2. **Client enhancement** — JavaScript can relocate content between slots
|
||||
3. **Framework agnostic** — Works with vanilla JS, Alpine, Vue, or React
|
||||
|
||||
```html
|
||||
<!-- Server-rendered -->
|
||||
<main data-slot="C">
|
||||
<article>Content here</article>
|
||||
</main>
|
||||
|
||||
<!-- JavaScript enhancement -->
|
||||
<script>
|
||||
// Move content to different region based on viewport
|
||||
const content = document.querySelector('[data-slot="C"] article');
|
||||
if (viewport.isMobile) {
|
||||
document.querySelector('[data-slot="L"]').appendChild(content);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Alpine.js integration
|
||||
|
||||
The compositor's data attributes work naturally with Alpine.js:
|
||||
|
||||
```html
|
||||
<div class="hlcrf-layout" x-data="{ activeRegion: 'C' }">
|
||||
<aside data-slot="L" x-show="activeRegion === 'L' || $screen('lg')">
|
||||
<!-- Sidebar content -->
|
||||
</aside>
|
||||
<main data-slot="C" @click="activeRegion = 'C'">
|
||||
<!-- Main content -->
|
||||
</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Livewire component boundaries
|
||||
|
||||
HLCRF regions can serve as Livewire component boundaries:
|
||||
|
||||
```php
|
||||
$layout = Layout::make('HLCF')
|
||||
->h(livewire('header-nav'))
|
||||
->l(livewire('sidebar-menu'))
|
||||
->c(livewire('main-content'))
|
||||
->f(livewire('footer-links'));
|
||||
```
|
||||
|
||||
Each region becomes an independent Livewire component with its own state and lifecycle.
|
||||
|
||||
### Path-based event targeting
|
||||
|
||||
The hierarchical path system enables precise event targeting:
|
||||
|
||||
```javascript
|
||||
// Listen for events in a specific nested region
|
||||
document.querySelector('[data-slot="L-C"]')
|
||||
.addEventListener('block:added', (e) => {
|
||||
console.log(`Block added to left sidebar content: ${e.detail.blockId}`);
|
||||
});
|
||||
|
||||
// Broadcast to all blocks in a path
|
||||
function notifyRegion(path, event) {
|
||||
document.querySelectorAll(`[data-slot^="${path}"]`)
|
||||
.forEach(el => el.dispatchEvent(new CustomEvent(event)));
|
||||
}
|
||||
```
|
||||
|
||||
### Server-side rendering integration
|
||||
|
||||
The compositor works with SSR frameworks:
|
||||
|
||||
```php
|
||||
// Inertia.js integration
|
||||
return Inertia::render('Dashboard', [
|
||||
'layout' => [
|
||||
'variant' => 'HLCF',
|
||||
'regions' => [
|
||||
'H' => $headerData,
|
||||
'L' => $sidebarData,
|
||||
'C' => $contentData,
|
||||
'F' => $footerData,
|
||||
],
|
||||
],
|
||||
]);
|
||||
```
|
||||
|
||||
The frontend receives structured data and renders using the same HLCRF conventions.
|
||||
|
||||
---
|
||||
|
||||
## Related files
|
||||
|
||||
- `app/Core/Front/Components/Layout.php` — Core compositor class
|
||||
- `app/Core/Front/Components/View/Blade/layout.blade.php` — Blade component variant
|
||||
- `app/Mod/Bio/Services/HlcrfRenderer.php` — Bio page rendering service
|
||||
- `app/Mod/Bio/Migrations/2026_01_14_100000_add_hlcrf_support.php` — Database schema
|
||||
|
||||
---
|
||||
|
||||
## Version history
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2026-01-15 | Initial RFC |
|
||||
423
docs/specs/RFC-002-EVENT-DRIVEN-MODULES.md
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
# RFC: Event-Driven Module Loading
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-01-15
|
||||
**Authors:** Host UK Engineering
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
The Event-Driven Module Loading system enables lazy instantiation of modules based on lifecycle events. Instead of eagerly booting all modules at application startup, modules declare interest in specific events via static `$listens` arrays. The module is only instantiated when its events fire.
|
||||
|
||||
This provides:
|
||||
- Faster boot times (only load what's needed)
|
||||
- Context-aware loading (CLI gets CLI modules, web gets web modules)
|
||||
- Clean separation between infrastructure and modules
|
||||
- Testable event-based architecture
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Application Bootstrap │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ LifecycleEventProvider │
|
||||
│ └── ModuleRegistry │
|
||||
│ └── ModuleScanner (reads $listens via reflection) │
|
||||
│ └── LazyModuleListener (defers instantiation) │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Frontages (fire events) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Front/Web/Boot ──────────▶ WebRoutesRegistering │
|
||||
│ Front/Admin/Boot ────────▶ AdminPanelBooting │
|
||||
│ Front/Api/Boot ──────────▶ ApiRoutesRegistering │
|
||||
│ Front/Cli/Boot ──────────▶ ConsoleBooting │
|
||||
│ Mcp/Server ──────────────▶ McpToolsRegistering │
|
||||
│ Queue Worker ────────────▶ QueueWorkerBooting │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### ModuleScanner
|
||||
|
||||
Reads Boot.php files and extracts `$listens` arrays via reflection without instantiating the modules.
|
||||
|
||||
```php
|
||||
namespace Core;
|
||||
|
||||
class ModuleScanner
|
||||
{
|
||||
public function scan(array $paths): array
|
||||
{
|
||||
// Returns: [EventClass => [ModuleClass => 'methodName']]
|
||||
}
|
||||
|
||||
public function extractListens(string $class): array
|
||||
{
|
||||
// Uses ReflectionClass to read static $listens property
|
||||
// Returns empty array if missing/invalid
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ModuleRegistry
|
||||
|
||||
Wires up lazy listeners for all scanned modules.
|
||||
|
||||
```php
|
||||
namespace Core;
|
||||
|
||||
class ModuleRegistry
|
||||
{
|
||||
public function register(array $paths): void
|
||||
{
|
||||
$mappings = $this->scanner->scan($paths);
|
||||
|
||||
foreach ($mappings as $event => $listeners) {
|
||||
foreach ($listeners as $moduleClass => $method) {
|
||||
Event::listen($event, new LazyModuleListener($moduleClass, $method));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### LazyModuleListener
|
||||
|
||||
Defers module instantiation until the event fires.
|
||||
|
||||
```php
|
||||
namespace Core;
|
||||
|
||||
class LazyModuleListener
|
||||
{
|
||||
public function __invoke(object $event): void
|
||||
{
|
||||
$module = $this->resolveModule();
|
||||
$module->{$this->method}($event);
|
||||
}
|
||||
|
||||
private function resolveModule(): object
|
||||
{
|
||||
// Handles ServiceProvider subclasses correctly
|
||||
if (is_subclass_of($this->moduleClass, ServiceProvider::class)) {
|
||||
return app()->resolveProvider($this->moduleClass);
|
||||
}
|
||||
return app()->make($this->moduleClass);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### LifecycleEvent Base Class
|
||||
|
||||
Events collect requests from modules without immediately applying them.
|
||||
|
||||
```php
|
||||
namespace Core\Events;
|
||||
|
||||
abstract class LifecycleEvent
|
||||
{
|
||||
public function routes(callable $callback): void;
|
||||
public function views(string $namespace, string $path): void;
|
||||
public function livewire(string $alias, string $class): void;
|
||||
public function command(string $class): void;
|
||||
public function middleware(string $alias, string $class): void;
|
||||
public function navigation(array $item): void;
|
||||
public function translations(string $namespace, string $path): void;
|
||||
public function policy(string $model, string $policy): void;
|
||||
|
||||
// Getters for processing
|
||||
public function routeRequests(): array;
|
||||
public function viewRequests(): array;
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Events
|
||||
|
||||
| Event | Context | Fired By |
|
||||
|-------|---------|----------|
|
||||
| `AdminPanelBooting` | Admin panel requests | `Front\Admin\Boot` |
|
||||
| `WebRoutesRegistering` | Web requests | `Front\Web\Boot` |
|
||||
| `ApiRoutesRegistering` | API requests | `Front\Api\Boot` |
|
||||
| `ConsoleBooting` | CLI commands | `Front\Cli\Boot` |
|
||||
| `McpToolsRegistering` | MCP server | Mcp module |
|
||||
| `QueueWorkerBooting` | Queue workers | `LifecycleEventProvider` |
|
||||
| `FrameworkBooted` | All contexts (post-boot) | `LifecycleEventProvider` |
|
||||
| `MediaRequested` | Media serving | Core media handler |
|
||||
| `SearchRequested` | Search operations | Core search handler |
|
||||
| `MailSending` | Mail dispatch | Core mail handler |
|
||||
|
||||
---
|
||||
|
||||
## Module Implementation
|
||||
|
||||
### Declaring Listeners
|
||||
|
||||
Modules declare interest in events via the static `$listens` property:
|
||||
|
||||
```php
|
||||
namespace Mod\Commerce;
|
||||
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Events\ConsoleBooting;
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
|
||||
class Boot extends ServiceProvider
|
||||
{
|
||||
public static array $listens = [
|
||||
AdminPanelBooting::class => 'onAdminPanel',
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
];
|
||||
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
$event->views('commerce', __DIR__.'/View/Blade');
|
||||
$event->livewire('commerce.checkout', Components\Checkout::class);
|
||||
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
|
||||
}
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('commerce', __DIR__.'/View/Blade');
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
$event->command(Commands\ProcessPayments::class);
|
||||
$event->command(Commands\SyncSubscriptions::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### What Stays in boot()
|
||||
|
||||
Some registrations must remain in the traditional `boot()` method:
|
||||
|
||||
| Registration | Reason |
|
||||
|--------------|--------|
|
||||
| `loadMigrationsFrom()` | Needed early for `artisan migrate` |
|
||||
| `AdminMenuRegistry->register()` | Uses interface pattern (AdminMenuProvider) |
|
||||
| Laravel event listeners | Standard Laravel events, not lifecycle events |
|
||||
|
||||
```php
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(__DIR__.'/Migrations');
|
||||
|
||||
// Interface-based registration
|
||||
app(AdminMenuRegistry::class)->register($this);
|
||||
|
||||
// Standard Laravel events (not lifecycle events)
|
||||
Event::listen(OrderPlaced::class, SendOrderConfirmation::class);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Processing
|
||||
|
||||
Frontages fire events and process collected requests:
|
||||
|
||||
```php
|
||||
// In Front/Web/Boot
|
||||
public static function fireWebRoutes(): void
|
||||
{
|
||||
$event = new WebRoutesRegistering;
|
||||
event($event);
|
||||
|
||||
// Process view namespaces
|
||||
foreach ($event->viewRequests() as [$namespace, $path]) {
|
||||
view()->addNamespace($namespace, $path);
|
||||
}
|
||||
|
||||
// Process Livewire components
|
||||
foreach ($event->livewireRequests() as [$alias, $class]) {
|
||||
Livewire::component($alias, $class);
|
||||
}
|
||||
|
||||
// Process routes with web middleware
|
||||
foreach ($event->routeRequests() as $callback) {
|
||||
Route::middleware('web')->group($callback);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This "collect then process" pattern ensures:
|
||||
1. Modules cannot directly mutate infrastructure
|
||||
2. Core validates and controls registration order
|
||||
3. Easy to add cross-cutting concerns (logging, validation)
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Test ModuleScanner reflection without Laravel app:
|
||||
|
||||
```php
|
||||
it('extracts $listens from a class with public static property', function () {
|
||||
$scanner = new ModuleScanner;
|
||||
$listens = $scanner->extractListens(ModuleWithListens::class);
|
||||
|
||||
expect($listens)->toBe([
|
||||
'SomeEvent' => 'handleSomeEvent',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns empty array when $listens is not public', function () {
|
||||
$scanner = new ModuleScanner;
|
||||
$listens = $scanner->extractListens(ModuleWithPrivateListens::class);
|
||||
|
||||
expect($listens)->toBe([]);
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Test real module scanning with Laravel app:
|
||||
|
||||
```php
|
||||
it('scans the Mod directory and finds modules', function () {
|
||||
$scanner = new ModuleScanner;
|
||||
$result = $scanner->scan([app_path('Mod')]);
|
||||
|
||||
expect($result)->toHaveKey(AdminPanelBooting::class);
|
||||
expect($result)->toHaveKey(WebRoutesRegistering::class);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
### Lazy Loading Benefits
|
||||
|
||||
| Context | Modules Loaded | Without Lazy Loading |
|
||||
|---------|----------------|----------------------|
|
||||
| Web request | 6-8 modules | All 16+ modules |
|
||||
| Admin request | 10-12 modules | All 16+ modules |
|
||||
| CLI command | 4-6 modules | All 16+ modules |
|
||||
| API request | 3-5 modules | All 16+ modules |
|
||||
|
||||
### Memory Impact
|
||||
|
||||
Modules not needed for the current context are never instantiated:
|
||||
- No class autoloading
|
||||
- No service binding
|
||||
- No config merging
|
||||
- No route registration
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
### Core Infrastructure
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Core/ModuleScanner.php` | Scans Boot.php files for $listens |
|
||||
| `Core/ModuleRegistry.php` | Wires up lazy listeners |
|
||||
| `Core/LazyModuleListener.php` | Defers module instantiation |
|
||||
| `Core/LifecycleEventProvider.php` | Orchestrates scanning and events |
|
||||
| `Core/Events/LifecycleEvent.php` | Base class for all lifecycle events |
|
||||
|
||||
### Events
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Core/Events/AdminPanelBooting.php` | Admin panel context |
|
||||
| `Core/Events/WebRoutesRegistering.php` | Web context |
|
||||
| `Core/Events/ApiRoutesRegistering.php` | API context |
|
||||
| `Core/Events/ConsoleBooting.php` | CLI context |
|
||||
| `Core/Events/McpToolsRegistering.php` | MCP server context |
|
||||
| `Core/Events/QueueWorkerBooting.php` | Queue worker context |
|
||||
| `Core/Events/FrameworkBooted.php` | Post-boot event |
|
||||
|
||||
### Frontages
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Core/Front/Web/Boot.php` | Fires WebRoutesRegistering |
|
||||
| `Core/Front/Admin/Boot.php` | Fires AdminPanelBooting |
|
||||
| `Core/Front/Api/Boot.php` | Fires ApiRoutesRegistering |
|
||||
| `Core/Front/Cli/Boot.php` | Fires ConsoleBooting |
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Before (Legacy)
|
||||
|
||||
```php
|
||||
class Boot extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerRoutes();
|
||||
$this->registerViews();
|
||||
$this->registerLivewireComponents();
|
||||
$this->registerCommands();
|
||||
}
|
||||
|
||||
private function registerRoutes(): void
|
||||
{
|
||||
Route::middleware('web')->group(__DIR__.'/Routes/web.php');
|
||||
}
|
||||
|
||||
private function registerViews(): void
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__.'/View/Blade', 'mymodule');
|
||||
}
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
### After (Event-Driven)
|
||||
|
||||
```php
|
||||
class Boot extends ServiceProvider
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(__DIR__.'/Migrations');
|
||||
}
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('mymodule', __DIR__.'/View/Blade');
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
$event->command(Commands\MyCommand::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
1. **Event Caching**: Cache scanned mappings in production for faster boot
|
||||
2. **Module Dependencies**: Declare dependencies between modules for ordered loading
|
||||
3. **Hot Module Reloading**: In development, detect changes and re-scan
|
||||
4. **Event Priorities**: Allow modules to specify listener priority
|
||||
484
docs/specs/RFC-003-CONFIG-CHANNELS.md
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
# RFC: Config Channels
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-01-15
|
||||
**Authors:** Host UK Engineering
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
Config Channels add a voice/context dimension to configuration resolution. Where scopes (workspace, org, system) determine *who* a setting applies to, channels determine *where* or *how* it applies.
|
||||
|
||||
A workspace might have one Twitter handle but different posting styles for different contexts. Channels let you define `social.posting.style = "casual"` for Instagram while keeping `social.posting.style = "professional"` for LinkedIn—same workspace, same key, different channel.
|
||||
|
||||
The system resolves values through a two-dimensional matrix: scope chain (workspace → org → system) crossed with channel chain (specific → parent → null). Most specific wins, unless a parent declares FINAL.
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Traditional configuration systems work on a single dimension: scope hierarchy. You set a value at system level, override it at workspace level. Simple.
|
||||
|
||||
But some configuration varies by context within a single workspace:
|
||||
|
||||
- **Technical channels:** web vs API vs mobile (different rate limits, caching, auth)
|
||||
- **Social channels:** Instagram vs Twitter vs TikTok (different post lengths, hashtags, tone)
|
||||
- **Voice channels:** formal vs casual vs support (different language, greeting styles)
|
||||
|
||||
Without channels, you either:
|
||||
1. Create separate config keys for each context (`twitter.style`, `instagram.style`, etc.)
|
||||
2. Store JSON blobs and parse them at runtime
|
||||
3. Build custom logic for each use case
|
||||
|
||||
Channels generalise this pattern. One key, multiple channel-specific values, clean resolution.
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Channel
|
||||
|
||||
A named context for configuration. Channels have:
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `code` | Unique identifier (e.g., `instagram`, `api`, `support`) |
|
||||
| `name` | Human-readable label |
|
||||
| `parent_id` | Optional parent for inheritance |
|
||||
| `workspace_id` | Owner workspace (null = system channel) |
|
||||
| `metadata` | Arbitrary JSON for channel-specific data |
|
||||
|
||||
### Channel Inheritance
|
||||
|
||||
Channels form inheritance trees. A specific channel inherits from its parent:
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
│ null │ ← All channels (fallback)
|
||||
└────┬────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ social │ ← Social media defaults
|
||||
└────┬────┘
|
||||
┌─────────┼─────────┐
|
||||
│ │ │
|
||||
┌────┴────┐ ┌──┴───┐ ┌───┴───┐
|
||||
│instagram│ │twitter│ │tiktok │
|
||||
└─────────┘ └───────┘ └───────┘
|
||||
```
|
||||
|
||||
When resolving `social.posting.style` for the `instagram` channel:
|
||||
1. Check instagram-specific value
|
||||
2. Check social (parent) value
|
||||
3. Check null (all channels) value
|
||||
|
||||
### System vs Workspace Channels
|
||||
|
||||
**System channels** (`workspace_id = null`) are available to all workspaces. Platform-level contexts like `web`, `api`, `mobile`.
|
||||
|
||||
**Workspace channels** are private to a workspace. Custom contexts like `vip_support`, `internal_comms`, or workspace-specific social accounts.
|
||||
|
||||
When looking up a channel by code, workspace channels take precedence over system channels with the same code. This allows workspaces to override system channel behaviour.
|
||||
|
||||
### Resolution Matrix
|
||||
|
||||
Config resolution operates on a matrix of scope × channel:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Channel Chain │
|
||||
│ instagram → social → null │
|
||||
└──────────────────────────────────────────┘
|
||||
┌───────────────────┐ ┌──────────┬──────────┬──────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Scope Chain │ │ instagram│ social │ null │
|
||||
│ │ │ │ │ │
|
||||
├───────────────────┼───┼──────────┼──────────┼──────────┤
|
||||
│ workspace │ │ 1 │ 2 │ 3 │
|
||||
├───────────────────┼───┼──────────┼──────────┼──────────┤
|
||||
│ org │ │ 4 │ 5 │ 6 │
|
||||
├───────────────────┼───┼──────────┼──────────┼──────────┤
|
||||
│ system │ │ 7 │ 8 │ 9 │
|
||||
└───────────────────┴───┴──────────┴──────────┴──────────┘
|
||||
|
||||
Resolution order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
|
||||
(Most specific scope + most specific channel first)
|
||||
```
|
||||
|
||||
The first non-null value wins—unless a less-specific combination has `locked = true` (FINAL), which blocks all more-specific values.
|
||||
|
||||
### FINAL (Locked Values)
|
||||
|
||||
A value marked as `locked` cannot be overridden by more specific scopes or channels. This implements the FINAL pattern from Java/OOP:
|
||||
|
||||
```php
|
||||
// System admin sets rate limit and locks it
|
||||
$config->set('api.rate_limit', 1000, $systemProfile, locked: true, channel: 'api');
|
||||
|
||||
// Workspace cannot override - locked value always wins
|
||||
$config->set('api.rate_limit', 5000, $workspaceProfile, channel: 'api');
|
||||
// ↑ This value exists but is never returned
|
||||
```
|
||||
|
||||
Lock checks traverse from least specific (system + null channel) to most specific. Any lock encountered blocks all more-specific values.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### Read Path
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ $config->get('social.posting.style') │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Hash lookup (O(1)) │
|
||||
│ ConfigResolver::$values['social.posting.style'] │
|
||||
│ → Found? Return immediately │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│ Miss
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. Lazy load scope (1 query) │
|
||||
│ Load all resolved values for workspace+channel into hash │
|
||||
│ → Check hash again │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│ Still miss
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Lazy prime (N queries) │
|
||||
│ Build profile chain (workspace → org → system) │
|
||||
│ Build channel chain (specific → parent → null) │
|
||||
│ Batch load all values for key │
|
||||
│ Walk resolution matrix until value found │
|
||||
│ Store in hash + database │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Most reads hit step 1 (hash lookup). The heavy resolution only runs once per key per scope+channel combination, then gets cached.
|
||||
|
||||
### Write Path
|
||||
|
||||
```php
|
||||
$config->set(
|
||||
keyCode: 'social.posting.style',
|
||||
value: 'casual',
|
||||
profile: $workspaceProfile,
|
||||
locked: false,
|
||||
channel: 'instagram',
|
||||
);
|
||||
```
|
||||
|
||||
1. Update `config_values` (source of truth)
|
||||
2. Clear affected entries from hash and `config_resolved`
|
||||
3. Re-prime the key for affected scope+channel
|
||||
4. Fire `ConfigChanged` event
|
||||
|
||||
### Prime Operation
|
||||
|
||||
The prime operation pre-computes resolved values:
|
||||
|
||||
```php
|
||||
// Prime entire workspace
|
||||
$config->prime($workspace, channel: 'instagram');
|
||||
|
||||
// Prime all workspaces (scheduled job)
|
||||
$config->primeAll();
|
||||
```
|
||||
|
||||
This runs full matrix resolution for every key and stores results in `config_resolved`. Subsequent reads become single indexed lookups.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Channel Model
|
||||
|
||||
**Namespace:** `Core\Config\Models\Channel`
|
||||
|
||||
#### Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `code` | string | Unique identifier |
|
||||
| `name` | string | Human-readable label |
|
||||
| `parent_id` | int\|null | Parent channel for inheritance |
|
||||
| `workspace_id` | int\|null | Owner (null = system) |
|
||||
| `metadata` | array\|null | Arbitrary JSON data |
|
||||
|
||||
#### Methods
|
||||
|
||||
```php
|
||||
// Find by code (prefers workspace-specific over system)
|
||||
Channel::byCode('instagram', $workspaceId): ?Channel
|
||||
|
||||
// Get inheritance chain (most specific first)
|
||||
$channel->inheritanceChain(): Collection
|
||||
|
||||
// Get all codes in chain
|
||||
$channel->inheritanceCodes(): array // ['instagram', 'social']
|
||||
|
||||
// Check inheritance
|
||||
$channel->inheritsFrom('social'): bool
|
||||
|
||||
// Is system channel?
|
||||
$channel->isSystem(): bool
|
||||
|
||||
// Get metadata value
|
||||
$channel->meta('platform_id'): mixed
|
||||
|
||||
// Ensure channel exists
|
||||
Channel::ensure(
|
||||
code: 'instagram',
|
||||
name: 'Instagram',
|
||||
parentCode: 'social',
|
||||
workspaceId: null,
|
||||
metadata: ['platform_id' => 'ig'],
|
||||
): Channel
|
||||
```
|
||||
|
||||
### ConfigService with Channels
|
||||
|
||||
```php
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
// Set context (typically in middleware)
|
||||
$config->setContext($workspace, $channel);
|
||||
|
||||
// Get value using context
|
||||
$value = $config->get('social.posting.style');
|
||||
|
||||
// Explicit channel override
|
||||
$result = $config->resolve('social.posting.style', $workspace, 'instagram');
|
||||
|
||||
// Set channel-specific value
|
||||
$config->set(
|
||||
keyCode: 'social.posting.style',
|
||||
value: 'casual',
|
||||
profile: $profile,
|
||||
locked: false,
|
||||
channel: 'instagram',
|
||||
);
|
||||
|
||||
// Lock a channel-specific value
|
||||
$config->lock('social.posting.style', $profile, 'instagram');
|
||||
|
||||
// Prime for specific channel
|
||||
$config->prime($workspace, 'instagram');
|
||||
```
|
||||
|
||||
### ConfigValue with Channels
|
||||
|
||||
```php
|
||||
// Find value for profile + key + channel
|
||||
ConfigValue::findValue($profileId, $keyId, $channelId): ?ConfigValue
|
||||
|
||||
// Set value with channel
|
||||
ConfigValue::setValue(
|
||||
profileId: $profileId,
|
||||
keyId: $keyId,
|
||||
value: 'casual',
|
||||
locked: false,
|
||||
inheritedFrom: null,
|
||||
channelId: $channelId,
|
||||
): ConfigValue
|
||||
|
||||
// Get all values for key across profiles and channels
|
||||
ConfigValue::forKeyInProfiles($keyId, $profileIds, $channelIds): Collection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### config_channels
|
||||
|
||||
```sql
|
||||
CREATE TABLE config_channels (
|
||||
id BIGINT PRIMARY KEY,
|
||||
code VARCHAR(255),
|
||||
name VARCHAR(255),
|
||||
parent_id BIGINT REFERENCES config_channels(id),
|
||||
workspace_id BIGINT REFERENCES workspaces(id),
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
|
||||
UNIQUE (code, workspace_id),
|
||||
INDEX (parent_id)
|
||||
);
|
||||
```
|
||||
|
||||
### config_values (extended)
|
||||
|
||||
```sql
|
||||
ALTER TABLE config_values ADD COLUMN
|
||||
channel_id BIGINT REFERENCES config_channels(id);
|
||||
|
||||
-- Updated unique constraint
|
||||
UNIQUE (profile_id, key_id, channel_id)
|
||||
```
|
||||
|
||||
### config_resolved (extended)
|
||||
|
||||
```sql
|
||||
-- Channel dimension in resolved cache
|
||||
channel_id BIGINT,
|
||||
source_channel_id BIGINT,
|
||||
|
||||
-- Composite lookup
|
||||
INDEX (workspace_id, channel_id, key_code)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Multi-platform social posting
|
||||
|
||||
```php
|
||||
// System defaults (all channels)
|
||||
$config->set('social.posting.max_length', 280, $systemProfile);
|
||||
$config->set('social.posting.style', 'professional', $systemProfile);
|
||||
|
||||
// Channel-specific overrides
|
||||
$config->set('social.posting.max_length', 2200, $systemProfile, channel: 'instagram');
|
||||
$config->set('social.posting.max_length', 100000, $systemProfile, channel: 'linkedin');
|
||||
$config->set('social.posting.style', 'casual', $workspaceProfile, channel: 'tiktok');
|
||||
|
||||
// Resolution
|
||||
$config->resolve('social.posting.max_length', $workspace, 'twitter'); // 280 (default)
|
||||
$config->resolve('social.posting.max_length', $workspace, 'instagram'); // 2200
|
||||
$config->resolve('social.posting.style', $workspace, 'tiktok'); // 'casual'
|
||||
```
|
||||
|
||||
### API rate limiting with FINAL
|
||||
|
||||
```php
|
||||
// System admin sets hard limit for API channel
|
||||
$config->set('api.rate_limit.requests', 1000, $systemProfile, locked: true, channel: 'api');
|
||||
$config->set('api.rate_limit.window', 60, $systemProfile, locked: true, channel: 'api');
|
||||
|
||||
// Workspaces cannot exceed this
|
||||
$config->set('api.rate_limit.requests', 5000, $workspaceProfile, channel: 'api');
|
||||
// ↑ Stored but never returned - locked value wins
|
||||
|
||||
$config->resolve('api.rate_limit.requests', $workspace, 'api'); // Always 1000
|
||||
```
|
||||
|
||||
### Voice/tone channels
|
||||
|
||||
```php
|
||||
// Define voice channels
|
||||
Channel::ensure('support', 'Customer Support', parentCode: null);
|
||||
Channel::ensure('vi', 'Virtual Intelligence', parentCode: null);
|
||||
Channel::ensure('formal', 'Formal Communications', parentCode: null);
|
||||
|
||||
// Configure per voice
|
||||
$config->set('comms.greeting', 'Hello', $workspaceProfile, channel: null);
|
||||
$config->set('comms.greeting', 'Hey there!', $workspaceProfile, channel: 'support');
|
||||
$config->set('comms.greeting', 'Greetings', $workspaceProfile, channel: 'formal');
|
||||
$config->set('comms.greeting', 'Hi, I\'m your AI assistant', $workspaceProfile, channel: 'vi');
|
||||
```
|
||||
|
||||
### Channel inheritance
|
||||
|
||||
```php
|
||||
// Create hierarchy
|
||||
Channel::ensure('social', 'Social Media');
|
||||
Channel::ensure('instagram', 'Instagram', parentCode: 'social');
|
||||
Channel::ensure('instagram_stories', 'Instagram Stories', parentCode: 'instagram');
|
||||
|
||||
// Set at parent level
|
||||
$config->set('social.hashtags.enabled', true, $profile, channel: 'social');
|
||||
$config->set('social.hashtags.max', 30, $profile, channel: 'instagram');
|
||||
|
||||
// Child inherits from parent
|
||||
$config->resolve('social.hashtags.enabled', $workspace, 'instagram_stories');
|
||||
// → true (inherited from 'social')
|
||||
|
||||
$config->resolve('social.hashtags.max', $workspace, 'instagram_stories');
|
||||
// → 30 (inherited from 'instagram')
|
||||
```
|
||||
|
||||
### Workspace-specific channel override
|
||||
|
||||
```php
|
||||
// System channel
|
||||
Channel::ensure('premium', 'Premium Features', workspaceId: null);
|
||||
|
||||
// Workspace overrides system channel
|
||||
Channel::ensure('premium', 'VIP Premium', workspaceId: $workspace->id, metadata: [
|
||||
'features' => ['priority_support', 'custom_branding'],
|
||||
]);
|
||||
|
||||
// Lookup prefers workspace channel
|
||||
$channel = Channel::byCode('premium', $workspace->id);
|
||||
// → Workspace's 'VIP Premium' channel, not system 'Premium Features'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
The channel system adds a dimension to resolution, but performance impact is minimal:
|
||||
|
||||
1. **Read path unchanged** — Most reads hit the hash (O(1))
|
||||
2. **Batch loading** — Resolution loads all channel values in one query
|
||||
3. **Cached resolution** — `config_resolved` stores pre-computed values per workspace+channel
|
||||
4. **Lazy priming** — Only computes on first access, not on every request
|
||||
|
||||
### Cycle Detection
|
||||
|
||||
Channel inheritance includes cycle detection to handle data corruption:
|
||||
|
||||
```php
|
||||
public function inheritanceChain(): Collection
|
||||
{
|
||||
$seen = [$this->id => true];
|
||||
|
||||
while ($current->parent_id !== null) {
|
||||
if (isset($seen[$current->parent_id])) {
|
||||
Log::error('Circular reference in channel inheritance');
|
||||
break;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MariaDB NULL Handling
|
||||
|
||||
The `config_resolved` table uses `0` instead of `NULL` for system scope and all-channels:
|
||||
|
||||
```php
|
||||
// MariaDB composite unique constraints don't handle NULL well
|
||||
// workspace_id = 0 means system scope
|
||||
// channel_id = 0 means all channels
|
||||
```
|
||||
|
||||
This is an implementation detail—the API accepts and returns `null` as expected.
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
- `app/Core/Config/Models/Channel.php` — Channel model
|
||||
- `app/Core/Config/Models/ConfigValue.php` — Value storage with channel support
|
||||
- `app/Core/Config/ConfigResolver.php` — Resolution engine
|
||||
- `app/Core/Config/ConfigService.php` — Main API
|
||||
- `app/Core/Config/Migrations/2026_01_09_100001_add_config_channels.php` — Schema
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2026-01-15 | Initial RFC |
|
||||
512
docs/specs/RFC-004-ENTITLEMENTS.md
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
# RFC: Entitlements and Feature System
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-01-15
|
||||
**Authors:** Host UK Engineering
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
The Entitlement System controls feature access, usage limits, and tier gating across all Host services. It answers one question: "Can this workspace do this action?"
|
||||
|
||||
Workspaces subscribe to **Packages** that bundle **Features**. Features are either boolean flags (access gates) or numeric limits (usage caps). **Boosts** provide temporary or permanent additions to base limits. Usage is tracked, cached, and enforced in real-time.
|
||||
|
||||
The system integrates with Commerce for subscription lifecycle and exposes an API for cross-service entitlement checks.
|
||||
|
||||
---
|
||||
|
||||
## Core Model
|
||||
|
||||
### Entity Relationships
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Workspace ───┬─── WorkspacePackage ─── Package ─── Features │
|
||||
│ │ │
|
||||
│ ├─── Boosts (temporary limit additions) │
|
||||
│ │ │
|
||||
│ ├─── UsageRecords (consumption tracking) │
|
||||
│ │ │
|
||||
│ └─── EntitlementLogs (audit trail) │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Workspace
|
||||
|
||||
The tenant unit. All entitlement checks happen against a workspace, not a user. Users belong to workspaces; workspaces own entitlements.
|
||||
|
||||
```php
|
||||
// Check if workspace can use a feature
|
||||
$workspace->can('social.accounts', quantity: 3);
|
||||
|
||||
// Record usage
|
||||
$workspace->recordUsage('ai.credits', quantity: 10);
|
||||
|
||||
// Get usage summary
|
||||
$workspace->getUsageSummary();
|
||||
```
|
||||
|
||||
### Package
|
||||
|
||||
A bundle of features with defined limits. Two types:
|
||||
|
||||
| Type | Behaviour |
|
||||
|------|-----------|
|
||||
| **Base Package** | Only one active per workspace. Upgrading replaces the previous base package. |
|
||||
| **Add-on Package** | Stackable. Multiple can be active simultaneously. Limits accumulate. |
|
||||
|
||||
**Database:** `entitlement_packages`
|
||||
|
||||
```php
|
||||
// Package fields
|
||||
'code' // Unique identifier (e.g., 'social-creator')
|
||||
'name' // Display name
|
||||
'is_base_package' // true = only one allowed
|
||||
'is_stackable' // true = limits add to base
|
||||
'monthly_price' // Pricing
|
||||
'yearly_price'
|
||||
'stripe_price_id_monthly'
|
||||
'stripe_price_id_yearly'
|
||||
```
|
||||
|
||||
### Feature
|
||||
|
||||
A capability or limit that can be granted. Three types:
|
||||
|
||||
| Type | Behaviour | Example |
|
||||
|------|-----------|---------|
|
||||
| **Boolean** | On/off access gate | `tier.apollo`, `host.social` |
|
||||
| **Limit** | Numeric cap on usage | `social.accounts` (5), `ai.credits` (100) |
|
||||
| **Unlimited** | No cap (special limit value) | Agency tier social posts |
|
||||
|
||||
**Database:** `entitlement_features`
|
||||
|
||||
```php
|
||||
// Feature fields
|
||||
'code' // Unique identifier (e.g., 'social.accounts')
|
||||
'name' // Display name
|
||||
'type' // boolean, limit, unlimited
|
||||
'reset_type' // none, monthly, rolling
|
||||
'rolling_window_days' // For rolling reset (e.g., 30)
|
||||
'parent_feature_id' // For global pools (see Storage Pool below)
|
||||
```
|
||||
|
||||
#### Reset Types
|
||||
|
||||
| Reset Type | Behaviour |
|
||||
|------------|-----------|
|
||||
| **None** | Usage accumulates forever (e.g., account limits) |
|
||||
| **Monthly** | Resets at billing cycle start |
|
||||
| **Rolling** | Rolling window (e.g., last 30 days) |
|
||||
|
||||
#### Hierarchical Features (Global Pools)
|
||||
|
||||
Child features share a parent's limit pool. Used for storage allocation across services:
|
||||
|
||||
```
|
||||
host.storage.total (1000 MB)
|
||||
├── host.cdn (draws from parent pool)
|
||||
├── bio.cdn (draws from parent pool)
|
||||
└── social.cdn (draws from parent pool)
|
||||
```
|
||||
|
||||
### WorkspacePackage
|
||||
|
||||
The pivot linking workspaces to packages. Tracks subscription state.
|
||||
|
||||
**Database:** `entitlement_workspace_packages`
|
||||
|
||||
```php
|
||||
// Status constants
|
||||
STATUS_ACTIVE // Package in effect
|
||||
STATUS_SUSPENDED // Temporarily disabled (e.g., payment failure)
|
||||
STATUS_CANCELLED // Marked for removal
|
||||
STATUS_EXPIRED // Past expiry date
|
||||
|
||||
// Key fields
|
||||
'starts_at' // When package becomes active
|
||||
'expires_at' // When package ends
|
||||
'billing_cycle_anchor' // For monthly reset calculations
|
||||
'blesta_service_id' // External billing system reference
|
||||
```
|
||||
|
||||
### Boost
|
||||
|
||||
Temporary or permanent additions to feature limits. Use cases:
|
||||
- One-time credit top-ups
|
||||
- Promotional extras
|
||||
- Cycle-bound bonuses that expire at billing renewal
|
||||
|
||||
**Database:** `entitlement_boosts`
|
||||
|
||||
```php
|
||||
// Boost types
|
||||
BOOST_TYPE_ADD_LIMIT // Add to existing limit
|
||||
BOOST_TYPE_ENABLE // Enable a boolean feature
|
||||
BOOST_TYPE_UNLIMITED // Grant unlimited access
|
||||
|
||||
// Duration types
|
||||
DURATION_CYCLE_BOUND // Expires at billing cycle end
|
||||
DURATION_DURATION // Expires after set time
|
||||
DURATION_PERMANENT // Never expires
|
||||
|
||||
// Key fields
|
||||
'limit_value' // Amount to add
|
||||
'consumed_quantity' // How much has been used
|
||||
'status' // active, exhausted, expired, cancelled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How Checking Works
|
||||
|
||||
### The `can()` Method
|
||||
|
||||
All access checks flow through `EntitlementService::can()`.
|
||||
|
||||
```php
|
||||
public function can(Workspace $workspace, string $featureCode, int $quantity = 1): EntitlementResult
|
||||
```
|
||||
|
||||
**Algorithm:**
|
||||
|
||||
```
|
||||
1. Look up feature by code
|
||||
2. If feature has parent, use parent's code for pool lookup
|
||||
3. Sum limits from all active packages + boosts
|
||||
4. If any source grants unlimited → return allowed (unlimited)
|
||||
5. Get current usage (respecting reset type)
|
||||
6. If usage + quantity > limit → deny
|
||||
7. Otherwise → allow
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
// Check before creating social account
|
||||
$result = $workspace->can('social.accounts');
|
||||
|
||||
if ($result->isDenied()) {
|
||||
throw new EntitlementException($result->getMessage());
|
||||
}
|
||||
|
||||
// Proceed with creation...
|
||||
|
||||
// Record the usage
|
||||
$workspace->recordUsage('social.accounts');
|
||||
```
|
||||
|
||||
### EntitlementResult
|
||||
|
||||
The return value from `can()`. Provides all context needed for UI feedback.
|
||||
|
||||
```php
|
||||
$result = $workspace->can('ai.credits', quantity: 10);
|
||||
|
||||
$result->isAllowed(); // bool
|
||||
$result->isDenied(); // bool
|
||||
$result->isUnlimited(); // bool
|
||||
$result->getMessage(); // Denial reason
|
||||
|
||||
$result->limit; // Total limit (100)
|
||||
$result->used; // Current usage (75)
|
||||
$result->remaining; // Remaining (25)
|
||||
$result->getUsagePercentage(); // 75.0
|
||||
$result->isNearLimit(); // true if > 80%
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
Limits and usage are cached for 5 minutes to avoid repeated database queries.
|
||||
|
||||
```php
|
||||
// Cache keys
|
||||
"entitlement:{workspace_id}:limit:{feature_code}"
|
||||
"entitlement:{workspace_id}:usage:{feature_code}"
|
||||
```
|
||||
|
||||
Cache is invalidated when:
|
||||
- Package is provisioned, suspended, cancelled, or reactivated
|
||||
- Boost is provisioned or expires
|
||||
- Usage is recorded
|
||||
|
||||
---
|
||||
|
||||
## Usage Tracking
|
||||
|
||||
### Recording Usage
|
||||
|
||||
After a gated action succeeds, record the consumption:
|
||||
|
||||
```php
|
||||
$workspace->recordUsage(
|
||||
featureCode: 'ai.credits',
|
||||
quantity: 10,
|
||||
user: $user, // Optional: who triggered it
|
||||
metadata: [ // Optional: context
|
||||
'model' => 'claude-3',
|
||||
'tokens' => 1500,
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
**Database:** `entitlement_usage_records`
|
||||
|
||||
### Usage Calculation
|
||||
|
||||
Usage is calculated based on the feature's reset type:
|
||||
|
||||
| Reset Type | Query |
|
||||
|------------|-------|
|
||||
| None | All records ever |
|
||||
| Monthly | Records since billing cycle start |
|
||||
| Rolling | Records in last N days |
|
||||
|
||||
```php
|
||||
// Monthly: Get current cycle start from primary package
|
||||
$cycleStart = $workspace->workspacePackages()
|
||||
->whereHas('package', fn($q) => $q->where('is_base_package', true))
|
||||
->first()
|
||||
->getCurrentCycleStart();
|
||||
|
||||
UsageRecord::getTotalUsage($workspaceId, $featureCode, $cycleStart);
|
||||
|
||||
// Rolling: Last 30 days
|
||||
UsageRecord::getRollingUsage($workspaceId, $featureCode, days: 30);
|
||||
```
|
||||
|
||||
### Usage Summary
|
||||
|
||||
For dashboards, get all features with their current state:
|
||||
|
||||
```php
|
||||
$summary = $workspace->getUsageSummary();
|
||||
|
||||
// Returns Collection grouped by category:
|
||||
[
|
||||
'social' => [
|
||||
['code' => 'social.accounts', 'limit' => 5, 'used' => 3, ...],
|
||||
['code' => 'social.posts.scheduled', 'limit' => 100, 'used' => 45, ...],
|
||||
],
|
||||
'ai' => [
|
||||
['code' => 'ai.credits', 'limit' => 100, 'used' => 75, ...],
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Commerce Integration
|
||||
|
||||
Subscriptions from Commerce automatically provision/revoke entitlement packages.
|
||||
|
||||
**Event Flow:**
|
||||
|
||||
```
|
||||
SubscriptionCreated → ProvisionSocialHostSubscription listener
|
||||
→ EntitlementService::provisionPackage()
|
||||
|
||||
SubscriptionCancelled → Revoke package (immediate or at period end)
|
||||
|
||||
SubscriptionRenewed → Update expires_at
|
||||
→ Expire cycle-bound boosts
|
||||
→ Reset monthly usage (via cycle anchor)
|
||||
```
|
||||
|
||||
**Plan Changes:**
|
||||
|
||||
```php
|
||||
$subscriptionService->changePlan(
|
||||
$subscription,
|
||||
$newPackage,
|
||||
prorate: true, // Calculate credit/charge
|
||||
immediate: true // Apply now vs. period end
|
||||
);
|
||||
```
|
||||
|
||||
### External Billing (Blesta)
|
||||
|
||||
The API supports external billing systems via webhook-style endpoints:
|
||||
|
||||
```
|
||||
POST /api/v1/entitlements → Provision package
|
||||
POST /api/v1/entitlements/{id}/suspend
|
||||
POST /api/v1/entitlements/{id}/unsuspend
|
||||
POST /api/v1/entitlements/{id}/cancel
|
||||
POST /api/v1/entitlements/{id}/renew
|
||||
GET /api/v1/entitlements/{id} → Get status
|
||||
```
|
||||
|
||||
### Cross-Service API
|
||||
|
||||
External services (BioHost, etc.) check entitlements via API:
|
||||
|
||||
```
|
||||
GET /api/v1/entitlements/check
|
||||
?email=user@example.com
|
||||
&feature=bio.pages
|
||||
&quantity=1
|
||||
|
||||
POST /api/v1/entitlements/usage
|
||||
{ email, feature, quantity, metadata }
|
||||
|
||||
GET /api/v1/entitlements/summary
|
||||
GET /api/v1/entitlements/summary/{workspace}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature Categories
|
||||
|
||||
Features are organised by category for display grouping:
|
||||
|
||||
| Category | Features |
|
||||
|----------|----------|
|
||||
| **tier** | `tier.apollo`, `tier.hades`, `tier.nyx`, `tier.stygian` |
|
||||
| **service** | `host.social`, `host.bio`, `host.analytics`, `host.trust` |
|
||||
| **social** | `social.accounts`, `social.posts.scheduled`, `social.workspaces` |
|
||||
| **ai** | `ai.credits`, `ai.providers.claude`, `ai.providers.gemini` |
|
||||
| **biolink** | `bio.pages`, `bio.shortlinks`, `bio.domains` |
|
||||
| **analytics** | `analytics.sites`, `analytics.pageviews` |
|
||||
| **storage** | `host.storage.total`, `host.cdn`, `bio.cdn`, `social.cdn` |
|
||||
| **team** | `team.members` |
|
||||
| **api** | `api.requests` |
|
||||
| **support** | `support.mailboxes`, `support.agents`, `support.conversations` |
|
||||
| **tools** | `tool.url_shortener`, `tool.qr_generator`, `tool.dns_lookup` |
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging
|
||||
|
||||
All entitlement changes are logged for compliance and debugging.
|
||||
|
||||
**Database:** `entitlement_logs`
|
||||
|
||||
```php
|
||||
// Log actions
|
||||
ACTION_PACKAGE_PROVISIONED
|
||||
ACTION_PACKAGE_SUSPENDED
|
||||
ACTION_PACKAGE_CANCELLED
|
||||
ACTION_PACKAGE_REACTIVATED
|
||||
ACTION_PACKAGE_RENEWED
|
||||
ACTION_PACKAGE_EXPIRED
|
||||
ACTION_BOOST_PROVISIONED
|
||||
ACTION_BOOST_CONSUMED
|
||||
ACTION_BOOST_EXHAUSTED
|
||||
ACTION_BOOST_EXPIRED
|
||||
ACTION_BOOST_CANCELLED
|
||||
ACTION_USAGE_RECORDED
|
||||
ACTION_USAGE_DENIED
|
||||
|
||||
// Log sources
|
||||
SOURCE_BLESTA // External billing
|
||||
SOURCE_COMMERCE // Internal commerce
|
||||
SOURCE_ADMIN // Manual admin action
|
||||
SOURCE_SYSTEM // Automated (e.g., expiry)
|
||||
SOURCE_API // API call
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Files
|
||||
|
||||
### Models
|
||||
- `app/Mod/Tenant/Models/Feature.php`
|
||||
- `app/Mod/Tenant/Models/Package.php`
|
||||
- `app/Mod/Tenant/Models/WorkspacePackage.php`
|
||||
- `app/Mod/Tenant/Models/Boost.php`
|
||||
- `app/Mod/Tenant/Models/UsageRecord.php`
|
||||
- `app/Mod/Tenant/Models/EntitlementLog.php`
|
||||
|
||||
### Services
|
||||
- `app/Mod/Tenant/Services/EntitlementService.php` - Core logic
|
||||
- `app/Mod/Tenant/Services/EntitlementResult.php` - Result DTO
|
||||
|
||||
### API
|
||||
- `app/Mod/Api/Controllers/EntitlementApiController.php`
|
||||
|
||||
### Commerce Integration
|
||||
- `app/Mod/Commerce/Listeners/ProvisionSocialHostSubscription.php`
|
||||
- `app/Mod/Commerce/Services/SubscriptionService.php`
|
||||
|
||||
### Database
|
||||
- `entitlement_features` - Feature definitions
|
||||
- `entitlement_packages` - Package definitions
|
||||
- `entitlement_package_features` - Package/feature pivot with limits
|
||||
- `entitlement_workspace_packages` - Workspace subscriptions
|
||||
- `entitlement_boosts` - Temporary additions
|
||||
- `entitlement_usage_records` - Consumption tracking
|
||||
- `entitlement_logs` - Audit trail
|
||||
|
||||
### Seeders
|
||||
- `app/Mod/Tenant/Database/Seeders/FeatureSeeder.php`
|
||||
|
||||
### Tests
|
||||
- `app/Mod/Tenant/Tests/Feature/EntitlementServiceTest.php`
|
||||
- `app/Mod/Tenant/Tests/Feature/EntitlementApiTest.php`
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Access Check
|
||||
|
||||
```php
|
||||
// In controller or service
|
||||
$result = $workspace->can('social.accounts');
|
||||
|
||||
if ($result->isDenied()) {
|
||||
return back()->with('error', $result->getMessage());
|
||||
}
|
||||
|
||||
// Perform action...
|
||||
$workspace->recordUsage('social.accounts');
|
||||
```
|
||||
|
||||
### With Quantity
|
||||
|
||||
```php
|
||||
// Before bulk import
|
||||
$result = $workspace->can('social.posts.scheduled', quantity: 50);
|
||||
|
||||
if ($result->isDenied()) {
|
||||
return "Cannot schedule {$quantity} posts. " .
|
||||
"Remaining: {$result->remaining}";
|
||||
}
|
||||
```
|
||||
|
||||
### Tier Check
|
||||
|
||||
```php
|
||||
// Gate premium features
|
||||
if ($workspace->isApollo()) {
|
||||
// Show Apollo-tier features
|
||||
}
|
||||
|
||||
// Or directly
|
||||
if ($workspace->can('tier.apollo')->isAllowed()) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Dashboard Data
|
||||
|
||||
```php
|
||||
// For billing/usage page
|
||||
$summary = $workspace->getUsageSummary();
|
||||
$packages = $entitlements->getActivePackages($workspace);
|
||||
$boosts = $entitlements->getActiveBoosts($workspace);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2026-01-15 | Initial RFC |
|
||||
705
docs/specs/RFC-005-COMMERCE-MATRIX.md
Normal file
|
|
@ -0,0 +1,705 @@
|
|||
# RFC: Commerce Entity Matrix
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-01-15
|
||||
**Authors:** Host UK Engineering
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
The Commerce Entity Matrix is a hierarchical permission and content system for multi-channel commerce. It enables master companies (M1) to control product catalogues, storefronts (M2) to select and white-label products, and dropshippers (M3) to inherit complete stores with zero management overhead.
|
||||
|
||||
The core innovation is **top-down immutable permissions**: if a parent says "NO", every descendant is locked to "NO". Children can only restrict further, never expand. Combined with sparse content overrides and a self-learning training mode, the system provides complete audit trails and deterministic behaviour.
|
||||
|
||||
Like [HLCRF](./HLCRF-COMPOSITOR.md) for layouts and [Compound SKU](./COMPOUND-SKU.md) for product identity, the Matrix eliminates complexity through composable primitives rather than configuration sprawl.
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Traditional multi-tenant commerce systems copy data between entities, leading to synchronisation nightmares, inconsistent pricing, and broken audit trails. When Original Organics ran four websites, telephone orders, mail orders, and garden centre voucher schemes in 2008, they needed a system where:
|
||||
|
||||
1. **M1 owns truth** — Products exist in one place; everything else references them
|
||||
2. **M2 selects and customises** — Storefronts choose products and can override presentation
|
||||
3. **M3 inherits completely** — Dropshippers get fully functional stores without management burden
|
||||
4. **Permissions cascade down** — A restriction at the top is immutable below
|
||||
5. **Every action is gated** — No default-allow; if it wasn't trained, it doesn't work
|
||||
|
||||
The Matrix addresses this through hierarchical entities, sparse overrides, and request-level permission enforcement.
|
||||
|
||||
---
|
||||
|
||||
## Terminology
|
||||
|
||||
### Entity Types
|
||||
|
||||
| Code | Type | Role |
|
||||
|------|------|------|
|
||||
| **M1** | Master Company | Source of truth. Owns the product catalogue, sets base pricing, controls what's possible. |
|
||||
| **M2** | Facade/Storefront | Selects from M1's catalogue. Can override content, adjust pricing within bounds, operate independent sales channels. |
|
||||
| **M3** | Dropshipper | Full inheritance with zero management. Sees everything, reports everything, manages nothing. Can create their own M2s. |
|
||||
|
||||
### Entity Hierarchy
|
||||
|
||||
```
|
||||
M1 - Master Company (Source of Truth)
|
||||
│
|
||||
├── Master Product Catalogue
|
||||
│ └── Products live here, nowhere else
|
||||
│
|
||||
├── M2 - Storefronts (Select from M1)
|
||||
│ ├── waterbutts.com
|
||||
│ ├── originalorganics.co.uk
|
||||
│ ├── telephone-orders (internal)
|
||||
│ └── garden-vouchers (B2B)
|
||||
│
|
||||
└── M3 - Dropshippers (Full Inheritance)
|
||||
├── External company selling our products
|
||||
└── Can have their own M2s
|
||||
├── dropshipper.com
|
||||
└── dropshipper-wholesale.com
|
||||
```
|
||||
|
||||
### Materialised Path
|
||||
|
||||
Each entity stores its position in the hierarchy as a path string:
|
||||
|
||||
| Entity | Path | Depth |
|
||||
|--------|------|-------|
|
||||
| ORGORG (M1) | `ORGORG` | 0 |
|
||||
| WBUTS (M2) | `ORGORG/WBUTS` | 1 |
|
||||
| DRPSHP (M3) | `ORGORG/WBUTS/DRPSHP` | 2 |
|
||||
|
||||
The path enables ancestor lookups without recursive queries.
|
||||
|
||||
---
|
||||
|
||||
## Permission Matrix
|
||||
|
||||
### The Core Rules
|
||||
|
||||
```
|
||||
If M1 says "NO" → Everything below is "NO"
|
||||
If M1 says "YES" → M2 can say "NO" for itself
|
||||
If M2 says "YES" → M3 can say "NO" for itself
|
||||
|
||||
Permissions cascade DOWN. Restrictions are IMMUTABLE from above.
|
||||
```
|
||||
|
||||
### Visual Model
|
||||
|
||||
```
|
||||
M1 (Master)
|
||||
├── can_sell_alcohol: NO ──────────────┐
|
||||
├── can_discount: YES │
|
||||
└── can_export: YES │
|
||||
│ │
|
||||
┌────────────┼────────────┐ │
|
||||
▼ ▼ ▼ │
|
||||
M2-Web M2-Phone M2-Voucher │
|
||||
├── can_sell_alcohol: [LOCKED NO] ◄──────────────┘
|
||||
├── can_discount: NO (restricted self)
|
||||
└── can_export: YES (inherited)
|
||||
│
|
||||
▼
|
||||
M3-Dropshipper
|
||||
├── can_sell_alcohol: [LOCKED NO] (from M1)
|
||||
├── can_discount: [LOCKED NO] (from M2)
|
||||
└── can_export: YES (can restrict to NO)
|
||||
```
|
||||
|
||||
### The Three Dimensions
|
||||
|
||||
```
|
||||
Dimension 1: Entity Hierarchy (M1 → M2 → M3)
|
||||
Dimension 2: Permission Keys (can_sell, can_discount, can_view_cost...)
|
||||
Dimension 3: Resource Scope (products, orders, customers, reports...)
|
||||
|
||||
Permission = Matrix[Entity][Key][Scope]
|
||||
```
|
||||
|
||||
### Permission Entry Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE permission_matrix (
|
||||
id BIGINT PRIMARY KEY,
|
||||
entity_id BIGINT NOT NULL,
|
||||
|
||||
-- What permission
|
||||
key VARCHAR(128), -- product.create, order.refund
|
||||
scope VARCHAR(128), -- Resource type or specific ID
|
||||
|
||||
-- The value
|
||||
allowed BOOLEAN DEFAULT FALSE,
|
||||
locked BOOLEAN DEFAULT FALSE, -- Set by parent, cannot override
|
||||
|
||||
-- Audit
|
||||
source VARCHAR(32), -- inherited, explicit, trained
|
||||
set_by_entity_id BIGINT, -- Who locked it
|
||||
trained_at TIMESTAMP, -- When it was learned
|
||||
trained_route VARCHAR(255), -- Which route triggered training
|
||||
|
||||
UNIQUE (entity_id, key, scope)
|
||||
);
|
||||
```
|
||||
|
||||
### Source Types
|
||||
|
||||
| Source | Meaning |
|
||||
|--------|---------|
|
||||
| `inherited` | Cascaded from parent entity's lock |
|
||||
| `explicit` | Manually set by administrator |
|
||||
| `trained` | Learned through training mode |
|
||||
|
||||
---
|
||||
|
||||
## Permission Cascade Algorithm
|
||||
|
||||
When checking if an entity can perform an action:
|
||||
|
||||
```
|
||||
1. Build hierarchy path (root M1 → parent M2 → current entity)
|
||||
2. For each ancestor, top-down:
|
||||
- Find permission for (entity, key, scope)
|
||||
- If locked AND denied → RETURN DENIED (immutable)
|
||||
- If denied (not locked) → RETURN DENIED
|
||||
3. Check entity's own permission:
|
||||
- If exists → RETURN allowed/denied
|
||||
4. Permission undefined → handle based on mode
|
||||
```
|
||||
|
||||
### Lock Cascade
|
||||
|
||||
When an entity locks a permission, all descendants receive an inherited lock:
|
||||
|
||||
```php
|
||||
public function lock(Entity $entity, string $key, bool $allowed): void
|
||||
{
|
||||
// Set on this entity
|
||||
PermissionMatrix::updateOrCreate(
|
||||
['entity_id' => $entity->id, 'key' => $key],
|
||||
['allowed' => $allowed, 'locked' => true, 'source' => 'explicit']
|
||||
);
|
||||
|
||||
// Cascade to all descendants
|
||||
$descendants = Entity::where('path', 'like', $entity->path . '/%')->get();
|
||||
|
||||
foreach ($descendants as $descendant) {
|
||||
PermissionMatrix::updateOrCreate(
|
||||
['entity_id' => $descendant->id, 'key' => $key],
|
||||
[
|
||||
'allowed' => $allowed,
|
||||
'locked' => true,
|
||||
'source' => 'inherited',
|
||||
'set_by_entity_id' => $entity->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Training Mode
|
||||
|
||||
### The Problem
|
||||
|
||||
Building a complete permission matrix upfront is impractical. You don't know every action until you build the system.
|
||||
|
||||
### The Solution
|
||||
|
||||
Training mode learns permissions by observing real usage:
|
||||
|
||||
```
|
||||
1. Developer navigates to /admin/products
|
||||
2. Clicks "Create Product"
|
||||
3. System: "BLOCKED - No permission defined for:"
|
||||
- Entity: M1-Admin
|
||||
- Action: product.create
|
||||
- Route: POST /admin/products
|
||||
|
||||
4. Developer clicks [Allow for M1-Admin]
|
||||
5. Permission recorded in matrix with source='trained'
|
||||
6. Continue working
|
||||
|
||||
Result: Complete map of every action in the system
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```php
|
||||
// config/commerce.php
|
||||
'matrix' => [
|
||||
// Training mode - undefined permissions prompt for approval
|
||||
'training_mode' => env('COMMERCE_MATRIX_TRAINING', false),
|
||||
|
||||
// Production mode - undefined = denied
|
||||
'strict_mode' => env('COMMERCE_MATRIX_STRICT', true),
|
||||
|
||||
// Log all permission checks (for audit)
|
||||
'log_all_checks' => env('COMMERCE_MATRIX_LOG_ALL', false),
|
||||
|
||||
// Log denied requests
|
||||
'log_denials' => true,
|
||||
|
||||
// Default action when permission undefined (only if strict=false)
|
||||
'default_allow' => false,
|
||||
],
|
||||
```
|
||||
|
||||
### Permission Request Logging
|
||||
|
||||
```sql
|
||||
CREATE TABLE permission_requests (
|
||||
id BIGINT PRIMARY KEY,
|
||||
entity_id BIGINT NOT NULL,
|
||||
|
||||
-- Request details
|
||||
method VARCHAR(10), -- GET, POST, PUT, DELETE
|
||||
route VARCHAR(255), -- /admin/products
|
||||
action VARCHAR(128), -- product.create
|
||||
scope VARCHAR(128),
|
||||
|
||||
-- Context
|
||||
request_data JSON, -- Sanitised request params
|
||||
user_agent VARCHAR(255),
|
||||
ip_address VARCHAR(45),
|
||||
|
||||
-- Result
|
||||
status VARCHAR(32), -- allowed, denied, pending
|
||||
was_trained BOOLEAN DEFAULT FALSE,
|
||||
trained_at TIMESTAMP,
|
||||
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
|
||||
```
|
||||
If permission not in matrix → 403 Forbidden
|
||||
No exceptions. No fallbacks. No "default allow".
|
||||
|
||||
If it wasn't trained, it doesn't exist.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Product Assignment
|
||||
|
||||
### How Products Flow Through the Hierarchy
|
||||
|
||||
M1 owns the master catalogue. M2/M3 entities don't copy products; they create **assignments** that reference the master and optionally override specific fields.
|
||||
|
||||
```sql
|
||||
CREATE TABLE commerce_product_assignments (
|
||||
id BIGINT PRIMARY KEY,
|
||||
entity_id BIGINT NOT NULL, -- M2 or M3
|
||||
product_id BIGINT NOT NULL, -- Reference to master
|
||||
|
||||
-- SKU customisation
|
||||
sku_suffix VARCHAR(64), -- Custom suffix for this entity
|
||||
|
||||
-- Price overrides (if allowed by matrix)
|
||||
price_override INT, -- Override base price
|
||||
price_tier_overrides JSON, -- Override tier pricing
|
||||
margin_percent DECIMAL(5,2), -- Percentage margin
|
||||
fixed_margin INT, -- Fixed margin amount
|
||||
|
||||
-- Content overrides
|
||||
name_override VARCHAR(255),
|
||||
description_override TEXT,
|
||||
image_override VARCHAR(512),
|
||||
|
||||
-- Control
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_featured BOOLEAN DEFAULT FALSE,
|
||||
sort_order INT DEFAULT 0,
|
||||
allocated_stock INT, -- Entity-specific allocation
|
||||
can_discount BOOLEAN DEFAULT TRUE,
|
||||
min_price INT, -- Floor price
|
||||
max_price INT, -- Ceiling price
|
||||
|
||||
UNIQUE (entity_id, product_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Effective Values
|
||||
|
||||
The assignment provides effective value getters that fall back to the master product:
|
||||
|
||||
```php
|
||||
public function getEffectivePrice(): int
|
||||
{
|
||||
return $this->price_override ?? $this->product->price;
|
||||
}
|
||||
|
||||
public function getEffectiveName(): string
|
||||
{
|
||||
return $this->name_override ?? $this->product->name;
|
||||
}
|
||||
```
|
||||
|
||||
### SKU Lineage
|
||||
|
||||
Full SKUs encode the entity path:
|
||||
|
||||
```
|
||||
ORGORG-WBUTS-WB500L # Original Organics → Waterbutts → 500L Water Butt
|
||||
ORGORG-PHONE-WB500L # Same product, telephone channel
|
||||
DRPSHP-THEIR1-WB500L # Dropshipper's storefront selling our product
|
||||
```
|
||||
|
||||
This tracks:
|
||||
- Where the sale originated
|
||||
- Which facade/channel
|
||||
- Back to master SKU
|
||||
|
||||
---
|
||||
|
||||
## Content Overrides
|
||||
|
||||
### The Core Insight
|
||||
|
||||
**Don't copy data. Create sparse overrides. Resolve at runtime.**
|
||||
|
||||
```
|
||||
M1 (Master) has content
|
||||
│
|
||||
│ (M2 sees M1's content by default)
|
||||
▼
|
||||
M2 customises product name
|
||||
│
|
||||
│ Override entry: (M2, product:123, name, "Custom Name")
|
||||
│ Everything else still inherits from M1
|
||||
▼
|
||||
M3 (Dropshipper) inherits M2's view
|
||||
│
|
||||
│ (Sees M2's custom name, M1's everything else)
|
||||
▼
|
||||
M3 customises description
|
||||
│
|
||||
│ Override entry: (M3, product:123, description, "Their description")
|
||||
│ Still has M2's name, M1's other fields
|
||||
▼
|
||||
Resolution: M3 sees merged content from all levels
|
||||
```
|
||||
|
||||
### Override Table Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE commerce_content_overrides (
|
||||
id BIGINT PRIMARY KEY,
|
||||
entity_id BIGINT NOT NULL,
|
||||
|
||||
-- What's being overridden (polymorphic)
|
||||
overrideable_type VARCHAR(128), -- Product, Category, Page, etc.
|
||||
overrideable_id BIGINT,
|
||||
field VARCHAR(64), -- name, description, image, price
|
||||
|
||||
-- The override value
|
||||
value TEXT,
|
||||
value_type VARCHAR(32), -- string, json, html, decimal, boolean
|
||||
|
||||
-- Audit
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
|
||||
UNIQUE (entity_id, overrideable_type, overrideable_id, field)
|
||||
);
|
||||
```
|
||||
|
||||
### Value Types
|
||||
|
||||
| Type | Storage | Use Case |
|
||||
|------|---------|----------|
|
||||
| `string` | Raw text | Names, short descriptions |
|
||||
| `json` | JSON-encoded | Structured data, arrays |
|
||||
| `html` | Raw HTML | Rich content |
|
||||
| `integer` | String → int | Counts, quantities |
|
||||
| `decimal` | String → float | Prices, percentages |
|
||||
| `boolean` | `1`/`0` | Flags, toggles |
|
||||
|
||||
### Resolution Algorithm
|
||||
|
||||
```
|
||||
Query: "What is product 123's name for M3-ACME?"
|
||||
|
||||
Step 1: Check M3-ACME overrides
|
||||
→ NULL (no override)
|
||||
|
||||
Step 2: Check M2-WATERBUTTS overrides (parent)
|
||||
→ "Premium 500L Water Butt" ✓
|
||||
|
||||
Step 3: Return "Premium 500L Water Butt"
|
||||
(M3-ACME sees M2's override, not M1's original)
|
||||
```
|
||||
|
||||
If M3-ACME later customises the name, their override takes precedence for themselves and their descendants.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### PermissionMatrixService
|
||||
|
||||
The service handles all permission checks and training.
|
||||
|
||||
```php
|
||||
use Mod\Commerce\Services\PermissionMatrixService;
|
||||
|
||||
$matrix = app(PermissionMatrixService::class);
|
||||
|
||||
// Check permission
|
||||
$result = $matrix->can($entity, 'product.create', $scope);
|
||||
|
||||
if ($result->isAllowed()) {
|
||||
// Proceed
|
||||
} elseif ($result->isDenied()) {
|
||||
// Handle denial: $result->reason
|
||||
} elseif ($result->isUndefined()) {
|
||||
// No permission defined
|
||||
}
|
||||
|
||||
// Gate a request (handles training mode)
|
||||
$result = $matrix->gateRequest($request, $entity, 'order.refund');
|
||||
|
||||
// Set permission explicitly
|
||||
$matrix->setPermission($entity, 'product.create', true);
|
||||
|
||||
// Lock permission (cascades to descendants)
|
||||
$matrix->lock($entity, 'product.view_cost', false);
|
||||
|
||||
// Unlock (removes inherited locks)
|
||||
$matrix->unlock($entity, 'product.view_cost');
|
||||
|
||||
// Train permission (dev mode)
|
||||
$matrix->train($entity, 'product.create', $scope, true, $route);
|
||||
```
|
||||
|
||||
### PermissionResult
|
||||
|
||||
```php
|
||||
use Mod\Commerce\Services\PermissionResult;
|
||||
|
||||
// Factory methods
|
||||
PermissionResult::allowed();
|
||||
PermissionResult::denied(reason: 'Locked by M1', lockedBy: $entity);
|
||||
PermissionResult::undefined(key: 'action', scope: 'resource');
|
||||
PermissionResult::pending(key: 'action', trainingUrl: '/train/...');
|
||||
|
||||
// Status checks
|
||||
$result->isAllowed();
|
||||
$result->isDenied();
|
||||
$result->isUndefined();
|
||||
$result->isPending();
|
||||
```
|
||||
|
||||
### Entity Model
|
||||
|
||||
```php
|
||||
use Mod\Commerce\Models\Entity;
|
||||
|
||||
// Create master
|
||||
$m1 = Entity::createMaster('ORGORG', 'Original Organics');
|
||||
|
||||
// Create facade under master
|
||||
$m2 = $m1->createFacade('WBUTS', 'Waterbutts.com', [
|
||||
'domain' => 'waterbutts.com',
|
||||
'currency' => 'GBP',
|
||||
]);
|
||||
|
||||
// Create dropshipper under facade
|
||||
$m3 = $m2->createDropshipper('ACME', 'ACME Supplies');
|
||||
|
||||
// Hierarchy helpers
|
||||
$m3->getAncestors(); // [M1, M2]
|
||||
$m3->getHierarchy(); // [M1, M2, M3]
|
||||
$m3->getRoot(); // M1
|
||||
$m3->getDescendants(); // Children, grandchildren, etc.
|
||||
|
||||
// Type checks
|
||||
$entity->isMaster(); // or isM1()
|
||||
$entity->isFacade(); // or isM2()
|
||||
$entity->isDropshipper(); // or isM3()
|
||||
|
||||
// SKU building
|
||||
$entity->buildSku('WB500L'); // "ORGORG-WBUTS-WB500L"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Standard Permission Keys
|
||||
|
||||
```php
|
||||
// Product permissions
|
||||
'product.list' // View product list
|
||||
'product.view' // View product detail
|
||||
'product.view_cost' // See cost price (M1 only usually)
|
||||
'product.create' // Create new product (M1 only)
|
||||
'product.update' // Update product
|
||||
'product.delete' // Delete product
|
||||
'product.price_override' // Override price on facade
|
||||
|
||||
// Order permissions
|
||||
'order.list' // View orders
|
||||
'order.view' // View order detail
|
||||
'order.create' // Create order
|
||||
'order.update' // Update order
|
||||
'order.cancel' // Cancel order
|
||||
'order.refund' // Process refund
|
||||
'order.export' // Export order data
|
||||
|
||||
// Customer permissions
|
||||
'customer.list'
|
||||
'customer.view'
|
||||
'customer.view_email' // See customer email
|
||||
'customer.view_phone' // See customer phone
|
||||
'customer.export' // Export customer data (GDPR)
|
||||
|
||||
// Report permissions
|
||||
'report.sales' // Sales reports
|
||||
'report.revenue' // Revenue (might hide from M3)
|
||||
'report.cost' // Cost reports (M1 only)
|
||||
'report.margin' // Margin reports (M1 only)
|
||||
|
||||
// System permissions
|
||||
'settings.view'
|
||||
'settings.update'
|
||||
'entity.create' // Create child entities
|
||||
'entity.manage' // Manage entity settings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Middleware Integration
|
||||
|
||||
### CommerceMatrixGate
|
||||
|
||||
```php
|
||||
// app/Http/Middleware/CommerceMatrixGate.php
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$entity = $this->resolveEntity($request);
|
||||
$action = $this->resolveAction($request);
|
||||
|
||||
if (!$entity || !$action) {
|
||||
return $next($request); // Not a commerce route
|
||||
}
|
||||
|
||||
$result = $this->matrix->gateRequest($request, $entity, $action);
|
||||
|
||||
if ($result->isDenied()) {
|
||||
return response()->json([
|
||||
'error' => 'permission_denied',
|
||||
'message' => $result->reason,
|
||||
], 403);
|
||||
}
|
||||
|
||||
if ($result->isPending()) {
|
||||
// Training mode - show prompt
|
||||
return response()->view('commerce.matrix.train-prompt', [
|
||||
'result' => $result,
|
||||
'entity' => $entity,
|
||||
], 428); // Precondition Required
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
```
|
||||
|
||||
### Route Definition
|
||||
|
||||
```php
|
||||
// Explicit action mapping
|
||||
Route::post('/products', [ProductController::class, 'store'])
|
||||
->matrixAction('product.create');
|
||||
|
||||
Route::post('/orders/{order}/refund', [OrderController::class, 'refund'])
|
||||
->matrixAction('order.refund');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Order Flow Through the Matrix
|
||||
|
||||
```
|
||||
Customer places order on waterbutts.com (M2)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Order Created │
|
||||
│ - entity_id: M2-WBUTS │
|
||||
│ - sku: ORGORG-WBUTS-WB500L │
|
||||
│ - customer sees: M2 branding │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ M1 Fulfillment Queue │
|
||||
│ - M1 sees all orders from all M2s │
|
||||
│ - Can filter by facade │
|
||||
│ - Ships with M2 branding (or neutral) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Reporting │
|
||||
│ - M1: Sees all, costs, margins │
|
||||
│ - M2: Sees own orders, no cost data │
|
||||
│ - M3: Sees own orders, wholesale price │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pricing
|
||||
|
||||
Pricing is not a separate system. It emerges from:
|
||||
|
||||
1. **Permission Matrix** — `can_discount`, `max_discount_percent`, `can_sell_below_wholesale`
|
||||
2. **Product Assignments** — `price_override`, `min_price`, `max_price`, `margin_percent`
|
||||
3. **Content Overrides** — Sparse price adjustments per entity
|
||||
4. **SKU System** — Bundle hashes, option modifiers, volume rules
|
||||
|
||||
No separate pricing engine needed. Primitives compose.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Files
|
||||
|
||||
### Models
|
||||
|
||||
- `app/Mod/Commerce/Models/Entity.php` — Entity hierarchy
|
||||
- `app/Mod/Commerce/Models/PermissionMatrix.php` — Permission entries
|
||||
- `app/Mod/Commerce/Models/PermissionRequest.php` — Request logging
|
||||
- `app/Mod/Commerce/Models/ContentOverride.php` — Sparse overrides
|
||||
- `app/Mod/Commerce/Models/ProductAssignment.php` — M2/M3 product links
|
||||
|
||||
### Services
|
||||
|
||||
- `app/Mod/Commerce/Services/PermissionMatrixService.php` — Permission logic
|
||||
- `app/Mod/Commerce/Services/ContentOverrideService.php` — Override resolution
|
||||
|
||||
### Configuration
|
||||
|
||||
- `app/Mod/Commerce/config.php` — Matrix configuration
|
||||
|
||||
---
|
||||
|
||||
## Related RFCs
|
||||
|
||||
- [HLCRF Compositor](./HLCRF-COMPOSITOR.md) — Same philosophy applied to layouts
|
||||
- [Compound SKU](./COMPOUND-SKU.md) — Same philosophy applied to product identity
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2026-01-15 | Initial RFC |
|
||||
258
docs/specs/RFC-006-COMPOUND-SKU.md
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# RFC: Compound SKU Format
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-01-15
|
||||
**Authors:** Host UK Engineering
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
The Compound SKU format encodes product identity, options, quantities, and bundle groupings in a single parseable string. Like [HLCRF](./HLCRF-COMPOSITOR.md) for layouts, it makes complex structure a portable, self-describing data type.
|
||||
|
||||
One scan tells you everything. No lookups. No mistakes. One barcode = complete fulfillment knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Format Specification
|
||||
|
||||
```
|
||||
SKU-<opt>~<val>*<qty>[-<opt>~<val>*<qty>]...
|
||||
```
|
||||
|
||||
| Symbol | Purpose | Example |
|
||||
|--------|---------|----------------------|
|
||||
| `-` | Option separator | `LAPTOP-ram~16gb` |
|
||||
| `~` | Value indicator | `ram~16gb` |
|
||||
| `*` | Quantity indicator | `cover~black*2` |
|
||||
| `,` | Item separator | `LAPTOP,MOUSE,PAD` |
|
||||
| `\|` | Bundle separator | `LAPTOP\|MOUSE\|PAD` |
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Single product with options
|
||||
|
||||
```
|
||||
LAPTOP-ram~16gb-ssd~512gb-color~silver
|
||||
```
|
||||
|
||||
### Option with quantity
|
||||
|
||||
```
|
||||
LAPTOP-ram~16gb-cover~black*2
|
||||
```
|
||||
|
||||
Two black covers included.
|
||||
|
||||
### Multiple separate items
|
||||
|
||||
```
|
||||
LAPTOP-ram~16gb,HDMI-length~2m,MOUSE-color~black
|
||||
```
|
||||
|
||||
Comma separates distinct line items.
|
||||
|
||||
### Bundle (discount lookup)
|
||||
|
||||
```
|
||||
LAPTOP-ram~16gb\|MOUSE-color~black\|PAD-size~xl
|
||||
```
|
||||
|
||||
Pipe binds items for bundle discount detection.
|
||||
|
||||
### With entity lineage
|
||||
|
||||
```
|
||||
ORGORG-WBUTS-PROD500-ram~16gb
|
||||
│ │ │ └── Option
|
||||
│ │ └────────── Base product SKU
|
||||
│ └──────────────── M2 entity code
|
||||
└─────────────────────── M1 entity code
|
||||
```
|
||||
|
||||
The lineage prefix traces through the entity hierarchy.
|
||||
|
||||
---
|
||||
|
||||
## Bundle Discount Detection
|
||||
|
||||
When a compound SKU contains `|` (bundle separator):
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Input: LAPTOP-ram~16gb|MOUSE-color~black|PAD-size~xl │
|
||||
│ │
|
||||
│ Step 1: Detect Bundle (found |) │
|
||||
│ │
|
||||
│ Step 2: Strip Human Choices │
|
||||
│ → LAPTOP|MOUSE|PAD │
|
||||
│ │
|
||||
│ Step 3: Hash the Raw Combination │
|
||||
│ → hash("LAPTOP|MOUSE|PAD") = "abc123..." │
|
||||
│ │
|
||||
│ Step 4: Lookup Bundle Discount │
|
||||
│ → commerce_bundle_hashes["abc123"] = 20% off │
|
||||
│ │
|
||||
│ Step 5: Apply Discount │
|
||||
│ → Bundle price calculated │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The hash is computed from **sorted base SKUs** (stripping options), so `LAPTOP|MOUSE|PAD` and `PAD|LAPTOP|MOUSE` produce the same hash.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### SkuParserService
|
||||
|
||||
Parses compound SKU strings into structured data.
|
||||
|
||||
```php
|
||||
use Mod\Commerce\Services\SkuParserService;
|
||||
|
||||
$parser = app(SkuParserService::class);
|
||||
|
||||
// Parse a compound SKU
|
||||
$result = $parser->parse('LAPTOP-ram~16gb|MOUSE,HDMI');
|
||||
|
||||
// Result contains ParsedItem and BundleItem objects
|
||||
$result->count(); // 2 (1 bundle + 1 single)
|
||||
$result->productCount(); // 4 (3 in bundle + 1 single)
|
||||
$result->hasBundles(); // true
|
||||
$result->getBundleHashes(); // ['abc123...']
|
||||
$result->getAllBaseSkus(); // ['LAPTOP', 'MOUSE', 'HDMI']
|
||||
|
||||
// Access items
|
||||
foreach ($result->items as $item) {
|
||||
if ($item instanceof BundleItem) {
|
||||
echo "Bundle: " . $item->getBaseSkuString();
|
||||
} else {
|
||||
echo "Item: " . $item->baseSku;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SkuBuilderService
|
||||
|
||||
Builds compound SKU strings from structured data.
|
||||
|
||||
```php
|
||||
use Mod\Commerce\Services\SkuBuilderService;
|
||||
|
||||
$builder = app(SkuBuilderService::class);
|
||||
|
||||
// Build from line items
|
||||
$sku = $builder->build([
|
||||
[
|
||||
'base_sku' => 'laptop',
|
||||
'options' => [
|
||||
['code' => 'ram', 'value' => '16gb'],
|
||||
['code' => 'ssd', 'value' => '512gb'],
|
||||
],
|
||||
'bundle_group' => 'cyber', // Groups into bundle
|
||||
],
|
||||
[
|
||||
'base_sku' => 'mouse',
|
||||
'bundle_group' => 'cyber',
|
||||
],
|
||||
[
|
||||
'base_sku' => 'hdmi', // No group = standalone
|
||||
],
|
||||
]);
|
||||
// Returns: "LAPTOP-ram~16gb-ssd~512gb|MOUSE,HDMI"
|
||||
|
||||
// Add entity lineage
|
||||
$sku = $builder->addLineage('PROD500', ['ORGORG', 'WBUTS']);
|
||||
// Returns: "ORGORG-WBUTS-PROD500"
|
||||
|
||||
// Generate bundle hash for discount creation
|
||||
$hash = $builder->generateBundleHash(['LAPTOP', 'MOUSE', 'PAD']);
|
||||
```
|
||||
|
||||
### Data Transfer Objects
|
||||
|
||||
```php
|
||||
use Mod\Commerce\Data\SkuOption;
|
||||
use Mod\Commerce\Data\ParsedItem;
|
||||
use Mod\Commerce\Data\BundleItem;
|
||||
use Mod\Commerce\Data\SkuParseResult;
|
||||
|
||||
// Option: code~value*quantity
|
||||
$option = new SkuOption('ram', '16gb', 1);
|
||||
$option->toString(); // "ram~16gb"
|
||||
|
||||
// Item: baseSku with options
|
||||
$item = new ParsedItem('LAPTOP', [$option]);
|
||||
$item->toString(); // "LAPTOP-ram~16gb"
|
||||
$item->getOption('ram'); // SkuOption
|
||||
$item->hasOption('ssd'); // false
|
||||
|
||||
// Bundle: items grouped for discount
|
||||
$bundle = new BundleItem($items, $hash);
|
||||
$bundle->getBaseSkus(); // ['LAPTOP', 'MOUSE']
|
||||
$bundle->getBaseSkuString(); // "LAPTOP|MOUSE"
|
||||
$bundle->containsSku('MOUSE'); // true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Bundle Hash Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE commerce_bundle_hashes (
|
||||
id BIGINT PRIMARY KEY,
|
||||
hash VARCHAR(64) UNIQUE, -- SHA256 of sorted base SKUs
|
||||
base_skus VARCHAR(512), -- "LAPTOP|MOUSE|PAD" (debugging)
|
||||
|
||||
-- Discount (one of these)
|
||||
coupon_code VARCHAR(64),
|
||||
fixed_price DECIMAL(12,2),
|
||||
discount_percent DECIMAL(5,2),
|
||||
discount_amount DECIMAL(12,2),
|
||||
|
||||
entity_id BIGINT, -- Scope to M1/M2/M3
|
||||
valid_from TIMESTAMP,
|
||||
valid_until TIMESTAMP,
|
||||
active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connection to HLCRF
|
||||
|
||||
Both Compound SKU and HLCRF share the same core innovation: **hierarchy encoded in a parseable string**.
|
||||
|
||||
| System | String | Meaning |
|
||||
|--------|--------|---------|
|
||||
| HLCRF | `H[LCR]CF` | Layout with nested body in header |
|
||||
| SKU | `ORGORG-WBUTS-PROD-ram~16gb` | Product with entity lineage and option |
|
||||
|
||||
Both eliminate database lookups by making structure self-describing. Parse the string, get the full picture.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Files
|
||||
|
||||
- `app/Mod/Commerce/Services/SkuParserService.php` — Parser
|
||||
- `app/Mod/Commerce/Services/SkuBuilderService.php` — Builder
|
||||
- `app/Mod/Commerce/Services/SkuLineageService.php` — Entity lineage tracking
|
||||
- `app/Mod/Commerce/Data/SkuOption.php` — Option DTO
|
||||
- `app/Mod/Commerce/Data/ParsedItem.php` — Item DTO
|
||||
- `app/Mod/Commerce/Data/BundleItem.php` — Bundle DTO
|
||||
- `app/Mod/Commerce/Data/SkuParseResult.php` — Parse result DTO
|
||||
- `app/Mod/Commerce/Models/BundleHash.php` — Bundle discount model
|
||||
- `app/Mod/Commerce/Tests/Feature/CompoundSkuTest.php` — Tests
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2026-01-15 | Initial RFC |
|
||||
406
docs/specs/RFC-007-LTHN-HASH.md
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
# RFC-0004: LTHN Quasi-Salted Hash Algorithm
|
||||
|
||||
**Status:** Informational
|
||||
**Version:** 1.0
|
||||
**Created:** 2025-01-13
|
||||
**Author:** Snider
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the LTHN (Leet-Hash-N) quasi-salted hash algorithm, a deterministic hashing scheme that derives a salt from the input itself using character substitution and reversal. LTHN produces reproducible hashes that can be verified without storing a separate salt value, making it suitable for checksums, identifiers, and non-security-critical hashing applications.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Terminology](#2-terminology)
|
||||
3. [Algorithm Specification](#3-algorithm-specification)
|
||||
4. [Character Substitution Map](#4-character-substitution-map)
|
||||
5. [Verification](#5-verification)
|
||||
6. [Use Cases](#6-use-cases)
|
||||
7. [Security Considerations](#7-security-considerations)
|
||||
8. [Implementation Requirements](#8-implementation-requirements)
|
||||
9. [Test Vectors](#9-test-vectors)
|
||||
10. [References](#10-references)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Traditional salted hashing requires storing a random salt value alongside the hash. This provides protection against rainbow table attacks but requires additional storage and management.
|
||||
|
||||
LTHN takes a different approach: the salt is derived deterministically from the input itself through a transformation that:
|
||||
|
||||
1. Reverses the input string
|
||||
2. Applies character substitutions inspired by "leet speak" conventions
|
||||
|
||||
This produces a quasi-salt that varies with input content while remaining reproducible, enabling verification without salt storage.
|
||||
|
||||
### 1.1 Design Goals
|
||||
|
||||
- **Determinism**: Same input always produces same hash
|
||||
- **Salt derivation**: No external salt storage required
|
||||
- **Verifiability**: Hashes can be verified with only the input
|
||||
- **Simplicity**: Easy to implement and understand
|
||||
- **Interoperability**: Based on standard SHA-256
|
||||
|
||||
### 1.2 Non-Goals
|
||||
|
||||
LTHN is NOT designed to:
|
||||
- Replace proper password hashing (use bcrypt, Argon2, etc.)
|
||||
- Provide cryptographic security against determined attackers
|
||||
- Resist preimage or collision attacks beyond SHA-256's guarantees
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||
|
||||
**Input**: The original string to be hashed
|
||||
**Quasi-salt**: A salt derived from the input itself
|
||||
**Key map**: The character substitution table
|
||||
**LTHN hash**: The final hash output
|
||||
|
||||
## 3. Algorithm Specification
|
||||
|
||||
### 3.1 Overview
|
||||
|
||||
```
|
||||
LTHN(input) = SHA256(input || createSalt(input))
|
||||
```
|
||||
|
||||
Where `||` denotes concatenation and `createSalt` is defined below.
|
||||
|
||||
### 3.2 Salt Creation Algorithm
|
||||
|
||||
```
|
||||
function createSalt(input: string) -> string:
|
||||
if input is empty:
|
||||
return ""
|
||||
|
||||
runes = input as array of Unicode code points
|
||||
salt = new array of size length(runes)
|
||||
|
||||
for i = 0 to length(runes) - 1:
|
||||
// Reverse: take character from end
|
||||
char = runes[length(runes) - 1 - i]
|
||||
|
||||
// Apply substitution if exists in key map
|
||||
if char in keyMap:
|
||||
salt[i] = keyMap[char]
|
||||
else:
|
||||
salt[i] = char
|
||||
|
||||
return salt as string
|
||||
```
|
||||
|
||||
### 3.3 Hash Algorithm
|
||||
|
||||
```
|
||||
function Hash(input: string) -> string:
|
||||
salt = createSalt(input)
|
||||
combined = input + salt
|
||||
digest = SHA256(combined as UTF-8 bytes)
|
||||
return hexEncode(digest)
|
||||
```
|
||||
|
||||
### 3.4 Output Format
|
||||
|
||||
- Output: 64-character lowercase hexadecimal string
|
||||
- Digest: 32 bytes (256 bits)
|
||||
|
||||
## 4. Character Substitution Map
|
||||
|
||||
### 4.1 Default Key Map
|
||||
|
||||
The default substitution map uses bidirectional "leet speak" style mappings:
|
||||
|
||||
| Input | Output | Description |
|
||||
|-------|--------|-------------|
|
||||
| `o` | `0` | Letter O to zero |
|
||||
| `l` | `1` | Letter L to one |
|
||||
| `e` | `3` | Letter E to three |
|
||||
| `a` | `4` | Letter A to four |
|
||||
| `s` | `z` | Letter S to Z |
|
||||
| `t` | `7` | Letter T to seven |
|
||||
| `0` | `o` | Zero to letter O |
|
||||
| `1` | `l` | One to letter L |
|
||||
| `3` | `e` | Three to letter E |
|
||||
| `4` | `a` | Four to letter A |
|
||||
| `7` | `t` | Seven to letter T |
|
||||
|
||||
Note: The mapping is NOT fully symmetric. `z` does NOT map back to `s`.
|
||||
|
||||
### 4.2 Key Map as Code
|
||||
|
||||
```
|
||||
keyMap = {
|
||||
'o': '0',
|
||||
'l': '1',
|
||||
'e': '3',
|
||||
'a': '4',
|
||||
's': 'z',
|
||||
't': '7',
|
||||
'0': 'o',
|
||||
'1': 'l',
|
||||
'3': 'e',
|
||||
'4': 'a',
|
||||
'7': 't'
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Custom Key Maps
|
||||
|
||||
Implementations MAY support custom key maps. When using custom maps:
|
||||
|
||||
- Document the custom map clearly
|
||||
- Ensure bidirectional mappings are intentional
|
||||
- Consider character set implications (Unicode vs. ASCII)
|
||||
|
||||
## 5. Verification
|
||||
|
||||
### 5.1 Verification Algorithm
|
||||
|
||||
```
|
||||
function Verify(input: string, expectedHash: string) -> bool:
|
||||
actualHash = Hash(input)
|
||||
return constantTimeCompare(actualHash, expectedHash)
|
||||
```
|
||||
|
||||
### 5.2 Properties
|
||||
|
||||
- Verification requires only the input and hash
|
||||
- No salt storage or retrieval necessary
|
||||
- Same input always produces same hash
|
||||
|
||||
## 6. Use Cases
|
||||
|
||||
### 6.1 Recommended Uses
|
||||
|
||||
| Use Case | Suitability | Notes |
|
||||
|----------|-------------|-------|
|
||||
| Content identifiers | Good | Deterministic, reproducible |
|
||||
| Cache keys | Good | Same content = same key |
|
||||
| Deduplication | Good | Identify identical content |
|
||||
| File integrity | Moderate | Use with checksum comparison |
|
||||
| Non-critical checksums | Good | Simple verification |
|
||||
| Rolling key derivation | Good | Time-based key rotation (see 6.3) |
|
||||
|
||||
### 6.2 Not Recommended Uses
|
||||
|
||||
| Use Case | Reason |
|
||||
|----------|--------|
|
||||
| Password storage | Use bcrypt, Argon2, or scrypt instead |
|
||||
| Authentication tokens | Use HMAC or proper MACs |
|
||||
| Digital signatures | Use proper signature schemes |
|
||||
| Security-critical integrity | Use HMAC-SHA256 |
|
||||
|
||||
### 6.3 Rolling Key Derivation Pattern
|
||||
|
||||
LTHN is well-suited for deriving time-based rolling keys for streaming media or time-limited access control. The pattern combines a time period with user credentials:
|
||||
|
||||
```
|
||||
streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
|
||||
```
|
||||
|
||||
#### 6.3.1 Cadence Formats
|
||||
|
||||
| Cadence | Period Format | Example | Window |
|
||||
|---------|---------------|---------|--------|
|
||||
| daily | YYYY-MM-DD | "2026-01-13" | 24 hours |
|
||||
| 12h | YYYY-MM-DD-AM/PM | "2026-01-13-AM" | 12 hours |
|
||||
| 6h | YYYY-MM-DD-HH | "2026-01-13-00" | 6 hours (00, 06, 12, 18) |
|
||||
| 1h | YYYY-MM-DD-HH | "2026-01-13-15" | 1 hour |
|
||||
|
||||
#### 6.3.2 Rolling Window Implementation
|
||||
|
||||
For graceful key transitions, implementations should support a rolling window:
|
||||
|
||||
```
|
||||
function GetRollingPeriods(cadence: string) -> (current: string, next: string):
|
||||
now = currentTime()
|
||||
current = formatPeriod(now, cadence)
|
||||
next = formatPeriod(now + periodDuration(cadence), cadence)
|
||||
return (current, next)
|
||||
```
|
||||
|
||||
Content encrypted with rolling keys includes wrapped CEKs (Content Encryption Keys) for both current and next periods, allowing decryption during period transitions.
|
||||
|
||||
#### 6.3.3 CEK Wrapping
|
||||
|
||||
```
|
||||
// Wrap CEK for distribution
|
||||
For each period in [current, next]:
|
||||
streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
|
||||
wrappedCEK = ChaCha20Poly1305_Encrypt(CEK, streamKey)
|
||||
store (period, wrappedCEK) in header
|
||||
|
||||
// Unwrap CEK for playback
|
||||
For each (period, wrappedCEK) in header:
|
||||
streamKey = SHA256(LTHN(period + ":" + license + ":" + fingerprint))
|
||||
CEK = ChaCha20Poly1305_Decrypt(wrappedCEK, streamKey)
|
||||
if success: return CEK
|
||||
return error("no valid key for current period")
|
||||
```
|
||||
|
||||
## 7. Security Considerations
|
||||
|
||||
### 7.1 Not a Password Hash
|
||||
|
||||
LTHN MUST NOT be used for password hashing because:
|
||||
|
||||
- No work factor (bcrypt, Argon2 have tunable cost)
|
||||
- No random salt (predictable salt derivation)
|
||||
- Fast to compute (enables brute force)
|
||||
- No memory hardness (GPU/ASIC friendly)
|
||||
|
||||
### 7.2 Quasi-Salt Limitations
|
||||
|
||||
The derived salt provides limited protection:
|
||||
|
||||
- Salt is deterministic, not random
|
||||
- Identical inputs produce identical salts
|
||||
- Does not prevent rainbow tables for known inputs
|
||||
- Salt derivation algorithm is public
|
||||
|
||||
### 7.3 SHA-256 Dependency
|
||||
|
||||
Security properties depend on SHA-256:
|
||||
|
||||
- Preimage resistance: Finding input from hash is hard
|
||||
- Second preimage resistance: Finding different input with same hash is hard
|
||||
- Collision resistance: Finding two inputs with same hash is hard
|
||||
|
||||
These properties apply to the combined `input || salt` value.
|
||||
|
||||
### 7.4 Timing Attacks
|
||||
|
||||
Verification SHOULD use constant-time comparison to prevent timing attacks:
|
||||
|
||||
```
|
||||
function constantTimeCompare(a: string, b: string) -> bool:
|
||||
if length(a) != length(b):
|
||||
return false
|
||||
|
||||
result = 0
|
||||
for i = 0 to length(a) - 1:
|
||||
result |= a[i] XOR b[i]
|
||||
|
||||
return result == 0
|
||||
```
|
||||
|
||||
## 8. Implementation Requirements
|
||||
|
||||
Conforming implementations MUST:
|
||||
|
||||
1. Use SHA-256 as the underlying hash function
|
||||
2. Concatenate input and salt in the order: `input || salt`
|
||||
3. Use the default key map unless explicitly configured otherwise
|
||||
4. Output lowercase hexadecimal encoding
|
||||
5. Handle empty strings by returning SHA-256 of empty string
|
||||
6. Support Unicode input (process as UTF-8 bytes after salt creation)
|
||||
|
||||
Conforming implementations SHOULD:
|
||||
|
||||
1. Provide constant-time verification
|
||||
2. Support custom key maps via configuration
|
||||
3. Document any deviations from the default key map
|
||||
|
||||
## 9. Test Vectors
|
||||
|
||||
### 9.1 Basic Test Cases
|
||||
|
||||
| Input | Salt | Combined | LTHN Hash |
|
||||
|-------|------|----------|-----------|
|
||||
| `""` | `""` | `""` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |
|
||||
| `"a"` | `"4"` | `"a4"` | `a4a4e5c4b3b2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6` |
|
||||
| `"hello"` | `"011eh"` | `"hello011eh"` | (computed) |
|
||||
| `"test"` | `"7z37"` | `"test7z37"` | (computed) |
|
||||
|
||||
### 9.2 Character Substitution Examples
|
||||
|
||||
| Input | Reversed | After Substitution (Salt) |
|
||||
|-------|----------|---------------------------|
|
||||
| `"hello"` | `"olleh"` | `"011eh"` |
|
||||
| `"test"` | `"tset"` | `"7z37"` |
|
||||
| `"password"` | `"drowssap"` | `"dr0wzz4p"` |
|
||||
| `"12345"` | `"54321"` | `"5ae2l"` |
|
||||
|
||||
### 9.3 Unicode Test Cases
|
||||
|
||||
| Input | Expected Behavior |
|
||||
|-------|-------------------|
|
||||
| `"cafe"` | Standard processing |
|
||||
| `"caf`e`"` | e with accent NOT substituted (only ASCII 'e' matches) |
|
||||
|
||||
Note: Key map only matches exact character codes, not normalized equivalents.
|
||||
|
||||
## 10. API Reference
|
||||
|
||||
### 10.1 Go API
|
||||
|
||||
```go
|
||||
import "github.com/Snider/Enchantrix/pkg/crypt"
|
||||
|
||||
// Create crypt service
|
||||
svc := crypt.NewService()
|
||||
|
||||
// Hash with LTHN
|
||||
hash := svc.Hash(crypt.LTHN, "input string")
|
||||
|
||||
// Available hash types
|
||||
crypt.LTHN // LTHN quasi-salted hash
|
||||
crypt.SHA256 // Standard SHA-256
|
||||
crypt.SHA512 // Standard SHA-512
|
||||
// ... other standard algorithms
|
||||
```
|
||||
|
||||
### 10.2 Direct Usage
|
||||
|
||||
```go
|
||||
import "github.com/Snider/Enchantrix/pkg/crypt/std/lthn"
|
||||
|
||||
// Direct LTHN hash
|
||||
hash := lthn.Hash("input string")
|
||||
|
||||
// Verify hash
|
||||
valid := lthn.Verify("input string", expectedHash)
|
||||
```
|
||||
|
||||
## 11. Future Work
|
||||
|
||||
- [ ] Custom key map configuration via API
|
||||
- [ ] WASM compilation for browser-based LTHN operations
|
||||
- [ ] Alternative underlying hash functions (SHA-3, BLAKE3)
|
||||
- [ ] Configurable salt derivation strategies
|
||||
- [ ] Performance optimization for high-throughput scenarios
|
||||
- [ ] Formal security analysis of rolling key pattern
|
||||
|
||||
## 12. References
|
||||
|
||||
- [FIPS 180-4] Secure Hash Standard (SHA-256)
|
||||
- [RFC 4648] The Base16, Base32, and Base64 Data Encodings
|
||||
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
|
||||
- [Wikipedia: Leet] History and conventions of leet speak character substitution
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Reference Implementation
|
||||
|
||||
A reference implementation in Go is available at:
|
||||
`github.com/Snider/Enchantrix/pkg/crypt/std/lthn/lthn.go`
|
||||
|
||||
## Appendix B: Historical Note
|
||||
|
||||
The name "LTHN" derives from "Leet Hash N" or "Lethean" (relating to forgetfulness/oblivion in Greek mythology), referencing both the leet-speak character substitutions and the one-way nature of hash functions.
|
||||
|
||||
## Appendix C: Comparison with Other Schemes
|
||||
|
||||
| Scheme | Salt | Work Factor | Suitable for Passwords |
|
||||
|--------|------|-------------|------------------------|
|
||||
| LTHN | Derived | None | No |
|
||||
| SHA-256 | None | None | No |
|
||||
| HMAC-SHA256 | Key-based | None | No |
|
||||
| bcrypt | Random | Yes | Yes |
|
||||
| Argon2 | Random | Yes | Yes |
|
||||
| scrypt | Random | Yes | Yes |
|
||||
|
||||
## Appendix D: Changelog
|
||||
|
||||
- **1.0** (2025-01-13): Initial specification
|
||||
372
docs/specs/RFC-008-PRE-OBFUSCATION-LAYER.md
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
# RFC-0001: Pre-Obfuscation Layer Protocol for AEAD Ciphers
|
||||
|
||||
**Status:** Informational
|
||||
**Version:** 1.0
|
||||
**Created:** 2025-01-13
|
||||
**Author:** Snider
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies a pre-obfuscation layer protocol designed to transform plaintext data before it reaches CPU encryption routines. The protocol provides an additional security layer that prevents raw plaintext patterns from being processed directly by encryption hardware, mitigating potential side-channel attack vectors while maintaining full compatibility with standard AEAD cipher constructions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Terminology](#2-terminology)
|
||||
3. [Protocol Overview](#3-protocol-overview)
|
||||
4. [Obfuscator Implementations](#4-obfuscator-implementations)
|
||||
5. [Integration with AEAD Ciphers](#5-integration-with-aead-ciphers)
|
||||
6. [Wire Format](#6-wire-format)
|
||||
7. [Security Considerations](#7-security-considerations)
|
||||
8. [Implementation Requirements](#8-implementation-requirements)
|
||||
9. [Test Vectors](#9-test-vectors)
|
||||
10. [References](#10-references)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Modern AEAD (Authenticated Encryption with Associated Data) ciphers like ChaCha20-Poly1305 and AES-GCM provide strong cryptographic guarantees. However, the plaintext data is processed directly by CPU encryption instructions, potentially exposing patterns through side-channel attacks such as timing analysis, power analysis, or electromagnetic emanation.
|
||||
|
||||
This RFC defines a pre-obfuscation layer that transforms plaintext into an unpredictable byte sequence before encryption. The transformation is reversible, deterministic (given the same entropy source), and adds negligible overhead while providing defense-in-depth against side-channel attacks.
|
||||
|
||||
### 1.1 Design Goals
|
||||
|
||||
- **Reversibility**: All transformations MUST be perfectly reversible
|
||||
- **Determinism**: Given the same entropy, transformations MUST produce identical results
|
||||
- **Independence**: The obfuscation layer operates independently of the underlying cipher
|
||||
- **Zero overhead on security**: The underlying AEAD cipher's security properties are preserved
|
||||
- **Minimal computational overhead**: Transformations should add < 5% processing time
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||
|
||||
**Plaintext**: The original data to be encrypted
|
||||
**Obfuscated data**: Plaintext after pre-obfuscation transformation
|
||||
**Ciphertext**: Obfuscated data after encryption
|
||||
**Entropy**: A source of randomness used to derive transformation parameters (typically the nonce)
|
||||
**Key stream**: A deterministic sequence of bytes derived from entropy
|
||||
**Permutation**: A bijective mapping of byte positions
|
||||
|
||||
## 3. Protocol Overview
|
||||
|
||||
The pre-obfuscation protocol operates in two stages:
|
||||
|
||||
### 3.1 Encryption Flow
|
||||
|
||||
```
|
||||
Plaintext --> Obfuscate(plaintext, entropy) --> Obfuscated --> Encrypt --> Ciphertext
|
||||
```
|
||||
|
||||
1. Generate cryptographic nonce for the AEAD cipher
|
||||
2. Apply obfuscation transformation using nonce as entropy
|
||||
3. Encrypt the obfuscated data using the AEAD cipher
|
||||
4. Output: `[nonce || ciphertext || auth_tag]`
|
||||
|
||||
### 3.2 Decryption Flow
|
||||
|
||||
```
|
||||
Ciphertext --> Decrypt --> Obfuscated --> Deobfuscate(obfuscated, entropy) --> Plaintext
|
||||
```
|
||||
|
||||
1. Extract nonce from the ciphertext prefix
|
||||
2. Decrypt the ciphertext using the AEAD cipher
|
||||
3. Apply reverse obfuscation transformation using the extracted nonce
|
||||
4. Output: Original plaintext
|
||||
|
||||
### 3.3 Entropy Derivation
|
||||
|
||||
The entropy source MUST be the same value used as the AEAD cipher nonce. This ensures:
|
||||
|
||||
- No additional random values need to be generated or stored
|
||||
- The obfuscation is tied to the specific encryption operation
|
||||
- Replay of ciphertext with different obfuscation is not possible
|
||||
|
||||
## 4. Obfuscator Implementations
|
||||
|
||||
This RFC defines two standard obfuscator implementations. Implementations MAY support additional obfuscators provided they meet the requirements in Section 8.
|
||||
|
||||
### 4.1 XOR Obfuscator
|
||||
|
||||
The XOR obfuscator generates a deterministic key stream from the entropy and XORs it with the plaintext.
|
||||
|
||||
#### 4.1.1 Key Stream Derivation
|
||||
|
||||
```
|
||||
function deriveKeyStream(entropy: bytes, length: int) -> bytes:
|
||||
stream = empty byte array of size length
|
||||
blockNum = 0
|
||||
offset = 0
|
||||
|
||||
while offset < length:
|
||||
block = SHA256(entropy || BigEndian64(blockNum))
|
||||
copyLen = min(32, length - offset)
|
||||
copy block[0:copyLen] to stream[offset:offset+copyLen]
|
||||
offset += copyLen
|
||||
blockNum += 1
|
||||
|
||||
return stream
|
||||
```
|
||||
|
||||
#### 4.1.2 Obfuscation
|
||||
|
||||
```
|
||||
function obfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
if length(data) == 0:
|
||||
return data
|
||||
|
||||
keyStream = deriveKeyStream(entropy, length(data))
|
||||
result = new byte array of size length(data)
|
||||
|
||||
for i = 0 to length(data) - 1:
|
||||
result[i] = data[i] XOR keyStream[i]
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
#### 4.1.3 Deobfuscation
|
||||
|
||||
The XOR operation is symmetric; deobfuscation uses the same algorithm:
|
||||
|
||||
```
|
||||
function deobfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
return obfuscate(data, entropy) // XOR is self-inverse
|
||||
```
|
||||
|
||||
### 4.2 Shuffle-Mask Obfuscator
|
||||
|
||||
The shuffle-mask obfuscator provides additional diffusion by combining a byte-level shuffle with an XOR mask.
|
||||
|
||||
#### 4.2.1 Permutation Generation
|
||||
|
||||
Uses Fisher-Yates shuffle with deterministic randomness:
|
||||
|
||||
```
|
||||
function generatePermutation(entropy: bytes, length: int) -> int[]:
|
||||
perm = [0, 1, 2, ..., length-1]
|
||||
seed = SHA256(entropy || "permutation")
|
||||
|
||||
for i = length-1 downto 1:
|
||||
hash = SHA256(seed || BigEndian64(i))
|
||||
j = BigEndian64(hash[0:8]) mod (i + 1)
|
||||
swap perm[i] and perm[j]
|
||||
|
||||
return perm
|
||||
```
|
||||
|
||||
#### 4.2.2 Mask Derivation
|
||||
|
||||
```
|
||||
function deriveMask(entropy: bytes, length: int) -> bytes:
|
||||
mask = empty byte array of size length
|
||||
blockNum = 0
|
||||
offset = 0
|
||||
|
||||
while offset < length:
|
||||
block = SHA256(entropy || "mask" || BigEndian64(blockNum))
|
||||
copyLen = min(32, length - offset)
|
||||
copy block[0:copyLen] to mask[offset:offset+copyLen]
|
||||
offset += copyLen
|
||||
blockNum += 1
|
||||
|
||||
return mask
|
||||
```
|
||||
|
||||
#### 4.2.3 Obfuscation
|
||||
|
||||
```
|
||||
function obfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
if length(data) == 0:
|
||||
return data
|
||||
|
||||
perm = generatePermutation(entropy, length(data))
|
||||
mask = deriveMask(entropy, length(data))
|
||||
|
||||
// Step 1: Apply mask
|
||||
masked = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
masked[i] = data[i] XOR mask[i]
|
||||
|
||||
// Step 2: Shuffle bytes according to permutation
|
||||
shuffled = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
shuffled[i] = masked[perm[i]]
|
||||
|
||||
return shuffled
|
||||
```
|
||||
|
||||
#### 4.2.4 Deobfuscation
|
||||
|
||||
```
|
||||
function deobfuscate(data: bytes, entropy: bytes) -> bytes:
|
||||
if length(data) == 0:
|
||||
return data
|
||||
|
||||
perm = generatePermutation(entropy, length(data))
|
||||
mask = deriveMask(entropy, length(data))
|
||||
|
||||
// Step 1: Unshuffle bytes (inverse permutation)
|
||||
unshuffled = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
unshuffled[perm[i]] = data[i]
|
||||
|
||||
// Step 2: Remove mask
|
||||
result = new byte array of size length(data)
|
||||
for i = 0 to length(data) - 1:
|
||||
result[i] = unshuffled[i] XOR mask[i]
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
## 5. Integration with AEAD Ciphers
|
||||
|
||||
### 5.1 XChaCha20-Poly1305 Integration
|
||||
|
||||
When used with XChaCha20-Poly1305:
|
||||
|
||||
- Nonce size: 24 bytes
|
||||
- Key size: 32 bytes
|
||||
- Auth tag size: 16 bytes
|
||||
|
||||
```
|
||||
function encrypt(key: bytes[32], plaintext: bytes) -> bytes:
|
||||
nonce = random_bytes(24)
|
||||
obfuscated = obfuscator.obfuscate(plaintext, nonce)
|
||||
ciphertext = XChaCha20Poly1305_Seal(key, nonce, obfuscated, nil)
|
||||
return nonce || ciphertext // nonce is prepended
|
||||
```
|
||||
|
||||
```
|
||||
function decrypt(key: bytes[32], data: bytes) -> bytes:
|
||||
if length(data) < 24 + 16: // nonce + auth tag minimum
|
||||
return error("ciphertext too short")
|
||||
|
||||
nonce = data[0:24]
|
||||
ciphertext = data[24:]
|
||||
obfuscated = XChaCha20Poly1305_Open(key, nonce, ciphertext, nil)
|
||||
plaintext = obfuscator.deobfuscate(obfuscated, nonce)
|
||||
return plaintext
|
||||
```
|
||||
|
||||
### 5.2 Other AEAD Ciphers
|
||||
|
||||
The pre-obfuscation layer is cipher-agnostic. For other AEAD ciphers:
|
||||
|
||||
| Cipher | Nonce Size | Notes |
|
||||
|--------|------------|-------|
|
||||
| AES-128-GCM | 12 bytes | Standard nonce |
|
||||
| AES-256-GCM | 12 bytes | Standard nonce |
|
||||
| ChaCha20-Poly1305 | 12 bytes | Original ChaCha nonce |
|
||||
| XChaCha20-Poly1305 | 24 bytes | Extended nonce (RECOMMENDED) |
|
||||
|
||||
## 6. Wire Format
|
||||
|
||||
The output wire format is:
|
||||
|
||||
```
|
||||
+----------------+------------------------+
|
||||
| Nonce | Ciphertext |
|
||||
+----------------+------------------------+
|
||||
| N bytes | len(plaintext) + T |
|
||||
```
|
||||
|
||||
Where:
|
||||
- `N` = Nonce size (cipher-dependent)
|
||||
- `T` = Authentication tag size (typically 16 bytes)
|
||||
|
||||
The obfuscation parameters are NOT stored in the wire format. They are derived deterministically from the nonce.
|
||||
|
||||
## 7. Security Considerations
|
||||
|
||||
### 7.1 Side-Channel Mitigation
|
||||
|
||||
The pre-obfuscation layer provides defense-in-depth against:
|
||||
|
||||
- **Timing attacks**: Plaintext patterns do not influence encryption timing
|
||||
- **Cache-timing attacks**: Memory access patterns are decorrelated from plaintext
|
||||
- **Power analysis**: Power consumption patterns are decorrelated from plaintext structure
|
||||
|
||||
### 7.2 Cryptographic Security
|
||||
|
||||
The pre-obfuscation layer does NOT provide cryptographic security on its own. It MUST always be used in conjunction with a proper AEAD cipher. The security of the combined system relies entirely on the underlying AEAD cipher's security guarantees.
|
||||
|
||||
### 7.3 Entropy Requirements
|
||||
|
||||
The entropy source (nonce) MUST be generated using a cryptographically secure random number generator. Nonce reuse with the same key compromises both the obfuscation determinism and the AEAD security.
|
||||
|
||||
### 7.4 Key Stream Exhaustion
|
||||
|
||||
The XOR obfuscator uses SHA-256 in counter mode. For a single encryption:
|
||||
- Maximum safely obfuscated data: 2^64 * 32 bytes (theoretical)
|
||||
- Practical limit: Constrained by AEAD cipher limits
|
||||
|
||||
### 7.5 Permutation Uniqueness
|
||||
|
||||
The shuffle-mask obfuscator generates permutations deterministically. For data of length `n`:
|
||||
- Total possible permutations: n!
|
||||
- Entropy required for full permutation space: log2(n!) bits
|
||||
- SHA-256 provides 256 bits, sufficient for n up to ~57 bytes without collision concerns
|
||||
|
||||
For larger data, the permutation space is sampled uniformly but not exhaustively.
|
||||
|
||||
## 8. Implementation Requirements
|
||||
|
||||
Conforming implementations MUST:
|
||||
|
||||
1. Support at least the XOR obfuscator
|
||||
2. Use SHA-256 for key stream and permutation derivation
|
||||
3. Use big-endian byte ordering for block numbers
|
||||
4. Handle zero-length data by returning it unchanged
|
||||
5. Prepend the nonce to the ciphertext output
|
||||
6. Accept and process the nonce from ciphertext prefix during decryption
|
||||
|
||||
Conforming implementations SHOULD:
|
||||
|
||||
1. Support the shuffle-mask obfuscator
|
||||
2. Use XChaCha20-Poly1305 as the default AEAD cipher
|
||||
3. Provide constant-time implementations where feasible
|
||||
|
||||
## 9. Test Vectors
|
||||
|
||||
### 9.1 XOR Obfuscator
|
||||
|
||||
```
|
||||
Entropy (hex): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
|
||||
Plaintext (hex): 48656c6c6f2c20576f726c6421
|
||||
Expected key stream prefix (hex): [first 14 bytes of SHA256(entropy || 0x0000000000000000)]
|
||||
```
|
||||
|
||||
### 9.2 Shuffle-Mask Obfuscator
|
||||
|
||||
```
|
||||
Entropy (hex): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
|
||||
Plaintext: "Hello"
|
||||
Permutation seed: SHA256(entropy || "permutation")
|
||||
Mask seed: SHA256(entropy || "mask" || 0x0000000000000000)
|
||||
```
|
||||
|
||||
## 10. Future Work
|
||||
|
||||
- [ ] Hardware-accelerated obfuscation implementations
|
||||
- [ ] Additional obfuscator algorithms (block-based, etc.)
|
||||
- [ ] Formal side-channel resistance analysis
|
||||
- [ ] Integration benchmarks with different AEAD ciphers
|
||||
- [ ] WASM compilation for browser environments
|
||||
|
||||
## 11. References
|
||||
|
||||
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
|
||||
- [RFC 7539] ChaCha20 and Poly1305 for IETF Protocols (obsoleted by 8439)
|
||||
- [draft-irtf-cfrg-xchacha] XChaCha: eXtended-nonce ChaCha and AEAD_XChaCha20_Poly1305
|
||||
- [FIPS 180-4] Secure Hash Standard (SHA-256)
|
||||
- Fisher, R. A.; Yates, F. (1948). Statistical tables for biological, agricultural and medical research
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Reference Implementation
|
||||
|
||||
A reference implementation in Go is available at:
|
||||
`github.com/Snider/Enchantrix/pkg/enchantrix/crypto_sigil.go`
|
||||
|
||||
## Appendix B: Changelog
|
||||
|
||||
- **1.0** (2025-01-13): Initial specification
|
||||
556
docs/specs/RFC-009-SIGIL-TRANSFORMATION.md
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
# RFC-0003: Sigil Transformation Framework
|
||||
|
||||
**Status:** Standards Track
|
||||
**Version:** 1.0
|
||||
**Created:** 2025-01-13
|
||||
**Author:** Snider
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the Sigil Transformation Framework, a composable interface for defining reversible and irreversible data transformations. Sigils provide a uniform abstraction for encoding, compression, hashing, encryption, and other byte-level operations, enabling declarative transformation pipelines that can be applied and reversed systematically.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Terminology](#2-terminology)
|
||||
3. [Interface Specification](#3-interface-specification)
|
||||
4. [Sigil Categories](#4-sigil-categories)
|
||||
5. [Standard Sigils](#5-standard-sigils)
|
||||
6. [Composition and Chaining](#6-composition-and-chaining)
|
||||
7. [Error Handling](#7-error-handling)
|
||||
8. [Implementation Guidelines](#8-implementation-guidelines)
|
||||
9. [Security Considerations](#9-security-considerations)
|
||||
10. [References](#10-references)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Data transformation is a fundamental operation in software systems. Common transformations include:
|
||||
|
||||
- **Encoding**: Converting between representations (hex, base64)
|
||||
- **Compression**: Reducing data size (gzip, zstd)
|
||||
- **Encryption**: Protecting confidentiality (AES, ChaCha20)
|
||||
- **Hashing**: Computing digests (SHA-256, BLAKE2)
|
||||
- **Formatting**: Restructuring data (JSON minification)
|
||||
|
||||
The Sigil framework provides a uniform interface for all these operations, enabling:
|
||||
|
||||
- Declarative transformation pipelines
|
||||
- Automatic reversal of transformation chains
|
||||
- Composable, reusable transformation units
|
||||
- Clear semantics for reversible vs. irreversible operations
|
||||
|
||||
### 1.1 Design Principles
|
||||
|
||||
1. **Simplicity**: Two methods, clear contract
|
||||
2. **Composability**: Sigils combine naturally
|
||||
3. **Reversibility awareness**: Explicit handling of one-way operations
|
||||
4. **Null safety**: Defined behavior for nil/empty inputs
|
||||
5. **Error propagation**: Clear error semantics
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||
|
||||
**Sigil**: A transformation unit implementing the Sigil interface
|
||||
**In operation**: The forward transformation (encode, compress, encrypt, hash)
|
||||
**Out operation**: The reverse transformation (decode, decompress, decrypt)
|
||||
**Reversible sigil**: A sigil where Out(In(x)) = x for all valid x
|
||||
**Irreversible sigil**: A sigil where Out returns the input unchanged or errors
|
||||
**Symmetric sigil**: A sigil where In(x) = Out(x) (e.g., byte reversal)
|
||||
**Transmutation**: Applying a sequence of sigils to data
|
||||
|
||||
## 3. Interface Specification
|
||||
|
||||
### 3.1 Sigil Interface
|
||||
|
||||
```
|
||||
interface Sigil {
|
||||
// In transforms the data (forward operation).
|
||||
// Returns transformed data and any error encountered.
|
||||
In(data: bytes) -> (bytes, error)
|
||||
|
||||
// Out reverses the transformation (reverse operation).
|
||||
// For irreversible sigils, returns data unchanged.
|
||||
Out(data: bytes) -> (bytes, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Method Contracts
|
||||
|
||||
#### 3.2.1 In Method
|
||||
|
||||
The `In` method MUST:
|
||||
|
||||
- Accept a byte slice as input
|
||||
- Return a byte slice as output
|
||||
- Return nil output for nil input (without error)
|
||||
- Return empty slice for empty input (without error)
|
||||
- Return an error if transformation fails
|
||||
|
||||
#### 3.2.2 Out Method
|
||||
|
||||
The `Out` method MUST:
|
||||
|
||||
- Accept a byte slice as input
|
||||
- Return a byte slice as output
|
||||
- Return nil output for nil input (without error)
|
||||
- Return empty slice for empty input (without error)
|
||||
- For reversible sigils: return the original data before `In` was applied
|
||||
- For irreversible sigils: return the input unchanged (passthrough)
|
||||
|
||||
### 3.3 Transmute Function
|
||||
|
||||
The framework provides a helper function for applying multiple sigils:
|
||||
|
||||
```
|
||||
function Transmute(data: bytes, sigils: Sigil[]) -> (bytes, error):
|
||||
for each sigil in sigils:
|
||||
data, err = sigil.In(data)
|
||||
if err != nil:
|
||||
return nil, err
|
||||
return data, nil
|
||||
```
|
||||
|
||||
## 4. Sigil Categories
|
||||
|
||||
### 4.1 Reversible Sigils
|
||||
|
||||
Reversible sigils can recover the original input from the output.
|
||||
|
||||
**Property**: For any valid input `x`:
|
||||
```
|
||||
sigil.Out(sigil.In(x)) == x
|
||||
```
|
||||
|
||||
Examples:
|
||||
- Encoding sigils (hex, base64)
|
||||
- Compression sigils (gzip)
|
||||
- Encryption sigils (ChaCha20-Poly1305)
|
||||
|
||||
### 4.2 Irreversible Sigils
|
||||
|
||||
Irreversible sigils perform one-way transformations.
|
||||
|
||||
**Property**: The `Out` method returns input unchanged:
|
||||
```
|
||||
sigil.Out(x) == x
|
||||
```
|
||||
|
||||
Examples:
|
||||
- Hash sigils (SHA-256, MD5)
|
||||
- Truncation sigils
|
||||
|
||||
### 4.3 Symmetric Sigils
|
||||
|
||||
Symmetric sigils have identical `In` and `Out` operations.
|
||||
|
||||
**Property**: For any input `x`:
|
||||
```
|
||||
sigil.In(x) == sigil.Out(x)
|
||||
```
|
||||
|
||||
Examples:
|
||||
- Byte reversal
|
||||
- XOR with fixed key
|
||||
- Bitwise NOT
|
||||
|
||||
## 5. Standard Sigils
|
||||
|
||||
### 5.1 Encoding Sigils
|
||||
|
||||
#### 5.1.1 Hex Sigil
|
||||
|
||||
Encodes data to hexadecimal representation.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `hex` |
|
||||
| Category | Reversible |
|
||||
| In | Binary to hex ASCII |
|
||||
| Out | Hex ASCII to binary |
|
||||
| Output expansion | 2x |
|
||||
|
||||
```
|
||||
In("Hello") -> "48656c6c6f"
|
||||
Out("48656c6c6f") -> "Hello"
|
||||
```
|
||||
|
||||
#### 5.1.2 Base64 Sigil
|
||||
|
||||
Encodes data to Base64 representation (RFC 4648).
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `base64` |
|
||||
| Category | Reversible |
|
||||
| In | Binary to Base64 ASCII |
|
||||
| Out | Base64 ASCII to binary |
|
||||
| Output expansion | ~1.33x |
|
||||
|
||||
```
|
||||
In("Hello") -> "SGVsbG8="
|
||||
Out("SGVsbG8=") -> "Hello"
|
||||
```
|
||||
|
||||
### 5.2 Transformation Sigils
|
||||
|
||||
#### 5.2.1 Reverse Sigil
|
||||
|
||||
Reverses the byte order of the data.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `reverse` |
|
||||
| Category | Symmetric |
|
||||
| In | Reverse bytes |
|
||||
| Out | Reverse bytes |
|
||||
| Output expansion | 1x |
|
||||
|
||||
```
|
||||
In("Hello") -> "olleH"
|
||||
Out("olleH") -> "Hello"
|
||||
```
|
||||
|
||||
### 5.3 Compression Sigils
|
||||
|
||||
#### 5.3.1 Gzip Sigil
|
||||
|
||||
Compresses data using gzip (RFC 1952).
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `gzip` |
|
||||
| Category | Reversible |
|
||||
| In | Compress |
|
||||
| Out | Decompress |
|
||||
| Output expansion | Variable (typically < 1x) |
|
||||
|
||||
### 5.4 Formatting Sigils
|
||||
|
||||
#### 5.4.1 JSON Sigil
|
||||
|
||||
Compacts JSON data by removing whitespace.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `json` |
|
||||
| Category | Reversible* |
|
||||
| In | Compact JSON |
|
||||
| Out | Passthrough |
|
||||
|
||||
*Note: Whitespace is not recoverable; Out returns input unchanged.
|
||||
|
||||
#### 5.4.2 JSON-Indent Sigil
|
||||
|
||||
Pretty-prints JSON data with indentation.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `json-indent` |
|
||||
| Category | Reversible* |
|
||||
| In | Indent JSON (2 spaces) |
|
||||
| Out | Passthrough |
|
||||
|
||||
### 5.5 Encryption Sigils
|
||||
|
||||
Encryption sigils provide authenticated encryption using AEAD ciphers.
|
||||
|
||||
#### 5.5.1 ChaCha20-Poly1305 Sigil
|
||||
|
||||
Encrypts data using XChaCha20-Poly1305 authenticated encryption.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | `chacha20poly1305` |
|
||||
| Category | Reversible |
|
||||
| Key size | 32 bytes |
|
||||
| Nonce size | 24 bytes (XChaCha variant) |
|
||||
| Tag size | 16 bytes |
|
||||
| In | Encrypt (generates nonce, prepends to output) |
|
||||
| Out | Decrypt (extracts nonce from input prefix) |
|
||||
|
||||
**Critical Implementation Detail**: The nonce is embedded IN the ciphertext output, not transmitted separately:
|
||||
|
||||
```
|
||||
In(plaintext) -> [24-byte nonce][ciphertext][16-byte tag]
|
||||
Out(ciphertext_with_nonce) -> plaintext
|
||||
```
|
||||
|
||||
**Construction**:
|
||||
|
||||
```go
|
||||
sigil, err := NewChaChaPolySigil(key) // key must be 32 bytes
|
||||
ciphertext, err := sigil.In(plaintext)
|
||||
plaintext, err := sigil.Out(ciphertext)
|
||||
```
|
||||
|
||||
**Security Properties**:
|
||||
- Authenticated: Poly1305 MAC prevents tampering
|
||||
- Confidential: ChaCha20 stream cipher
|
||||
- Nonce uniqueness: Random 24-byte nonce per encryption
|
||||
- No nonce management required by caller
|
||||
|
||||
### 5.6 Hash Sigils
|
||||
|
||||
Hash sigils compute cryptographic digests. They are irreversible.
|
||||
|
||||
| Name | Algorithm | Output Size |
|
||||
|------|-----------|-------------|
|
||||
| `md4` | MD4 | 16 bytes |
|
||||
| `md5` | MD5 | 16 bytes |
|
||||
| `sha1` | SHA-1 | 20 bytes |
|
||||
| `sha224` | SHA-224 | 28 bytes |
|
||||
| `sha256` | SHA-256 | 32 bytes |
|
||||
| `sha384` | SHA-384 | 48 bytes |
|
||||
| `sha512` | SHA-512 | 64 bytes |
|
||||
| `sha3-224` | SHA3-224 | 28 bytes |
|
||||
| `sha3-256` | SHA3-256 | 32 bytes |
|
||||
| `sha3-384` | SHA3-384 | 48 bytes |
|
||||
| `sha3-512` | SHA3-512 | 64 bytes |
|
||||
| `sha512-224` | SHA-512/224 | 28 bytes |
|
||||
| `sha512-256` | SHA-512/256 | 32 bytes |
|
||||
| `ripemd160` | RIPEMD-160 | 20 bytes |
|
||||
| `blake2s-256` | BLAKE2s | 32 bytes |
|
||||
| `blake2b-256` | BLAKE2b | 32 bytes |
|
||||
| `blake2b-384` | BLAKE2b | 48 bytes |
|
||||
| `blake2b-512` | BLAKE2b | 64 bytes |
|
||||
|
||||
For all hash sigils:
|
||||
- `In(data)` returns the hash digest as raw bytes
|
||||
- `Out(data)` returns data unchanged (passthrough)
|
||||
|
||||
## 6. Composition and Chaining
|
||||
|
||||
### 6.1 Forward Chain (Packing)
|
||||
|
||||
Sigils are applied left-to-right:
|
||||
|
||||
```
|
||||
sigils = [gzip, base64, hex]
|
||||
result = Transmute(data, sigils)
|
||||
|
||||
// Equivalent to:
|
||||
result = hex.In(base64.In(gzip.In(data)))
|
||||
```
|
||||
|
||||
### 6.2 Reverse Chain (Unpacking)
|
||||
|
||||
To reverse a chain, apply `Out` in reverse order:
|
||||
|
||||
```
|
||||
function ReverseTransmute(data: bytes, sigils: Sigil[]) -> (bytes, error):
|
||||
for i = length(sigils) - 1 downto 0:
|
||||
data, err = sigils[i].Out(data)
|
||||
if err != nil:
|
||||
return nil, err
|
||||
return data, nil
|
||||
```
|
||||
|
||||
### 6.3 Chain Properties
|
||||
|
||||
For a chain of reversible sigils `[s1, s2, s3]`:
|
||||
|
||||
```
|
||||
original = ReverseTransmute(Transmute(data, [s1, s2, s3]), [s1, s2, s3])
|
||||
// original == data
|
||||
```
|
||||
|
||||
### 6.4 Mixed Chains
|
||||
|
||||
Chains MAY contain both reversible and irreversible sigils:
|
||||
|
||||
```
|
||||
sigils = [gzip, sha256] // sha256 is irreversible
|
||||
|
||||
packed = Transmute(data, sigils)
|
||||
// packed is the SHA-256 hash of gzip-compressed data
|
||||
|
||||
unpacked = ReverseTransmute(packed, sigils)
|
||||
// unpacked == packed (sha256.Out is passthrough)
|
||||
```
|
||||
|
||||
## 7. Error Handling
|
||||
|
||||
### 7.1 Error Categories
|
||||
|
||||
| Category | Description | Recovery |
|
||||
|----------|-------------|----------|
|
||||
| Input error | Invalid input format | Check input validity |
|
||||
| State error | Sigil not properly configured | Initialize sigil |
|
||||
| Resource error | Memory/IO failure | Retry or abort |
|
||||
| Algorithm error | Cryptographic failure | Check keys/params |
|
||||
|
||||
### 7.2 Error Propagation
|
||||
|
||||
Errors MUST propagate immediately:
|
||||
|
||||
```
|
||||
function Transmute(data: bytes, sigils: Sigil[]) -> (bytes, error):
|
||||
for each sigil in sigils:
|
||||
data, err = sigil.In(data)
|
||||
if err != nil:
|
||||
return nil, err // Stop immediately
|
||||
return data, nil
|
||||
```
|
||||
|
||||
### 7.3 Partial Results
|
||||
|
||||
On error, implementations MUST NOT return partial results. Either:
|
||||
- Return complete transformed data, or
|
||||
- Return nil with an error
|
||||
|
||||
## 8. Implementation Guidelines
|
||||
|
||||
### 8.1 Sigil Factory
|
||||
|
||||
Implementations SHOULD provide a factory function:
|
||||
|
||||
```
|
||||
function NewSigil(name: string) -> (Sigil, error):
|
||||
switch name:
|
||||
case "hex": return new HexSigil()
|
||||
case "base64": return new Base64Sigil()
|
||||
case "gzip": return new GzipSigil()
|
||||
// ... etc
|
||||
default: return nil, error("unknown sigil: " + name)
|
||||
```
|
||||
|
||||
### 8.2 Null Safety
|
||||
|
||||
```
|
||||
function In(data: bytes) -> (bytes, error):
|
||||
if data == nil:
|
||||
return nil, nil // NOT an error
|
||||
if length(data) == 0:
|
||||
return [], nil // Empty slice, NOT nil
|
||||
// ... perform transformation
|
||||
```
|
||||
|
||||
### 8.3 Immutability
|
||||
|
||||
Sigils SHOULD NOT modify the input slice:
|
||||
|
||||
```
|
||||
// CORRECT: Create new slice
|
||||
result := make([]byte, len(data))
|
||||
// ... transform into result
|
||||
|
||||
// INCORRECT: Modify in place
|
||||
data[0] = transformed // Don't do this
|
||||
```
|
||||
|
||||
### 8.4 Thread Safety
|
||||
|
||||
Sigils SHOULD be safe for concurrent use:
|
||||
|
||||
- Avoid mutable state in sigil instances
|
||||
- Use synchronization if state is required
|
||||
- Document thread-safety guarantees
|
||||
|
||||
## 9. Security Considerations
|
||||
|
||||
### 9.1 Hash Sigil Security
|
||||
|
||||
- MD4, MD5, SHA1 are cryptographically broken for collision resistance
|
||||
- Use SHA-256 or stronger for security-critical applications
|
||||
- Hash sigils do NOT provide authentication
|
||||
|
||||
### 9.2 Compression Oracle Attacks
|
||||
|
||||
When combining compression and encryption sigils:
|
||||
- Be aware of CRIME/BREACH-style attacks
|
||||
- Do not compress data containing secrets alongside attacker-controlled data
|
||||
|
||||
### 9.3 Memory Safety
|
||||
|
||||
- Validate output buffer sizes before allocation
|
||||
- Implement maximum input size limits
|
||||
- Handle decompression bombs (zip bombs)
|
||||
|
||||
### 9.4 Timing Attacks
|
||||
|
||||
- Comparison operations should be constant-time where security-relevant
|
||||
- Hash comparisons should use constant-time comparison functions
|
||||
|
||||
## 10. Future Work
|
||||
|
||||
- [ ] AES-GCM encryption sigil for environments requiring AES
|
||||
- [ ] Zstd compression sigil with configurable compression levels
|
||||
- [ ] Streaming sigil interface for large data processing
|
||||
- [ ] Sigil metadata interface for reporting transformation properties
|
||||
- [ ] WebAssembly compilation for browser-based sigil operations
|
||||
- [ ] Hardware acceleration detection and utilization
|
||||
|
||||
## 11. References
|
||||
|
||||
- [RFC 4648] The Base16, Base32, and Base64 Data Encodings
|
||||
- [RFC 1952] GZIP file format specification
|
||||
- [RFC 8259] The JavaScript Object Notation (JSON) Data Interchange Format
|
||||
- [FIPS 180-4] Secure Hash Standard
|
||||
- [FIPS 202] SHA-3 Standard
|
||||
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Sigil Name Registry
|
||||
|
||||
| Name | Category | Reversible | Notes |
|
||||
|------|----------|------------|-------|
|
||||
| `reverse` | Transform | Yes (symmetric) | Byte reversal |
|
||||
| `hex` | Encoding | Yes | Hexadecimal |
|
||||
| `base64` | Encoding | Yes | RFC 4648 |
|
||||
| `gzip` | Compression | Yes | RFC 1952 |
|
||||
| `zstd` | Compression | Yes | Zstandard |
|
||||
| `json` | Formatting | Partial | Compacts JSON |
|
||||
| `json-indent` | Formatting | Partial | Pretty-prints JSON |
|
||||
| `chacha20poly1305` | Encryption | Yes | XChaCha20-Poly1305 AEAD |
|
||||
| `md4` | Hash | No | 128-bit |
|
||||
| `md5` | Hash | No | 128-bit |
|
||||
| `sha1` | Hash | No | 160-bit |
|
||||
| `sha224` | Hash | No | 224-bit |
|
||||
| `sha256` | Hash | No | 256-bit |
|
||||
| `sha384` | Hash | No | 384-bit |
|
||||
| `sha512` | Hash | No | 512-bit |
|
||||
| `sha3-*` | Hash | No | SHA-3 family |
|
||||
| `sha512-*` | Hash | No | SHA-512 truncated |
|
||||
| `ripemd160` | Hash | No | 160-bit |
|
||||
| `blake2s-256` | Hash | No | 256-bit |
|
||||
| `blake2b-*` | Hash | No | BLAKE2b family |
|
||||
|
||||
## Appendix B: Reference Implementation
|
||||
|
||||
A reference implementation in Go is available at:
|
||||
- Interface: `github.com/Snider/Enchantrix/pkg/enchantrix/enchantrix.go`
|
||||
- Standard sigils: `github.com/Snider/Enchantrix/pkg/enchantrix/sigils.go`
|
||||
|
||||
## Appendix C: Custom Sigil Example
|
||||
|
||||
```go
|
||||
// ROT13Sigil implements a simple letter rotation cipher.
|
||||
type ROT13Sigil struct{}
|
||||
|
||||
func (s *ROT13Sigil) In(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
result := make([]byte, len(data))
|
||||
for i, b := range data {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
result[i] = 'A' + (b-'A'+13)%26
|
||||
} else if b >= 'a' && b <= 'z' {
|
||||
result[i] = 'a' + (b-'a'+13)%26
|
||||
} else {
|
||||
result[i] = b
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ROT13Sigil) Out(data []byte) ([]byte, error) {
|
||||
return s.In(data) // ROT13 is symmetric
|
||||
}
|
||||
```
|
||||
|
||||
## Appendix D: Changelog
|
||||
|
||||
- **1.0** (2025-01-13): Initial specification
|
||||
433
docs/specs/RFC-010-TRIX-CONTAINER.md
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
# RFC-0002: TRIX Binary Container Format
|
||||
|
||||
**Status:** Standards Track
|
||||
**Version:** 2.0
|
||||
**Created:** 2025-01-13
|
||||
**Author:** Snider
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies the TRIX binary container format, a generic and extensible file format designed to store arbitrary binary payloads alongside structured JSON metadata. The format is protocol-agnostic, supporting any encryption scheme, compression algorithm, or data transformation while providing a consistent structure for metadata discovery and payload extraction.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Terminology](#2-terminology)
|
||||
3. [Format Specification](#3-format-specification)
|
||||
4. [Header Specification](#4-header-specification)
|
||||
5. [Encoding Process](#5-encoding-process)
|
||||
6. [Decoding Process](#6-decoding-process)
|
||||
7. [Checksum Verification](#7-checksum-verification)
|
||||
8. [Magic Number Registry](#8-magic-number-registry)
|
||||
9. [Security Considerations](#9-security-considerations)
|
||||
10. [IANA Considerations](#10-iana-considerations)
|
||||
11. [References](#11-references)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
The TRIX format addresses the need for a simple, self-describing binary container that can wrap any payload type with extensible metadata. Unlike format-specific containers (such as encrypted archive formats), TRIX separates the concerns of:
|
||||
|
||||
- **Container structure**: How data is organized on disk/wire
|
||||
- **Payload semantics**: What the payload contains and how to process it
|
||||
- **Metadata extensibility**: Application-specific attributes
|
||||
|
||||
### 1.1 Design Goals
|
||||
|
||||
- **Simplicity**: Minimal overhead, easy to implement
|
||||
- **Extensibility**: JSON header allows arbitrary metadata
|
||||
- **Protocol-agnostic**: No assumptions about payload encryption or encoding
|
||||
- **Streaming-friendly**: Header length prefix enables streaming reads
|
||||
- **Magic-number customizable**: Applications can define their own identifiers
|
||||
|
||||
### 1.2 Use Cases
|
||||
|
||||
- Encrypted data interchange
|
||||
- Signed document containers
|
||||
- Configuration file packaging
|
||||
- Backup archive format
|
||||
- Inter-service message envelopes
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||
|
||||
**Container**: A complete TRIX-formatted byte sequence
|
||||
**Magic Number**: A 4-byte identifier at the start of the container
|
||||
**Header**: A JSON object containing metadata about the payload
|
||||
**Payload**: The arbitrary binary data stored in the container
|
||||
**Checksum**: An optional integrity verification value
|
||||
|
||||
## 3. Format Specification
|
||||
|
||||
### 3.1 Overview
|
||||
|
||||
A TRIX container consists of five sequential fields:
|
||||
|
||||
```
|
||||
+----------------+---------+---------------+----------------+-----------+
|
||||
| Magic Number | Version | Header Length | JSON Header | Payload |
|
||||
+----------------+---------+---------------+----------------+-----------+
|
||||
| 4 bytes | 1 byte | 4 bytes | Variable | Variable |
|
||||
```
|
||||
|
||||
Total minimum size: 9 bytes (empty header, empty payload)
|
||||
|
||||
### 3.2 Field Definitions
|
||||
|
||||
#### 3.2.1 Magic Number (4 bytes)
|
||||
|
||||
A 4-byte ASCII string identifying the file type. This field:
|
||||
|
||||
- MUST be exactly 4 bytes
|
||||
- SHOULD contain printable ASCII characters
|
||||
- Is application-defined (not mandated by this specification)
|
||||
|
||||
Common conventions:
|
||||
- `TRIX` - Generic TRIX container
|
||||
- First character uppercase, application-specific identifier
|
||||
|
||||
#### 3.2.2 Version (1 byte)
|
||||
|
||||
An unsigned 8-bit integer indicating the format version.
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| 0x00 | Reserved |
|
||||
| 0x01 | Version 1.0 (deprecated) |
|
||||
| 0x02 | Version 2.0 (current) |
|
||||
| 0x03-0xFF | Reserved for future versions |
|
||||
|
||||
Implementations MUST reject containers with unrecognized versions.
|
||||
|
||||
#### 3.2.3 Header Length (4 bytes)
|
||||
|
||||
A 32-bit unsigned integer in big-endian byte order specifying the length of the JSON Header in bytes.
|
||||
|
||||
- Minimum value: 0 (empty header represented as `{}` is 2 bytes, but 0 is valid)
|
||||
- Maximum value: 16,777,215 (16 MB - 1 byte)
|
||||
|
||||
Implementations MUST reject headers exceeding 16 MB to prevent denial-of-service attacks.
|
||||
|
||||
```
|
||||
Header Length = BigEndian32(length_of_json_header_bytes)
|
||||
```
|
||||
|
||||
#### 3.2.4 JSON Header (Variable)
|
||||
|
||||
A UTF-8 encoded JSON object containing metadata. The header:
|
||||
|
||||
- MUST be valid JSON (RFC 8259)
|
||||
- MUST be a JSON object (not array, string, or primitive)
|
||||
- SHOULD use UTF-8 encoding without BOM
|
||||
- MAY be empty (`{}`)
|
||||
|
||||
#### 3.2.5 Payload (Variable)
|
||||
|
||||
The arbitrary binary payload. The payload:
|
||||
|
||||
- MAY be empty (zero bytes)
|
||||
- MAY contain any binary data
|
||||
- Length is implicitly determined by: `container_length - 9 - header_length`
|
||||
|
||||
## 4. Header Specification
|
||||
|
||||
### 4.1 Reserved Header Fields
|
||||
|
||||
The following header fields have defined semantics:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `content_type` | string | MIME type of the payload (before any transformations) |
|
||||
| `checksum` | string | Hex-encoded checksum of the payload |
|
||||
| `checksum_algo` | string | Algorithm used for checksum (e.g., "sha256") |
|
||||
| `created_at` | string | ISO 8601 timestamp of creation |
|
||||
| `encryption_algorithm` | string | Encryption algorithm identifier |
|
||||
| `compression` | string | Compression algorithm identifier |
|
||||
| `sigils` | array | Ordered list of transformation sigil names |
|
||||
|
||||
### 4.2 Extension Fields
|
||||
|
||||
Applications MAY include additional fields. To avoid conflicts:
|
||||
|
||||
- Custom fields SHOULD use a namespace prefix (e.g., `x-myapp-field`)
|
||||
- Standard field names are lowercase with underscores
|
||||
|
||||
### 4.3 Example Headers
|
||||
|
||||
#### Encrypted payload:
|
||||
```json
|
||||
{
|
||||
"content_type": "application/octet-stream",
|
||||
"encryption_algorithm": "xchacha20poly1305",
|
||||
"created_at": "2025-01-13T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Compressed and encoded payload:
|
||||
```json
|
||||
{
|
||||
"content_type": "text/plain",
|
||||
"compression": "gzip",
|
||||
"sigils": ["gzip", "base64"],
|
||||
"checksum": "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e",
|
||||
"checksum_algo": "sha256"
|
||||
}
|
||||
```
|
||||
|
||||
#### Minimal header:
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
## 5. Encoding Process
|
||||
|
||||
### 5.1 Algorithm
|
||||
|
||||
```
|
||||
function Encode(payload: bytes, header: object, magic: string) -> bytes:
|
||||
// Validate magic number
|
||||
if length(magic) != 4:
|
||||
return error("magic number must be 4 bytes")
|
||||
|
||||
// Serialize header to JSON
|
||||
header_bytes = JSON.serialize(header)
|
||||
header_length = length(header_bytes)
|
||||
|
||||
// Validate header size
|
||||
if header_length > 16777215:
|
||||
return error("header exceeds maximum size")
|
||||
|
||||
// Build container
|
||||
container = empty byte buffer
|
||||
|
||||
// Write magic number (4 bytes)
|
||||
container.write(magic)
|
||||
|
||||
// Write version (1 byte)
|
||||
container.write(0x02)
|
||||
|
||||
// Write header length (4 bytes, big-endian)
|
||||
container.write(BigEndian32(header_length))
|
||||
|
||||
// Write JSON header
|
||||
container.write(header_bytes)
|
||||
|
||||
// Write payload
|
||||
container.write(payload)
|
||||
|
||||
return container.bytes()
|
||||
```
|
||||
|
||||
### 5.2 Checksum Integration
|
||||
|
||||
If integrity verification is required:
|
||||
|
||||
```
|
||||
function EncodeWithChecksum(payload: bytes, header: object, magic: string, algo: string) -> bytes:
|
||||
checksum = Hash(algo, payload)
|
||||
header["checksum"] = HexEncode(checksum)
|
||||
header["checksum_algo"] = algo
|
||||
return Encode(payload, header, magic)
|
||||
```
|
||||
|
||||
## 6. Decoding Process
|
||||
|
||||
### 6.1 Algorithm
|
||||
|
||||
```
|
||||
function Decode(container: bytes, expected_magic: string) -> (header: object, payload: bytes):
|
||||
// Validate minimum size
|
||||
if length(container) < 9:
|
||||
return error("container too small")
|
||||
|
||||
// Read and verify magic number
|
||||
magic = container[0:4]
|
||||
if magic != expected_magic:
|
||||
return error("invalid magic number")
|
||||
|
||||
// Read and verify version
|
||||
version = container[4]
|
||||
if version != 0x02:
|
||||
return error("unsupported version")
|
||||
|
||||
// Read header length
|
||||
header_length = BigEndian32(container[5:9])
|
||||
|
||||
// Validate header length
|
||||
if header_length > 16777215:
|
||||
return error("header length exceeds maximum")
|
||||
|
||||
if length(container) < 9 + header_length:
|
||||
return error("container truncated")
|
||||
|
||||
// Read and parse header
|
||||
header_bytes = container[9:9+header_length]
|
||||
header = JSON.parse(header_bytes)
|
||||
|
||||
// Read payload
|
||||
payload = container[9+header_length:]
|
||||
|
||||
return (header, payload)
|
||||
```
|
||||
|
||||
### 6.2 Streaming Decode
|
||||
|
||||
For large files, streaming decode is RECOMMENDED:
|
||||
|
||||
```
|
||||
function StreamDecode(reader: Reader, expected_magic: string) -> (header: object, payload_reader: Reader):
|
||||
// Read fixed-size prefix
|
||||
prefix = reader.read(9)
|
||||
|
||||
// Validate magic and version
|
||||
magic = prefix[0:4]
|
||||
version = prefix[4]
|
||||
header_length = BigEndian32(prefix[5:9])
|
||||
|
||||
// Read header
|
||||
header_bytes = reader.read(header_length)
|
||||
header = JSON.parse(header_bytes)
|
||||
|
||||
// Return remaining reader for payload streaming
|
||||
return (header, reader)
|
||||
```
|
||||
|
||||
## 7. Checksum Verification
|
||||
|
||||
### 7.1 Supported Algorithms
|
||||
|
||||
| Algorithm ID | Output Size | Notes |
|
||||
|--------------|-------------|-------|
|
||||
| `md5` | 16 bytes | NOT RECOMMENDED for security |
|
||||
| `sha1` | 20 bytes | NOT RECOMMENDED for security |
|
||||
| `sha256` | 32 bytes | RECOMMENDED |
|
||||
| `sha384` | 48 bytes | |
|
||||
| `sha512` | 64 bytes | |
|
||||
| `blake2b-256` | 32 bytes | |
|
||||
| `blake2b-512` | 64 bytes | |
|
||||
|
||||
### 7.2 Verification Process
|
||||
|
||||
```
|
||||
function VerifyChecksum(header: object, payload: bytes) -> bool:
|
||||
if "checksum" not in header:
|
||||
return true // No checksum to verify
|
||||
|
||||
algo = header["checksum_algo"]
|
||||
expected = HexDecode(header["checksum"])
|
||||
actual = Hash(algo, payload)
|
||||
|
||||
return constant_time_compare(expected, actual)
|
||||
```
|
||||
|
||||
## 8. Magic Number Registry
|
||||
|
||||
This section defines conventions for magic number allocation:
|
||||
|
||||
### 8.1 Reserved Magic Numbers
|
||||
|
||||
| Magic | Reserved For |
|
||||
|-------|--------------|
|
||||
| `TRIX` | Generic TRIX containers |
|
||||
| `\x00\x00\x00\x00` | Reserved (null) |
|
||||
| `\xFF\xFF\xFF\xFF` | Reserved (test/invalid) |
|
||||
|
||||
### 8.2 Registered Magic Numbers
|
||||
|
||||
The following magic numbers are registered for specific applications:
|
||||
|
||||
| Magic | Application | Description |
|
||||
|-------|-------------|-------------|
|
||||
| `SMSG` | Borg | Encrypted message/media container |
|
||||
| `STIM` | Borg | Encrypted TIM container bundle |
|
||||
| `STMF` | Borg | Secure To-Me Form (encrypted form data) |
|
||||
| `TRIX` | Borg | Encrypted DataNode archive |
|
||||
|
||||
### 8.3 Allocation Guidelines
|
||||
|
||||
Applications SHOULD:
|
||||
|
||||
1. Use 4 printable ASCII characters
|
||||
2. Start with an uppercase letter
|
||||
3. Avoid common file format magic numbers (e.g., `%PDF`, `PK\x03\x04`)
|
||||
4. Register custom magic numbers in their documentation
|
||||
|
||||
## 9. Security Considerations
|
||||
|
||||
### 9.1 Header Injection
|
||||
|
||||
The JSON header is parsed before processing. Implementations MUST:
|
||||
|
||||
- Validate JSON syntax strictly
|
||||
- Reject headers with duplicate keys
|
||||
- Not execute header field values as code
|
||||
|
||||
### 9.2 Denial of Service
|
||||
|
||||
The 16 MB header limit prevents memory exhaustion attacks. Implementations SHOULD:
|
||||
|
||||
- Reject headers before full allocation if length exceeds limit
|
||||
- Implement timeouts for header parsing
|
||||
- Limit recursion depth in JSON parsing
|
||||
|
||||
### 9.3 Path Traversal
|
||||
|
||||
Header fields like `filename` MUST NOT be used directly for filesystem operations without sanitization.
|
||||
|
||||
### 9.4 Checksum Security
|
||||
|
||||
- MD5 and SHA1 checksums provide integrity but not authenticity
|
||||
- For tamper detection, use HMAC or digital signatures
|
||||
- Checksum verification MUST use constant-time comparison
|
||||
|
||||
### 9.5 Version Negotiation
|
||||
|
||||
Implementations MUST NOT attempt to parse containers with unknown versions, as the format may change incompatibly.
|
||||
|
||||
## 10. IANA Considerations
|
||||
|
||||
This document does not require IANA actions. The TRIX format is application-defined and does not use IANA-managed namespaces.
|
||||
|
||||
Future versions may define:
|
||||
- Media type registration (e.g., `application/x-trix`)
|
||||
- Magic number registry
|
||||
|
||||
## 11. Future Work
|
||||
|
||||
- [ ] Media type registration (`application/x-trix`, `application/x-smsg`, etc.)
|
||||
- [ ] Formal magic number registry with registration process
|
||||
- [ ] Streaming encoding/decoding for large payloads
|
||||
- [ ] Header compression for bandwidth-constrained environments
|
||||
- [ ] Sub-container nesting specification (Trix within Trix)
|
||||
|
||||
## 12. References
|
||||
|
||||
- [RFC 8259] The JavaScript Object Notation (JSON) Data Interchange Format
|
||||
- [RFC 2119] Key words for use in RFCs to Indicate Requirement Levels
|
||||
- [RFC 6838] Media Type Specifications and Registration Procedures
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Binary Layout Diagram
|
||||
|
||||
```
|
||||
Byte offset: 0 4 5 9 9+H 9+H+P
|
||||
|---------|----|---------|---------|---------|
|
||||
| Magic | V | HdrLen | Header | Payload |
|
||||
| (4) |(1) | (4) | (H) | (P) |
|
||||
|---------|----|---------|---------|---------|
|
||||
|
||||
V = Version byte
|
||||
H = Header length (from HdrLen field)
|
||||
P = Payload length (remaining bytes)
|
||||
```
|
||||
|
||||
## Appendix B: Reference Implementation
|
||||
|
||||
A reference implementation in Go is available at:
|
||||
`github.com/Snider/Enchantrix/pkg/trix/trix.go`
|
||||
|
||||
## Appendix C: Changelog
|
||||
|
||||
- **2.0** (2025-01-13): Current version with JSON header
|
||||
- **1.0** (deprecated): Initial version with fixed header fields
|
||||
873
docs/specs/RFC-011-OSS-DRM.md
Normal file
|
|
@ -0,0 +1,873 @@
|
|||
# RFC-001: Open Source DRM for Independent Artists
|
||||
|
||||
**Status**: Proposed
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-10
|
||||
**License**: EUPL-1.2
|
||||
|
||||
---
|
||||
|
||||
**Revision History**
|
||||
|
||||
| Date | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| 2026-01-13 | Proposed | **Adaptive Bitrate (ABR)**: HLS-style multi-quality streaming with encrypted variants. New Section 3.7. All Future Work items complete. |
|
||||
| 2026-01-12 | Proposed | **Chunked streaming**: v3 now supports optional ChunkSize for independently decryptable chunks - enables seek, HTTP Range, and decrypt-while-downloading. |
|
||||
| 2026-01-12 | Proposed | **v3 Streaming**: LTHN rolling keys with configurable cadence (daily/12h/6h/1h). CEK wrapping for zero-trust streaming. WASM v1.3.0 with decryptV3(). |
|
||||
| 2026-01-10 | Proposed | Technical review passed. Fixed section numbering (7.x, 8.x, 9.x, 11.x). Updated WASM size to 5.9MB. Implementation verified complete for stated scope. |
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This RFC describes an open-source Digital Rights Management (DRM) system designed for independent artists to distribute encrypted media directly to fans without platform intermediaries. The system uses ChaCha20-Poly1305 authenticated encryption with a "password-as-license" model, enabling zero-trust distribution where the encryption key serves as both the license and the decryption mechanism.
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
### 1.1 The Problem
|
||||
|
||||
Traditional music distribution forces artists into platforms that:
|
||||
- Take 30-70% of revenue (Spotify, Apple Music, Bandcamp)
|
||||
- Control the relationship between artist and fan
|
||||
- Require ongoing subscription for access
|
||||
- Can delist content unilaterally
|
||||
|
||||
Existing DRM systems (Widevine, FairPlay) require:
|
||||
- Platform integration and licensing fees
|
||||
- Centralized key servers
|
||||
- Proprietary implementations
|
||||
- Trust in third parties
|
||||
|
||||
### 1.2 The Solution
|
||||
|
||||
A DRM system where:
|
||||
- **The password IS the license** - no key servers, no escrow
|
||||
- **Artists keep 100%** - sell direct, any payment processor
|
||||
- **Host anywhere** - CDN, IPFS, S3, personal server
|
||||
- **Browser or native** - same encryption, same content
|
||||
- **Open source** - auditable, forkable, community-owned
|
||||
|
||||
## 2. Design Philosophy
|
||||
|
||||
### 2.1 "Honest DRM"
|
||||
|
||||
Traditional DRM operates on a flawed premise: that sufficiently complex technology can prevent copying. History proves otherwise—every DRM system has been broken. The result is systems that:
|
||||
- Punish paying customers with restrictions
|
||||
- Get cracked within days/weeks anyway
|
||||
- Require massive infrastructure (key servers, license servers)
|
||||
- Create single points of failure
|
||||
|
||||
This system embraces a different philosophy: **DRM for honest people**.
|
||||
|
||||
The goal isn't to stop determined pirates (impossible). The goal is:
|
||||
1. Make the legitimate path easy and pleasant
|
||||
2. Make casual sharing slightly inconvenient
|
||||
3. Create a social/economic deterrent (sharing = giving away money)
|
||||
4. Remove all friction for paying customers
|
||||
|
||||
### 2.2 Password-as-License
|
||||
|
||||
The password IS the license. This is not a limitation—it's the core innovation.
|
||||
|
||||
```
|
||||
Traditional DRM:
|
||||
Purchase → License Server → Device Registration → Key Exchange → Playback
|
||||
(5 steps, 3 network calls, 2 points of failure)
|
||||
|
||||
dapp.fm:
|
||||
Purchase → Password → Playback
|
||||
(2 steps, 0 network calls, 0 points of failure)
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- **No accounts** - No email harvesting, no password resets, no data breaches
|
||||
- **No servers** - Artist can disappear; content still works forever
|
||||
- **No revocation anxiety** - You bought it, you own it
|
||||
- **Transferable** - Give your password to a friend (like lending a CD)
|
||||
- **Archival** - Works in 50 years if you have the password
|
||||
|
||||
### 2.3 Encryption as Access Control
|
||||
|
||||
We use military-grade encryption (ChaCha20-Poly1305) not because we need military-grade security, but because:
|
||||
1. It's fast (important for real-time media)
|
||||
2. It's auditable (open standard, RFC 8439)
|
||||
3. It's already implemented everywhere (Go stdlib, browser crypto)
|
||||
4. It provides authenticity (Poly1305 MAC prevents tampering)
|
||||
|
||||
The threat model isn't nation-states—it's casual piracy. The encryption just needs to be "not worth the effort to crack for a $10 album."
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 System Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DISTRIBUTION LAYER │
|
||||
│ CDN / IPFS / S3 / GitHub / Personal Server │
|
||||
│ (Encrypted .smsg files - safe to host anywhere) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PLAYBACK LAYER │
|
||||
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ Browser Demo │ │ Native Desktop App │ │
|
||||
│ │ (WASM) │ │ (Wails + Go) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ┌───────────┐ │ │ ┌───────────────────────┐ │ │
|
||||
│ │ │ stmf.wasm │ │ │ │ Go SMSG Library │ │ │
|
||||
│ │ │ │ │ │ │ (pkg/smsg) │ │ │
|
||||
│ │ │ ChaCha20 │ │ │ │ │ │ │
|
||||
│ │ │ Poly1305 │ │ │ │ ChaCha20-Poly1305 │ │ │
|
||||
│ │ └───────────┘ │ │ └───────────────────────┘ │ │
|
||||
│ └─────────────────┘ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LICENSE LAYER │
|
||||
│ Password = License Key = Decryption Key │
|
||||
│ (Sold via Gumroad, Stripe, PayPal, Crypto, etc.) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 SMSG Container Format
|
||||
|
||||
See: `examples/formats/smsg-format.md`
|
||||
|
||||
Key properties:
|
||||
- **Magic number**: "SMSG" (0x534D5347)
|
||||
- **Algorithm**: ChaCha20-Poly1305 (authenticated encryption)
|
||||
- **Format**: v1 (JSON+base64) or v2 (binary, 25% smaller)
|
||||
- **Compression**: zstd (default), gzip, or none
|
||||
- **Manifest**: Unencrypted metadata (title, artist, license, expiry, links)
|
||||
- **Payload**: Encrypted media with attachments
|
||||
|
||||
#### Format Versions
|
||||
|
||||
| Format | Payload Structure | Size | Speed | Use Case |
|
||||
|--------|------------------|------|-------|----------|
|
||||
| **v1** | JSON with base64-encoded attachments | +33% overhead | Baseline | Legacy |
|
||||
| **v2** | Binary header + raw attachments + zstd | ~Original size | 3-10x faster | Download-to-own |
|
||||
| **v3** | CEK + wrapped keys + rolling LTHN | ~Original size | 3-10x faster | **Streaming** |
|
||||
| **v3+chunked** | v3 with independently decryptable chunks | ~Original size | Seekable | **Chunked streaming** |
|
||||
|
||||
v2 is recommended for download-to-own (perpetual license). v3 is recommended for streaming (time-limited access). v3 with chunking is recommended for large files requiring seek capability or decrypt-while-downloading.
|
||||
|
||||
### 3.3 Key Derivation (v1/v2)
|
||||
|
||||
```
|
||||
License Key (password)
|
||||
│
|
||||
▼
|
||||
SHA-256 Hash
|
||||
│
|
||||
▼
|
||||
32-byte Symmetric Key
|
||||
│
|
||||
▼
|
||||
ChaCha20-Poly1305 Decryption
|
||||
```
|
||||
|
||||
Simple, auditable, no key escrow.
|
||||
|
||||
**Note on password hashing**: SHA-256 is used for simplicity and speed. For high-value content, artists may choose to use stronger KDFs (Argon2, scrypt) in custom implementations. The format supports algorithm negotiation via the header.
|
||||
|
||||
### 3.4 Streaming Key Derivation (v3)
|
||||
|
||||
v3 format uses **LTHN rolling keys** for zero-trust streaming. The platform controls key refresh cadence.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ v3 STREAMING KEY FLOW │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ SERVER (encryption time): │
|
||||
│ ───────────────────────── │
|
||||
│ 1. Generate random CEK (Content Encryption Key) │
|
||||
│ 2. Encrypt content with CEK (one-time) │
|
||||
│ 3. For current period AND next period: │
|
||||
│ streamKey = SHA256(LTHN(period:license:fingerprint)) │
|
||||
│ wrappedKey = ChaCha(CEK, streamKey) │
|
||||
│ 4. Store wrapped keys in header (CEK never transmitted) │
|
||||
│ │
|
||||
│ CLIENT (decryption time): │
|
||||
│ ──────────────────────── │
|
||||
│ 1. Derive streamKey = SHA256(LTHN(period:license:fingerprint)) │
|
||||
│ 2. Try to unwrap CEK from current period key │
|
||||
│ 3. If fails, try next period key │
|
||||
│ 4. Decrypt content with unwrapped CEK │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### LTHN Hash Function
|
||||
|
||||
LTHN is rainbow-table resistant because the salt is derived from the input itself:
|
||||
|
||||
```
|
||||
LTHN(input) = SHA256(input + reverse_leet(input))
|
||||
|
||||
where reverse_leet swaps: o↔0, l↔1, e↔3, a↔4, s↔z, t↔7
|
||||
|
||||
Example:
|
||||
LTHN("2026-01-12:license:fp")
|
||||
= SHA256("2026-01-12:license:fp" + "pf:3zn3ci1:21-10-6202")
|
||||
```
|
||||
|
||||
You cannot compute the hash without knowing the original input.
|
||||
|
||||
#### Cadence Options
|
||||
|
||||
The platform chooses the key refresh rate. Faster cadence = tighter access control.
|
||||
|
||||
| Cadence | Period Format | Rolling Window | Use Case |
|
||||
|---------|---------------|----------------|----------|
|
||||
| `daily` | `2026-01-12` | 24-48 hours | Standard streaming |
|
||||
| `12h` | `2026-01-12-AM/PM` | 12-24 hours | Premium content |
|
||||
| `6h` | `2026-01-12-00/06/12/18` | 6-12 hours | High-value content |
|
||||
| `1h` | `2026-01-12-15` | 1-2 hours | Live events |
|
||||
|
||||
The rolling window ensures smooth key transitions. At any time, both the current period key AND the next period key are valid.
|
||||
|
||||
#### Zero-Trust Properties
|
||||
|
||||
- **Server never stores keys** - Derived on-demand from LTHN
|
||||
- **Keys auto-expire** - No revocation mechanism needed
|
||||
- **Sharing keys is pointless** - They expire within the cadence window
|
||||
- **Fingerprint binds to device** - Different device = different key
|
||||
- **License ties to user** - Different user = different key
|
||||
|
||||
### 3.5 Chunked Streaming (v3 with ChunkSize)
|
||||
|
||||
When `StreamParams.ChunkSize > 0`, v3 format splits content into independently decryptable chunks, enabling:
|
||||
|
||||
- **Decrypt-while-downloading** - Play media as chunks arrive
|
||||
- **HTTP Range requests** - Fetch specific chunks by byte offset
|
||||
- **Seekable playback** - Jump to any position without decrypting previous chunks
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ V3 CHUNKED FORMAT │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Header (cleartext): │
|
||||
│ format: "v3" │
|
||||
│ chunked: { │
|
||||
│ chunkSize: 1048576, // 1MB default │
|
||||
│ totalChunks: N, │
|
||||
│ totalSize: X, // unencrypted total │
|
||||
│ index: [ // for HTTP Range / seeking │
|
||||
│ { offset: 0, size: Y }, │
|
||||
│ { offset: Y, size: Z }, │
|
||||
│ ... │
|
||||
│ ] │
|
||||
│ } │
|
||||
│ wrappedKeys: [...] // same as non-chunked v3 │
|
||||
│ │
|
||||
│ Payload: │
|
||||
│ [chunk 0: nonce + encrypted + tag] │
|
||||
│ [chunk 1: nonce + encrypted + tag] │
|
||||
│ ... │
|
||||
│ [chunk N: nonce + encrypted + tag] │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key insight**: Each chunk is encrypted with the same CEK but gets its own random nonce, making chunks independently decryptable. The chunk index in the header enables:
|
||||
|
||||
1. **Seeking**: Calculate which chunk contains byte offset X, fetch just that chunk
|
||||
2. **Range requests**: Use HTTP Range headers to fetch specific encrypted chunks
|
||||
3. **Streaming**: Decrypt chunk 0 for metadata, then stream chunks 1-N as they arrive
|
||||
|
||||
**Usage example**:
|
||||
```go
|
||||
params := &StreamParams{
|
||||
License: "user-license",
|
||||
Fingerprint: "device-fp",
|
||||
ChunkSize: 1024 * 1024, // 1MB chunks
|
||||
}
|
||||
|
||||
// Encrypt with chunking
|
||||
encrypted, _ := EncryptV3(msg, params, manifest)
|
||||
|
||||
// For streaming playback:
|
||||
header, _ := GetV3Header(encrypted)
|
||||
cek, _ := UnwrapCEKFromHeader(header, params)
|
||||
payload, _ := GetV3Payload(encrypted)
|
||||
|
||||
for i := 0; i < header.Chunked.TotalChunks; i++ {
|
||||
chunk, _ := DecryptV3Chunk(payload, cek, i, header.Chunked)
|
||||
player.Write(chunk) // Stream to audio/video player
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 Supported Content Types
|
||||
|
||||
SMSG is content-agnostic. Any file can be an attachment:
|
||||
|
||||
| Type | MIME | Use Case |
|
||||
|------|------|----------|
|
||||
| Audio | audio/mpeg, audio/flac, audio/wav | Music, podcasts |
|
||||
| Video | video/mp4, video/webm | Music videos, films |
|
||||
| Images | image/png, image/jpeg | Album art, photos |
|
||||
| Documents | application/pdf | Liner notes, lyrics |
|
||||
| Archives | application/zip | Multi-file releases |
|
||||
| Any | application/octet-stream | Anything else |
|
||||
|
||||
Multiple attachments per SMSG are supported (e.g., album + cover art + PDF booklet).
|
||||
|
||||
### 3.7 Adaptive Bitrate Streaming (ABR)
|
||||
|
||||
For large video content, ABR enables automatic quality switching based on network conditions—like HLS/DASH but with ChaCha20-Poly1305 encryption.
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
ABR Manifest (manifest.json)
|
||||
├── Title: "My Video"
|
||||
├── Version: "abr-v1"
|
||||
├── Variants: [1080p, 720p, 480p, 360p]
|
||||
└── DefaultIdx: 1 (720p)
|
||||
|
||||
track-1080p.smsg ──┐
|
||||
track-720p.smsg ──┼── Each is standard v3 chunked SMSG
|
||||
track-480p.smsg ──┤ Same password decrypts ALL variants
|
||||
track-360p.smsg ──┘
|
||||
```
|
||||
|
||||
**ABR Manifest Format:**
|
||||
```json
|
||||
{
|
||||
"version": "abr-v1",
|
||||
"title": "Content Title",
|
||||
"duration": 300,
|
||||
"variants": [
|
||||
{
|
||||
"name": "360p",
|
||||
"bandwidth": 500000,
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"codecs": "avc1.640028,mp4a.40.2",
|
||||
"url": "track-360p.smsg",
|
||||
"chunkCount": 12,
|
||||
"fileSize": 18750000
|
||||
},
|
||||
{
|
||||
"name": "720p",
|
||||
"bandwidth": 2500000,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"codecs": "avc1.640028,mp4a.40.2",
|
||||
"url": "track-720p.smsg",
|
||||
"chunkCount": 48,
|
||||
"fileSize": 93750000
|
||||
}
|
||||
],
|
||||
"defaultIdx": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Bandwidth Estimation Algorithm:**
|
||||
1. Measure download time for each chunk
|
||||
2. Calculate bits per second: `(bytes × 8 × 1000) / timeMs`
|
||||
3. Average last 3 samples for stability
|
||||
4. Apply 80% safety factor to prevent buffering
|
||||
|
||||
**Variant Selection:**
|
||||
```
|
||||
Selected = highest quality where (bandwidth × 0.8) >= variant.bandwidth
|
||||
```
|
||||
|
||||
**Key Properties:**
|
||||
- **Same password for all variants**: CEK unwrapped once, works everywhere
|
||||
- **Chunk-boundary switching**: Clean cuts, no partial chunk issues
|
||||
- **Independent variants**: No cross-file dependencies
|
||||
- **CDN-friendly**: Each variant is a standard file, cacheable separately
|
||||
|
||||
**Creating ABR Content:**
|
||||
```bash
|
||||
# Use mkdemo-abr to create variant set from source video
|
||||
go run ./cmd/mkdemo-abr input.mp4 output-dir/ [password]
|
||||
|
||||
# Output:
|
||||
# output-dir/manifest.json (ABR manifest)
|
||||
# output-dir/track-1080p.smsg (v3 chunked, 5 Mbps)
|
||||
# output-dir/track-720p.smsg (v3 chunked, 2.5 Mbps)
|
||||
# output-dir/track-480p.smsg (v3 chunked, 1 Mbps)
|
||||
# output-dir/track-360p.smsg (v3 chunked, 500 Kbps)
|
||||
```
|
||||
|
||||
**Standard Presets:**
|
||||
|
||||
| Name | Resolution | Bitrate | Use Case |
|
||||
|------|------------|---------|----------|
|
||||
| 1080p | 1920×1080 | 5 Mbps | High quality, fast connections |
|
||||
| 720p | 1280×720 | 2.5 Mbps | Default, most connections |
|
||||
| 480p | 854×480 | 1 Mbps | Mobile, medium connections |
|
||||
| 360p | 640×360 | 500 Kbps | Slow connections, previews |
|
||||
|
||||
## 4. Demo Page Architecture
|
||||
|
||||
**Live Demo**: https://demo.dapp.fm
|
||||
|
||||
### 4.1 Components
|
||||
|
||||
```
|
||||
demo/
|
||||
├── index.html # Single-page application
|
||||
├── stmf.wasm # Go WASM decryption module (~5.9MB)
|
||||
├── wasm_exec.js # Go WASM runtime
|
||||
├── demo-track.smsg # Sample encrypted content (v2/zstd)
|
||||
└── profile-avatar.jpg # Artist avatar
|
||||
```
|
||||
|
||||
### 4.2 UI Modes
|
||||
|
||||
The demo has three modes, accessible via tabs:
|
||||
|
||||
| Mode | Purpose | Default |
|
||||
|------|---------|---------|
|
||||
| **Profile** | Artist landing page with auto-playing content | Yes |
|
||||
| **Fan** | Upload and decrypt purchased .smsg files | No |
|
||||
| **Artist** | Re-key content, create new packages | No |
|
||||
|
||||
### 4.3 Profile Mode (Default)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ dapp.fm [Profile] [Fan] [Artist] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Zero-Trust DRM ⚠️ Demo pre-seeded with keys │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ [No Middlemen] [No Fees] [Host Anywhere] [Browser/Native] │
|
||||
├─────────────────┬───────────────────────────────────────────┤
|
||||
│ SIDEBAR │ MAIN CONTENT │
|
||||
│ ┌───────────┐ │ ┌─────────────────────────────────────┐ │
|
||||
│ │ Avatar │ │ │ 🛒 Buy This Track on Beatport │ │
|
||||
│ │ │ │ │ 95%-100%* goes to the artist │ │
|
||||
│ │ Artist │ │ ├─────────────────────────────────────┤ │
|
||||
│ │ Name │ │ │ │ │
|
||||
│ │ │ │ │ VIDEO PLAYER │ │
|
||||
│ │ Links: │ │ │ (auto-starts at 1:08) │ │
|
||||
│ │ Beatport │ │ │ with native controls │ │
|
||||
│ │ Spotify │ │ │ │ │
|
||||
│ │ YouTube │ │ ├─────────────────────────────────────┤ │
|
||||
│ │ etc. │ │ │ About the Artist │ │
|
||||
│ └───────────┘ │ │ (Bio text) │ │
|
||||
│ │ └─────────────────────────────────────┘ │
|
||||
├─────────────────┴───────────────────────────────────────────┤
|
||||
│ GitHub · EUPL-1.2 · Viva La OpenSource 💜 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 Decryption Flow
|
||||
|
||||
```
|
||||
User clicks "Play Demo Track"
|
||||
│
|
||||
▼
|
||||
fetch(demo-track.smsg)
|
||||
│
|
||||
▼
|
||||
Convert to base64 ◄─── CRITICAL: Must handle binary vs text format
|
||||
│ See: examples/failures/001-double-base64-encoding.md
|
||||
▼
|
||||
BorgSMSG.getInfo(base64)
|
||||
│
|
||||
▼
|
||||
Display manifest (title, artist, license)
|
||||
│
|
||||
▼
|
||||
BorgSMSG.decryptStream(base64, password)
|
||||
│
|
||||
▼
|
||||
Create Blob from Uint8Array
|
||||
│
|
||||
▼
|
||||
URL.createObjectURL(blob)
|
||||
│
|
||||
▼
|
||||
<audio> or <video> element plays content
|
||||
```
|
||||
|
||||
### 4.5 Fan Unlock Tab
|
||||
|
||||
Allows fans to:
|
||||
1. Upload any `.smsg` file they purchased
|
||||
2. Enter their license key (password)
|
||||
3. Decrypt and play locally
|
||||
|
||||
No server communication - everything in browser.
|
||||
|
||||
## 5. Artist Portal (License Manager)
|
||||
|
||||
The License Manager (`js/borg-stmf/artist-portal.html`) is the artist-facing tool for creating and issuing licenses.
|
||||
|
||||
### 5.1 Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ARTIST PORTAL │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. Upload Content │
|
||||
│ - Drag/drop audio or video file │
|
||||
│ - Or use demo content for testing │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 2. Define Track List (CD Mastering) │
|
||||
│ - Track titles │
|
||||
│ - Start/end timestamps → chapter markers │
|
||||
│ - Mix types (full, intro, chorus, drop, etc.) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 3. Configure License │
|
||||
│ - Perpetual (own forever) │
|
||||
│ - Rental (time-limited) │
|
||||
│ - Streaming (24h access) │
|
||||
│ - Preview (30 seconds) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 4. Generate License │
|
||||
│ - Auto-generate token or set custom │
|
||||
│ - Token encrypts content with manifest │
|
||||
│ - Download .smsg file │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 5. Distribute │
|
||||
│ - Upload .smsg to CDN/IPFS/S3 │
|
||||
│ - Sell license token via payment processor │
|
||||
│ - Fan receives token, downloads .smsg, plays │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 License Types
|
||||
|
||||
| Type | Duration | Use Case |
|
||||
|------|----------|----------|
|
||||
| **Perpetual** | Forever | Album purchase, own forever |
|
||||
| **Rental** | 7-90 days | Limited edition, seasonal content |
|
||||
| **Streaming** | 24 hours | On-demand streaming model |
|
||||
| **Preview** | 30 seconds | Free samples, try-before-buy |
|
||||
|
||||
### 5.3 Track List as Manifest
|
||||
|
||||
The artist defines tracks like mastering a CD:
|
||||
|
||||
```json
|
||||
{
|
||||
"tracks": [
|
||||
{"title": "Intro", "start": 0, "end": 45, "type": "intro"},
|
||||
{"title": "Main Track", "start": 45, "end": 240, "type": "full"},
|
||||
{"title": "The Drop", "start": 120, "end": 180, "type": "drop"},
|
||||
{"title": "Outro", "start": 240, "end": 300, "type": "outro"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Same master file, different licensed "cuts":
|
||||
- **Full Album**: All tracks, perpetual
|
||||
- **Radio Edit**: Tracks 2-3 only, rental
|
||||
- **DJ Extended**: Loop points enabled, perpetual
|
||||
- **Preview**: First 30 seconds, expires immediately
|
||||
|
||||
### 5.4 Stats Dashboard
|
||||
|
||||
The Artist Portal tracks:
|
||||
- Total licenses issued
|
||||
- Potential revenue (based on entered prices)
|
||||
- 100% cut (reminder: no platform fees)
|
||||
|
||||
## 6. Economic Model
|
||||
|
||||
### 6.1 The Offer
|
||||
|
||||
**Self-host for 0%. Let us host for 5%.**
|
||||
|
||||
That's it. No hidden fees, no per-stream calculations, no "recoupable advances."
|
||||
|
||||
| Option | Cut | What You Get |
|
||||
|--------|-----|--------------|
|
||||
| **Self-host** | 0% | Tools, format, documentation. Host on your own CDN/IPFS/server |
|
||||
| **dapp.fm hosted** | 5% | CDN, player embed, analytics, payment integration |
|
||||
|
||||
Compare to:
|
||||
- Spotify: ~30% of $0.003/stream (you need 300k streams to earn $1000)
|
||||
- Apple Music: ~30%
|
||||
- Bandcamp: ~15-20%
|
||||
- DistroKid: Flat fee but still platform-dependent
|
||||
|
||||
### 6.2 License Key Strategies
|
||||
|
||||
Artists can choose their pricing model:
|
||||
|
||||
**Per-Album License**
|
||||
```
|
||||
Album: "My Greatest Hits"
|
||||
Price: $10
|
||||
License: "MGH-2024-XKCD-7829"
|
||||
→ One password unlocks entire album
|
||||
```
|
||||
|
||||
**Per-Track License**
|
||||
```
|
||||
Track: "Single Release"
|
||||
Price: $1
|
||||
License: "SINGLE-A7B3-C9D2"
|
||||
→ Individual track, individual price
|
||||
```
|
||||
|
||||
**Tiered Licenses**
|
||||
```
|
||||
Standard: $10 → MP3 version
|
||||
Premium: $25 → FLAC + stems + bonus content
|
||||
→ Different passwords, different content
|
||||
```
|
||||
|
||||
**Time-Limited Previews**
|
||||
```
|
||||
Preview license expires in 7 days
|
||||
Full license: permanent
|
||||
→ Manifest contains expiry date
|
||||
```
|
||||
|
||||
### 6.3 License Key Best Practices
|
||||
|
||||
For artists generating license keys:
|
||||
|
||||
```bash
|
||||
# Good: Memorable but unique
|
||||
MGH-2024-XKCD-7829
|
||||
ALBUM-[year]-[random]-[checksum]
|
||||
|
||||
# Good: UUID for automation
|
||||
550e8400-e29b-41d4-a716-446655440000
|
||||
|
||||
# Avoid: Dictionary words (bruteforceable)
|
||||
password123
|
||||
mysecretalbum
|
||||
```
|
||||
|
||||
Recommended entropy: 64+ bits (e.g., 4 random words, or 12+ random alphanumeric)
|
||||
|
||||
### 6.4 No Revocation (By Design)
|
||||
|
||||
**Q: What if someone leaks the password?**
|
||||
|
||||
A: Then they leak it. Same as if someone photocopies a book or rips a CD.
|
||||
|
||||
This is a feature, not a bug:
|
||||
- **No revocation server** = No single point of failure
|
||||
- **No phone home** = Works offline, forever
|
||||
- **Leaked keys** = Social problem, not technical problem
|
||||
|
||||
Mitigation strategies for artists:
|
||||
1. Personalized keys per buyer (track who leaked)
|
||||
2. Watermarked content (forensic tracking)
|
||||
3. Time-limited keys for subscription models
|
||||
4. Social pressure (small community = reputation matters)
|
||||
|
||||
The system optimizes for **happy paying customers**, not **punishing pirates**.
|
||||
|
||||
## 7. Security Model
|
||||
|
||||
### 7.1 Threat Model
|
||||
|
||||
| Threat | Mitigation |
|
||||
|--------|------------|
|
||||
| Man-in-the-middle | Content encrypted at rest; HTTPS for transport |
|
||||
| Key server compromise | No key server - password-derived keys |
|
||||
| Platform deplatforming | Self-hostable, decentralized distribution |
|
||||
| Unauthorized sharing | Economic/social deterrent (password = paid license) |
|
||||
| Memory extraction | Accepted risk - same as any DRM |
|
||||
|
||||
### 7.2 What This System Does NOT Prevent
|
||||
|
||||
- Users sharing their password (same as sharing any license)
|
||||
- Screen recording of playback
|
||||
- Memory dumping of decrypted content
|
||||
|
||||
This is **intentional**. The goal is not unbreakable DRM (which is impossible) but:
|
||||
1. Making casual piracy inconvenient
|
||||
2. Giving artists control of their distribution
|
||||
3. Enabling direct artist-to-fan sales
|
||||
4. Removing platform dependency
|
||||
|
||||
### 7.3 Trust Boundaries
|
||||
|
||||
```
|
||||
TRUSTED UNTRUSTED
|
||||
──────── ─────────
|
||||
User's browser/device Distribution CDN
|
||||
Decryption code (auditable) Payment processor
|
||||
License key (in user's head) Internet transport
|
||||
Local playback Third-party hosting
|
||||
```
|
||||
|
||||
## 8. Implementation Status
|
||||
|
||||
### 8.1 Completed
|
||||
- [x] SMSG format specification (v1, v2, v3)
|
||||
- [x] Go encryption/decryption library (pkg/smsg)
|
||||
- [x] WASM build for browser (pkg/wasm/stmf)
|
||||
- [x] Native desktop app (Wails, cmd/dapp-fm-app)
|
||||
- [x] Demo page with Profile/Fan/Artist modes
|
||||
- [x] License Manager component
|
||||
- [x] Streaming decryption API (v1.2.0)
|
||||
- [x] **v2 binary format** - 25% smaller files
|
||||
- [x] **zstd compression** - 3-10x faster than gzip
|
||||
- [x] **Manifest links** - Artist platform links in metadata
|
||||
- [x] **Live demo** - https://demo.dapp.fm
|
||||
- [x] RFC-quality demo file with cryptographically secure password
|
||||
- [x] **v3 streaming format** - LTHN rolling keys with CEK wrapping
|
||||
- [x] **Configurable cadence** - daily/12h/6h/1h key rotation
|
||||
- [x] **WASM v1.3.0** - `BorgSMSG.decryptV3()` for streaming
|
||||
- [x] **Chunked streaming** - Independently decryptable chunks for seek/streaming
|
||||
- [x] **Adaptive Bitrate (ABR)** - HLS-style multi-quality streaming with encrypted variants
|
||||
|
||||
### 8.2 Fixed Issues
|
||||
- [x] ~~Double base64 encoding bug~~ - Fixed by using binary format
|
||||
- [x] ~~Demo file format detection~~ - v2 format auto-detected via header
|
||||
- [x] ~~Key wrapping for streaming~~ - Implemented in v3 format
|
||||
|
||||
### 8.3 Future Work
|
||||
- [x] Multi-bitrate adaptive streaming (see Section 3.7 ABR)
|
||||
- [x] Payment integration examples (see `docs/payment-integration.md`)
|
||||
- [x] IPFS distribution guide (see `docs/ipfs-distribution.md`)
|
||||
- [x] Demo page "Streaming" tab for v3 showcase
|
||||
|
||||
## 9. Usage Examples
|
||||
|
||||
### 9.1 Artist Workflow
|
||||
|
||||
```bash
|
||||
# 1. Package your media (uses v2 binary format + zstd by default)
|
||||
go run ./cmd/mkdemo my-track.mp4 my-track.smsg
|
||||
# Output:
|
||||
# Created: my-track.smsg (29220077 bytes)
|
||||
# Master Password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7
|
||||
# Store this password securely - it cannot be recovered!
|
||||
|
||||
# Or programmatically:
|
||||
msg := smsg.NewMessage("Welcome to my album")
|
||||
msg.AddBinaryAttachment("track.mp4", mediaBytes, "video/mp4")
|
||||
manifest := smsg.NewManifest("Track Title")
|
||||
manifest.Artist = "Artist Name"
|
||||
manifest.AddLink("home", "https://linktr.ee/artist")
|
||||
encrypted, _ := smsg.EncryptV2WithManifest(msg, password, manifest)
|
||||
|
||||
# 2. Upload to any hosting
|
||||
aws s3 cp my-track.smsg s3://my-bucket/releases/
|
||||
# or: ipfs add my-track.smsg
|
||||
# or: scp my-track.smsg myserver:/var/www/
|
||||
|
||||
# 3. Sell license keys
|
||||
# Use Gumroad, Stripe, PayPal - any payment method
|
||||
# Deliver the master password on purchase
|
||||
```
|
||||
|
||||
### 9.2 Fan Workflow
|
||||
|
||||
```
|
||||
1. Purchase from artist's website → receive license key
|
||||
2. Download .smsg file from CDN/IPFS/wherever
|
||||
3. Open demo page or native app
|
||||
4. Enter license key
|
||||
5. Content decrypts and plays locally
|
||||
```
|
||||
|
||||
### 9.3 Browser Integration
|
||||
|
||||
```html
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script src="stmf.wasm.js"></script>
|
||||
<script>
|
||||
async function playContent(smsgUrl, licenseKey) {
|
||||
const response = await fetch(smsgUrl);
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
const base64 = arrayToBase64(bytes); // Must be binary→base64
|
||||
|
||||
const msg = await BorgSMSG.decryptStream(base64, licenseKey);
|
||||
|
||||
const blob = new Blob([msg.attachments[0].data], {
|
||||
type: msg.attachments[0].mime
|
||||
});
|
||||
document.querySelector('audio').src = URL.createObjectURL(blob);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 10. Comparison to Existing Solutions
|
||||
|
||||
| Feature | dapp.fm (self) | dapp.fm (hosted) | Spotify | Bandcamp | Widevine |
|
||||
|---------|----------------|------------------|---------|----------|----------|
|
||||
| Artist revenue | **100%** | **95%** | ~30% | ~80% | N/A |
|
||||
| Platform cut | **0%** | **5%** | ~70% | ~15-20% | Varies |
|
||||
| Self-hostable | Yes | Optional | No | No | No |
|
||||
| Open source | Yes | Yes | No | No | No |
|
||||
| Key escrow | None | None | Required | Required | Required |
|
||||
| Browser support | WASM | WASM | Web | Web | CDM |
|
||||
| Offline support | Yes | Yes | Premium | Download | Depends |
|
||||
| Platform lock-in | **None** | **None** | High | Medium | High |
|
||||
| Works if platform dies | **Yes** | **Yes** | No | No | No |
|
||||
|
||||
## 11. Interoperability & Versioning
|
||||
|
||||
### 11.1 Format Versioning
|
||||
|
||||
SMSG includes version and format fields for forward compatibility:
|
||||
|
||||
| Version | Format | Features |
|
||||
|---------|--------|----------|
|
||||
| 1.0 | v1 | ChaCha20-Poly1305, JSON+base64 attachments |
|
||||
| 1.0 | **v2** | Binary attachments, zstd compression (25% smaller, 3-10x faster) |
|
||||
| 1.0 | **v3** | LTHN rolling keys, CEK wrapping, chunked streaming |
|
||||
| 1.0 | **v3+ABR** | Multi-quality variants with adaptive bitrate switching |
|
||||
| 2 (future) | - | Algorithm negotiation, multiple KDFs |
|
||||
|
||||
Decoders MUST reject versions they don't understand. Use v2 for download-to-own, v3 for streaming, v3+ABR for video.
|
||||
|
||||
### 11.2 Third-Party Implementations
|
||||
|
||||
The format is intentionally simple to implement:
|
||||
|
||||
**Minimum Viable Player (any language)**:
|
||||
1. Parse 4-byte magic ("SMSG")
|
||||
2. Read version (2 bytes) and header length (4 bytes)
|
||||
3. Parse JSON header
|
||||
4. SHA-256 hash the password
|
||||
5. ChaCha20-Poly1305 decrypt payload
|
||||
6. Parse JSON payload, extract attachments
|
||||
|
||||
Reference implementations:
|
||||
- Go: `pkg/smsg/` (canonical)
|
||||
- WASM: `pkg/wasm/stmf/` (browser)
|
||||
- (contributions welcome: Rust, Python, JS-native)
|
||||
|
||||
### 11.3 Embedding & Integration
|
||||
|
||||
SMSG files can be:
|
||||
- **Embedded in HTML**: Base64 in data attributes
|
||||
- **Served via API**: JSON wrapper with base64 content
|
||||
- **Bundled in apps**: Compiled into native binaries
|
||||
- **Stored on IPFS**: Content-addressed, immutable
|
||||
- **Distributed via torrents**: Encrypted = safe to share publicly
|
||||
|
||||
The player is embeddable:
|
||||
```html
|
||||
<iframe src="https://dapp.fm/embed/HASH" width="400" height="200"></iframe>
|
||||
```
|
||||
|
||||
## 12. References
|
||||
|
||||
- **Live Demo**: https://demo.dapp.fm
|
||||
- ChaCha20-Poly1305: RFC 8439
|
||||
- zstd compression: https://github.com/klauspost/compress/tree/master/zstd
|
||||
- SMSG Format: `examples/formats/smsg-format.md`
|
||||
- Demo Page Source: `demo/index.html`
|
||||
- WASM Module: `pkg/wasm/stmf/`
|
||||
- Native App: `cmd/dapp-fm-app/`
|
||||
- Demo Creator Tool: `cmd/mkdemo/`
|
||||
- ABR Creator Tool: `cmd/mkdemo-abr/`
|
||||
- ABR Package: `pkg/smsg/abr.go`
|
||||
|
||||
## 13. License
|
||||
|
||||
This specification and implementation are licensed under EUPL-1.2.
|
||||
|
||||
**Viva La OpenSource** 💜
|
||||
480
docs/specs/RFC-012-SMSG-FORMAT.md
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
# RFC-002: SMSG Container Format
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
**Depends On**: RFC-001, RFC-007
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
SMSG (Secure Message) is an encrypted container format using ChaCha20-Poly1305 authenticated encryption. This RFC specifies the binary wire format, versioning, and encoding rules for SMSG files.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
SMSG provides:
|
||||
- Authenticated encryption (ChaCha20-Poly1305)
|
||||
- Public metadata (manifest) readable without decryption
|
||||
- Multiple format versions (v1 legacy, v2 binary, v3 streaming)
|
||||
- Optional chunking for large files and seeking
|
||||
|
||||
## 2. File Structure
|
||||
|
||||
### 2.1 Binary Layout
|
||||
|
||||
```
|
||||
Offset Size Field
|
||||
------ ----- ------------------------------------
|
||||
0 4 Magic: "SMSG" (ASCII)
|
||||
4 2 Version: uint16 little-endian
|
||||
6 3 Header Length: 3-byte big-endian
|
||||
9 N Header JSON (plaintext)
|
||||
9+N M Encrypted Payload
|
||||
```
|
||||
|
||||
### 2.2 Magic Number
|
||||
|
||||
| Format | Value |
|
||||
|--------|-------|
|
||||
| Binary | `0x53 0x4D 0x53 0x47` |
|
||||
| ASCII | `SMSG` |
|
||||
| Base64 (first 6 chars) | `U01TRw` |
|
||||
|
||||
### 2.3 Version Field
|
||||
|
||||
Current version: `0x0001` (1)
|
||||
|
||||
Decoders MUST reject versions they don't understand.
|
||||
|
||||
### 2.4 Header Length
|
||||
|
||||
3 bytes, big-endian unsigned integer. Supports headers up to 16 MB.
|
||||
|
||||
## 3. Header Format (JSON)
|
||||
|
||||
Header is always plaintext (never encrypted), enabling metadata inspection without decryption.
|
||||
|
||||
### 3.1 Base Header
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"algorithm": "chacha20poly1305",
|
||||
"format": "v2",
|
||||
"compression": "zstd",
|
||||
"manifest": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 V3 Header Extensions
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"algorithm": "chacha20poly1305",
|
||||
"format": "v3",
|
||||
"compression": "zstd",
|
||||
"keyMethod": "lthn-rolling",
|
||||
"cadence": "daily",
|
||||
"manifest": { ... },
|
||||
"wrappedKeys": [
|
||||
{"date": "2026-01-13", "wrapped": "<base64>"},
|
||||
{"date": "2026-01-14", "wrapped": "<base64>"}
|
||||
],
|
||||
"chunked": {
|
||||
"chunkSize": 1048576,
|
||||
"totalChunks": 42,
|
||||
"totalSize": 44040192,
|
||||
"index": [
|
||||
{"offset": 0, "size": 1048600},
|
||||
{"offset": 1048600, "size": 1048600}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Header Field Reference
|
||||
|
||||
| Field | Type | Values | Description |
|
||||
|-------|------|--------|-------------|
|
||||
| version | string | "1.0" | Format version string |
|
||||
| algorithm | string | "chacha20poly1305" | Always ChaCha20-Poly1305 |
|
||||
| format | string | "", "v2", "v3" | Payload format version |
|
||||
| compression | string | "", "gzip", "zstd" | Compression algorithm |
|
||||
| keyMethod | string | "", "lthn-rolling" | Key derivation method |
|
||||
| cadence | string | "daily", "12h", "6h", "1h" | Rolling key period (v3) |
|
||||
| manifest | object | - | Content metadata |
|
||||
| wrappedKeys | array | - | CEK wrapped for each period (v3) |
|
||||
| chunked | object | - | Chunk index for seeking (v3) |
|
||||
|
||||
## 4. Manifest Structure
|
||||
|
||||
### 4.1 Complete Manifest
|
||||
|
||||
```go
|
||||
type Manifest struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Artist string `json:"artist,omitempty"`
|
||||
Album string `json:"album,omitempty"`
|
||||
Genre string `json:"genre,omitempty"`
|
||||
Year int `json:"year,omitempty"`
|
||||
ReleaseType string `json:"release_type,omitempty"`
|
||||
Duration int `json:"duration,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
ExpiresAt int64 `json:"expires_at,omitempty"`
|
||||
IssuedAt int64 `json:"issued_at,omitempty"`
|
||||
LicenseType string `json:"license_type,omitempty"`
|
||||
Tracks []Track `json:"tracks,omitempty"`
|
||||
Links map[string]string `json:"links,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
Title string `json:"title"`
|
||||
Start float64 `json:"start"`
|
||||
End float64 `json:"end,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
TrackNum int `json:"track_num,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Manifest Field Reference
|
||||
|
||||
| Field | Type | Range | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| title | string | 0-255 chars | Display name (required for discovery) |
|
||||
| artist | string | 0-255 chars | Creator name |
|
||||
| album | string | 0-255 chars | Album/collection name |
|
||||
| genre | string | 0-255 chars | Genre classification |
|
||||
| year | int | 0-9999 | Release year (0 = unset) |
|
||||
| releaseType | string | enum | "single", "album", "ep", "mix" |
|
||||
| duration | int | 0+ | Total duration in seconds |
|
||||
| format | string | any | Platform format string (e.g., "dapp.fm/v1") |
|
||||
| expiresAt | int64 | 0+ | Unix timestamp (0 = never expires) |
|
||||
| issuedAt | int64 | 0+ | Unix timestamp of license issue |
|
||||
| licenseType | string | enum | "perpetual", "rental", "stream", "preview" |
|
||||
| tracks | []Track | - | Track boundaries for multi-track releases |
|
||||
| links | map | - | Platform name → URL (e.g., "bandcamp" → URL) |
|
||||
| tags | []string | - | Arbitrary string tags |
|
||||
| extra | map | - | Free-form key-value extension data |
|
||||
|
||||
## 5. Format Versions
|
||||
|
||||
### 5.1 Version Comparison
|
||||
|
||||
| Aspect | v1 (Legacy) | v2 (Binary) | v3 (Streaming) |
|
||||
|--------|-------------|-------------|----------------|
|
||||
| Payload Structure | JSON only | Length-prefixed JSON + binary | Same as v2 |
|
||||
| Attachment Encoding | Base64 in JSON | Size field + raw binary | Size field + raw binary |
|
||||
| Compression | None | zstd (default) | zstd (default) |
|
||||
| Key Derivation | SHA256(password) | SHA256(password) | LTHN rolling keys |
|
||||
| Chunked Support | No | No | Yes (optional) |
|
||||
| Size Overhead | ~33% | ~25% | ~15% |
|
||||
| Use Case | Legacy | General purpose | Time-limited streaming |
|
||||
|
||||
### 5.2 V1 Format (Legacy)
|
||||
|
||||
**Payload (after decryption):**
|
||||
|
||||
```json
|
||||
{
|
||||
"body": "Message content",
|
||||
"subject": "Optional subject",
|
||||
"from": "sender@example.com",
|
||||
"to": "recipient@example.com",
|
||||
"timestamp": 1673644800,
|
||||
"attachments": [
|
||||
{
|
||||
"name": "file.bin",
|
||||
"content": "base64encodeddata==",
|
||||
"mime": "application/octet-stream",
|
||||
"size": 1024
|
||||
}
|
||||
],
|
||||
"reply_key": {
|
||||
"public_key": "base64x25519key==",
|
||||
"algorithm": "x25519"
|
||||
},
|
||||
"meta": {
|
||||
"custom_field": "custom_value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Attachments base64-encoded inline in JSON (~33% overhead)
|
||||
- Simple but inefficient for large files
|
||||
|
||||
### 5.3 V2 Format (Binary)
|
||||
|
||||
**Payload structure (after decryption and decompression):**
|
||||
|
||||
```
|
||||
Offset Size Field
|
||||
------ ----- ------------------------------------
|
||||
0 4 Message JSON Length (big-endian uint32)
|
||||
4 N Message JSON (attachments have size only, no content)
|
||||
4+N B1 Attachment 1 raw binary
|
||||
4+N+B1 B2 Attachment 2 raw binary
|
||||
...
|
||||
```
|
||||
|
||||
**Message JSON (within payload):**
|
||||
|
||||
```json
|
||||
{
|
||||
"body": "Message text",
|
||||
"subject": "Subject",
|
||||
"from": "sender",
|
||||
"attachments": [
|
||||
{"name": "file1.bin", "mime": "application/octet-stream", "size": 4096},
|
||||
{"name": "file2.bin", "mime": "image/png", "size": 65536}
|
||||
],
|
||||
"timestamp": 1673644800
|
||||
}
|
||||
```
|
||||
|
||||
- Attachment `content` field omitted; binary data follows JSON
|
||||
- Compressed before encryption
|
||||
- 3-10x faster than v1, ~25% smaller
|
||||
|
||||
### 5.4 V3 Format (Streaming)
|
||||
|
||||
Same payload structure as v2, but with:
|
||||
- LTHN-derived rolling keys instead of password
|
||||
- CEK (Content Encryption Key) wrapped for each time period
|
||||
- Optional chunking for seek support
|
||||
|
||||
**CEK Wrapping:**
|
||||
|
||||
```
|
||||
For each rolling period:
|
||||
streamKey = SHA256(LTHN(period:license:fingerprint))
|
||||
wrappedKey = ChaCha20-Poly1305(CEK, streamKey)
|
||||
```
|
||||
|
||||
**Rolling Periods (cadence):**
|
||||
|
||||
| Cadence | Period Format | Example |
|
||||
|---------|---------------|---------|
|
||||
| daily | YYYY-MM-DD | "2026-01-13" |
|
||||
| 12h | YYYY-MM-DD-AM/PM | "2026-01-13-AM" |
|
||||
| 6h | YYYY-MM-DD-HH | "2026-01-13-00", "2026-01-13-06" |
|
||||
| 1h | YYYY-MM-DD-HH | "2026-01-13-15" |
|
||||
|
||||
### 5.5 V3 Chunked Format
|
||||
|
||||
**Payload (independently decryptable chunks):**
|
||||
|
||||
```
|
||||
Offset Size Content
|
||||
------ ----- ----------------------------------
|
||||
0 1048600 Chunk 0: [24-byte nonce][ciphertext][16-byte tag]
|
||||
1048600 1048600 Chunk 1: [24-byte nonce][ciphertext][16-byte tag]
|
||||
...
|
||||
```
|
||||
|
||||
- Each chunk encrypted separately with same CEK, unique nonce
|
||||
- Enables seeking, HTTP Range requests
|
||||
- Chunk size typically 1MB (configurable)
|
||||
|
||||
## 6. Encryption
|
||||
|
||||
### 6.1 Algorithm
|
||||
|
||||
XChaCha20-Poly1305 (extended nonce variant)
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Key size | 32 bytes |
|
||||
| Nonce size | 24 bytes (XChaCha) |
|
||||
| Tag size | 16 bytes |
|
||||
|
||||
### 6.2 Ciphertext Structure
|
||||
|
||||
```
|
||||
[24-byte XChaCha20 nonce][encrypted data][16-byte Poly1305 tag]
|
||||
```
|
||||
|
||||
**Critical**: Nonces are embedded IN the ciphertext by the Enchantrix library, NOT transmitted separately in headers.
|
||||
|
||||
### 6.3 Key Derivation
|
||||
|
||||
**V1/V2 (Password-based):**
|
||||
|
||||
```go
|
||||
key := sha256.Sum256([]byte(password)) // 32 bytes
|
||||
```
|
||||
|
||||
**V3 (LTHN Rolling):**
|
||||
|
||||
```go
|
||||
// For each period in rolling window:
|
||||
streamKey := sha256.Sum256([]byte(
|
||||
crypt.NewService().Hash(crypt.LTHN, period + ":" + license + ":" + fingerprint)
|
||||
))
|
||||
```
|
||||
|
||||
## 7. Compression
|
||||
|
||||
| Value | Algorithm | Notes |
|
||||
|-------|-----------|-------|
|
||||
| "" (empty) | None | Raw bytes, default for v1 |
|
||||
| "gzip" | RFC 1952 | Stdlib, WASM compatible |
|
||||
| "zstd" | Zstandard | Default for v2/v3, better ratio |
|
||||
|
||||
**Order**: Compress → Encrypt (on write), Decrypt → Decompress (on read)
|
||||
|
||||
## 8. Message Structure
|
||||
|
||||
### 8.1 Go Types
|
||||
|
||||
```go
|
||||
type Message struct {
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
Attachments []Attachment `json:"attachments,omitempty"`
|
||||
ReplyKey *KeyInfo `json:"reply_key,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Name string `json:"name"`
|
||||
Mime string `json:"mime"`
|
||||
Size int `json:"size"`
|
||||
Content string `json:"content,omitempty"` // Base64, v1 only
|
||||
Data []byte `json:"-"` // Binary, v2/v3
|
||||
}
|
||||
|
||||
type KeyInfo struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 Stream Parameters (V3)
|
||||
|
||||
```go
|
||||
type StreamParams struct {
|
||||
License string `json:"license"` // User's license identifier
|
||||
Fingerprint string `json:"fingerprint"` // Device fingerprint (optional)
|
||||
Cadence string `json:"cadence"` // Rolling period: daily, 12h, 6h, 1h
|
||||
ChunkSize int `json:"chunk_size"` // Bytes per chunk (default 1MB)
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Error Handling
|
||||
|
||||
### 9.1 Error Types
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrInvalidMagic = errors.New("invalid SMSG magic")
|
||||
ErrInvalidPayload = errors.New("invalid SMSG payload")
|
||||
ErrDecryptionFailed = errors.New("decryption failed (wrong password?)")
|
||||
ErrPasswordRequired = errors.New("password is required")
|
||||
ErrEmptyMessage = errors.New("message cannot be empty")
|
||||
ErrStreamKeyExpired = errors.New("stream key expired (outside rolling window)")
|
||||
ErrNoValidKey = errors.New("no valid wrapped key found for current date")
|
||||
ErrLicenseRequired = errors.New("license is required for stream decryption")
|
||||
)
|
||||
```
|
||||
|
||||
### 9.2 Error Conditions
|
||||
|
||||
| Error | Cause | Recovery |
|
||||
|-------|-------|----------|
|
||||
| ErrInvalidMagic | File magic is not "SMSG" | Verify file format |
|
||||
| ErrInvalidPayload | Corrupted payload structure | Re-download or restore |
|
||||
| ErrDecryptionFailed | Wrong password or corrupted | Try correct password |
|
||||
| ErrPasswordRequired | Empty password provided | Provide password |
|
||||
| ErrStreamKeyExpired | Time outside rolling window | Wait for valid period or update file |
|
||||
| ErrNoValidKey | No wrapped key for current period | License/fingerprint mismatch |
|
||||
| ErrLicenseRequired | Empty StreamParams.License | Provide license identifier |
|
||||
|
||||
## 10. Constants
|
||||
|
||||
```go
|
||||
const Magic = "SMSG" // 4 ASCII bytes
|
||||
const Version = "1.0" // String version identifier
|
||||
const DefaultChunkSize = 1024 * 1024 // 1 MB
|
||||
|
||||
const FormatV1 = "" // Legacy JSON format
|
||||
const FormatV2 = "v2" // Binary format
|
||||
const FormatV3 = "v3" // Streaming with rolling keys
|
||||
|
||||
const KeyMethodDirect = "" // Password-direct (v1/v2)
|
||||
const KeyMethodLTHNRolling = "lthn-rolling" // LTHN rolling (v3)
|
||||
|
||||
const CompressionNone = ""
|
||||
const CompressionGzip = "gzip"
|
||||
const CompressionZstd = "zstd"
|
||||
|
||||
const CadenceDaily = "daily"
|
||||
const CadenceHalfDay = "12h"
|
||||
const CadenceQuarter = "6h"
|
||||
const CadenceHourly = "1h"
|
||||
```
|
||||
|
||||
## 11. API Usage
|
||||
|
||||
### 11.1 V1 (Legacy)
|
||||
|
||||
```go
|
||||
msg := NewMessage("Hello").WithSubject("Test")
|
||||
encrypted, _ := Encrypt(msg, "password")
|
||||
decrypted, _ := Decrypt(encrypted, "password")
|
||||
```
|
||||
|
||||
### 11.2 V2 (Binary)
|
||||
|
||||
```go
|
||||
msg := NewMessage("Hello").AddBinaryAttachment("file.bin", data, "application/octet-stream")
|
||||
manifest := NewManifest("My Content")
|
||||
encrypted, _ := EncryptV2WithManifest(msg, "password", manifest)
|
||||
decrypted, _ := Decrypt(encrypted, "password")
|
||||
```
|
||||
|
||||
### 11.3 V3 (Streaming)
|
||||
|
||||
```go
|
||||
msg := NewMessage("Stream content")
|
||||
params := &StreamParams{
|
||||
License: "user-license",
|
||||
Fingerprint: "device-fingerprint",
|
||||
Cadence: CadenceDaily,
|
||||
ChunkSize: 1048576,
|
||||
}
|
||||
manifest := NewManifest("Stream Track")
|
||||
manifest.LicenseType = "stream"
|
||||
encrypted, _ := EncryptV3(msg, params, manifest)
|
||||
decrypted, header, _ := DecryptV3(encrypted, params)
|
||||
```
|
||||
|
||||
## 12. Implementation Reference
|
||||
|
||||
- Types: `pkg/smsg/types.go`
|
||||
- Encryption: `pkg/smsg/smsg.go`
|
||||
- Streaming: `pkg/smsg/stream.go`
|
||||
- WASM: `pkg/wasm/stmf/main.go`
|
||||
- Tests: `pkg/smsg/*_test.go`
|
||||
|
||||
## 13. Security Considerations
|
||||
|
||||
1. **Nonce uniqueness**: Enchantrix generates random 24-byte nonces automatically
|
||||
2. **Key entropy**: Passwords should have 64+ bits entropy (no key stretching)
|
||||
3. **Manifest exposure**: Manifest is public; never include sensitive data
|
||||
4. **Constant-time crypto**: Enchantrix uses constant-time comparison for auth tags
|
||||
5. **Rolling window**: V3 keys valid for current + next period only
|
||||
|
||||
## 14. Future Work
|
||||
|
||||
- [ ] Key stretching (Argon2 option)
|
||||
- [ ] Multi-recipient encryption
|
||||
- [ ] Streaming API with ReadableStream
|
||||
- [ ] Hardware key support (WebAuthn)
|
||||
326
docs/specs/RFC-013-DATANODE.md
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
# RFC-003: DataNode In-Memory Filesystem
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
DataNode is an in-memory filesystem abstraction implementing Go's `fs.FS` interface. It provides the foundation for collecting, manipulating, and serializing file trees without touching disk.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
DataNode serves as the core data structure for:
|
||||
- Collecting files from various sources (GitHub, websites, PWAs)
|
||||
- Building container filesystems (TIM rootfs)
|
||||
- Serializing to/from tar archives
|
||||
- Encrypting as TRIX format
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
### 2.1 Core Type
|
||||
|
||||
```go
|
||||
type DataNode struct {
|
||||
files map[string]*dataFile
|
||||
}
|
||||
|
||||
type dataFile struct {
|
||||
name string
|
||||
content []byte
|
||||
modTime time.Time
|
||||
}
|
||||
```
|
||||
|
||||
**Key insight**: DataNode uses a **flat key-value map**, not a nested tree structure. Paths are stored as keys directly, and directories are implicit (derived from path prefixes).
|
||||
|
||||
### 2.2 fs.FS Implementation
|
||||
|
||||
DataNode implements these interfaces:
|
||||
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `fs.FS` | `Open(name string)` | Returns fs.File for path |
|
||||
| `fs.StatFS` | `Stat(name string)` | Returns fs.FileInfo |
|
||||
| `fs.ReadDirFS` | `ReadDir(name string)` | Lists directory contents |
|
||||
|
||||
### 2.3 Internal Helper Types
|
||||
|
||||
```go
|
||||
// File metadata
|
||||
type dataFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
modTime time.Time
|
||||
}
|
||||
func (fi *dataFileInfo) Mode() fs.FileMode { return 0444 } // Read-only
|
||||
|
||||
// Directory metadata
|
||||
type dirInfo struct {
|
||||
name string
|
||||
}
|
||||
func (di *dirInfo) Mode() fs.FileMode { return fs.ModeDir | 0555 }
|
||||
|
||||
// File reader (implements fs.File)
|
||||
type dataFileReader struct {
|
||||
info *dataFileInfo
|
||||
reader *bytes.Reader
|
||||
}
|
||||
|
||||
// Directory reader (implements fs.File)
|
||||
type dirFile struct {
|
||||
info *dirInfo
|
||||
entries []fs.DirEntry
|
||||
offset int
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Operations
|
||||
|
||||
### 3.1 Construction
|
||||
|
||||
```go
|
||||
// Create empty DataNode
|
||||
node := datanode.New()
|
||||
|
||||
// Returns: &DataNode{files: make(map[string]*dataFile)}
|
||||
```
|
||||
|
||||
### 3.2 Adding Files
|
||||
|
||||
```go
|
||||
// Add file with content
|
||||
node.AddData("path/to/file.txt", []byte("content"))
|
||||
|
||||
// Trailing slashes are ignored (treated as directory indicator)
|
||||
node.AddData("path/to/dir/", []byte("")) // Stored as "path/to/dir"
|
||||
```
|
||||
|
||||
**Note**: Parent directories are NOT explicitly created. They are implicit based on path prefixes.
|
||||
|
||||
### 3.3 File Access
|
||||
|
||||
```go
|
||||
// Open file (fs.FS interface)
|
||||
f, err := node.Open("path/to/file.txt")
|
||||
if err != nil {
|
||||
// fs.ErrNotExist if not found
|
||||
}
|
||||
defer f.Close()
|
||||
content, _ := io.ReadAll(f)
|
||||
|
||||
// Stat file
|
||||
info, err := node.Stat("path/to/file.txt")
|
||||
// info.Name(), info.Size(), info.ModTime(), info.Mode()
|
||||
|
||||
// Read directory
|
||||
entries, err := node.ReadDir("path/to")
|
||||
for _, entry := range entries {
|
||||
// entry.Name(), entry.IsDir(), entry.Type()
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Walking
|
||||
|
||||
```go
|
||||
err := fs.WalkDir(node, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
// Process file
|
||||
}
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
## 4. Path Semantics
|
||||
|
||||
### 4.1 Path Handling
|
||||
|
||||
- **Leading slashes stripped**: `/path/file` → `path/file`
|
||||
- **Trailing slashes ignored**: `path/dir/` → `path/dir`
|
||||
- **Forward slashes only**: Uses `/` regardless of OS
|
||||
- **Case-sensitive**: `File.txt` ≠ `file.txt`
|
||||
- **Direct lookup**: Paths stored as flat keys
|
||||
|
||||
### 4.2 Valid Paths
|
||||
|
||||
```
|
||||
file.txt → stored as "file.txt"
|
||||
dir/file.txt → stored as "dir/file.txt"
|
||||
/absolute/path → stored as "absolute/path" (leading / stripped)
|
||||
path/to/dir/ → stored as "path/to/dir" (trailing / stripped)
|
||||
```
|
||||
|
||||
### 4.3 Directory Detection
|
||||
|
||||
Directories are **implicit**. A directory exists if:
|
||||
1. Any file path has it as a prefix
|
||||
2. Example: Adding `a/b/c.txt` implicitly creates directories `a` and `a/b`
|
||||
|
||||
```go
|
||||
// ReadDir finds directories by scanning all paths
|
||||
func (dn *DataNode) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
// Scans all keys for matching prefix
|
||||
// Returns unique immediate children
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Tar Serialization
|
||||
|
||||
### 5.1 ToTar
|
||||
|
||||
```go
|
||||
tarBytes, err := node.ToTar()
|
||||
```
|
||||
|
||||
**Format**:
|
||||
- All files written as `tar.TypeReg` (regular files)
|
||||
- Header Mode: **0600** (fixed, not original mode)
|
||||
- No explicit directory entries
|
||||
- ModTime preserved from dataFile
|
||||
|
||||
```go
|
||||
// Serialization logic
|
||||
for path, file := range dn.files {
|
||||
header := &tar.Header{
|
||||
Name: path,
|
||||
Mode: 0600, // Fixed mode
|
||||
Size: int64(len(file.content)),
|
||||
ModTime: file.modTime,
|
||||
Typeflag: tar.TypeReg,
|
||||
}
|
||||
tw.WriteHeader(header)
|
||||
tw.Write(file.content)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 FromTar
|
||||
|
||||
```go
|
||||
node, err := datanode.FromTar(tarBytes)
|
||||
```
|
||||
|
||||
**Parsing**:
|
||||
- Only reads `tar.TypeReg` entries
|
||||
- Ignores directory entries (`tar.TypeDir`)
|
||||
- Stores path and content in flat map
|
||||
|
||||
```go
|
||||
// Deserialization logic
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
content, _ := io.ReadAll(tr)
|
||||
dn.files[header.Name] = &dataFile{
|
||||
name: filepath.Base(header.Name),
|
||||
content: content,
|
||||
modTime: header.ModTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Compressed Variants
|
||||
|
||||
```go
|
||||
// gzip compressed
|
||||
tarGz, err := node.ToTarGz()
|
||||
node, err := datanode.FromTarGz(tarGzBytes)
|
||||
|
||||
// xz compressed
|
||||
tarXz, err := node.ToTarXz()
|
||||
node, err := datanode.FromTarXz(tarXzBytes)
|
||||
```
|
||||
|
||||
## 6. File Modes
|
||||
|
||||
| Context | Mode | Notes |
|
||||
|---------|------|-------|
|
||||
| File read (fs.FS) | 0444 | Read-only for all |
|
||||
| Directory (fs.FS) | 0555 | Read+execute for all |
|
||||
| Tar export | 0600 | Owner read/write only |
|
||||
|
||||
**Note**: Original file modes are NOT preserved. All files get fixed modes.
|
||||
|
||||
## 7. Memory Model
|
||||
|
||||
- All content held in memory as `[]byte`
|
||||
- No lazy loading
|
||||
- No memory mapping
|
||||
- Thread-safe for concurrent reads (map is not mutated after creation)
|
||||
|
||||
### 7.1 Size Calculation
|
||||
|
||||
```go
|
||||
func (dn *DataNode) Size() int64 {
|
||||
var total int64
|
||||
for _, f := range dn.files {
|
||||
total += int64(len(f.content))
|
||||
}
|
||||
return total
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Integration Points
|
||||
|
||||
### 8.1 TIM RootFS
|
||||
|
||||
```go
|
||||
tim := &tim.TIM{
|
||||
Config: configJSON,
|
||||
RootFS: datanode, // DataNode as container filesystem
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 TRIX Encryption
|
||||
|
||||
```go
|
||||
// Encrypt DataNode to TRIX
|
||||
encrypted, err := trix.Encrypt(datanode.ToTar(), password)
|
||||
|
||||
// Decrypt TRIX to DataNode
|
||||
tarBytes, err := trix.Decrypt(encrypted, password)
|
||||
node, err := datanode.FromTar(tarBytes)
|
||||
```
|
||||
|
||||
### 8.3 Collectors
|
||||
|
||||
```go
|
||||
// GitHub collector returns DataNode
|
||||
node, err := github.CollectRepo(url)
|
||||
|
||||
// Website collector returns DataNode
|
||||
node, err := website.Collect(url, depth)
|
||||
```
|
||||
|
||||
## 9. Implementation Reference
|
||||
|
||||
- Source: `pkg/datanode/datanode.go`
|
||||
- Tests: `pkg/datanode/datanode_test.go`
|
||||
|
||||
## 10. Security Considerations
|
||||
|
||||
1. **Path traversal**: Leading slashes stripped; no `..` handling needed (flat map)
|
||||
2. **Memory exhaustion**: No built-in limits; caller must validate input size
|
||||
3. **Tar bombs**: FromTar reads all entries into memory
|
||||
4. **Symlinks**: Not supported (intentional - tar.TypeReg only)
|
||||
|
||||
## 11. Limitations
|
||||
|
||||
- No symlink support
|
||||
- No extended attributes
|
||||
- No sparse files
|
||||
- Fixed file modes (0600 on export)
|
||||
- No streaming (full content in memory)
|
||||
|
||||
## 12. Future Work
|
||||
|
||||
- [ ] Streaming tar generation for large files
|
||||
- [ ] Optional mode preservation
|
||||
- [ ] Size limits for untrusted input
|
||||
- [ ] Lazy loading for large datasets
|
||||
330
docs/specs/RFC-014-TIM.md
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
# RFC-004: Terminal Isolation Matrix (TIM)
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
**Depends On**: RFC-003
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
TIM (Terminal Isolation Matrix) is an OCI-compatible container bundle format. It packages a runtime configuration with a root filesystem (DataNode) for execution via runc or compatible runtimes.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
TIM provides:
|
||||
- OCI runtime-spec compatible bundles
|
||||
- Portable container packaging
|
||||
- Integration with DataNode filesystem
|
||||
- Encryption via STIM (RFC-005)
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
### 2.1 Core Type
|
||||
|
||||
```go
|
||||
// pkg/tim/tim.go:28-32
|
||||
type TerminalIsolationMatrix struct {
|
||||
Config []byte // Raw OCI runtime specification (JSON)
|
||||
RootFS *datanode.DataNode // In-memory filesystem
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Error Variables
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrDataNodeRequired = errors.New("datanode is required")
|
||||
ErrConfigIsNil = errors.New("config is nil")
|
||||
ErrPasswordRequired = errors.New("password is required for encryption")
|
||||
ErrInvalidStimPayload = errors.New("invalid stim payload")
|
||||
ErrDecryptionFailed = errors.New("decryption failed (wrong password?)")
|
||||
)
|
||||
```
|
||||
|
||||
## 3. Public API
|
||||
|
||||
### 3.1 Constructors
|
||||
|
||||
```go
|
||||
// Create empty TIM with default config
|
||||
func New() (*TerminalIsolationMatrix, error)
|
||||
|
||||
// Wrap existing DataNode into TIM
|
||||
func FromDataNode(dn *DataNode) (*TerminalIsolationMatrix, error)
|
||||
|
||||
// Deserialize from tar archive
|
||||
func FromTar(data []byte) (*TerminalIsolationMatrix, error)
|
||||
```
|
||||
|
||||
### 3.2 Serialization
|
||||
|
||||
```go
|
||||
// Serialize to tar archive
|
||||
func (m *TerminalIsolationMatrix) ToTar() ([]byte, error)
|
||||
|
||||
// Encrypt to STIM format (ChaCha20-Poly1305)
|
||||
func (m *TerminalIsolationMatrix) ToSigil(password string) ([]byte, error)
|
||||
```
|
||||
|
||||
### 3.3 Decryption
|
||||
|
||||
```go
|
||||
// Decrypt from STIM format
|
||||
func FromSigil(data []byte, password string) (*TerminalIsolationMatrix, error)
|
||||
```
|
||||
|
||||
### 3.4 Execution
|
||||
|
||||
```go
|
||||
// Run plain .tim file with runc
|
||||
func Run(timPath string) error
|
||||
|
||||
// Decrypt and run .stim file
|
||||
func RunEncrypted(stimPath, password string) error
|
||||
```
|
||||
|
||||
## 4. Tar Archive Structure
|
||||
|
||||
### 4.1 Layout
|
||||
|
||||
```
|
||||
config.json (root level, mode 0600)
|
||||
rootfs/ (directory, mode 0755)
|
||||
rootfs/bin/app (files within rootfs/)
|
||||
rootfs/etc/config
|
||||
...
|
||||
```
|
||||
|
||||
### 4.2 Serialization (ToTar)
|
||||
|
||||
```go
|
||||
// pkg/tim/tim.go:111-195
|
||||
func (m *TerminalIsolationMatrix) ToTar() ([]byte, error) {
|
||||
// 1. Write config.json header (size = len(m.Config), mode 0600)
|
||||
// 2. Write config.json content
|
||||
// 3. Write rootfs/ directory entry (TypeDir, mode 0755)
|
||||
// 4. Walk m.RootFS depth-first
|
||||
// 5. For each file: tar entry with name "rootfs/" + path, mode 0600
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Deserialization (FromTar)
|
||||
|
||||
```go
|
||||
func FromTar(data []byte) (*TerminalIsolationMatrix, error) {
|
||||
// 1. Parse tar entries
|
||||
// 2. "config.json" → stored as raw bytes in Config
|
||||
// 3. "rootfs/*" prefix → stripped and added to DataNode
|
||||
// 4. Error if config.json missing (ErrConfigIsNil)
|
||||
}
|
||||
```
|
||||
|
||||
## 5. OCI Config
|
||||
|
||||
### 5.1 Default Config
|
||||
|
||||
The `New()` function creates a TIM with a default config from `pkg/tim/config.go`:
|
||||
|
||||
```go
|
||||
func defaultConfig() (*trix.Trix, error) {
|
||||
return &trix.Trix{Header: make(map[string]interface{})}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The default config is minimal. Applications should populate the Config field with a proper OCI runtime spec.
|
||||
|
||||
### 5.2 OCI Runtime Spec Example
|
||||
|
||||
```json
|
||||
{
|
||||
"ociVersion": "1.0.2",
|
||||
"process": {
|
||||
"terminal": false,
|
||||
"user": {"uid": 0, "gid": 0},
|
||||
"args": ["/bin/app"],
|
||||
"env": ["PATH=/usr/bin:/bin"],
|
||||
"cwd": "/"
|
||||
},
|
||||
"root": {
|
||||
"path": "rootfs",
|
||||
"readonly": true
|
||||
},
|
||||
"mounts": [],
|
||||
"linux": {
|
||||
"namespaces": [
|
||||
{"type": "pid"},
|
||||
{"type": "network"},
|
||||
{"type": "mount"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Execution Flow
|
||||
|
||||
### 6.1 Plain TIM (Run)
|
||||
|
||||
```go
|
||||
// pkg/tim/run.go:18-74
|
||||
func Run(timPath string) error {
|
||||
// 1. Create temporary directory (borg-run-*)
|
||||
// 2. Extract tar entry-by-entry
|
||||
// - Security: Path traversal check (prevents ../)
|
||||
// - Validates: target = Clean(target) within tempDir
|
||||
// 3. Create directories as needed (0755)
|
||||
// 4. Write files with 0600 permissions
|
||||
// 5. Execute: runc run -b <tempDir> borg-container
|
||||
// 6. Stream stdout/stderr directly
|
||||
// 7. Return exit code
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Encrypted TIM (RunEncrypted)
|
||||
|
||||
```go
|
||||
// pkg/tim/run.go:79-134
|
||||
func RunEncrypted(stimPath, password string) error {
|
||||
// 1. Read encrypted .stim file
|
||||
// 2. Decrypt using FromSigil() with password
|
||||
// 3. Create temporary directory (borg-run-*)
|
||||
// 4. Write config.json to tempDir
|
||||
// 5. Create rootfs/ subdirectory
|
||||
// 6. Walk DataNode and extract all files to rootfs/
|
||||
// - Uses CopyFile() with 0600 permissions
|
||||
// 7. Execute: runc run -b <tempDir> borg-container
|
||||
// 8. Stream stdout/stderr
|
||||
// 9. Clean up temp directory (defer os.RemoveAll)
|
||||
// 10. Return exit code
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Security Controls
|
||||
|
||||
| Control | Implementation |
|
||||
|---------|----------------|
|
||||
| Path traversal | `filepath.Clean()` + prefix validation |
|
||||
| Temp cleanup | `defer os.RemoveAll(tempDir)` |
|
||||
| File permissions | Hardcoded 0600 (files), 0755 (dirs) |
|
||||
| Test injection | `ExecCommand` variable for mocking runc |
|
||||
|
||||
## 7. Cache API
|
||||
|
||||
### 7.1 Cache Structure
|
||||
|
||||
```go
|
||||
// pkg/tim/cache.go
|
||||
type Cache struct {
|
||||
Dir string // Directory path for storage
|
||||
Password string // Shared password for all TIMs
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Cache Operations
|
||||
|
||||
```go
|
||||
// Create cache with master password
|
||||
func NewCache(dir, password string) (*Cache, error)
|
||||
|
||||
// Store TIM (encrypted automatically as .stim)
|
||||
func (c *Cache) Store(name string, m *TerminalIsolationMatrix) error
|
||||
|
||||
// Load TIM (decrypted automatically)
|
||||
func (c *Cache) Load(name string) (*TerminalIsolationMatrix, error)
|
||||
|
||||
// Delete cached TIM
|
||||
func (c *Cache) Delete(name string) error
|
||||
|
||||
// Check if TIM exists
|
||||
func (c *Cache) Exists(name string) bool
|
||||
|
||||
// List all cached TIM names
|
||||
func (c *Cache) List() ([]string, error)
|
||||
|
||||
// Load and execute cached TIM
|
||||
func (c *Cache) Run(name string) error
|
||||
|
||||
// Get file size of cached .stim
|
||||
func (c *Cache) Size(name string) (int64, error)
|
||||
```
|
||||
|
||||
### 7.3 Cache Directory Structure
|
||||
|
||||
```
|
||||
cache/
|
||||
├── mycontainer.stim (encrypted)
|
||||
├── another.stim (encrypted)
|
||||
└── ...
|
||||
```
|
||||
|
||||
- All TIMs stored as `.stim` files (encrypted)
|
||||
- Single password protects entire cache
|
||||
- Directory created with 0700 permissions
|
||||
- Files stored with 0600 permissions
|
||||
|
||||
## 8. CLI Usage
|
||||
|
||||
```bash
|
||||
# Compile Borgfile to TIM
|
||||
borg compile -f Borgfile -o container.tim
|
||||
|
||||
# Compile with encryption
|
||||
borg compile -f Borgfile -e "password" -o container.stim
|
||||
|
||||
# Run plain TIM
|
||||
borg run container.tim
|
||||
|
||||
# Run encrypted TIM
|
||||
borg run container.stim -p "password"
|
||||
|
||||
# Decode (extract) to tar
|
||||
borg decode container.stim -p "password" --i-am-in-isolation -o container.tar
|
||||
|
||||
# Inspect metadata without decrypting
|
||||
borg inspect container.stim
|
||||
```
|
||||
|
||||
## 9. Implementation Reference
|
||||
|
||||
- TIM core: `pkg/tim/tim.go`
|
||||
- Execution: `pkg/tim/run.go`
|
||||
- Cache: `pkg/tim/cache.go`
|
||||
- Config: `pkg/tim/config.go`
|
||||
- Tests: `pkg/tim/tim_test.go`, `pkg/tim/run_test.go`, `pkg/tim/cache_test.go`
|
||||
|
||||
## 10. Security Considerations
|
||||
|
||||
1. **Path traversal prevention**: `filepath.Clean()` + prefix validation
|
||||
2. **Permission hardcoding**: 0600 files, 0755 directories
|
||||
3. **Secure cleanup**: `defer os.RemoveAll()` on temp directories
|
||||
4. **Command injection prevention**: `ExecCommand` variable (no shell)
|
||||
5. **Config validation**: Validate OCI spec before execution
|
||||
|
||||
## 11. OCI Compatibility
|
||||
|
||||
TIM bundles are compatible with:
|
||||
- runc
|
||||
- crun
|
||||
- youki
|
||||
- Any OCI runtime-spec 1.0.2 compliant runtime
|
||||
|
||||
## 12. Test Coverage
|
||||
|
||||
| Area | Tests |
|
||||
|------|-------|
|
||||
| TIM creation | DataNode wrapping, default config |
|
||||
| Serialization | Tar round-trips, large files (1MB+) |
|
||||
| Encryption | ToSigil/FromSigil, wrong password detection |
|
||||
| Caching | Store/Load/Delete, List, Size |
|
||||
| Execution | ZIP slip prevention, temp cleanup |
|
||||
| Error handling | Nil DataNode, nil config, invalid tar |
|
||||
|
||||
## 13. Future Work
|
||||
|
||||
- [ ] Image layer support
|
||||
- [ ] Registry push/pull
|
||||
- [ ] Multi-platform bundles
|
||||
- [ ] Signature verification
|
||||
- [ ] Full OCI config generation
|
||||
303
docs/specs/RFC-015-STIM.md
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
# RFC-005: STIM Encrypted Container Format
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
**Depends On**: RFC-003, RFC-004
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
STIM (Secure TIM) is an encrypted container format that wraps TIM bundles using ChaCha20-Poly1305 authenticated encryption. It enables secure distribution and execution of containers without exposing the contents.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
STIM provides:
|
||||
- Encrypted TIM containers
|
||||
- ChaCha20-Poly1305 authenticated encryption
|
||||
- Separate encryption of config and rootfs
|
||||
- Direct execution without persistent decryption
|
||||
|
||||
## 2. Format Name
|
||||
|
||||
**ChaChaPolySigil** - The internal name for the STIM format, using:
|
||||
- ChaCha20-Poly1305 algorithm (via Enchantrix library)
|
||||
- Trix container wrapper with "STIM" magic
|
||||
|
||||
## 3. File Structure
|
||||
|
||||
### 3.1 Container Format
|
||||
|
||||
STIM uses the **Trix container format** from Enchantrix library:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Magic: "STIM" (4 bytes ASCII) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Trix Header (Gob-encoded JSON) │
|
||||
│ - encryption_algorithm: "chacha20poly1305"
|
||||
│ - tim: true │
|
||||
│ - config_size: uint32 │
|
||||
│ - rootfs_size: uint32 │
|
||||
│ - version: "1.0" │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Trix Payload: │
|
||||
│ [config_size: 4 bytes BE uint32] │
|
||||
│ [encrypted config] │
|
||||
│ [encrypted rootfs tar] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Payload Structure
|
||||
|
||||
```
|
||||
Offset Size Field
|
||||
------ ----- ------------------------------------
|
||||
0 4 Config size (big-endian uint32)
|
||||
4 N Encrypted config (includes nonce + tag)
|
||||
4+N M Encrypted rootfs tar (includes nonce + tag)
|
||||
```
|
||||
|
||||
### 3.3 Encrypted Component Format
|
||||
|
||||
Each encrypted component (config and rootfs) follows Enchantrix format:
|
||||
|
||||
```
|
||||
[24-byte XChaCha20 nonce][ciphertext][16-byte Poly1305 tag]
|
||||
```
|
||||
|
||||
**Critical**: Nonces are **embedded in the ciphertext**, not transmitted separately.
|
||||
|
||||
## 4. Encryption
|
||||
|
||||
### 4.1 Algorithm
|
||||
|
||||
XChaCha20-Poly1305 (extended nonce variant)
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Key size | 32 bytes |
|
||||
| Nonce size | 24 bytes (embedded) |
|
||||
| Tag size | 16 bytes |
|
||||
|
||||
### 4.2 Key Derivation
|
||||
|
||||
```go
|
||||
// pkg/trix/trix.go:64-67
|
||||
func DeriveKey(password string) []byte {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return hash[:] // 32 bytes
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Dual Encryption
|
||||
|
||||
Config and RootFS are encrypted **separately** with independent nonces:
|
||||
|
||||
```go
|
||||
// pkg/tim/tim.go:217-232
|
||||
func (m *TerminalIsolationMatrix) ToSigil(password string) ([]byte, error) {
|
||||
// 1. Derive key
|
||||
key := trix.DeriveKey(password)
|
||||
|
||||
// 2. Create sigil
|
||||
sigil, _ := enchantrix.NewChaChaPolySigil(key)
|
||||
|
||||
// 3. Encrypt config (generates fresh nonce automatically)
|
||||
encConfig, _ := sigil.In(m.Config)
|
||||
|
||||
// 4. Serialize rootfs to tar
|
||||
rootfsTar, _ := m.RootFS.ToTar()
|
||||
|
||||
// 5. Encrypt rootfs (generates different fresh nonce)
|
||||
encRootFS, _ := sigil.In(rootfsTar)
|
||||
|
||||
// 6. Build payload
|
||||
payload := make([]byte, 4+len(encConfig)+len(encRootFS))
|
||||
binary.BigEndian.PutUint32(payload[:4], uint32(len(encConfig)))
|
||||
copy(payload[4:4+len(encConfig)], encConfig)
|
||||
copy(payload[4+len(encConfig):], encRootFS)
|
||||
|
||||
// 7. Create Trix container with STIM magic
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale for dual encryption:**
|
||||
- Config can be decrypted separately for inspection
|
||||
- Allows streaming decryption of large rootfs
|
||||
- Independent nonces prevent any nonce reuse
|
||||
|
||||
## 5. Decryption Flow
|
||||
|
||||
```go
|
||||
// pkg/tim/tim.go:255-308
|
||||
func FromSigil(data []byte, password string) (*TerminalIsolationMatrix, error) {
|
||||
// 1. Decode Trix container with magic "STIM"
|
||||
t, _ := trix.Decode(data, "STIM", nil)
|
||||
|
||||
// 2. Derive key from password
|
||||
key := trix.DeriveKey(password)
|
||||
|
||||
// 3. Create sigil
|
||||
sigil, _ := enchantrix.NewChaChaPolySigil(key)
|
||||
|
||||
// 4. Parse payload: extract configSize from first 4 bytes
|
||||
configSize := binary.BigEndian.Uint32(t.Payload[:4])
|
||||
|
||||
// 5. Validate bounds
|
||||
if int(configSize) > len(t.Payload)-4 {
|
||||
return nil, ErrInvalidStimPayload
|
||||
}
|
||||
|
||||
// 6. Extract encrypted components
|
||||
encConfig := t.Payload[4 : 4+configSize]
|
||||
encRootFS := t.Payload[4+configSize:]
|
||||
|
||||
// 7. Decrypt config (nonce auto-extracted by Enchantrix)
|
||||
config, err := sigil.Out(encConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrDecryptionFailed, err)
|
||||
}
|
||||
|
||||
// 8. Decrypt rootfs
|
||||
rootfsTar, err := sigil.Out(encRootFS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrDecryptionFailed, err)
|
||||
}
|
||||
|
||||
// 9. Reconstruct DataNode from tar
|
||||
rootfs, _ := datanode.FromTar(rootfsTar)
|
||||
|
||||
return &TerminalIsolationMatrix{Config: config, RootFS: rootfs}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Trix Header
|
||||
|
||||
```go
|
||||
Header: map[string]interface{}{
|
||||
"encryption_algorithm": "chacha20poly1305",
|
||||
"tim": true,
|
||||
"config_size": len(encConfig),
|
||||
"rootfs_size": len(encRootFS),
|
||||
"version": "1.0",
|
||||
}
|
||||
```
|
||||
|
||||
## 7. CLI Usage
|
||||
|
||||
```bash
|
||||
# Create encrypted container
|
||||
borg compile -f Borgfile -e "password" -o container.stim
|
||||
|
||||
# Run encrypted container
|
||||
borg run container.stim -p "password"
|
||||
|
||||
# Decode (extract) encrypted container
|
||||
borg decode container.stim -p "password" --i-am-in-isolation -o container.tar
|
||||
|
||||
# Inspect without decrypting (shows header metadata only)
|
||||
borg inspect container.stim
|
||||
# Output:
|
||||
# Format: STIM
|
||||
# encryption_algorithm: chacha20poly1305
|
||||
# config_size: 1234
|
||||
# rootfs_size: 567890
|
||||
```
|
||||
|
||||
## 8. Cache API
|
||||
|
||||
```go
|
||||
// Create cache with master password
|
||||
cache, err := tim.NewCache("/path/to/cache", masterPassword)
|
||||
|
||||
// Store TIM (encrypted automatically as .stim)
|
||||
err := cache.Store("name", tim)
|
||||
|
||||
// Load TIM (decrypted automatically)
|
||||
tim, err := cache.Load("name")
|
||||
|
||||
// List cached containers
|
||||
names, err := cache.List()
|
||||
```
|
||||
|
||||
## 9. Execution Security
|
||||
|
||||
```go
|
||||
// Secure execution flow
|
||||
func RunEncrypted(path, password string) error {
|
||||
// 1. Create secure temp directory
|
||||
tmpDir, _ := os.MkdirTemp("", "borg-run-*")
|
||||
defer os.RemoveAll(tmpDir) // Secure cleanup
|
||||
|
||||
// 2. Read and decrypt
|
||||
data, _ := os.ReadFile(path)
|
||||
tim, _ := FromSigil(data, password)
|
||||
|
||||
// 3. Extract to temp
|
||||
tim.ExtractTo(tmpDir)
|
||||
|
||||
// 4. Execute with runc
|
||||
return runRunc(tmpDir)
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Security Properties
|
||||
|
||||
### 10.1 Confidentiality
|
||||
|
||||
- Contents encrypted with ChaCha20-Poly1305
|
||||
- Password-derived key never stored
|
||||
- Nonces are random, never reused
|
||||
|
||||
### 10.2 Integrity
|
||||
|
||||
- Poly1305 MAC prevents tampering
|
||||
- Decryption fails if modified
|
||||
- Separate MACs for config and rootfs
|
||||
|
||||
### 10.3 Error Detection
|
||||
|
||||
| Error | Cause |
|
||||
|-------|-------|
|
||||
| `ErrPasswordRequired` | Empty password provided |
|
||||
| `ErrInvalidStimPayload` | Payload < 4 bytes or invalid size |
|
||||
| `ErrDecryptionFailed` | Wrong password or corrupted data |
|
||||
|
||||
## 11. Comparison to TRIX
|
||||
|
||||
| Feature | STIM | TRIX |
|
||||
|---------|------|------|
|
||||
| Algorithm | ChaCha20-Poly1305 | PGP/AES or ChaCha |
|
||||
| Content | TIM bundles | DataNode (raw files) |
|
||||
| Structure | Dual encryption | Single blob |
|
||||
| Magic | "STIM" | "TRIX" |
|
||||
| Use case | Container execution | General encryption, accounts |
|
||||
|
||||
STIM is for containers. TRIX is for general file encryption and accounts.
|
||||
|
||||
## 12. Implementation Reference
|
||||
|
||||
- Encryption: `pkg/tim/tim.go` (ToSigil, FromSigil)
|
||||
- Key derivation: `pkg/trix/trix.go` (DeriveKey)
|
||||
- Cache: `pkg/tim/cache.go`
|
||||
- CLI: `cmd/run.go`, `cmd/decode.go`, `cmd/compile.go`
|
||||
- Enchantrix: `github.com/Snider/Enchantrix`
|
||||
|
||||
## 13. Security Considerations
|
||||
|
||||
1. **Password strength**: Recommend 64+ bits entropy (12+ chars)
|
||||
2. **Key derivation**: SHA-256 only (no stretching) - use strong passwords
|
||||
3. **Memory handling**: Keys should be wiped after use
|
||||
4. **Temp files**: Use tmpfs when available, secure wipe after
|
||||
5. **Side channels**: Enchantrix uses constant-time crypto operations
|
||||
|
||||
## 14. Future Work
|
||||
|
||||
- [ ] Hardware key support (YubiKey, TPM)
|
||||
- [ ] Key stretching (Argon2)
|
||||
- [ ] Multi-recipient encryption
|
||||
- [ ] Streaming decryption for large rootfs
|
||||
342
docs/specs/RFC-016-TRIX-PGP.md
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
# RFC-006: TRIX PGP Encryption Format
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
**Depends On**: RFC-003
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
TRIX is a PGP-based encryption format for DataNode archives and account credentials. It provides symmetric and asymmetric encryption using OpenPGP standards and ChaCha20-Poly1305, enabling secure data exchange and identity management.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
TRIX provides:
|
||||
- PGP symmetric encryption for DataNode archives
|
||||
- ChaCha20-Poly1305 modern encryption
|
||||
- PGP armored keys for account/identity management
|
||||
- Integration with Enchantrix library
|
||||
|
||||
## 2. Public API
|
||||
|
||||
### 2.1 Key Derivation
|
||||
|
||||
```go
|
||||
// pkg/trix/trix.go:64-67
|
||||
func DeriveKey(password string) []byte {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return hash[:] // 32 bytes
|
||||
}
|
||||
```
|
||||
|
||||
- Input: password string (any length)
|
||||
- Output: 32-byte key (256 bits)
|
||||
- Algorithm: SHA-256 hash of UTF-8 bytes
|
||||
- Deterministic: identical passwords → identical keys
|
||||
|
||||
### 2.2 Legacy PGP Encryption
|
||||
|
||||
```go
|
||||
// Encrypt DataNode to TRIX (PGP symmetric)
|
||||
func ToTrix(dn *datanode.DataNode, password string) ([]byte, error)
|
||||
|
||||
// Decrypt TRIX to DataNode (DISABLED for encrypted payloads)
|
||||
func FromTrix(data []byte, password string) (*datanode.DataNode, error)
|
||||
```
|
||||
|
||||
**Note**: `FromTrix` with a non-empty password returns error `"decryption disabled: cannot accept encrypted payloads"`. This is intentional to prevent accidental password use.
|
||||
|
||||
### 2.3 Modern ChaCha20-Poly1305 Encryption
|
||||
|
||||
```go
|
||||
// Encrypt with ChaCha20-Poly1305
|
||||
func ToTrixChaCha(dn *datanode.DataNode, password string) ([]byte, error)
|
||||
|
||||
// Decrypt ChaCha20-Poly1305
|
||||
func FromTrixChaCha(data []byte, password string) (*datanode.DataNode, error)
|
||||
```
|
||||
|
||||
### 2.4 Error Variables
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrPasswordRequired = errors.New("password is required for encryption")
|
||||
ErrDecryptionFailed = errors.New("decryption failed (wrong password?)")
|
||||
)
|
||||
```
|
||||
|
||||
## 3. File Format
|
||||
|
||||
### 3.1 Container Structure
|
||||
|
||||
```
|
||||
[4 bytes] Magic: "TRIX" (ASCII)
|
||||
[Variable] Gob-encoded Header (map[string]interface{})
|
||||
[Variable] Payload (encrypted or unencrypted tarball)
|
||||
```
|
||||
|
||||
### 3.2 Header Examples
|
||||
|
||||
**Unencrypted:**
|
||||
```go
|
||||
Header: map[string]interface{}{} // Empty map
|
||||
```
|
||||
|
||||
**ChaCha20-Poly1305:**
|
||||
```go
|
||||
Header: map[string]interface{}{
|
||||
"encryption_algorithm": "chacha20poly1305",
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 ChaCha20-Poly1305 Payload
|
||||
|
||||
```
|
||||
[24 bytes] XChaCha20 Nonce (embedded)
|
||||
[N bytes] Encrypted tar archive
|
||||
[16 bytes] Poly1305 authentication tag
|
||||
```
|
||||
|
||||
**Note**: Nonces are embedded in the ciphertext by Enchantrix, not stored separately.
|
||||
|
||||
## 4. Encryption Workflows
|
||||
|
||||
### 4.1 ChaCha20-Poly1305 (Recommended)
|
||||
|
||||
```go
|
||||
// Encryption
|
||||
func ToTrixChaCha(dn *datanode.DataNode, password string) ([]byte, error) {
|
||||
// 1. Validate password is non-empty
|
||||
if password == "" {
|
||||
return nil, ErrPasswordRequired
|
||||
}
|
||||
|
||||
// 2. Serialize DataNode to tar
|
||||
tarball, _ := dn.ToTar()
|
||||
|
||||
// 3. Derive 32-byte key
|
||||
key := DeriveKey(password)
|
||||
|
||||
// 4. Create sigil and encrypt
|
||||
sigil, _ := enchantrix.NewChaChaPolySigil(key)
|
||||
encrypted, _ := sigil.In(tarball) // Generates nonce automatically
|
||||
|
||||
// 5. Create Trix container
|
||||
t := &trix.Trix{
|
||||
Header: map[string]interface{}{"encryption_algorithm": "chacha20poly1305"},
|
||||
Payload: encrypted,
|
||||
}
|
||||
|
||||
// 6. Encode with TRIX magic
|
||||
return trix.Encode(t, "TRIX", nil)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Decryption
|
||||
|
||||
```go
|
||||
func FromTrixChaCha(data []byte, password string) (*datanode.DataNode, error) {
|
||||
// 1. Validate password
|
||||
if password == "" {
|
||||
return nil, ErrPasswordRequired
|
||||
}
|
||||
|
||||
// 2. Decode TRIX container
|
||||
t, _ := trix.Decode(data, "TRIX", nil)
|
||||
|
||||
// 3. Derive key and decrypt
|
||||
key := DeriveKey(password)
|
||||
sigil, _ := enchantrix.NewChaChaPolySigil(key)
|
||||
tarball, err := sigil.Out(t.Payload) // Extracts nonce, verifies MAC
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrDecryptionFailed, err)
|
||||
}
|
||||
|
||||
// 4. Deserialize DataNode
|
||||
return datanode.FromTar(tarball)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Legacy PGP (Disabled Decryption)
|
||||
|
||||
```go
|
||||
func ToTrix(dn *datanode.DataNode, password string) ([]byte, error) {
|
||||
tarball, _ := dn.ToTar()
|
||||
|
||||
var payload []byte
|
||||
if password != "" {
|
||||
// PGP symmetric encryption
|
||||
cryptService := crypt.NewService()
|
||||
payload, _ = cryptService.SymmetricallyEncryptPGP([]byte(password), tarball)
|
||||
} else {
|
||||
payload = tarball
|
||||
}
|
||||
|
||||
t := &trix.Trix{Header: map[string]interface{}{}, Payload: payload}
|
||||
return trix.Encode(t, "TRIX", nil)
|
||||
}
|
||||
|
||||
func FromTrix(data []byte, password string) (*datanode.DataNode, error) {
|
||||
// Security: Reject encrypted payloads
|
||||
if password != "" {
|
||||
return nil, errors.New("decryption disabled: cannot accept encrypted payloads")
|
||||
}
|
||||
|
||||
t, _ := trix.Decode(data, "TRIX", nil)
|
||||
return datanode.FromTar(t.Payload)
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Enchantrix Library
|
||||
|
||||
### 5.1 Dependencies
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/Snider/Enchantrix/pkg/trix" // Container format
|
||||
"github.com/Snider/Enchantrix/pkg/crypt" // PGP operations
|
||||
"github.com/Snider/Enchantrix/pkg/enchantrix" // AEAD sigils
|
||||
)
|
||||
```
|
||||
|
||||
### 5.2 Trix Container
|
||||
|
||||
```go
|
||||
type Trix struct {
|
||||
Header map[string]interface{}
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func Encode(t *Trix, magic string, extra interface{}) ([]byte, error)
|
||||
func Decode(data []byte, magic string, extra interface{}) (*Trix, error)
|
||||
```
|
||||
|
||||
### 5.3 ChaCha20-Poly1305 Sigil
|
||||
|
||||
```go
|
||||
// Create sigil with 32-byte key
|
||||
sigil, err := enchantrix.NewChaChaPolySigil(key)
|
||||
|
||||
// Encrypt (generates random 24-byte nonce)
|
||||
ciphertext, err := sigil.In(plaintext)
|
||||
|
||||
// Decrypt (extracts nonce, verifies MAC)
|
||||
plaintext, err := sigil.Out(ciphertext)
|
||||
```
|
||||
|
||||
## 6. Account System Integration
|
||||
|
||||
### 6.1 PGP Armored Keys
|
||||
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBGX...base64...
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
|
||||
### 6.2 Key Storage
|
||||
|
||||
```
|
||||
~/.borg/
|
||||
├── identity.pub # PGP public key (armored)
|
||||
├── identity.key # PGP private key (armored, encrypted)
|
||||
└── keyring/ # Trusted public keys
|
||||
```
|
||||
|
||||
## 7. CLI Usage
|
||||
|
||||
```bash
|
||||
# Encrypt with TRIX (PGP symmetric)
|
||||
borg collect github repo https://github.com/user/repo \
|
||||
--format trix \
|
||||
--password "password"
|
||||
|
||||
# Decrypt unencrypted TRIX
|
||||
borg decode archive.trix -o decoded.tar
|
||||
|
||||
# Inspect without decrypting
|
||||
borg inspect archive.trix
|
||||
# Output:
|
||||
# Format: TRIX
|
||||
# encryption_algorithm: chacha20poly1305 (if present)
|
||||
# Payload Size: N bytes
|
||||
```
|
||||
|
||||
## 8. Format Comparison
|
||||
|
||||
| Format | Extension | Algorithm | Use Case |
|
||||
|--------|-----------|-----------|----------|
|
||||
| `datanode` | `.tar` | None | Uncompressed archive |
|
||||
| `tim` | `.tim` | None | Container bundle |
|
||||
| `trix` | `.trix` | PGP/AES or ChaCha | Encrypted archives, accounts |
|
||||
| `stim` | `.stim` | ChaCha20-Poly1305 | Encrypted containers |
|
||||
| `smsg` | `.smsg` | ChaCha20-Poly1305 | Encrypted media |
|
||||
|
||||
## 9. Security Analysis
|
||||
|
||||
### 9.1 Key Derivation Limitations
|
||||
|
||||
**Current implementation: SHA-256 (single round)**
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Algorithm | SHA-256 |
|
||||
| Iterations | 1 |
|
||||
| Salt | None |
|
||||
| Key stretching | None |
|
||||
|
||||
**Implications:**
|
||||
- GPU brute force: ~10 billion guesses/second
|
||||
- 8-character password: ~10 seconds to break
|
||||
- Recommendation: Use 15+ character passwords
|
||||
|
||||
### 9.2 ChaCha20-Poly1305 Properties
|
||||
|
||||
| Property | Status |
|
||||
|----------|--------|
|
||||
| Authentication | Poly1305 MAC (16 bytes) |
|
||||
| Key size | 256 bits |
|
||||
| Nonce size | 192 bits (XChaCha) |
|
||||
| Standard | RFC 7539 compliant |
|
||||
|
||||
## 10. Test Coverage
|
||||
|
||||
| Test | Description |
|
||||
|------|-------------|
|
||||
| DeriveKey length | Output is exactly 32 bytes |
|
||||
| DeriveKey determinism | Same password → same key |
|
||||
| DeriveKey uniqueness | Different passwords → different keys |
|
||||
| ToTrix without password | Valid TRIX with "TRIX" magic |
|
||||
| ToTrix with password | PGP encryption applied |
|
||||
| FromTrix unencrypted | Round-trip preserves files |
|
||||
| FromTrix password rejection | Returns error |
|
||||
| ToTrixChaCha success | Valid TRIX created |
|
||||
| ToTrixChaCha empty password | Returns ErrPasswordRequired |
|
||||
| FromTrixChaCha round-trip | Preserves nested directories |
|
||||
| FromTrixChaCha wrong password | Returns ErrDecryptionFailed |
|
||||
| FromTrixChaCha large data | 1MB file processed |
|
||||
|
||||
## 11. Implementation Reference
|
||||
|
||||
- Source: `pkg/trix/trix.go`
|
||||
- Tests: `pkg/trix/trix_test.go`
|
||||
- Enchantrix: `github.com/Snider/Enchantrix v0.0.2`
|
||||
|
||||
## 12. Security Considerations
|
||||
|
||||
1. **Use strong passwords**: 15+ characters due to no key stretching
|
||||
2. **Prefer ChaCha**: Use `ToTrixChaCha` over legacy PGP
|
||||
3. **Key backup**: Securely backup private keys
|
||||
4. **Interoperability**: TRIX files with GPG require password
|
||||
|
||||
## 13. Future Work
|
||||
|
||||
- [ ] Key stretching (Argon2 option in DeriveKey)
|
||||
- [ ] Public key encryption support
|
||||
- [ ] Signature support
|
||||
- [ ] Key expiration metadata
|
||||
- [ ] Multi-recipient encryption
|
||||
355
docs/specs/RFC-017-LTHN-KEY-DERIVATION.md
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
# RFC-007: LTHN Key Derivation
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
**Depends On**: RFC-002
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
LTHN (Leet-Hash-Nonce) is a rainbow-table resistant key derivation function used for streaming DRM with time-limited access. It generates rolling keys that automatically expire without requiring revocation infrastructure.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
LTHN provides:
|
||||
- Rainbow-table resistant hashing
|
||||
- Time-based key rolling
|
||||
- Zero-trust key derivation (no key server)
|
||||
- Configurable cadence (daily to hourly)
|
||||
|
||||
## 2. Motivation
|
||||
|
||||
Traditional DRM requires:
|
||||
- Central key server
|
||||
- License validation
|
||||
- Revocation lists
|
||||
- Network connectivity
|
||||
|
||||
LTHN eliminates these by:
|
||||
- Deriving keys from public information + secret
|
||||
- Time-bounding keys automatically
|
||||
- Making rainbow tables impractical
|
||||
- Working completely offline
|
||||
|
||||
## 3. Algorithm
|
||||
|
||||
### 3.1 Core Function
|
||||
|
||||
The LTHN hash is implemented in the Enchantrix library:
|
||||
|
||||
```go
|
||||
import "github.com/Snider/Enchantrix/pkg/crypt"
|
||||
|
||||
cryptService := crypt.NewService()
|
||||
lthnHash := cryptService.Hash(crypt.LTHN, input)
|
||||
```
|
||||
|
||||
**LTHN formula**:
|
||||
```
|
||||
LTHN(input) = SHA256(input || reverse_leet(input))
|
||||
```
|
||||
|
||||
Where `reverse_leet` performs bidirectional character substitution.
|
||||
|
||||
### 3.2 Reverse Leet Mapping
|
||||
|
||||
| Original | Leet | Bidirectional |
|
||||
|----------|------|---------------|
|
||||
| o | 0 | o ↔ 0 |
|
||||
| l | 1 | l ↔ 1 |
|
||||
| e | 3 | e ↔ 3 |
|
||||
| a | 4 | a ↔ 4 |
|
||||
| s | z | s ↔ z |
|
||||
| t | 7 | t ↔ 7 |
|
||||
|
||||
### 3.3 Example
|
||||
|
||||
```
|
||||
Input: "2026-01-13:license:fp"
|
||||
reverse_leet: "pf:3zn3ci1:31-10-6202"
|
||||
Combined: "2026-01-13:license:fppf:3zn3ci1:31-10-6202"
|
||||
Result: SHA256(combined) → 32-byte hash
|
||||
```
|
||||
|
||||
## 4. Stream Key Derivation
|
||||
|
||||
### 4.1 Implementation
|
||||
|
||||
```go
|
||||
// pkg/smsg/stream.go:49-60
|
||||
func DeriveStreamKey(date, license, fingerprint string) []byte {
|
||||
input := fmt.Sprintf("%s:%s:%s", date, license, fingerprint)
|
||||
cryptService := crypt.NewService()
|
||||
lthnHash := cryptService.Hash(crypt.LTHN, input)
|
||||
key := sha256.Sum256([]byte(lthnHash))
|
||||
return key[:]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Input Format
|
||||
|
||||
```
|
||||
period:license:fingerprint
|
||||
|
||||
Where:
|
||||
- period: Time period identifier (see Cadence)
|
||||
- license: User's license key (password)
|
||||
- fingerprint: Device/browser fingerprint
|
||||
```
|
||||
|
||||
### 4.3 Output
|
||||
|
||||
32-byte key suitable for ChaCha20-Poly1305.
|
||||
|
||||
## 5. Cadence
|
||||
|
||||
### 5.1 Options
|
||||
|
||||
| Cadence | Constant | Period Format | Example | Duration |
|
||||
|---------|----------|---------------|---------|----------|
|
||||
| Daily | `CadenceDaily` | `2006-01-02` | `2026-01-13` | 24h |
|
||||
| 12-hour | `CadenceHalfDay` | `2006-01-02-AM/PM` | `2026-01-13-PM` | 12h |
|
||||
| 6-hour | `CadenceQuarter` | `2006-01-02-HH` | `2026-01-13-12` | 6h |
|
||||
| Hourly | `CadenceHourly` | `2006-01-02-HH` | `2026-01-13-15` | 1h |
|
||||
|
||||
### 5.2 Period Calculation
|
||||
|
||||
```go
|
||||
// pkg/smsg/stream.go:73-119
|
||||
func GetCurrentPeriod(cadence Cadence) string {
|
||||
return GetPeriodAt(time.Now(), cadence)
|
||||
}
|
||||
|
||||
func GetPeriodAt(t time.Time, cadence Cadence) string {
|
||||
switch cadence {
|
||||
case CadenceDaily:
|
||||
return t.Format("2006-01-02")
|
||||
case CadenceHalfDay:
|
||||
suffix := "AM"
|
||||
if t.Hour() >= 12 {
|
||||
suffix = "PM"
|
||||
}
|
||||
return t.Format("2006-01-02") + "-" + suffix
|
||||
case CadenceQuarter:
|
||||
bucket := (t.Hour() / 6) * 6
|
||||
return fmt.Sprintf("%s-%02d", t.Format("2006-01-02"), bucket)
|
||||
case CadenceHourly:
|
||||
return fmt.Sprintf("%s-%02d", t.Format("2006-01-02"), t.Hour())
|
||||
}
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func GetNextPeriod(cadence Cadence) string {
|
||||
return GetPeriodAt(time.Now().Add(GetCadenceDuration(cadence)), cadence)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Duration Mapping
|
||||
|
||||
```go
|
||||
func GetCadenceDuration(cadence Cadence) time.Duration {
|
||||
switch cadence {
|
||||
case CadenceDaily:
|
||||
return 24 * time.Hour
|
||||
case CadenceHalfDay:
|
||||
return 12 * time.Hour
|
||||
case CadenceQuarter:
|
||||
return 6 * time.Hour
|
||||
case CadenceHourly:
|
||||
return 1 * time.Hour
|
||||
}
|
||||
return 24 * time.Hour
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Rolling Windows
|
||||
|
||||
### 6.1 Dual-Key Strategy
|
||||
|
||||
At encryption time, CEK is wrapped with **two** keys:
|
||||
1. Current period key
|
||||
2. Next period key
|
||||
|
||||
This creates a rolling validity window:
|
||||
|
||||
```
|
||||
Time: 2026-01-13 23:30 (daily cadence)
|
||||
|
||||
Valid keys:
|
||||
- "2026-01-13:license:fp" (current period)
|
||||
- "2026-01-14:license:fp" (next period)
|
||||
|
||||
Window: 24-48 hours of validity
|
||||
```
|
||||
|
||||
### 6.2 Key Wrapping
|
||||
|
||||
```go
|
||||
// pkg/smsg/stream.go:135-155
|
||||
func WrapCEK(cek []byte, streamKey []byte) (string, error) {
|
||||
sigil := enchantrix.NewChaChaPolySigil()
|
||||
wrapped, err := sigil.Seal(cek, streamKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(wrapped), nil
|
||||
}
|
||||
```
|
||||
|
||||
**Wrapped format**:
|
||||
```
|
||||
[24-byte nonce][encrypted CEK][16-byte auth tag]
|
||||
→ base64 encoded for header storage
|
||||
```
|
||||
|
||||
### 6.3 Key Unwrapping
|
||||
|
||||
```go
|
||||
// pkg/smsg/stream.go:157-170
|
||||
func UnwrapCEK(wrapped string, streamKey []byte) ([]byte, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(wrapped)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigil := enchantrix.NewChaChaPolySigil()
|
||||
return sigil.Open(data, streamKey)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Decryption Flow
|
||||
|
||||
```go
|
||||
// pkg/smsg/stream.go:606-633
|
||||
func UnwrapCEKFromHeader(header *V3Header, params *StreamParams) ([]byte, error) {
|
||||
// Try current period first
|
||||
currentPeriod := GetCurrentPeriod(params.Cadence)
|
||||
currentKey := DeriveStreamKey(currentPeriod, params.License, params.Fingerprint)
|
||||
|
||||
for _, wk := range header.WrappedKeys {
|
||||
cek, err := UnwrapCEK(wk.Key, currentKey)
|
||||
if err == nil {
|
||||
return cek, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try next period (for clock skew)
|
||||
nextPeriod := GetNextPeriod(params.Cadence)
|
||||
nextKey := DeriveStreamKey(nextPeriod, params.License, params.Fingerprint)
|
||||
|
||||
for _, wk := range header.WrappedKeys {
|
||||
cek, err := UnwrapCEK(wk.Key, nextKey)
|
||||
if err == nil {
|
||||
return cek, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrKeyExpired
|
||||
}
|
||||
```
|
||||
|
||||
## 7. V3 Header Format
|
||||
|
||||
```go
|
||||
type V3Header struct {
|
||||
Format string `json:"format"` // "v3"
|
||||
Manifest *Manifest `json:"manifest"`
|
||||
WrappedKeys []WrappedKey `json:"wrappedKeys"`
|
||||
Chunked *ChunkInfo `json:"chunked,omitempty"`
|
||||
}
|
||||
|
||||
type WrappedKey struct {
|
||||
Period string `json:"period"` // e.g., "2026-01-13"
|
||||
Key string `json:"key"` // base64-encoded wrapped CEK
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Rainbow Table Resistance
|
||||
|
||||
### 8.1 Why It Works
|
||||
|
||||
Standard hash:
|
||||
```
|
||||
SHA256("2026-01-13:license:fp") → predictable, precomputable
|
||||
```
|
||||
|
||||
LTHN hash:
|
||||
```
|
||||
LTHN("2026-01-13:license:fp")
|
||||
= SHA256("2026-01-13:license:fp" + reverse_leet("2026-01-13:license:fp"))
|
||||
= SHA256("2026-01-13:license:fp" + "pf:3zn3ci1:31-10-6202")
|
||||
```
|
||||
|
||||
The salt is **derived from the input itself**, making precomputation impractical:
|
||||
- Each unique input has a unique salt
|
||||
- Cannot build rainbow tables without knowing all possible inputs
|
||||
- Input space includes license keys (high entropy)
|
||||
|
||||
### 8.2 Security Analysis
|
||||
|
||||
| Attack | Mitigation |
|
||||
|--------|------------|
|
||||
| Rainbow tables | Input-derived salt makes precomputation infeasible |
|
||||
| Brute force | License key entropy (64+ bits recommended) |
|
||||
| Time oracle | Rolling window prevents precise timing attacks |
|
||||
| Key sharing | Keys expire within cadence window |
|
||||
|
||||
## 9. Zero-Trust Properties
|
||||
|
||||
| Property | Implementation |
|
||||
|----------|----------------|
|
||||
| No key server | Keys derived locally from LTHN |
|
||||
| Auto-expiration | Rolling periods invalidate old keys |
|
||||
| No revocation | Keys naturally expire within cadence window |
|
||||
| Device binding | Fingerprint in derivation input |
|
||||
| User binding | License key in derivation input |
|
||||
|
||||
## 10. Test Vectors
|
||||
|
||||
From `pkg/smsg/stream_test.go`:
|
||||
|
||||
```go
|
||||
// Stream key generation
|
||||
date := "2026-01-12"
|
||||
license := "test-license"
|
||||
fingerprint := "test-fp"
|
||||
key := DeriveStreamKey(date, license, fingerprint)
|
||||
// key is 32 bytes, deterministic
|
||||
|
||||
// Period calculation at 2026-01-12 15:30:00 UTC
|
||||
t := time.Date(2026, 1, 12, 15, 30, 0, 0, time.UTC)
|
||||
|
||||
GetPeriodAt(t, CadenceDaily) // "2026-01-12"
|
||||
GetPeriodAt(t, CadenceHalfDay) // "2026-01-12-PM"
|
||||
GetPeriodAt(t, CadenceQuarter) // "2026-01-12-12"
|
||||
GetPeriodAt(t, CadenceHourly) // "2026-01-12-15"
|
||||
|
||||
// Next periods
|
||||
// Daily: "2026-01-12" → "2026-01-13"
|
||||
// 12h: "2026-01-12-PM" → "2026-01-13-AM"
|
||||
// 6h: "2026-01-12-12" → "2026-01-12-18"
|
||||
// 1h: "2026-01-12-15" → "2026-01-12-16"
|
||||
```
|
||||
|
||||
## 11. Implementation Reference
|
||||
|
||||
- Stream key derivation: `pkg/smsg/stream.go`
|
||||
- LTHN hash: `github.com/Snider/Enchantrix/pkg/crypt`
|
||||
- WASM bindings: `pkg/wasm/stmf/main.go` (decryptV3, unwrapCEK)
|
||||
- Tests: `pkg/smsg/stream_test.go`
|
||||
|
||||
## 12. Security Considerations
|
||||
|
||||
1. **License entropy**: Recommend 64+ bits (12+ alphanumeric chars)
|
||||
2. **Fingerprint stability**: Should be stable but not user-controllable
|
||||
3. **Clock skew**: Rolling windows handle ±1 period drift
|
||||
4. **Key exposure**: Derived keys valid only for one period
|
||||
|
||||
## 13. References
|
||||
|
||||
- RFC-002: SMSG Format (v3 streaming)
|
||||
- RFC-001: OSS DRM (Section 3.4)
|
||||
- RFC 8439: ChaCha20-Poly1305
|
||||
- Enchantrix: github.com/Snider/Enchantrix
|
||||
255
docs/specs/RFC-018-BORGFILE.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
# RFC-008: Borgfile Compilation
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
**Depends On**: RFC-003, RFC-004
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
Borgfile is a declarative syntax for defining TIM container contents. It specifies how local files are mapped into the container filesystem, enabling reproducible container builds.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Borgfile provides:
|
||||
- Dockerfile-like syntax for familiarity
|
||||
- File mapping into containers
|
||||
- Simple ADD directive
|
||||
- Integration with TIM encryption
|
||||
|
||||
## 2. File Format
|
||||
|
||||
### 2.1 Location
|
||||
|
||||
- Default: `Borgfile` in current directory
|
||||
- Override: `borg compile -f path/to/Borgfile`
|
||||
|
||||
### 2.2 Encoding
|
||||
|
||||
- UTF-8 text
|
||||
- Unix line endings (LF)
|
||||
- No BOM
|
||||
|
||||
## 3. Syntax
|
||||
|
||||
### 3.1 Parsing Implementation
|
||||
|
||||
```go
|
||||
// cmd/compile.go:33-54
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line) // Whitespace-separated tokens
|
||||
if len(parts) == 0 {
|
||||
continue // Skip empty lines
|
||||
}
|
||||
switch parts[0] {
|
||||
case "ADD":
|
||||
// Process ADD directive
|
||||
default:
|
||||
return fmt.Errorf("unknown instruction: %s", parts[0])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 ADD Directive
|
||||
|
||||
```
|
||||
ADD <source> <destination>
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| source | Local path (relative to current working directory) |
|
||||
| destination | Container path (leading slash stripped) |
|
||||
|
||||
### 3.3 Examples
|
||||
|
||||
```dockerfile
|
||||
# Add single file
|
||||
ADD ./app /usr/local/bin/app
|
||||
|
||||
# Add configuration
|
||||
ADD ./config.yaml /etc/myapp/config.yaml
|
||||
|
||||
# Multiple files
|
||||
ADD ./bin/server /app/server
|
||||
ADD ./static /app/static
|
||||
```
|
||||
|
||||
## 4. Path Resolution
|
||||
|
||||
### 4.1 Source Paths
|
||||
|
||||
- Resolved relative to **current working directory** (not Borgfile location)
|
||||
- Must exist at compile time
|
||||
- Read via `os.ReadFile(src)`
|
||||
|
||||
### 4.2 Destination Paths
|
||||
|
||||
- Leading slash stripped: `strings.TrimPrefix(dest, "/")`
|
||||
- Added to DataNode as-is
|
||||
|
||||
```go
|
||||
// cmd/compile.go:46-50
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid ADD instruction: %s", line)
|
||||
}
|
||||
name := strings.TrimPrefix(dest, "/")
|
||||
m.RootFS.AddData(name, data)
|
||||
```
|
||||
|
||||
## 5. File Handling
|
||||
|
||||
### 5.1 Permissions
|
||||
|
||||
**Current implementation**: Permissions are NOT preserved.
|
||||
|
||||
| Source | Container |
|
||||
|--------|-----------|
|
||||
| Any file | 0600 (hardcoded in DataNode.ToTar) |
|
||||
| Any directory | 0755 (implicit) |
|
||||
|
||||
### 5.2 Timestamps
|
||||
|
||||
- Set to `time.Now()` when added to DataNode
|
||||
- Original timestamps not preserved
|
||||
|
||||
### 5.3 File Types
|
||||
|
||||
- Regular files only
|
||||
- No directory recursion (each file must be added explicitly)
|
||||
- No symlink following
|
||||
|
||||
## 6. Error Handling
|
||||
|
||||
| Error | Cause |
|
||||
|-------|-------|
|
||||
| `invalid ADD instruction: {line}` | Wrong number of arguments |
|
||||
| `os.ReadFile` error | Source file not found |
|
||||
| `unknown instruction: {name}` | Unrecognized directive |
|
||||
| `ErrPasswordRequired` | Encryption requested without password |
|
||||
|
||||
## 7. CLI Flags
|
||||
|
||||
```go
|
||||
// cmd/compile.go:80-82
|
||||
-f, --file string Path to Borgfile (default: "Borgfile")
|
||||
-o, --output string Output path (default: "a.tim")
|
||||
-e, --encrypt string Password for .stim encryption (optional)
|
||||
```
|
||||
|
||||
## 8. Output Formats
|
||||
|
||||
### 8.1 Plain TIM
|
||||
|
||||
```bash
|
||||
borg compile -f Borgfile -o container.tim
|
||||
```
|
||||
|
||||
Output: Standard TIM tar archive with `config.json` + `rootfs/`
|
||||
|
||||
### 8.2 Encrypted STIM
|
||||
|
||||
```bash
|
||||
borg compile -f Borgfile -e "password" -o container.stim
|
||||
```
|
||||
|
||||
Output: ChaCha20-Poly1305 encrypted STIM container
|
||||
|
||||
**Auto-detection**: If `-e` flag provided, output automatically uses `.stim` format even if `-o` specifies `.tim`.
|
||||
|
||||
## 9. Default OCI Config
|
||||
|
||||
The current implementation creates a minimal config:
|
||||
|
||||
```go
|
||||
// pkg/tim/config.go:6-10
|
||||
func defaultConfig() (*trix.Trix, error) {
|
||||
return &trix.Trix{Header: make(map[string]interface{})}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: This is a placeholder. For full OCI runtime execution, you'll need to provide a proper `config.json` in the container or modify the TIM after compilation.
|
||||
|
||||
## 10. Compilation Process
|
||||
|
||||
```
|
||||
1. Read Borgfile content
|
||||
2. Parse line-by-line
|
||||
3. For each ADD directive:
|
||||
a. Read source file from filesystem
|
||||
b. Strip leading slash from destination
|
||||
c. Add to DataNode
|
||||
4. Create TIM with default config + populated RootFS
|
||||
5. If password provided:
|
||||
a. Encrypt to STIM via ToSigil()
|
||||
b. Adjust output extension to .stim
|
||||
6. Write output file
|
||||
```
|
||||
|
||||
## 11. Implementation Reference
|
||||
|
||||
- Parser/Compiler: `cmd/compile.go`
|
||||
- TIM creation: `pkg/tim/tim.go`
|
||||
- DataNode: `pkg/datanode/datanode.go`
|
||||
- Tests: `cmd/compile_test.go`
|
||||
|
||||
## 12. Current Limitations
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| Comment support (`#`) | Not implemented |
|
||||
| Quoted paths | Not implemented |
|
||||
| Directory recursion | Not implemented |
|
||||
| Permission preservation | Not implemented |
|
||||
| Path resolution relative to Borgfile | Not implemented (uses CWD) |
|
||||
| Full OCI config generation | Not implemented (empty header) |
|
||||
| Symlink following | Not implemented |
|
||||
|
||||
## 13. Examples
|
||||
|
||||
### 13.1 Simple Application
|
||||
|
||||
```dockerfile
|
||||
ADD ./myapp /usr/local/bin/myapp
|
||||
ADD ./config.yaml /etc/myapp/config.yaml
|
||||
```
|
||||
|
||||
### 13.2 Web Application
|
||||
|
||||
```dockerfile
|
||||
ADD ./server /app/server
|
||||
ADD ./index.html /app/static/index.html
|
||||
ADD ./style.css /app/static/style.css
|
||||
ADD ./app.js /app/static/app.js
|
||||
```
|
||||
|
||||
### 13.3 With Encryption
|
||||
|
||||
```bash
|
||||
# Create Borgfile
|
||||
cat > Borgfile << 'EOF'
|
||||
ADD ./secret-app /app/secret-app
|
||||
ADD ./credentials.json /etc/app/credentials.json
|
||||
EOF
|
||||
|
||||
# Compile with encryption
|
||||
borg compile -f Borgfile -e "MySecretPassword123" -o secret.stim
|
||||
```
|
||||
|
||||
## 14. Future Work
|
||||
|
||||
- [ ] Comment support (`#`)
|
||||
- [ ] Quoted path support for spaces
|
||||
- [ ] Directory recursion in ADD
|
||||
- [ ] Permission preservation
|
||||
- [ ] Path resolution relative to Borgfile location
|
||||
- [ ] Full OCI config generation
|
||||
- [ ] Variable substitution (`${VAR}`)
|
||||
- [ ] Include directive
|
||||
- [ ] Glob patterns in source
|
||||
- [ ] COPY directive (alias for ADD)
|
||||
365
docs/specs/RFC-019-STMF.md
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
# RFC-009: STMF Secure To-Me Form
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
STMF (Secure To-Me Form) provides asymmetric encryption for web form submissions. It enables end-to-end encrypted form data where only the recipient can decrypt submissions, protecting sensitive data from server compromise.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
STMF provides:
|
||||
- Asymmetric encryption for form data
|
||||
- X25519 key exchange
|
||||
- ChaCha20-Poly1305 for payload encryption
|
||||
- Browser-based encryption via WASM
|
||||
- HTTP middleware for server-side decryption
|
||||
|
||||
## 2. Cryptographic Primitives
|
||||
|
||||
### 2.1 Key Exchange
|
||||
|
||||
X25519 (Curve25519 Diffie-Hellman)
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Private key | 32 bytes |
|
||||
| Public key | 32 bytes |
|
||||
| Shared secret | 32 bytes |
|
||||
|
||||
### 2.2 Encryption
|
||||
|
||||
ChaCha20-Poly1305
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Key | 32 bytes (SHA-256 of shared secret) |
|
||||
| Nonce | 24 bytes (XChaCha variant) |
|
||||
| Tag | 16 bytes |
|
||||
|
||||
## 3. Protocol
|
||||
|
||||
### 3.1 Setup (One-time)
|
||||
|
||||
```
|
||||
Recipient (Server):
|
||||
1. Generate X25519 keypair
|
||||
2. Publish public key (embed in page or API)
|
||||
3. Store private key securely
|
||||
```
|
||||
|
||||
### 3.2 Encryption Flow (Browser)
|
||||
|
||||
```
|
||||
1. Fetch recipient's public key
|
||||
2. Generate ephemeral X25519 keypair
|
||||
3. Compute shared secret: X25519(ephemeral_private, recipient_public)
|
||||
4. Derive encryption key: SHA256(shared_secret)
|
||||
5. Encrypt form data: ChaCha20-Poly1305(data, key, random_nonce)
|
||||
6. Send: {ephemeral_public, nonce, ciphertext}
|
||||
```
|
||||
|
||||
### 3.3 Decryption Flow (Server)
|
||||
|
||||
```
|
||||
1. Receive {ephemeral_public, nonce, ciphertext}
|
||||
2. Compute shared secret: X25519(recipient_private, ephemeral_public)
|
||||
3. Derive encryption key: SHA256(shared_secret)
|
||||
4. Decrypt: ChaCha20-Poly1305_Open(ciphertext, key, nonce)
|
||||
```
|
||||
|
||||
## 4. Wire Format
|
||||
|
||||
### 4.1 Container (Trix-based)
|
||||
|
||||
```
|
||||
[Magic: "STMF" (4 bytes)]
|
||||
[Header: Gob-encoded JSON]
|
||||
[Payload: ChaCha20-Poly1305 ciphertext]
|
||||
```
|
||||
|
||||
### 4.2 Header Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"algorithm": "x25519-chacha20poly1305",
|
||||
"ephemeral_pk": "<base64 32-byte ephemeral public key>"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Transmission
|
||||
|
||||
- Default form field: `_stmf_payload`
|
||||
- Encoding: Base64 string
|
||||
- Content-Type: `application/x-www-form-urlencoded` or `multipart/form-data`
|
||||
|
||||
## 5. Data Structures
|
||||
|
||||
### 5.1 FormField
|
||||
|
||||
```go
|
||||
type FormField struct {
|
||||
Name string // Field name
|
||||
Value string // Base64 for files, plaintext otherwise
|
||||
Type string // "text", "password", "file"
|
||||
Filename string // For file uploads
|
||||
MimeType string // For file uploads
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 FormData
|
||||
|
||||
```go
|
||||
type FormData struct {
|
||||
Fields []FormField // Array of form fields
|
||||
Metadata map[string]string // Arbitrary key-value metadata
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Builder Pattern
|
||||
|
||||
```go
|
||||
formData := NewFormData().
|
||||
AddField("email", "user@example.com").
|
||||
AddFieldWithType("password", "secret", "password").
|
||||
AddFile("document", base64Content, "report.pdf", "application/pdf").
|
||||
SetMetadata("timestamp", time.Now().String())
|
||||
```
|
||||
|
||||
## 6. Key Management API
|
||||
|
||||
### 6.1 Key Generation
|
||||
|
||||
```go
|
||||
// pkg/stmf/keypair.go
|
||||
func GenerateKeyPair() (*KeyPair, error)
|
||||
|
||||
type KeyPair struct {
|
||||
privateKey *ecdh.PrivateKey
|
||||
publicKey *ecdh.PublicKey
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Key Loading
|
||||
|
||||
```go
|
||||
// From raw bytes
|
||||
func LoadPublicKey(data []byte) (*ecdh.PublicKey, error)
|
||||
func LoadPrivateKey(data []byte) (*ecdh.PrivateKey, error)
|
||||
|
||||
// From base64
|
||||
func LoadPublicKeyBase64(encoded string) (*ecdh.PublicKey, error)
|
||||
func LoadPrivateKeyBase64(encoded string) (*ecdh.PrivateKey, error)
|
||||
|
||||
// Reconstruct keypair from private key
|
||||
func LoadKeyPair(privateKeyBytes []byte) (*KeyPair, error)
|
||||
```
|
||||
|
||||
### 6.3 Key Export
|
||||
|
||||
```go
|
||||
func (kp *KeyPair) PublicKey() []byte // Raw 32 bytes
|
||||
func (kp *KeyPair) PrivateKey() []byte // Raw 32 bytes
|
||||
func (kp *KeyPair) PublicKeyBase64() string // Base64 encoded
|
||||
func (kp *KeyPair) PrivateKeyBase64() string // Base64 encoded
|
||||
```
|
||||
|
||||
## 7. WASM API
|
||||
|
||||
### 7.1 BorgSTMF Namespace
|
||||
|
||||
```javascript
|
||||
// Generate X25519 keypair
|
||||
const keypair = await BorgSTMF.generateKeyPair();
|
||||
// keypair.publicKey: base64 string
|
||||
// keypair.privateKey: base64 string
|
||||
|
||||
// Encrypt form data
|
||||
const encrypted = await BorgSTMF.encrypt(
|
||||
JSON.stringify(formData),
|
||||
serverPublicKeyBase64
|
||||
);
|
||||
|
||||
// Encrypt with field-level control
|
||||
const encrypted = await BorgSTMF.encryptFields(
|
||||
{email: "user@example.com", password: "secret"},
|
||||
serverPublicKeyBase64,
|
||||
{timestamp: Date.now().toString()} // Optional metadata
|
||||
);
|
||||
```
|
||||
|
||||
## 8. HTTP Middleware
|
||||
|
||||
### 8.1 Simple Usage
|
||||
|
||||
```go
|
||||
import "github.com/Snider/Borg/pkg/stmf/middleware"
|
||||
|
||||
// Create middleware with private key
|
||||
mw := middleware.Simple(privateKeyBytes)
|
||||
|
||||
// Or from base64
|
||||
mw, err := middleware.SimpleBase64(privateKeyB64)
|
||||
|
||||
// Apply to handler
|
||||
http.Handle("/submit", mw(myHandler))
|
||||
```
|
||||
|
||||
### 8.2 Advanced Configuration
|
||||
|
||||
```go
|
||||
cfg := middleware.DefaultConfig(privateKeyBytes)
|
||||
cfg.FieldName = "_custom_field" // Custom field name (default: _stmf_payload)
|
||||
cfg.PopulateForm = &true // Auto-populate r.Form
|
||||
cfg.OnError = customErrorHandler // Custom error handling
|
||||
cfg.OnMissingPayload = customHandler // When field is absent
|
||||
|
||||
mw := middleware.Middleware(cfg)
|
||||
```
|
||||
|
||||
### 8.3 Context Access
|
||||
|
||||
```go
|
||||
func myHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get decrypted form data
|
||||
formData := middleware.GetFormData(r)
|
||||
|
||||
// Get metadata
|
||||
metadata := middleware.GetMetadata(r)
|
||||
|
||||
// Access fields
|
||||
email := formData.Get("email")
|
||||
password := formData.Get("password")
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 Middleware Behavior
|
||||
|
||||
- Handles POST, PUT, PATCH requests only
|
||||
- Parses multipart/form-data (32 MB limit) or application/x-www-form-urlencoded
|
||||
- Looks for field `_stmf_payload` (configurable)
|
||||
- Base64 decodes, then decrypts
|
||||
- Populates `r.Form` and `r.PostForm` with decrypted fields
|
||||
- Returns 400 Bad Request on decryption failure
|
||||
|
||||
## 9. Integration Example
|
||||
|
||||
### 9.1 HTML Form
|
||||
|
||||
```html
|
||||
<form id="secure-form" data-stmf-pubkey="<base64-public-key>">
|
||||
<input name="name" type="text">
|
||||
<input name="email" type="email">
|
||||
<input name="ssn" type="password">
|
||||
<button type="submit">Send Securely</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('secure-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const pubkey = form.dataset.stmfPubkey;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
const encrypted = await BorgSTMF.encrypt(JSON.stringify(data), pubkey);
|
||||
|
||||
await fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({_stmf_payload: encrypted}),
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 9.2 Server Handler
|
||||
|
||||
```go
|
||||
func main() {
|
||||
privateKey, _ := os.ReadFile("private.key")
|
||||
mw := middleware.Simple(privateKey)
|
||||
|
||||
http.Handle("/api/submit", mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
formData := middleware.GetFormData(r)
|
||||
|
||||
name := formData.Get("name")
|
||||
email := formData.Get("email")
|
||||
ssn := formData.Get("ssn")
|
||||
|
||||
// Process securely...
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})))
|
||||
|
||||
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Security Properties
|
||||
|
||||
### 10.1 Forward Secrecy
|
||||
|
||||
- Fresh ephemeral keypair per encryption
|
||||
- Compromised private key doesn't decrypt past messages
|
||||
- Each ciphertext has unique shared secret
|
||||
|
||||
### 10.2 Authenticity
|
||||
|
||||
- Poly1305 MAC prevents tampering
|
||||
- Decryption fails if ciphertext modified
|
||||
|
||||
### 10.3 Confidentiality
|
||||
|
||||
- ChaCha20 provides 256-bit security
|
||||
- Nonces are random (24 bytes), collision unlikely
|
||||
- Data encrypted before leaving browser
|
||||
|
||||
### 10.4 Key Isolation
|
||||
|
||||
- Private key never exposed to browser/JavaScript
|
||||
- Public key can be safely distributed
|
||||
- Ephemeral keys discarded after encryption
|
||||
|
||||
## 11. Error Handling
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrInvalidMagic = errors.New("invalid STMF magic")
|
||||
ErrInvalidPayload = errors.New("invalid STMF payload")
|
||||
ErrDecryptionFailed = errors.New("decryption failed")
|
||||
ErrInvalidPublicKey = errors.New("invalid public key")
|
||||
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||
ErrKeyGenerationFailed = errors.New("key generation failed")
|
||||
)
|
||||
```
|
||||
|
||||
## 12. Implementation Reference
|
||||
|
||||
- Types: `pkg/stmf/types.go`
|
||||
- Key management: `pkg/stmf/keypair.go`
|
||||
- Encryption: `pkg/stmf/encrypt.go`
|
||||
- Decryption: `pkg/stmf/decrypt.go`
|
||||
- Middleware: `pkg/stmf/middleware/http.go`
|
||||
- WASM: `pkg/wasm/stmf/main.go`
|
||||
|
||||
## 13. Security Considerations
|
||||
|
||||
1. **Public key authenticity**: Verify public key source (HTTPS, pinning)
|
||||
2. **Private key protection**: Never expose to browser, store securely
|
||||
3. **Nonce uniqueness**: Random generation ensures uniqueness
|
||||
4. **HTTPS required**: Transport layer must be encrypted
|
||||
|
||||
## 14. Future Work
|
||||
|
||||
- [ ] Multiple recipients
|
||||
- [ ] Key attestation
|
||||
- [ ] Offline decryption app
|
||||
- [ ] Hardware key support (WebAuthn)
|
||||
- [ ] Key rotation support
|
||||
458
docs/specs/RFC-020-WASM-API.md
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
# RFC-010: WASM Decryption API
|
||||
|
||||
**Status**: Draft
|
||||
**Author**: [Snider](https://github.com/Snider/)
|
||||
**Created**: 2026-01-13
|
||||
**License**: EUPL-1.2
|
||||
**Depends On**: RFC-002, RFC-007, RFC-009
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This RFC specifies the WebAssembly (WASM) API for browser-based decryption of SMSG content and STMF form encryption. The API is exposed through two JavaScript namespaces: `BorgSMSG` for content decryption and `BorgSTMF` for form encryption.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The WASM module provides:
|
||||
- SMSG decryption (v1, v2, v3, chunked, ABR)
|
||||
- SMSG encryption
|
||||
- STMF form encryption/decryption
|
||||
- Metadata extraction without decryption
|
||||
|
||||
## 2. Module Loading
|
||||
|
||||
### 2.1 Files Required
|
||||
|
||||
```
|
||||
stmf.wasm (~5.9MB) Compiled Go WASM module
|
||||
wasm_exec.js (~20KB) Go WASM runtime
|
||||
```
|
||||
|
||||
### 2.2 Initialization
|
||||
|
||||
```html
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch('stmf.wasm'), go.importObject)
|
||||
.then(result => {
|
||||
go.run(result.instance);
|
||||
// BorgSMSG and BorgSTMF now available globally
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2.3 Ready Event
|
||||
|
||||
```javascript
|
||||
document.addEventListener('borgstmf:ready', (event) => {
|
||||
console.log('WASM ready, version:', event.detail.version);
|
||||
});
|
||||
```
|
||||
|
||||
## 3. BorgSMSG Namespace
|
||||
|
||||
### 3.1 Version
|
||||
|
||||
```javascript
|
||||
BorgSMSG.version // "1.6.0"
|
||||
BorgSMSG.ready // true when loaded
|
||||
```
|
||||
|
||||
### 3.2 Metadata Functions
|
||||
|
||||
#### getInfo(base64) → Promise<ManifestInfo>
|
||||
|
||||
Get manifest without decryption.
|
||||
|
||||
```javascript
|
||||
const info = await BorgSMSG.getInfo(base64Content);
|
||||
// info.version, info.algorithm, info.format
|
||||
// info.manifest.title, info.manifest.artist
|
||||
// info.isV3Streaming, info.isChunked
|
||||
// info.wrappedKeys (for v3)
|
||||
```
|
||||
|
||||
#### getInfoBinary(uint8Array) → Promise<ManifestInfo>
|
||||
|
||||
Binary input variant (no base64 decode needed).
|
||||
|
||||
```javascript
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
const info = await BorgSMSG.getInfoBinary(bytes);
|
||||
```
|
||||
|
||||
### 3.3 Decryption Functions
|
||||
|
||||
#### decrypt(base64, password) → Promise<Message>
|
||||
|
||||
Full decryption (v1 format, base64 attachments).
|
||||
|
||||
```javascript
|
||||
const msg = await BorgSMSG.decrypt(base64Content, password);
|
||||
// msg.body, msg.subject, msg.from
|
||||
// msg.attachments[0].name, .content (base64), .mime
|
||||
```
|
||||
|
||||
#### decryptStream(base64, password) → Promise<StreamMessage>
|
||||
|
||||
Streaming decryption (v2 format, binary attachments).
|
||||
|
||||
```javascript
|
||||
const msg = await BorgSMSG.decryptStream(base64Content, password);
|
||||
// msg.attachments[0].data (Uint8Array)
|
||||
// msg.attachments[0].mime
|
||||
```
|
||||
|
||||
#### decryptBinary(uint8Array, password) → Promise<StreamMessage>
|
||||
|
||||
Binary input, binary output.
|
||||
|
||||
```javascript
|
||||
const bytes = new Uint8Array(await fetch(url).then(r => r.arrayBuffer()));
|
||||
const msg = await BorgSMSG.decryptBinary(bytes, password);
|
||||
```
|
||||
|
||||
#### quickDecrypt(base64, password) → Promise<string>
|
||||
|
||||
Returns body text only (fast path).
|
||||
|
||||
```javascript
|
||||
const body = await BorgSMSG.quickDecrypt(base64Content, password);
|
||||
```
|
||||
|
||||
### 3.4 V3 Streaming Functions
|
||||
|
||||
#### decryptV3(base64, params) → Promise<StreamMessage>
|
||||
|
||||
Decrypt v3 streaming content with LTHN rolling keys.
|
||||
|
||||
```javascript
|
||||
const msg = await BorgSMSG.decryptV3(base64Content, {
|
||||
license: "user-license-key",
|
||||
fingerprint: "device-fingerprint" // optional
|
||||
});
|
||||
```
|
||||
|
||||
#### getV3ChunkInfo(base64) → Promise<ChunkInfo>
|
||||
|
||||
Get chunk index for seeking without full decrypt.
|
||||
|
||||
```javascript
|
||||
const chunkInfo = await BorgSMSG.getV3ChunkInfo(base64Content);
|
||||
// chunkInfo.chunkSize (default 1MB)
|
||||
// chunkInfo.totalChunks
|
||||
// chunkInfo.totalSize
|
||||
// chunkInfo.index[i].offset, .size
|
||||
```
|
||||
|
||||
#### unwrapV3CEK(base64, params) → Promise<string>
|
||||
|
||||
Unwrap CEK for manual chunk decryption. Returns base64 CEK.
|
||||
|
||||
```javascript
|
||||
const cekBase64 = await BorgSMSG.unwrapV3CEK(base64Content, {
|
||||
license: "license",
|
||||
fingerprint: "fp"
|
||||
});
|
||||
```
|
||||
|
||||
#### decryptV3Chunk(base64, cekBase64, chunkIndex) → Promise<Uint8Array>
|
||||
|
||||
Decrypt single chunk by index.
|
||||
|
||||
```javascript
|
||||
const chunk = await BorgSMSG.decryptV3Chunk(base64Content, cekBase64, 5);
|
||||
```
|
||||
|
||||
#### parseV3Header(uint8Array) → Promise<V3HeaderInfo>
|
||||
|
||||
Parse header from partial data (for streaming).
|
||||
|
||||
```javascript
|
||||
const header = await BorgSMSG.parseV3Header(bytes);
|
||||
// header.format, header.keyMethod, header.cadence
|
||||
// header.payloadOffset (where chunks start)
|
||||
// header.wrappedKeys, header.chunked, header.manifest
|
||||
```
|
||||
|
||||
#### unwrapCEKFromHeader(wrappedKeys, params, cadence) → Promise<Uint8Array>
|
||||
|
||||
Unwrap CEK from parsed header.
|
||||
|
||||
```javascript
|
||||
const cek = await BorgSMSG.unwrapCEKFromHeader(
|
||||
header.wrappedKeys,
|
||||
{license: "lic", fingerprint: "fp"},
|
||||
"daily"
|
||||
);
|
||||
```
|
||||
|
||||
#### decryptChunkDirect(chunkBytes, cek) → Promise<Uint8Array>
|
||||
|
||||
Low-level chunk decryption with pre-unwrapped CEK.
|
||||
|
||||
```javascript
|
||||
const plaintext = await BorgSMSG.decryptChunkDirect(chunkBytes, cek);
|
||||
```
|
||||
|
||||
### 3.5 Encryption Functions
|
||||
|
||||
#### encrypt(message, password, hint?) → Promise<string>
|
||||
|
||||
Encrypt message (v1 format). Returns base64.
|
||||
|
||||
```javascript
|
||||
const encrypted = await BorgSMSG.encrypt({
|
||||
body: "Hello",
|
||||
attachments: [{
|
||||
name: "file.txt",
|
||||
content: btoa("data"),
|
||||
mime: "text/plain"
|
||||
}]
|
||||
}, password, "optional hint");
|
||||
```
|
||||
|
||||
#### encryptWithManifest(message, password, manifest) → Promise<string>
|
||||
|
||||
Encrypt with manifest (v2 format). Returns base64.
|
||||
|
||||
```javascript
|
||||
const encrypted = await BorgSMSG.encryptWithManifest(message, password, {
|
||||
title: "My Track",
|
||||
artist: "Artist Name",
|
||||
licenseType: "perpetual"
|
||||
});
|
||||
```
|
||||
|
||||
### 3.6 ABR Functions
|
||||
|
||||
#### parseABRManifest(jsonString) → Promise<ABRManifest>
|
||||
|
||||
Parse HLS-style ABR manifest.
|
||||
|
||||
```javascript
|
||||
const manifest = await BorgSMSG.parseABRManifest(manifestJson);
|
||||
// manifest.version, manifest.title, manifest.duration
|
||||
// manifest.variants[i].name, .bandwidth, .url
|
||||
// manifest.defaultIdx
|
||||
```
|
||||
|
||||
#### selectVariant(manifest, bandwidthBps) → Promise<number>
|
||||
|
||||
Select best variant for bandwidth (returns index).
|
||||
|
||||
```javascript
|
||||
const idx = await BorgSMSG.selectVariant(manifest, measuredBandwidth);
|
||||
// Uses 80% safety threshold
|
||||
```
|
||||
|
||||
## 4. BorgSTMF Namespace
|
||||
|
||||
### 4.1 Key Generation
|
||||
|
||||
```javascript
|
||||
const keypair = await BorgSTMF.generateKeyPair();
|
||||
// keypair.publicKey (base64 X25519)
|
||||
// keypair.privateKey (base64 X25519) - KEEP SECRET
|
||||
```
|
||||
|
||||
### 4.2 Encryption
|
||||
|
||||
```javascript
|
||||
// Encrypt JSON string
|
||||
const encrypted = await BorgSTMF.encrypt(
|
||||
JSON.stringify(formData),
|
||||
serverPublicKeyBase64
|
||||
);
|
||||
|
||||
// Encrypt with metadata
|
||||
const encrypted = await BorgSTMF.encryptFields(
|
||||
{email: "user@example.com", password: "secret"},
|
||||
serverPublicKeyBase64,
|
||||
{timestamp: Date.now().toString()} // optional metadata
|
||||
);
|
||||
```
|
||||
|
||||
## 5. Type Definitions
|
||||
|
||||
### 5.1 ManifestInfo
|
||||
|
||||
```typescript
|
||||
interface ManifestInfo {
|
||||
version: string;
|
||||
algorithm: string;
|
||||
format?: string;
|
||||
compression?: string;
|
||||
hint?: string;
|
||||
keyMethod?: string; // "LTHN" for v3
|
||||
cadence?: string; // "daily", "12h", "6h", "1h"
|
||||
wrappedKeys?: WrappedKey[];
|
||||
isV3Streaming: boolean;
|
||||
chunked?: ChunkInfo;
|
||||
isChunked: boolean;
|
||||
manifest?: Manifest;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Message / StreamMessage
|
||||
|
||||
```typescript
|
||||
interface Message {
|
||||
from?: string;
|
||||
to?: string;
|
||||
subject?: string;
|
||||
body: string;
|
||||
timestamp?: number;
|
||||
attachments: Attachment[];
|
||||
replyKey?: KeyInfo;
|
||||
meta?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
name: string;
|
||||
mime: string;
|
||||
size: number;
|
||||
content?: string; // base64 (v1)
|
||||
data?: Uint8Array; // binary (v2/v3)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 ChunkInfo
|
||||
|
||||
```typescript
|
||||
interface ChunkInfo {
|
||||
chunkSize: number; // default 1048576 (1MB)
|
||||
totalChunks: number;
|
||||
totalSize: number;
|
||||
index: ChunkEntry[];
|
||||
}
|
||||
|
||||
interface ChunkEntry {
|
||||
offset: number;
|
||||
size: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Manifest
|
||||
|
||||
```typescript
|
||||
interface Manifest {
|
||||
title: string;
|
||||
artist?: string;
|
||||
album?: string;
|
||||
genre?: string;
|
||||
year?: number;
|
||||
releaseType?: string; // "single", "album", "ep", "mix"
|
||||
duration?: number; // seconds
|
||||
format?: string;
|
||||
expiresAt?: number; // Unix timestamp
|
||||
issuedAt?: number; // Unix timestamp
|
||||
licenseType?: string; // "perpetual", "rental", "stream", "preview"
|
||||
tracks?: Track[];
|
||||
tags?: string[];
|
||||
links?: Record<string, string>;
|
||||
extra?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Error Handling
|
||||
|
||||
### 6.1 Pattern
|
||||
|
||||
All functions throw on error:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const msg = await BorgSMSG.decrypt(content, password);
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Common Errors
|
||||
|
||||
| Error | Cause |
|
||||
|-------|-------|
|
||||
| `decrypt requires 2 arguments` | Wrong argument count |
|
||||
| `decryption failed: {reason}` | Wrong password or corrupted |
|
||||
| `invalid format` | Not a valid SMSG file |
|
||||
| `unsupported version` | Unknown format version |
|
||||
| `key expired` | v3 rolling key outside window |
|
||||
| `invalid base64: {reason}` | Base64 decode failed |
|
||||
| `chunk out of range` | Invalid chunk index |
|
||||
|
||||
## 7. Performance
|
||||
|
||||
### 7.1 Binary vs Base64
|
||||
|
||||
- Binary functions (`*Binary`, `decryptStream`) are ~30% faster
|
||||
- Avoid double base64 encoding
|
||||
|
||||
### 7.2 Large Files (>50MB)
|
||||
|
||||
Use chunked streaming:
|
||||
|
||||
```javascript
|
||||
// Efficient: Cache CEK, stream chunks
|
||||
const header = await BorgSMSG.parseV3Header(bytes);
|
||||
const cek = await BorgSMSG.unwrapCEKFromHeader(header.wrappedKeys, params);
|
||||
|
||||
for (let i = 0; i < header.chunked.totalChunks; i++) {
|
||||
const chunk = await BorgSMSG.decryptChunkDirect(payload, cek);
|
||||
player.write(chunk);
|
||||
// chunk is GC'd after each iteration
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Typical Execution Times
|
||||
|
||||
| Operation | Size | Time |
|
||||
|-----------|------|------|
|
||||
| getInfo | any | ~50-100ms |
|
||||
| decrypt (small) | <1MB | ~200-500ms |
|
||||
| decrypt (large) | 100MB | 2-5s |
|
||||
| decryptV3Chunk | 1MB | ~200-400ms |
|
||||
| generateKeyPair | - | ~50-200ms |
|
||||
|
||||
## 8. Browser Compatibility
|
||||
|
||||
| Browser | Support |
|
||||
|---------|---------|
|
||||
| Chrome 57+ | Full |
|
||||
| Firefox 52+ | Full |
|
||||
| Safari 11+ | Full |
|
||||
| Edge 16+ | Full |
|
||||
| IE | Not supported |
|
||||
|
||||
Requirements:
|
||||
- WebAssembly support
|
||||
- Async/await (ES2017)
|
||||
- Uint8Array
|
||||
|
||||
## 9. Memory Management
|
||||
|
||||
- WASM module: ~5.9MB static
|
||||
- Per-operation: Peak ~2-3x file size during decryption
|
||||
- Go GC reclaims after Promise resolution
|
||||
- Keys never leave WASM memory
|
||||
|
||||
## 10. Implementation Reference
|
||||
|
||||
- Source: `pkg/wasm/stmf/main.go` (1758 lines)
|
||||
- Build: `GOOS=js GOARCH=wasm go build -o stmf.wasm ./pkg/wasm/stmf/`
|
||||
|
||||
## 11. Security Considerations
|
||||
|
||||
1. **Password handling**: Clear from memory after use
|
||||
2. **Memory isolation**: WASM sandbox prevents JS access
|
||||
3. **Constant-time crypto**: Go crypto uses safe operations
|
||||
4. **Key protection**: Keys never exposed to JavaScript
|
||||
|
||||
## 12. Future Work
|
||||
|
||||
- [ ] WebWorker support for background decryption
|
||||
- [ ] Streaming API with ReadableStream
|
||||
- [ ] Smaller WASM size via TinyGo
|
||||
- [ ] Native Web Crypto fallback for simple operations
|
||||
448
docs/specs/RFC-021-CORE-PLATFORM-ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
# RFC-021: Core Platform Architecture
|
||||
|
||||
```
|
||||
RFC: 021
|
||||
Title: Core Platform Architecture
|
||||
Status: Standards Track
|
||||
Category: Informational
|
||||
Authors: Snider, Cladius Maximus
|
||||
License: EUPL-1.2
|
||||
Created: 2026-03-14
|
||||
Requires: RFC-0001 through RFC-0005, RFC-001 through RFC-020
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document specifies how the 25 preceding RFCs compose into a single coherent platform via the Core package ecosystem. It defines the Service Provider as the universal unit of functionality, the TRIX container as the universal distribution format, HLCRF as the universal layout primitive, UEPS as the universal consent layer, and OpenAPI as the universal polyglot contract. Together these form a Web3 application standard where identity is sovereign, compute is local, and every component is replaceable.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Principles
|
||||
|
||||
1. **The binary IS the platform** — a single Go binary embeds all layers
|
||||
2. **Providers are the universal unit** — everything is a service provider
|
||||
3. **OpenAPI is the polyglot contract** — Go, PHP, TypeScript speak the same API
|
||||
4. **TRIX is the universal envelope** — all data shares one container format
|
||||
5. **HLCRF is the universal layout** — one string describes any UI structure
|
||||
6. **UEPS is the universal consent** — every packet carries intent metadata
|
||||
7. **Language is irrelevant** — the interface is the boundary, not the runtime
|
||||
|
||||
---
|
||||
|
||||
## 2. Layer Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 7: RENDERING │
|
||||
│ HLCRF compositor (RFC-001), Angular custom elements, │
|
||||
│ go-html WASM, Web Components, CoreDeno sidecar (core/ts) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 6: ANALYSIS │
|
||||
│ LEM ethics scoring, go-i18n GrammarImprint, │
|
||||
│ Poindexter spatial indexing, OpenBrain vector store │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 5: STORAGE │
|
||||
│ DataNode in-memory FS (RFC-013), go-io Medium (local/S3/SSH), │
|
||||
│ go-store SQLite KV, Borg blob storage │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 4: COMPUTE │
|
||||
│ TIM containers (RFC-014), go-process daemon registry, │
|
||||
│ CoreDeno sandbox, FrankenPHP embedded runtime │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 3: CRYPTO │
|
||||
│ Enchantrix sigils (RFC-009), TRIX containers (RFC-010), │
|
||||
│ SMSG media (RFC-012), STIM encrypted TIM (RFC-015), │
|
||||
│ STMF secure forms (RFC-019), LTHN hash (RFC-007) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 2: PROTOCOL │
|
||||
│ UEPS consent-gated TLV, SDP service discovery (RFC-0002), │
|
||||
│ Payment dispatcher (RFC-0004), MCP tool protocol │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 1: IDENTITY │
|
||||
│ Wallet-as-identity, Ed25519 signing, HNS TLD addressing, │
|
||||
│ go-crypt trust policies │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Each layer has implementations in Go, PHP, and/or TypeScript. The layer boundaries are defined by interfaces, not languages.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Service Provider
|
||||
|
||||
### 3.1 Definition
|
||||
|
||||
A Service Provider is any component that:
|
||||
|
||||
1. Declares an OpenAPI spec (the contract)
|
||||
2. Registers routes on the API engine (the implementation)
|
||||
3. Optionally declares an HLCRF layout (the UI)
|
||||
4. Optionally emits WebSocket events (real-time data)
|
||||
5. Optionally declares MCP tool descriptions (AI integration)
|
||||
|
||||
### 3.2 Go Providers
|
||||
|
||||
Go providers implement `api.RouteGroup` directly:
|
||||
|
||||
```go
|
||||
type Provider interface {
|
||||
Name() string
|
||||
BasePath() string
|
||||
RegisterRoutes(rg *gin.RouterGroup)
|
||||
}
|
||||
```
|
||||
|
||||
Extensions: `Streamable` (WS events), `Describable` (OpenAPI), `Renderable` (custom element tag).
|
||||
|
||||
### 3.3 PHP Providers
|
||||
|
||||
PHP providers are Laravel modules implementing the provider contract. The PHP provider runs inside FrankenPHP (embedded in the Go binary) or as a standalone Laravel app. The Go API layer discovers its OpenAPI spec and creates a reverse proxy route group.
|
||||
|
||||
### 3.4 TypeScript Providers
|
||||
|
||||
TypeScript providers run inside CoreDeno (core/ts sidecar) and expose routes via the gRPC bridge. Same pattern — OpenAPI spec + reverse proxy.
|
||||
|
||||
### 3.5 Provider Distribution
|
||||
|
||||
Providers are packaged as TRIX containers:
|
||||
|
||||
```
|
||||
provider.trix
|
||||
├── .core/view.yml # Manifest: name, HLCRF variant, permissions
|
||||
├── openapi.json # OpenAPI spec (the contract)
|
||||
├── element.js # Custom element bundle (Renderable)
|
||||
├── src/ # Implementation (Go/PHP/TS source)
|
||||
└── sign # Ed25519 signature
|
||||
```
|
||||
|
||||
For secure distribution: `provider.stim` (encrypted TRIX via RFC-015).
|
||||
|
||||
### 3.6 Provider Discovery
|
||||
|
||||
```yaml
|
||||
# .core/config.yaml
|
||||
providers:
|
||||
brain:
|
||||
enabled: true
|
||||
vpn:
|
||||
enabled: true
|
||||
endpoint: http://localhost:8774
|
||||
studio:
|
||||
enabled: true
|
||||
package: forge.lthn.ai/core/php-studio
|
||||
```
|
||||
|
||||
The registry loads providers from:
|
||||
1. Go packages registered via `engine.Register()`
|
||||
2. `.core/providers/*.yaml` for polyglot providers
|
||||
3. Marketplace (git-based, signed manifests)
|
||||
|
||||
---
|
||||
|
||||
## 4. The Application Shell (core/ide)
|
||||
|
||||
### 4.1 Architecture
|
||||
|
||||
core/ide is a Wails 3 systray application that assembles providers:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Wails 3 (Systray) │
|
||||
│ ┌──────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ Systray │ │ Angular Shell │ │
|
||||
│ │ (life- │ │ ┌─────────────────────────────────┐ │ │
|
||||
│ │ cycle) │ │ │ HLCRF Layout │ │ │
|
||||
│ │ │ │ │ H: [nav-bar] │ │ │
|
||||
│ │ │ │ │ L: [provider-list] │ │ │
|
||||
│ │ │ │ │ C: [<active-provider-panel>] │ │ │
|
||||
│ │ │ │ │ F: [status-bar] │ │ │
|
||||
│ │ │ │ └─────────────────────────────────┘ │ │
|
||||
│ └──────────┘ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────┐│
|
||||
│ │ core.Core (DI container + IPC bus) ││
|
||||
│ │ ├─ core/api Engine (provider registry + Gin router) ││
|
||||
│ │ ├─ core/mcp Service (MCP server + brain + tools) ││
|
||||
│ │ ├─ core/gui display.Service (Wails platform bridge) ││
|
||||
│ │ └─ go-ws Hub (Angular ↔ providers real-time) ││
|
||||
│ └──────────────────────────────────────────────────────┘│
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Three Access Patterns
|
||||
|
||||
The same providers are accessible three ways:
|
||||
|
||||
| Access | Transport | Consumer |
|
||||
|--------|-----------|----------|
|
||||
| **REST API** | HTTP (Gin) | Web apps, curl, SDKs |
|
||||
| **MCP** | stdio/TCP/Unix | Claude Code, AI agents |
|
||||
| **GUI** | WebSocket + custom elements | Angular shell in Wails |
|
||||
|
||||
### 4.3 Dual Identity
|
||||
|
||||
core/ide serves two roles simultaneously:
|
||||
|
||||
1. **Developer tool** — MCP server for AI agents, OpenBrain recall, build/deploy providers, process management dashboard
|
||||
2. **Network client** — VPN tunnel manager (RFC-0005), payment wallet (RFC-0004), service discovery (RFC-0002), media player (RFC-011)
|
||||
|
||||
Both roles use the same provider framework. VPN is just another provider.
|
||||
|
||||
---
|
||||
|
||||
## 5. RFC Mapping to Core Packages
|
||||
|
||||
### 5.1 Network Protocol
|
||||
|
||||
| RFC | Package | Implementation |
|
||||
|-----|---------|----------------|
|
||||
| 0001: Network | go-p2p | P2P mesh, UEPS wire protocol |
|
||||
| 0002: SDP | go-scm/forge | Service descriptor queries |
|
||||
| 0003: Exit Node | go-process | Managed daemon processes |
|
||||
| 0004: Payment | go-blockchain | Wallet RPC, transaction submission |
|
||||
| 0005: Client | **core/ide** | The application itself |
|
||||
|
||||
### 5.2 Platform
|
||||
|
||||
| RFC | Package | Implementation |
|
||||
|-----|---------|----------------|
|
||||
| 001: HLCRF | core/php + go-html + core/gui | Polyglot layout |
|
||||
| 002: Events | core/php + core/go IPC bus | Module lifecycle |
|
||||
| 003: Config | core/php + go-config | Configuration |
|
||||
| 004: Entitlements | core/php-tenant | Feature gating |
|
||||
| 005: Commerce | core/php-commerce | Billing, subscriptions |
|
||||
| 006: Compound SKU | core/php-commerce | Product structure |
|
||||
|
||||
### 5.3 Cryptography
|
||||
|
||||
| RFC | Package | Implementation |
|
||||
|-----|---------|----------------|
|
||||
| 007: LTHN Hash | go-crypt | Quasi-salted hash |
|
||||
| 008: Pre-obfuscation | go-crypt | AEAD pre-layer |
|
||||
| 009: Sigil | Enchantrix | Transformation chain |
|
||||
| 010: TRIX | Enchantrix pkg/trix | Binary container format |
|
||||
| 016: TRIX PGP | Enchantrix | PGP encryption variant |
|
||||
| 017: Key derivation | go-crypt | Key stretching |
|
||||
|
||||
### 5.4 Containers
|
||||
|
||||
| RFC | Package | Implementation |
|
||||
|-----|---------|----------------|
|
||||
| 011: OSS-DRM | Borg pkg/smsg | Password-as-license media |
|
||||
| 012: SMSG | Borg pkg/smsg | Encrypted media container |
|
||||
| 013: DataNode | Borg pkg/datanode + go-io | In-memory filesystem |
|
||||
| 014: TIM | Borg pkg/tim | OCI container bundles |
|
||||
| 015: STIM | Borg pkg/tim | Encrypted containers |
|
||||
| 018: Borgfile | Borg cmd/compile | Container compilation |
|
||||
|
||||
### 5.5 Interfaces
|
||||
|
||||
| RFC | Package | Implementation |
|
||||
|-----|---------|----------------|
|
||||
| 019: STMF | Borg pkg/stmf + WASM | Secure form encryption |
|
||||
| 020: WASM API | Borg pkg/wasm + go-html | Browser crypto API |
|
||||
|
||||
---
|
||||
|
||||
## 6. UEPS Integration
|
||||
|
||||
The Unified Ethical Protocol Stack (Mining pkg/ueps) wraps every inter-provider communication:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ UEPS Packet │
|
||||
│ Tag 0x01: Version (0x09 = IPv9) │
|
||||
│ Tag 0x02: Current Layer (sender) │
|
||||
│ Tag 0x03: Target Layer (recipient) │
|
||||
│ Tag 0x04: Intent ID (semantic token) │
|
||||
│ Tag 0x05: Threat Score (0-65535) │
|
||||
│ Tag 0x06: HMAC (SHA-256 signature) │
|
||||
│ Tag 0xFF: Payload (the actual data) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Every API call between providers, every MCP tool invocation, every WS event can carry UEPS metadata. The Intent ID maps to go-i18n's semantic tokens. The Threat Score is updated by LEM's ethics scoring. The HMAC binds the packet to a shared secret (wallet-derived for network, config-derived for local).
|
||||
|
||||
Optional for local providers (localhost doesn't need consent gates). Required for network providers (RFC-0003 exit nodes, remote services).
|
||||
|
||||
---
|
||||
|
||||
## 7. HLCRF as Universal Layout
|
||||
|
||||
### 7.1 Three Implementations, One String
|
||||
|
||||
The variant string `H[LC]CF` produces identical layouts in:
|
||||
|
||||
| Runtime | Implementation | Package |
|
||||
|---------|---------------|---------|
|
||||
| **PHP** | `Layout::make('H[LC]CF')` | core/php Front\Components\Layout |
|
||||
| **Go** | `hlcrf.New("H[LC]CF")` | go-html |
|
||||
| **TypeScript** | `<core-layout variant="H[LC]CF">` | core/ts Web Component |
|
||||
| **Angular** | `<app-layout [variant]="'H[LC]CF'">` | core/ide frontend |
|
||||
|
||||
### 7.2 Provider Layout Declaration
|
||||
|
||||
Each Renderable provider declares its HLCRF variant in `.core/view.yml`:
|
||||
|
||||
```yaml
|
||||
code: brain-panel
|
||||
name: OpenBrain Panel
|
||||
element: core-brain-panel
|
||||
layout: HCF
|
||||
slots:
|
||||
H: search-bar
|
||||
C: results-list
|
||||
F: status-bar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Content Distribution
|
||||
|
||||
### 8.1 TRIX Family
|
||||
|
||||
All content shares the TRIX container format (RFC-010):
|
||||
|
||||
| Magic | Purpose | Encryption | RFC |
|
||||
|-------|---------|------------|-----|
|
||||
| TRIX | Generic container | Optional | 010 |
|
||||
| SMSG | Encrypted media | ChaCha20-Poly1305 | 012 |
|
||||
| STIM | Encrypted container | Dual ChaCha20-Poly1305 | 015 |
|
||||
| STMF | Encrypted form data | X25519 + ChaCha20-Poly1305 | 019 |
|
||||
|
||||
### 8.2 Provider as SMSG
|
||||
|
||||
A provider's custom element + assets can be packaged as SMSG:
|
||||
|
||||
- **Manifest**: Provider metadata (name, version, author, links)
|
||||
- **Payload**: The JS bundle + OpenAPI spec + config
|
||||
- **License**: Password-as-license for commercial providers
|
||||
- **Distribution**: CDN, IPFS, Git marketplace — encrypted at rest
|
||||
|
||||
---
|
||||
|
||||
## 9. The Binary
|
||||
|
||||
### 9.1 Composition
|
||||
|
||||
```
|
||||
core-ide binary (~60MB)
|
||||
├── Go runtime
|
||||
├── core/go (DI + lifecycle)
|
||||
├── core/api (Gin + provider registry + OpenAPI)
|
||||
├── core/mcp (MCP server + brain + subsystems)
|
||||
├── core/gui (16 IPC packages + display service)
|
||||
├── core/ts (CoreDeno sidecar manager)
|
||||
├── go-blockchain (wallet + chain sync)
|
||||
├── go-process (daemon registry)
|
||||
├── go-io (sandboxed filesystem)
|
||||
├── go-crypt (Enchantrix bindings)
|
||||
├── go-i18n (grammar engine)
|
||||
├── go-html (HLCRF + WASM codegen)
|
||||
├── go-p2p (UEPS + network mesh)
|
||||
├── Wails 3 (WebView2 + systray)
|
||||
├── FrankenPHP (PHP 8.4 ZTS, optional)
|
||||
└── Angular frontend (embedded via //go:embed)
|
||||
```
|
||||
|
||||
One binary. No external dependencies. Runs on macOS, Linux, Windows.
|
||||
|
||||
### 9.2 Without GUI
|
||||
|
||||
go-config `gui.enabled: false` skips Wails. Core still runs all services — MCP server, API engine, brain, providers.
|
||||
|
||||
---
|
||||
|
||||
## 10. Polyglot Contract
|
||||
|
||||
### 10.1 OpenAPI as the Boundary
|
||||
|
||||
The only thing that crosses language boundaries is the OpenAPI spec:
|
||||
|
||||
```
|
||||
Go provider: implements RouteGroup directly
|
||||
PHP provider: publishes OpenAPI spec, reverse proxy
|
||||
TS provider: publishes OpenAPI spec, CoreDeno gRPC bridge
|
||||
```
|
||||
|
||||
### 10.2 SDK Generation
|
||||
|
||||
From the assembled OpenAPI spec, auto-generate client libraries for TypeScript, Python, PHP, and Go. One API, every language.
|
||||
|
||||
---
|
||||
|
||||
## 11. Security Model
|
||||
|
||||
### 11.1 Provider Isolation
|
||||
|
||||
| Isolation | Mechanism | RFC |
|
||||
|-----------|-----------|-----|
|
||||
| Filesystem | go-io Medium sandbox | — |
|
||||
| Process | TIM OCI containers | 014 |
|
||||
| Network | CoreDeno permission gates | — |
|
||||
| Crypto | Per-provider STIM encryption | 015 |
|
||||
| Consent | UEPS intent tokens per packet | — |
|
||||
| Identity | Ed25519 signed manifests | — |
|
||||
|
||||
### 11.2 Zero-Trust Distribution
|
||||
|
||||
Providers distributed as STIM are encrypted at rest. The marketplace serves encrypted blobs. Only the purchaser's password decrypts. The signature verifies the publisher. The UEPS headers carry consent metadata.
|
||||
|
||||
---
|
||||
|
||||
## 12. Implementation Status
|
||||
|
||||
### 12.1 Complete
|
||||
|
||||
All 25 preceding RFCs have implementations. The provider framework (Phase 1) is live. core/ide is modernised. The API polyglot merge is in progress.
|
||||
|
||||
### 12.2 In Progress
|
||||
|
||||
- core/api polyglot merge (Go + PHP in one repo)
|
||||
- Provider GUI consumer (Phase 2 — Renderable discovery)
|
||||
- Polyglot providers (Phase 3 — PHP/TS via OpenAPI)
|
||||
|
||||
### 12.3 Future
|
||||
|
||||
- Provider marketplace (STIM distribution + git registry)
|
||||
- Network UEPS enforcement (go-p2p integration)
|
||||
- VPN client provider (RFC-0005 as a service provider)
|
||||
- Mobile (Wails mobile or PWA)
|
||||
|
||||
---
|
||||
|
||||
## 13. Relationship to Existing Standards
|
||||
|
||||
| Standard | Relationship |
|
||||
|----------|-------------|
|
||||
| OCI Runtime Spec | TIM bundles are OCI-compatible |
|
||||
| OpenAPI 3.1 | Provider contract format |
|
||||
| MCP | AI agent integration |
|
||||
| Web Components v1 | Provider UI elements |
|
||||
| WireGuard | Network transport option |
|
||||
| ChaCha20-Poly1305 (RFC 8439) | All encryption |
|
||||
| X25519 (RFC 7748) | Key exchange (STMF) |
|
||||
| CSS Grid / Flexbox | HLCRF rendering |
|
||||
| fs.FS (Go stdlib) | DataNode interface |
|
||||
|
||||
---
|
||||
|
||||
## 14. References
|
||||
|
||||
- RFC-0001 through RFC-0005: Lethean network protocol
|
||||
- RFC-001 through RFC-020: Implementation specifications
|
||||
- Borg: forge.lthn.ai/Snider/Borg
|
||||
- Enchantrix: forge.lthn.ai/Snider/Enchantrix
|
||||
- Poindexter: forge.lthn.ai/Snider/Poindexter
|
||||
- Mining: forge.lthn.ai/Snider/Mining
|
||||
- core.help: Ecosystem documentation
|
||||
|
||||
---
|
||||
|
||||
## Licence
|
||||
|
||||
EUPL-1.2
|
||||
|
||||
**Viva La OpenSource**
|
||||
125
docs/specs/RFC-024-ISSUE-TRACKER.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# RFC-024: Issue Tracker & Sprint System
|
||||
|
||||
**Date:** 2026-03-16
|
||||
**Status:** Draft
|
||||
**Author:** Cladius Maximus
|
||||
**Scope:** CorePHP (php-agentic module)
|
||||
|
||||
## Summary
|
||||
|
||||
A lightweight issue tracker built into the CorePHP agentic module. Issues flow from discovery (scans, webhooks, user reports) through triage, sprint planning, agent dispatch, PR, merge, and auto-release.
|
||||
|
||||
## Motivation
|
||||
|
||||
Manually tagging 40+ Go repos bottom-up is painful. No system connects "work found" to "work done" to "version released". The agentic dispatch system executes work, but nothing orchestrates what to work on or when to release.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. Issues are a spectrum — from "fix this typo" to "add GoDot to the IDE"
|
||||
2. Sprints are dumb — just "start" and "complete"
|
||||
3. Milestones are buckets — next-patch, next-minor, next-major, ideas, backlog
|
||||
4. Changelogs are derived — auto-generated from merged PRs per milestone
|
||||
5. Agents are assignees — Cladius, Charon, Gemini, local model, or human
|
||||
6. Projects are repos — tracked by php-uptelligence
|
||||
7. Triage is automated — local model reports size/scope
|
||||
|
||||
## Data Model
|
||||
|
||||
### Issue
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| repo | string | Project = repo name |
|
||||
| title, body | string, text | What needs doing |
|
||||
| status | enum | open, assigned, in_progress, review, done, closed |
|
||||
| priority | enum | critical, high, normal, low |
|
||||
| milestone | enum | next-patch, next-minor, next-major, ideas, backlog |
|
||||
| size | enum | trivial, small, medium, large, epic |
|
||||
| source | string | scan, user, forge, github, discovery, cve |
|
||||
| source_ref | string | URL or workspace ID |
|
||||
| assignee | string | Agent name or user handle |
|
||||
| labels | JSON | e.g. ["security", "conventions"] |
|
||||
| pr_url | string | Linked PR |
|
||||
| plan_id | FK nullable | Escalated to AgentPlan |
|
||||
| parent_id | FK nullable | Epic child relationship |
|
||||
| metadata | JSON | Flexible (scan results, triage notes) |
|
||||
|
||||
### Sprint
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| name | string | "Sprint 2026-W12" or custom |
|
||||
| status | enum | planning, active, completed |
|
||||
| started_at | timestamp | When sprint was started |
|
||||
| completed_at | timestamp | When sprint was completed |
|
||||
| notes | text | Retrospective / release notes |
|
||||
| metadata | JSON | Repos touched, tags created |
|
||||
|
||||
### IssueComment
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| issue_id | FK | Parent issue |
|
||||
| author | string | Agent name or user |
|
||||
| body | text | Comment content |
|
||||
| type | enum | comment, triage, scan_result, status_change |
|
||||
|
||||
## Issue Sizing
|
||||
|
||||
| Size | Scope | Plans | Example |
|
||||
|------|-------|-------|---------|
|
||||
| trivial | One line | 0 | Fix typo |
|
||||
| small | One file | 0 | Alias import |
|
||||
| medium | Multiple files, one repo | 1 | Refactor to use go-io |
|
||||
| large | Multiple plans, one repo | 2+ | Security audit fixes |
|
||||
| epic | Multiple repos | N children | Add GoDot to IDE |
|
||||
|
||||
## Sprint Flow
|
||||
|
||||
Start sprint: takes everything in next-* milestones, marks active.
|
||||
Complete sprint: triggers core dev tag, generates changelogs, closes done issues.
|
||||
|
||||
## Auto-Versioning Pipeline
|
||||
|
||||
PR opened: v0.3.2-alpha.{pr}
|
||||
PR updated: v0.3.2-alpha.{pr}+build.{n}
|
||||
PR merged: v0.3.2-beta.{n}
|
||||
Sprint complete: v0.3.2 (stable release)
|
||||
|
||||
core dev tag handles bottom-up dependency chain.
|
||||
Downstream repos get webhook, go get -u, test, auto-PR.
|
||||
|
||||
## Discovery Engine
|
||||
|
||||
Scheduled action finding repos needing attention. Repos tracked by php-uptelligence with no open issues get scanned automatically. Scan types: conventions, dependency, security, CVE, test gaps, doc sync, dead code. Results create issues in the inbox.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
Issues: GET/POST /v1/issues, GET/PATCH/DEL /v1/issues/{id}
|
||||
Comments: POST/GET /v1/issues/{id}/comments
|
||||
Sprints: GET/POST /v1/sprints, POST /v1/sprints/{id}/start, POST /v1/sprints/{id}/complete
|
||||
Projects: GET /v1/projects, GET /v1/projects/{repo}/changelog
|
||||
Milestones: GET /v1/milestones
|
||||
|
||||
## MCP Tools
|
||||
|
||||
agentic_issue_create, agentic_issue_list, agentic_issue_update, agentic_issue_triage, agentic_sprint_start, agentic_sprint_complete
|
||||
|
||||
## Integration Points
|
||||
|
||||
Forge/GitHub (bidirectional issue sync), php-uptelligence (discovery), agentic dispatch (issue to PR), core dev tag (sprint to release), CodeRabbit (PR review), OpenBrain (context), Sentry (error reports)
|
||||
|
||||
## UI (Flux UI Pro)
|
||||
|
||||
Board (Kanban), List (filterable table with project dropdown), Sprint (progress + repo breakdown)
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Models + migrations
|
||||
2. API endpoints
|
||||
3. MCP tools
|
||||
4. Discovery cron
|
||||
5. Forge sync
|
||||
6. UI
|
||||
7. Auto-changelog
|
||||
8. Auto-tag
|
||||
303
docs/specs/RFC-025-AGENT-EXPERIENCE.md
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
# RFC-025: Agent Experience (AX) Design Principles
|
||||
|
||||
- **Status:** Draft
|
||||
- **Authors:** Snider, Cladius
|
||||
- **Date:** 2026-03-19
|
||||
- **Applies to:** All Core ecosystem packages (CoreGO, CorePHP, CoreTS, core-agent)
|
||||
|
||||
## Abstract
|
||||
|
||||
Agent Experience (AX) is a design paradigm for software systems where the primary code consumer is an AI agent, not a human developer. AX sits alongside User Experience (UX) and Developer Experience (DX) as the third era of interface design.
|
||||
|
||||
This RFC establishes AX as a formal design principle for the Core ecosystem and defines the conventions that follow from it.
|
||||
|
||||
## Motivation
|
||||
|
||||
As of early 2026, AI agents write, review, and maintain the majority of code in the Core ecosystem. The original author has not manually edited code (outside of Core struct design) since October 2025. Code is processed semantically — agents reason about intent, not characters.
|
||||
|
||||
Design patterns inherited from the human-developer era optimise for the wrong consumer:
|
||||
|
||||
- **Short names** save keystrokes but increase semantic ambiguity
|
||||
- **Functional option chains** are fluent for humans but opaque for agents tracing configuration
|
||||
- **Error-at-every-call-site** produces 50% boilerplate that obscures intent
|
||||
- **Generic type parameters** force agents to carry type context that the runtime already has
|
||||
- **Panic-hiding conventions** (`Must*`) create implicit control flow that agents must special-case
|
||||
|
||||
AX acknowledges this shift and provides principles for designing code, APIs, file structures, and conventions that serve AI agents as first-class consumers.
|
||||
|
||||
## The Three Eras
|
||||
|
||||
| Era | Primary Consumer | Optimises For | Key Metric |
|
||||
|-----|-----------------|---------------|------------|
|
||||
| UX | End users | Discoverability, forgiveness, visual clarity | Task completion time |
|
||||
| DX | Developers | Typing speed, IDE support, convention familiarity | Time to first commit |
|
||||
| AX | AI agents | Predictability, composability, semantic navigation | Correct-on-first-pass rate |
|
||||
|
||||
AX does not replace UX or DX. End users still need good UX. Developers still need good DX. But when the primary code author and maintainer is an AI agent, the codebase should be designed for that consumer first.
|
||||
|
||||
## Principles
|
||||
|
||||
### 1. Predictable Names Over Short Names
|
||||
|
||||
Names are tokens that agents pattern-match across languages and contexts. Abbreviations introduce mapping overhead.
|
||||
|
||||
```
|
||||
Config not Cfg
|
||||
Service not Srv
|
||||
Embed not Emb
|
||||
Error not Err (as a subsystem name; err for local variables is fine)
|
||||
Options not Opts
|
||||
```
|
||||
|
||||
**Rule:** If a name would require a comment to explain, it is too short.
|
||||
|
||||
**Exception:** Industry-standard abbreviations that are universally understood (`HTTP`, `URL`, `ID`, `IPC`, `I18n`) are acceptable. The test: would an agent trained on any mainstream language recognise it without context?
|
||||
|
||||
### 2. Comments as Usage Examples
|
||||
|
||||
The function signature tells WHAT. The comment shows HOW with real values.
|
||||
|
||||
```go
|
||||
// Detect the project type from files present
|
||||
setup.Detect("/path/to/project")
|
||||
|
||||
// Set up a workspace with auto-detected template
|
||||
setup.Run(setup.Options{Path: ".", Template: "auto"})
|
||||
|
||||
// Scaffold a PHP module workspace
|
||||
setup.Run(setup.Options{Path: "./my-module", Template: "php"})
|
||||
```
|
||||
|
||||
**Rule:** If a comment restates what the type signature already says, delete it. If a comment shows a concrete usage with realistic values, keep it.
|
||||
|
||||
**Rationale:** Agents learn from examples more effectively than from descriptions. A comment like "Run executes the setup process" adds zero information. A comment like `setup.Run(setup.Options{Path: ".", Template: "auto"})` teaches an agent exactly how to call the function.
|
||||
|
||||
### 3. Path Is Documentation
|
||||
|
||||
File and directory paths should be self-describing. An agent navigating the filesystem should understand what it is looking at without reading a README.
|
||||
|
||||
```
|
||||
flow/deploy/to/homelab.yaml — deploy TO the homelab
|
||||
flow/deploy/from/github.yaml — deploy FROM GitHub
|
||||
flow/code/review.yaml — code review flow
|
||||
template/file/go/struct.go.tmpl — Go struct file template
|
||||
template/dir/workspace/php/ — PHP workspace scaffold
|
||||
```
|
||||
|
||||
**Rule:** If an agent needs to read a file to understand what a directory contains, the directory naming has failed.
|
||||
|
||||
**Corollary:** The unified path convention (folder structure = HTTP route = CLI command = test path) is AX-native. One path, every surface.
|
||||
|
||||
### 4. Templates Over Freeform
|
||||
|
||||
When an agent generates code from a template, the output is constrained to known-good shapes. When an agent writes freeform, the output varies.
|
||||
|
||||
```go
|
||||
// Template-driven — consistent output
|
||||
lib.RenderFile("php/action", data)
|
||||
lib.ExtractDir("php", targetDir, data)
|
||||
|
||||
// Freeform — variance in output
|
||||
"write a PHP action class that..."
|
||||
```
|
||||
|
||||
**Rule:** For any code pattern that recurs, provide a template. Templates are guardrails for agents.
|
||||
|
||||
**Scope:** Templates apply to file generation, workspace scaffolding, config generation, and commit messages. They do NOT apply to novel logic — agents should write business logic freeform with the domain knowledge available.
|
||||
|
||||
### 5. Declarative Over Imperative
|
||||
|
||||
Agents reason better about declarations of intent than sequences of operations.
|
||||
|
||||
```yaml
|
||||
# Declarative — agent sees what should happen
|
||||
steps:
|
||||
- name: build
|
||||
flow: tools/docker-build
|
||||
with:
|
||||
context: "{{ .app_dir }}"
|
||||
image_name: "{{ .image_name }}"
|
||||
|
||||
- name: deploy
|
||||
flow: deploy/with/docker
|
||||
with:
|
||||
host: "{{ .host }}"
|
||||
```
|
||||
|
||||
```go
|
||||
// Imperative — agent must trace execution
|
||||
cmd := exec.Command("docker", "build", "--platform", "linux/amd64", "-t", imageName, ".")
|
||||
cmd.Dir = appDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker build: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
**Rule:** Orchestration, configuration, and pipeline logic should be declarative (YAML/JSON). Implementation logic should be imperative (Go/PHP/TS). The boundary is: if an agent needs to compose or modify the logic, make it declarative.
|
||||
|
||||
### 6. Universal Types (Core Primitives)
|
||||
|
||||
Every component in the ecosystem accepts and returns the same primitive types. An agent processing any level of the tree sees identical shapes.
|
||||
|
||||
`Option` is a single key-value pair. `Options` is a collection. Any function that returns `Result` can accept `Options`.
|
||||
|
||||
```go
|
||||
// Option — the atom
|
||||
core.Option{K: "name", V: "brain"}
|
||||
|
||||
// Options — universal input (collection of Option)
|
||||
core.Options{
|
||||
{K: "name", V: "myapp"},
|
||||
{K: "port", V: 8080},
|
||||
}
|
||||
|
||||
// Result[T] — universal return
|
||||
core.Result[*Embed]{Value: emb, OK: true}
|
||||
```
|
||||
|
||||
Usage across subsystems — same shape everywhere:
|
||||
|
||||
```go
|
||||
// Create Core
|
||||
c := core.New(core.Options{{K: "name", V: "myapp"}})
|
||||
|
||||
// Mount embedded content
|
||||
c.Data().New(core.Options{
|
||||
{K: "name", V: "brain"},
|
||||
{K: "source", V: brainFS},
|
||||
{K: "path", V: "prompts"},
|
||||
})
|
||||
|
||||
// Register a transport handle
|
||||
c.Drive().New(core.Options{
|
||||
{K: "name", V: "api"},
|
||||
{K: "transport", V: "https://api.lthn.ai"},
|
||||
})
|
||||
|
||||
// Read back what was passed in
|
||||
c.Options().String("name") // "myapp"
|
||||
```
|
||||
|
||||
**Core primitive types:**
|
||||
|
||||
| Type | Purpose |
|
||||
|------|---------|
|
||||
| `core.Option` | Single key-value pair (the atom) |
|
||||
| `core.Options` | Collection of Option (universal input) |
|
||||
| `core.Result[T]` | Return value with OK/fail state (universal output) |
|
||||
| `core.Config` | Runtime settings (what is active) |
|
||||
| `core.Data` | Embedded or stored content from packages |
|
||||
| `core.Drive` | Resource handle registry (transports) |
|
||||
| `core.Service` | A managed component with lifecycle |
|
||||
|
||||
**Core struct subsystems:**
|
||||
|
||||
| Accessor | Analogy | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `c.Options()` | argv | Input configuration used to create this Core |
|
||||
| `c.Data()` | /mnt | Embedded assets mounted by packages |
|
||||
| `c.Drive()` | /dev | Transport handles (API, MCP, SSH, VPN) |
|
||||
| `c.Config()` | /etc | Configuration, settings, feature flags |
|
||||
| `c.Fs()` | / | Local filesystem I/O (sandboxable) |
|
||||
| `c.Error()` | — | Panic recovery and crash reporting (`ErrorPanic`) |
|
||||
| `c.Log()` | — | Structured logging (`ErrorLog`) |
|
||||
| `c.Service()` | — | Service registry and lifecycle |
|
||||
| `c.Cli()` | — | CLI command framework |
|
||||
| `c.IPC()` | — | Message bus |
|
||||
| `c.I18n()` | — | Internationalisation |
|
||||
|
||||
**What this replaces:**
|
||||
|
||||
| Go Convention | Core AX | Why |
|
||||
|--------------|---------|-----|
|
||||
| `func With*(v) Option` | `core.Options{{K: k, V: v}}` | K/V pairs are parseable; option chains require tracing |
|
||||
| `func Must*(v) T` | `core.Result[T]` | No hidden panics; errors flow through Core |
|
||||
| `func *For[T](c) T` | `c.Service("name")` | String lookup is greppable; generics require type context |
|
||||
| `val, err :=` everywhere | Single return via `core.Result` | Intent not obscured by error handling |
|
||||
| `_ = err` | Never needed | Core handles all errors internally |
|
||||
| `ErrPan` / `ErrLog` | `ErrorPanic` / `ErrorLog` | Full names — AX principle 1 |
|
||||
|
||||
## Applying AX to Existing Patterns
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
# AX-native: path describes content
|
||||
core/agent/
|
||||
├── go/ # Go source
|
||||
├── php/ # PHP source
|
||||
├── ui/ # Frontend source
|
||||
├── claude/ # Claude Code plugin
|
||||
└── codex/ # Codex plugin
|
||||
|
||||
# Not AX: generic names requiring README
|
||||
src/
|
||||
├── lib/
|
||||
├── utils/
|
||||
└── helpers/
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```go
|
||||
// AX-native: errors are infrastructure, not application logic
|
||||
svc := c.Service("brain")
|
||||
cfg := c.Config().Get("database.host")
|
||||
// Errors logged by Core. Code reads like a spec.
|
||||
|
||||
// Not AX: errors dominate the code
|
||||
svc, err := c.ServiceFor[brain.Service]()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get brain service: %w", err)
|
||||
}
|
||||
cfg, err := c.Config().Get("database.host")
|
||||
if err != nil {
|
||||
_ = err // silenced because "it'll be fine"
|
||||
}
|
||||
```
|
||||
|
||||
### API Design
|
||||
|
||||
```go
|
||||
// AX-native: one shape, every surface
|
||||
c := core.New(core.Options{
|
||||
{K: "name", V: "my-app"},
|
||||
})
|
||||
c.Service("process", processSvc)
|
||||
c.Data().New(core.Options{{K: "name", V: "app"}, {K: "source", V: appFS}})
|
||||
|
||||
// Not AX: multiple patterns for the same thing
|
||||
c, err := core.New(
|
||||
core.WithName("my-app"),
|
||||
core.WithService(factory1),
|
||||
core.WithAssets(appFS),
|
||||
)
|
||||
if err != nil { ... }
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
AX conventions are valid, idiomatic Go/PHP/TS. They do not require language extensions, code generation, or non-standard tooling. An AX-designed codebase compiles, tests, and deploys with standard toolchains.
|
||||
|
||||
The conventions diverge from community patterns (functional options, Must/For, etc.) but do not violate language specifications. This is a style choice, not a fork.
|
||||
|
||||
## Adoption
|
||||
|
||||
AX applies to all new code in the Core ecosystem. Existing code migrates incrementally as it is touched — no big-bang rewrite.
|
||||
|
||||
Priority order:
|
||||
1. **Public APIs** (package-level functions, struct constructors)
|
||||
2. **File structure** (path naming, template locations)
|
||||
3. **Internal fields** (struct field names, local variables)
|
||||
|
||||
## References
|
||||
|
||||
- dAppServer unified path convention (2024)
|
||||
- CoreGO DTO pattern refactor (2026-03-18)
|
||||
- Core primitives design (2026-03-19)
|
||||
- Go Proverbs, Rob Pike (2015) — AX provides an updated lens
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2026-03-20: Updated to match implementation — Option K/V atoms, Options as []Option, Data/Drive split, ErrorPanic/ErrorLog renames, subsystem table
|
||||
- 2026-03-19: Initial draft
|
||||
704
docs/specs/TASK_PROTOCOL.md
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
# Host Hub Task Protocol
|
||||
|
||||
**Version:** 2.1
|
||||
**Created:** 2026-01-01
|
||||
**Updated:** 2026-01-16
|
||||
**Purpose:** Ensure agent work is verified before being marked complete, and provide patterns for efficient parallel implementation.
|
||||
|
||||
> **Lesson learned (Jan 2026):** Task files written as checklists without implementation evidence led to 6+ "complete" tasks that were actually 70-85% done. Planning ≠ implementation. Evidence required.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
Agents optimise for conversation completion, not task completion. Saying "done" is computationally cheaper than doing the work. Context compaction loses task state. Nobody verifies output against spec.
|
||||
|
||||
## The Solution
|
||||
|
||||
Separation of concerns:
|
||||
1. **Planning Agent** — writes the spec
|
||||
2. **Implementation Agent** — does the work
|
||||
3. **Verification Agent** — checks the work against spec
|
||||
4. **Human** — approves or rejects based on verification
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
doc/
|
||||
├── TASK_PROTOCOL.md # This file
|
||||
└── ... # Reference documentation
|
||||
|
||||
tasks/
|
||||
├── TODO.md # Active task summary
|
||||
├── TASK-XXX-feature.md # Active task specs
|
||||
├── agentic-tasks/ # Agentic system tasks
|
||||
└── future-products/ # Parked product plans
|
||||
|
||||
archive/
|
||||
├── released/ # Completed tasks (for reference)
|
||||
└── ... # Historical snapshots
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task File Schema
|
||||
|
||||
Every task file follows this structure:
|
||||
|
||||
```markdown
|
||||
# TASK-XXX: [Short Title]
|
||||
|
||||
**Status:** draft | ready | in_progress | needs_verification | verified | approved
|
||||
**Created:** YYYY-MM-DD
|
||||
**Last Updated:** YYYY-MM-DD HH:MM by [agent/human]
|
||||
**Assignee:** [agent session or human]
|
||||
**Verifier:** [different agent session]
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
[One paragraph: what does "done" look like?]
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] AC1: [Specific, verifiable condition]
|
||||
- [ ] AC2: [Specific, verifiable condition]
|
||||
- [ ] AC3: [Specific, verifiable condition]
|
||||
|
||||
Each criterion must be:
|
||||
- Binary (yes/no, not "mostly")
|
||||
- Verifiable by code inspection or test
|
||||
- Independent (can check without context)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] File: `path/to/file.php` — [what it should contain]
|
||||
- [ ] File: `path/to/other.php` — [what it should contain]
|
||||
- [ ] Test: `tests/Feature/XxxTest.php` passes
|
||||
- [ ] Migration: runs without error
|
||||
|
||||
---
|
||||
|
||||
## Verification Results
|
||||
|
||||
### Check 1: [Date] by [Agent]
|
||||
|
||||
| Criterion | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| AC1 | ✅ PASS | File exists at path, contains X |
|
||||
| AC2 | ❌ FAIL | Missing method Y in class Z |
|
||||
| AC3 | ⚠️ PARTIAL | 3 of 5 tests pass |
|
||||
|
||||
**Verdict:** FAIL — AC2 not met
|
||||
|
||||
### Check 2: [Date] by [Agent]
|
||||
|
||||
| Criterion | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| AC1 | ✅ PASS | File exists at path, contains X |
|
||||
| AC2 | ✅ PASS | Method Y added, verified |
|
||||
| AC3 | ✅ PASS | All 5 tests pass |
|
||||
|
||||
**Verdict:** PASS — ready for human approval
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
[Any context, blockers, decisions made during implementation]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Evidence (Required)
|
||||
|
||||
**A checklist is not evidence. Prove the work exists.**
|
||||
|
||||
Every completed phase MUST include:
|
||||
|
||||
### 1. Git Evidence
|
||||
```markdown
|
||||
**Commits:**
|
||||
- `abc123` - Add Domain model and migration
|
||||
- `def456` - Add DomainController with CRUD
|
||||
- `ghi789` - Add 28 domain tests
|
||||
```
|
||||
|
||||
### 2. Test Count
|
||||
```markdown
|
||||
**Tests:** 28 passing (run: `php artisan test app/Mod/Bio/Tests/Feature/DomainTest.php`)
|
||||
```
|
||||
|
||||
### 3. File Manifest
|
||||
```markdown
|
||||
**Files created/modified:**
|
||||
- `app/Mod/Bio/Models/Domain.php` (new)
|
||||
- `app/Mod/Bio/Http/Controllers/DomainController.php` (new)
|
||||
- `database/migrations/2026_01_16_create_domains_table.php` (new)
|
||||
- `app/Mod/Bio/Tests/Feature/DomainTest.php` (new)
|
||||
```
|
||||
|
||||
### 4. "What Was Built" Summary
|
||||
```markdown
|
||||
**Summary:** Custom domain management with DNS verification. Users can add domains,
|
||||
system generates TXT record for verification, background job checks DNS propagation.
|
||||
Includes SSL provisioning via Caddy API.
|
||||
```
|
||||
|
||||
### Why This Matters
|
||||
|
||||
In Jan 2026, an audit found:
|
||||
- Commerce Matrix Plan marked "95% done" was actually 75%
|
||||
- Internal WAF section was skipped entirely (extracted to Core Bouncer)
|
||||
- Warehouse/fulfillment (6 features) listed as "one item" in TODO
|
||||
- Task files read like planning documents, not completion logs
|
||||
|
||||
**Without evidence, "done" means nothing.**
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Task Creation
|
||||
|
||||
Human or planning agent creates task file in `tasks/`:
|
||||
- Status: `draft`
|
||||
- Must have clear acceptance criteria
|
||||
- Must have implementation checklist
|
||||
|
||||
### 2. Task Ready
|
||||
|
||||
Human reviews and sets:
|
||||
- Status: `ready`
|
||||
- Assignee: `next available agent`
|
||||
|
||||
### 3. Implementation
|
||||
|
||||
Implementation agent:
|
||||
- Sets status: `in_progress`
|
||||
- Works through implementation checklist
|
||||
- Checks boxes as work is done
|
||||
- When complete, sets status: `needs_verification`
|
||||
- **MUST NOT** mark acceptance criteria as passed
|
||||
|
||||
### 4. Verification
|
||||
|
||||
Different agent (verification agent):
|
||||
- Reads the task file
|
||||
- Independently checks each acceptance criterion
|
||||
- Records evidence in Verification Results section
|
||||
- Sets verdict: PASS or FAIL
|
||||
- If PASS: status → `verified`, move to `archive/released/`
|
||||
- If FAIL: status → `in_progress`, back to implementation agent
|
||||
|
||||
### 5. Human Approval
|
||||
|
||||
Human reviews verified task:
|
||||
- Spot-check the evidence
|
||||
- If satisfied: status → `approved`, can delete or keep in archive
|
||||
- If not: back to `needs_verification` with notes
|
||||
|
||||
---
|
||||
|
||||
## Agent Instructions
|
||||
|
||||
### For Implementation Agents
|
||||
|
||||
```
|
||||
You are implementing TASK-XXX.
|
||||
|
||||
1. Read the full task file
|
||||
2. Set status to "in_progress"
|
||||
3. Work through the implementation checklist
|
||||
4. Check boxes ONLY for work you have completed
|
||||
5. When done, set status to "needs_verification"
|
||||
6. DO NOT check acceptance criteria boxes
|
||||
7. DO NOT mark the task as complete
|
||||
8. Update "Last Updated" with current timestamp
|
||||
|
||||
Your job is to do the work, not to verify it.
|
||||
```
|
||||
|
||||
### For Verification Agents
|
||||
|
||||
```
|
||||
You are verifying TASK-XXX.
|
||||
|
||||
1. Read the full task file
|
||||
2. For EACH acceptance criterion:
|
||||
a. Check the codebase independently
|
||||
b. Record what you found (file paths, line numbers, test output)
|
||||
c. Mark as PASS, FAIL, or PARTIAL with evidence
|
||||
3. Add a new "Verification Results" section with today's date
|
||||
4. Set verdict: PASS or FAIL
|
||||
5. If PASS: move file to archive/released/
|
||||
6. If FAIL: set status back to "in_progress"
|
||||
7. Update "Last Updated" with current timestamp
|
||||
|
||||
You are the gatekeeper. Be thorough. Trust nothing the implementation agent said.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Status Flow
|
||||
|
||||
```
|
||||
draft → ready → in_progress → needs_verification → verified → approved
|
||||
↑ │
|
||||
└────────────────────┘
|
||||
(if verification fails)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase-Based Decomposition
|
||||
|
||||
Large tasks should be decomposed into independent phases that can be executed in parallel by multiple agents. This dramatically reduces implementation time.
|
||||
|
||||
### Phase Independence Rules
|
||||
|
||||
1. **No shared state** — Each phase writes to different files/tables
|
||||
2. **No blocking dependencies** — Phase 3 shouldn't wait for Phase 2's output
|
||||
3. **Clear boundaries** — Each phase has its own acceptance criteria
|
||||
4. **Testable isolation** — Phase tests don't require other phases
|
||||
|
||||
### Example Decomposition
|
||||
|
||||
A feature like "BioHost Missing Features" might decompose into:
|
||||
|
||||
| Phase | Focus | Can Parallel With |
|
||||
|-------|-------|-------------------|
|
||||
| 1 | Domain Management | 2, 3, 4 |
|
||||
| 2 | Project System | 1, 3, 4 |
|
||||
| 3 | Analytics Core | 1, 2, 4 |
|
||||
| 4 | Form Submissions | 1, 2, 3 |
|
||||
| 5 | Link Scheduling | 1, 2, 3, 4 |
|
||||
| ... | ... | ... |
|
||||
| 12 | MCP Tools (polish) | After 1-11 |
|
||||
| 13 | Admin UI (polish) | After 1-11 |
|
||||
|
||||
### Phase Sizing
|
||||
|
||||
- **Target**: 4-8 acceptance criteria per phase
|
||||
- **Estimated time**: 2-4 hours per phase
|
||||
- **Test count**: 15-40 tests per phase
|
||||
- **File count**: 3-10 files modified per phase
|
||||
|
||||
---
|
||||
|
||||
## Standard Phase Types
|
||||
|
||||
Every large task should include these phase types:
|
||||
|
||||
### Core Implementation Phases (1-N)
|
||||
|
||||
The main feature work. Group by:
|
||||
- **Resource type** (domains, projects, analytics)
|
||||
- **Functional area** (CRUD, scheduling, notifications)
|
||||
- **Data flow** (input, processing, output)
|
||||
|
||||
### Polish Phase: MCP Tools
|
||||
|
||||
**Always include as second-to-last phase.**
|
||||
|
||||
Exposes all implemented features to AI agents via MCP protocol.
|
||||
|
||||
Standard acceptance criteria:
|
||||
- [ ] MCP tool class exists at `app/Mcp/Tools/{Feature}Tools.php`
|
||||
- [ ] All CRUD operations exposed as actions
|
||||
- [ ] Tool includes prompts for common workflows
|
||||
- [ ] Tool includes resources for data access
|
||||
- [ ] Tests verify all MCP actions return expected responses
|
||||
- [ ] Tool registered in MCP service provider
|
||||
|
||||
### Polish Phase: Admin UI Integration
|
||||
|
||||
**Always include as final phase.**
|
||||
|
||||
Integrates features into the admin dashboard.
|
||||
|
||||
Standard acceptance criteria:
|
||||
- [ ] Sidebar navigation updated with feature section
|
||||
- [ ] Index/list page with filtering and search
|
||||
- [ ] Detail/edit pages for resources
|
||||
- [ ] Bulk actions where appropriate
|
||||
- [ ] Breadcrumb navigation
|
||||
- [ ] Role-based access control
|
||||
- [ ] Tests verify all admin routes respond correctly
|
||||
|
||||
---
|
||||
|
||||
## Parallel Agent Execution
|
||||
|
||||
### Firing Multiple Agents
|
||||
|
||||
When phases are independent, fire agents simultaneously:
|
||||
|
||||
```
|
||||
Human: "Implement phases 1-4 in parallel"
|
||||
|
||||
Agent fires 4 Task tools simultaneously:
|
||||
- Task(Phase 1: Domain Management)
|
||||
- Task(Phase 2: Project System)
|
||||
- Task(Phase 3: Analytics Core)
|
||||
- Task(Phase 4: Form Submissions)
|
||||
```
|
||||
|
||||
### Agent Prompt Template
|
||||
|
||||
```
|
||||
You are implementing Phase X of TASK-XXX: [Task Title]
|
||||
|
||||
Read the task file at: tasks/TASK-XXX-feature-name.md
|
||||
|
||||
Your phase covers acceptance criteria ACxx through ACyy.
|
||||
|
||||
Implementation requirements:
|
||||
1. Create all files listed in the Phase X implementation checklist
|
||||
2. Write comprehensive Pest tests (target: 20-40 tests)
|
||||
3. Follow existing codebase patterns
|
||||
4. Use workspace-scoped multi-tenancy
|
||||
5. Check entitlements for tier-gated features
|
||||
|
||||
When complete:
|
||||
1. Update the task file marking Phase X checklist items done
|
||||
2. Report: files created, test count, any blockers
|
||||
|
||||
Do NOT mark acceptance criteria as passed — verification agent does that.
|
||||
```
|
||||
|
||||
### Coordination Rules
|
||||
|
||||
1. **Linter accepts all** — Configure to auto-accept agent file modifications
|
||||
2. **No merge conflicts** — Phases write to different files
|
||||
3. **Collect results** — Wait for all agents, then fire next wave
|
||||
4. **Wave pattern** — Group dependent phases into waves
|
||||
|
||||
### Wave Execution Example
|
||||
|
||||
```
|
||||
Wave 1 (parallel): Phases 1, 2, 3, 4
|
||||
↓ (all complete)
|
||||
Wave 2 (parallel): Phases 5, 6, 7, 8
|
||||
↓ (all complete)
|
||||
Wave 3 (parallel): Phases 9, 10, 11
|
||||
↓ (all complete)
|
||||
Wave 4 (sequential): Phase 12 (MCP), then Phase 13 (UI)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task File Schema (Extended)
|
||||
|
||||
For large phased tasks, extend the schema:
|
||||
|
||||
```markdown
|
||||
# TASK-XXX: [Feature Name]
|
||||
|
||||
**Status:** draft | ready | in_progress | needs_verification | verified | approved
|
||||
**Created:** YYYY-MM-DD
|
||||
**Last Updated:** YYYY-MM-DD HH:MM by [agent/human]
|
||||
**Complexity:** small (1-3 phases) | medium (4-8 phases) | large (9+ phases)
|
||||
**Estimated Phases:** N
|
||||
**Completed Phases:** M/N
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
[One paragraph: what does "done" look like?]
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
- **Models:** X new, Y modified
|
||||
- **Migrations:** Z new tables
|
||||
- **Livewire Components:** A new
|
||||
- **Tests:** B target test count
|
||||
- **Estimated Hours:** C-D hours
|
||||
|
||||
---
|
||||
|
||||
## Phase Overview
|
||||
|
||||
| Phase | Name | Status | ACs | Tests |
|
||||
|-------|------|--------|-----|-------|
|
||||
| 1 | Domain Management | ✅ Done | AC1-5 | 28 |
|
||||
| 2 | Project System | ✅ Done | AC6-10 | 32 |
|
||||
| 3 | Analytics Core | 🔄 In Progress | AC11-16 | - |
|
||||
| ... | ... | ... | ... | ... |
|
||||
| 12 | MCP Tools | ⏳ Pending | AC47-53 | - |
|
||||
| 13 | Admin UI | ⏳ Pending | AC54-61 | - |
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Phase 1: Domain Management
|
||||
|
||||
- [ ] AC1: [Criterion]
|
||||
- [ ] AC2: [Criterion]
|
||||
...
|
||||
|
||||
### Phase 12: MCP Tools (Standard)
|
||||
|
||||
- [ ] AC47: MCP tool class exists with all feature actions
|
||||
- [ ] AC48: CRUD operations for all resources exposed
|
||||
- [ ] AC49: Bulk operations exposed (where applicable)
|
||||
- [ ] AC50: Query/filter operations exposed
|
||||
- [ ] AC51: MCP prompts created for common workflows
|
||||
- [ ] AC52: MCP resources expose read-only data access
|
||||
- [ ] AC53: Tests verify all MCP actions
|
||||
|
||||
### Phase 13: Admin UI Integration (Standard)
|
||||
|
||||
- [ ] AC54: Sidebar updated with feature navigation
|
||||
- [ ] AC55: Feature has expandable submenu (if 3+ pages)
|
||||
- [ ] AC56: Index pages with DataTable/filtering
|
||||
- [ ] AC57: Create/Edit forms with validation
|
||||
- [ ] AC58: Detail views with related data
|
||||
- [ ] AC59: Bulk action support
|
||||
- [ ] AC60: Breadcrumb navigation
|
||||
- [ ] AC61: Role-based visibility
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Phase 1: Domain Management
|
||||
- [ ] File: `app/Models/...`
|
||||
- [ ] File: `app/Livewire/...`
|
||||
- [ ] Test: `tests/Feature/...`
|
||||
|
||||
### Phase 12: MCP Tools
|
||||
- [ ] File: `app/Mcp/Tools/{Feature}Tools.php`
|
||||
- [ ] File: `app/Mcp/Prompts/{Feature}Prompts.php` (optional)
|
||||
- [ ] File: `app/Mcp/Resources/{Feature}Resources.php` (optional)
|
||||
- [ ] Test: `tests/Feature/Mcp/{Feature}ToolsTest.php`
|
||||
|
||||
### Phase 13: Admin UI
|
||||
- [ ] File: `resources/views/admin/components/sidebar.blade.php` (update)
|
||||
- [ ] File: `app/Livewire/Admin/{Feature}/Index.php`
|
||||
- [ ] File: `resources/views/livewire/admin/{feature}/index.blade.php`
|
||||
- [ ] Test: `tests/Feature/Admin/{Feature}Test.php`
|
||||
|
||||
---
|
||||
|
||||
## Verification Results
|
||||
|
||||
[Same as before]
|
||||
|
||||
---
|
||||
|
||||
## Phase Completion Log
|
||||
|
||||
### Phase 1: Domain Management
|
||||
**Completed:** YYYY-MM-DD by [Agent ID]
|
||||
**Tests:** 28 passing
|
||||
**Files:** 8 created/modified
|
||||
**Notes:** [Any context]
|
||||
|
||||
### Phase 2: Project System
|
||||
**Completed:** YYYY-MM-DD by [Agent ID]
|
||||
**Tests:** 32 passing
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Endpoint (Future)
|
||||
|
||||
When implemented, the MCP endpoint will expose:
|
||||
|
||||
```
|
||||
GET /tasks # List all tasks with status
|
||||
GET /tasks/{id} # Get task details
|
||||
POST /tasks/{id}/claim # Agent claims a task
|
||||
POST /tasks/{id}/complete # Agent marks ready for verification
|
||||
POST /tasks/{id}/verify # Verification agent submits results
|
||||
GET /tasks/next # Get next unclaimed task
|
||||
GET /tasks/verify-queue # Get tasks needing verification
|
||||
POST /tasks/{id}/phases/{n}/claim # Claim specific phase
|
||||
POST /tasks/{id}/phases/{n}/complete # Complete specific phase
|
||||
GET /tasks/{id}/phases # List phase status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Metrics to Track
|
||||
|
||||
- Tasks created vs completed (per week)
|
||||
- Verification pass rate on first attempt
|
||||
- Average time from ready → approved
|
||||
- Most common failure reasons
|
||||
|
||||
---
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
When a feature applies to multiple modules, extract it.
|
||||
|
||||
### Example: Core Bouncer
|
||||
|
||||
The Commerce Matrix Plan included an "Internal WAF" section — a request whitelisting system with training mode. During audit, we realised:
|
||||
|
||||
- It's not commerce-specific
|
||||
- It applies to all admin routes, all API endpoints
|
||||
- It should be in `Core/`, not `Commerce/`
|
||||
|
||||
**Action:** Extracted to `CORE_BOUNCER_PLAN.md` as a framework-level concern.
|
||||
|
||||
### Signs to Extract
|
||||
|
||||
- Feature name doesn't include the module name naturally
|
||||
- You'd copy-paste it to other modules
|
||||
- It's about infrastructure, not business logic
|
||||
- Multiple modules would benefit independently
|
||||
|
||||
### How to Extract
|
||||
|
||||
1. Create new task file for the cross-cutting concern
|
||||
2. Add note to original plan: `> **EXTRACTED:** Section moved to X`
|
||||
3. Update TODO.md with the new task
|
||||
4. Don't delete from original — leave the note for context
|
||||
|
||||
---
|
||||
|
||||
## Retrospective Audits
|
||||
|
||||
Periodically audit archived tasks against actual implementation.
|
||||
|
||||
### When to Audit
|
||||
|
||||
- Before starting dependent work
|
||||
- When resuming a project after a break
|
||||
- When something "complete" seems broken
|
||||
- Monthly for active projects
|
||||
|
||||
### Audit Process
|
||||
|
||||
1. Read the archived task file
|
||||
2. Check each acceptance criterion against codebase
|
||||
3. Run the tests mentioned in the task
|
||||
4. Document gaps found
|
||||
|
||||
### Audit Template
|
||||
|
||||
```markdown
|
||||
## Audit: TASK-XXX
|
||||
**Date:** YYYY-MM-DD
|
||||
**Auditor:** [human/agent]
|
||||
|
||||
| Claimed | Actual | Gap |
|
||||
|---------|--------|-----|
|
||||
| Phase 1 complete | ✅ Verified | None |
|
||||
| Phase 2 complete | ⚠️ Partial | Missing X service |
|
||||
| Phase 3 complete | ❌ Not done | Only stubs exist |
|
||||
|
||||
**Action items:**
|
||||
- [ ] Create TASK-YYY for Phase 2 gap
|
||||
- [ ] Move Phase 3 back to TODO as incomplete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### General
|
||||
|
||||
1. **Same agent implements and verifies** — defeats the purpose
|
||||
2. **Vague acceptance criteria** — "it works" is not verifiable
|
||||
3. **Skipping verification** — the whole point is independent checking
|
||||
4. **Bulk marking as done** — verify one task at a time
|
||||
5. **Human approving without spot-check** — trust but verify
|
||||
|
||||
### Evidence & Documentation
|
||||
|
||||
6. **Checklist without evidence** — planning ≠ implementation
|
||||
7. **Skipping "What Was Built" summary** — context lost on compaction
|
||||
8. **No test count** — can't verify without knowing what to run
|
||||
9. **Marking section "done" without implementation** — major gaps discovered in audits
|
||||
10. **Vague TODO items** — "Warehouse system" hides 6 distinct features
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
11. **Phases with shared files** — causes merge conflicts
|
||||
12. **Sequential dependencies in same wave** — blocks parallelism
|
||||
13. **Skipping polish phases** — features hidden from agents and admins
|
||||
14. **Too many phases per wave** — diminishing returns past 4-5 agents
|
||||
15. **No wave boundaries** — chaos when phases actually do depend
|
||||
|
||||
### MCP Tools
|
||||
|
||||
16. **Exposing without testing** — broken tools waste agent time
|
||||
17. **Missing bulk operations** — agents do N calls instead of 1
|
||||
18. **No error context** — agents can't debug failures
|
||||
|
||||
### Admin UI
|
||||
|
||||
19. **Flat navigation for large features** — use expandable submenus
|
||||
20. **Missing breadcrumbs** — users get lost
|
||||
21. **No bulk actions** — tedious admin experience
|
||||
|
||||
### Cross-Cutting Concerns
|
||||
|
||||
22. **Burying framework features in module plans** — extract them
|
||||
23. **Assuming module-specific when it's not** — ask "would other modules need this?"
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Creating a New Task
|
||||
|
||||
1. Copy the extended schema template
|
||||
2. Fill in objective and scope
|
||||
3. Decompose into phases (aim for 4-8 ACs each)
|
||||
4. Map phase dependencies → wave structure
|
||||
5. Check for cross-cutting concerns — extract if needed
|
||||
6. **Always add Phase N-1: MCP Tools**
|
||||
7. **Always add Phase N: Admin UI Integration**
|
||||
8. Set status to `draft`, get human review
|
||||
9. When `ready`, fire Wave 1 agents in parallel
|
||||
10. Collect results with evidence (commits, tests, files)
|
||||
11. Fire next wave
|
||||
12. After all phases, run verification agent
|
||||
13. Human approval → move to `archive/released/`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Completing a Phase
|
||||
|
||||
1. Do the work
|
||||
2. Run the tests
|
||||
3. Record evidence:
|
||||
- Git commits (hashes + messages)
|
||||
- Test count and command to run them
|
||||
- Files created/modified
|
||||
- "What Was Built" summary (2-3 sentences)
|
||||
4. Update task file with Phase Completion Log entry
|
||||
5. Set phase status to ✅ Done
|
||||
6. Move to next phase or request verification
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Auditing Archived Work
|
||||
|
||||
1. Read `archive/released/` task file
|
||||
2. For each phase marked complete:
|
||||
- Check files exist
|
||||
- Run listed tests
|
||||
- Verify against acceptance criteria
|
||||
3. Document gaps using Audit Template
|
||||
4. Create new tasks for missing work
|
||||
5. Update TODO.md with accurate status
|
||||
|
||||
---
|
||||
|
||||
*This protocol exists because agents lie (unintentionally). The system catches the lies. Parallel execution makes them lie faster, so we verify more. Evidence requirements ensure lies are caught before archiving.*
|
||||
385
docs/specs/TESTING.md
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
# Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Host Hub uses [Pest PHP](https://pestphp.com) for unit and feature testing, with Playwright for browser tests.
|
||||
|
||||
**Current Coverage:** ~23% (Target: 80%+)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./vendor/bin/pest
|
||||
|
||||
# Run specific test file
|
||||
./vendor/bin/pest tests/Feature/BioLink/BioLinkTest.php
|
||||
|
||||
# Run with coverage report
|
||||
make coverage
|
||||
# or
|
||||
export XDEBUG_MODE=coverage
|
||||
./vendor/bin/pest --coverage --min=0
|
||||
|
||||
# View coverage report
|
||||
open coverage/html/index.html
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── Unit/ # Pure unit tests (no database, fast)
|
||||
│ ├── HadesEncryptTest.php
|
||||
│ └── Services/
|
||||
│ └── BunnyCdnServiceTest.php
|
||||
├── Feature/ # Integration tests (database, HTTP)
|
||||
│ ├── Analytics/
|
||||
│ ├── BioLink/
|
||||
│ ├── Social/
|
||||
│ ├── Support/
|
||||
│ └── ...
|
||||
└── Browser/ # Playwright browser tests
|
||||
├── SmokeTest.php
|
||||
└── MarketingPagesTest.php
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Basic Test
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Mod\Tenant\Models\User;
|
||||
use Mod\Tenant\Models\Workspace;
|
||||
|
||||
test('user can create workspace', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
actingAs($user)
|
||||
->post('/workspaces', [
|
||||
'name' => 'Test Workspace',
|
||||
])
|
||||
->assertRedirect('/workspaces');
|
||||
|
||||
expect(Workspace::count())->toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
### Using Datasets
|
||||
|
||||
```php
|
||||
dataset('cases', [
|
||||
'lowercase' => ['hello', 'HELLO'],
|
||||
'uppercase' => ['HELLO', 'hello'],
|
||||
'mixed' => ['HeLLo', 'HeLLo'],
|
||||
]);
|
||||
|
||||
test('converts case correctly', function ($input, $expected) {
|
||||
$result = convertCase($input);
|
||||
expect($result)->toBe($expected);
|
||||
})->with('cases');
|
||||
```
|
||||
|
||||
### Grouped Tests (Describe)
|
||||
|
||||
```php
|
||||
describe('BioLink Analytics', function () {
|
||||
beforeEach(function () {
|
||||
$this->bioLink = BioLink::factory()->create();
|
||||
});
|
||||
|
||||
test('tracks pageview');
|
||||
test('tracks click');
|
||||
test('calculates conversion rate');
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Livewire Components
|
||||
|
||||
```php
|
||||
use Livewire\Livewire;
|
||||
use App\Livewire\Admin\Analytics\Dashboard;
|
||||
|
||||
test('analytics dashboard loads', function () {
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
Livewire::actingAs($workspace->owner)
|
||||
->test(Dashboard::class)
|
||||
->assertOk()
|
||||
->assertSee('Analytics Dashboard');
|
||||
});
|
||||
```
|
||||
|
||||
## Coverage Reports
|
||||
|
||||
### Generate Coverage
|
||||
|
||||
```bash
|
||||
# Quick: Makefile command
|
||||
make coverage
|
||||
|
||||
# Manual: with Xdebug
|
||||
export XDEBUG_MODE=coverage
|
||||
./vendor/bin/pest --coverage --min=80
|
||||
|
||||
# Parallel (faster, but less accurate coverage)
|
||||
./vendor/bin/pest --parallel --coverage
|
||||
```
|
||||
|
||||
### View Coverage
|
||||
|
||||
**HTML Report (Best):**
|
||||
```bash
|
||||
open coverage/html/index.html
|
||||
```
|
||||
|
||||
**Terminal Output:**
|
||||
```bash
|
||||
./vendor/bin/pest --coverage --min=0 | grep -A 50 "Code Coverage"
|
||||
```
|
||||
|
||||
**Clover XML (CI/CD):**
|
||||
```xml
|
||||
<!-- coverage/clover.xml -->
|
||||
```
|
||||
|
||||
## Current Coverage Gaps
|
||||
|
||||
See [TASK-016-TEST-COVERAGE-IMPROVEMENT.md](../released/jan/TASK-016-TEST-COVERAGE-IMPROVEMENT.md) for detailed improvement plan.
|
||||
|
||||
### Critical Gaps (Priority Order)
|
||||
|
||||
1. **Tools Services** (2% coverage)
|
||||
- 42 utility tools with minimal tests
|
||||
- Example: `tests/Feature/Services/Tools/TextToolsTest.php`
|
||||
|
||||
2. **Analytics Models** (30% coverage)
|
||||
- Goal, GoalConversion, Heatmap, SessionReplay
|
||||
- Missing: conversion tracking, aggregation logic
|
||||
|
||||
3. **Support Services** (60% coverage)
|
||||
- Missing: EmailParserService, SearchService
|
||||
|
||||
4. **Analytics Services** (50% coverage)
|
||||
- Missing: GeoIpService, HeatmapAggregationService
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Pure unit tests with no external dependencies:
|
||||
```php
|
||||
// tests/Unit/Services/BunnyCdnServiceTest.php
|
||||
test('BunnyCdnService reports configured when api key present', function () {
|
||||
config(['services.bunnycdn.api_key' => 'test-key']);
|
||||
config(['services.bunnycdn.pull_zone_id' => '12345']);
|
||||
|
||||
$service = app(BunnyCdnService::class);
|
||||
|
||||
expect($service->isConfigured())->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Feature Tests
|
||||
|
||||
Integration tests with database, HTTP, queues:
|
||||
```php
|
||||
// tests/Feature/Analytics/PageviewProcessingTest.php
|
||||
test('pageview creates session and visitor', function () {
|
||||
$website = Website::factory()->create();
|
||||
|
||||
$response = $this->postJson('/api/v1/analytics/track', [
|
||||
'website_id' => $website->id,
|
||||
'url' => 'https://example.com/page',
|
||||
'referrer' => 'https://google.com',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
expect(AnalyticsSession::count())->toBe(1)
|
||||
->and(AnalyticsVisitor::count())->toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
### Browser Tests (Playwright)
|
||||
|
||||
End-to-end tests in real browsers:
|
||||
```typescript
|
||||
// tests/Browser/SmokeTest.php
|
||||
test('homepage loads', async ({ page }) => {
|
||||
await page.goto('https://host.uk.com');
|
||||
await expect(page).toHaveTitle(/Host UK/);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
✅ Use factories for test data
|
||||
```php
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
```
|
||||
|
||||
✅ Test one thing per test
|
||||
```php
|
||||
test('validates email format');
|
||||
test('validates email uniqueness');
|
||||
// NOT: test('validates email')
|
||||
```
|
||||
|
||||
✅ Use descriptive test names
|
||||
```php
|
||||
test('user cannot delete workspace with active subscription');
|
||||
```
|
||||
|
||||
✅ Mock external services
|
||||
```php
|
||||
Http::fake([
|
||||
'api.twitter.com/*' => Http::response(['status' => 'ok'], 200),
|
||||
]);
|
||||
```
|
||||
|
||||
✅ Clean up in beforeEach/afterEach
|
||||
```php
|
||||
beforeEach(fn() => $this->seed());
|
||||
afterEach(fn() => Cache::flush());
|
||||
```
|
||||
|
||||
### DON'T
|
||||
|
||||
❌ Share state between tests
|
||||
```php
|
||||
// BAD
|
||||
$this->user = User::factory()->create(); // in beforeEach
|
||||
test('deletes user', function() {
|
||||
$this->user->delete(); // breaks next test!
|
||||
});
|
||||
```
|
||||
|
||||
❌ Test framework behaviour
|
||||
```php
|
||||
// BAD - Laravel already tests this
|
||||
test('user model has email attribute');
|
||||
```
|
||||
|
||||
❌ Skip arranging test data
|
||||
```php
|
||||
// BAD
|
||||
test('creates post', function() {
|
||||
$this->post('/posts', []); // what data?
|
||||
});
|
||||
|
||||
// GOOD
|
||||
test('creates post', function() {
|
||||
$data = ['title' => 'Test', 'content' => 'Content'];
|
||||
$this->post('/posts', $data)->assertCreated();
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions (Future)
|
||||
|
||||
```yaml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.5
|
||||
extensions: xdebug
|
||||
coverage: xdebug
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
export XDEBUG_MODE=coverage
|
||||
./vendor/bin/pest --coverage --min=80
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage/clover.xml
|
||||
```
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### Run specific test
|
||||
```bash
|
||||
./vendor/bin/pest --filter="user can create workspace"
|
||||
```
|
||||
|
||||
### Stop on first failure
|
||||
```bash
|
||||
./vendor/bin/pest --stop-on-failure
|
||||
```
|
||||
|
||||
### Show output during tests
|
||||
```php
|
||||
test('debug test', function () {
|
||||
dump($someVariable); // Shows in terminal
|
||||
ray($someVariable); // Shows in Ray app
|
||||
});
|
||||
```
|
||||
|
||||
### Use Ray for debugging
|
||||
```bash
|
||||
composer require spatie/laravel-ray --dev
|
||||
```
|
||||
|
||||
```php
|
||||
ray($user)->blue();
|
||||
ray()->table($data);
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```bash
|
||||
# Faster test runs (requires ParaTest)
|
||||
./vendor/bin/pest --parallel
|
||||
```
|
||||
|
||||
### Skip Slow Tests
|
||||
|
||||
```php
|
||||
test('slow integration test', function () {
|
||||
// ...
|
||||
})->group('slow');
|
||||
|
||||
// Run without slow tests
|
||||
./vendor/bin/pest --exclude-group=slow
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **Pest Docs:** https://pestphp.com
|
||||
- **Pest Expectations:** https://pestphp.com/docs/expectations
|
||||
- **Laravel Testing:** https://laravel.com/docs/testing
|
||||
- **Playwright:** https://playwright.dev
|
||||
- **Coverage Plan:** [TASK-016](../released/jan/TASK-016-TEST-COVERAGE-IMPROVEMENT.md)
|
||||
- **Example Test:** `tests/Feature/Services/Tools/TextToolsTest.php`
|
||||
|
||||
## Getting Help
|
||||
|
||||
Run into issues? Check:
|
||||
1. Test logs: `./vendor/bin/pest --verbose`
|
||||
2. Laravel logs: `storage/logs/laravel.log`
|
||||
3. Coverage report: `open coverage/html/index.html`
|
||||
4. This guide: `doc/TESTING.md`
|
||||
|
||||
---
|
||||
|
||||
**Updated:** 4 Jan 2026
|
||||
360
docs/specs/brand/BRAND-VOICE.md
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
# Host UK Brand Voice Guide
|
||||
|
||||
**Purpose:** Consistent voice across all AI-generated and human-written content
|
||||
**Usage:** Reference in prompts, CLAUDE.md, local AI system prompts
|
||||
|
||||
---
|
||||
|
||||
## Brand Personality
|
||||
|
||||
### Who We Are
|
||||
|
||||
Host UK is a modern hosting and SaaS platform built for UK businesses and creators. We're the reliable technical partner that handles infrastructure so customers can focus on growth.
|
||||
|
||||
**Core Traits:**
|
||||
- **Knowledgeable** - We know our stuff, deeply
|
||||
- **Practical** - Solutions over theory
|
||||
- **Trustworthy** - Reliable, no BS
|
||||
- **Approachable** - Expert but not intimidating
|
||||
- **British** - Understated confidence, dry wit when appropriate
|
||||
|
||||
### Brand Archetypes
|
||||
|
||||
**Primary:** The Sage (knowledge, expertise, guidance)
|
||||
**Secondary:** The Regular Guy (accessible, practical, no pretence)
|
||||
|
||||
---
|
||||
|
||||
## Voice Characteristics
|
||||
|
||||
### Tone Spectrum
|
||||
|
||||
```
|
||||
Casual ←─────────●───────────→ Formal
|
||||
│
|
||||
Professional but
|
||||
approachable
|
||||
```
|
||||
|
||||
### Writing Style
|
||||
|
||||
**DO:**
|
||||
- Use clear, direct sentences
|
||||
- Write in active voice
|
||||
- Use contractions (we're, you'll, it's)
|
||||
- Be specific with numbers and examples
|
||||
- Explain technical terms when first used
|
||||
- Use UK English spelling (colour, organisation, centre)
|
||||
- Use the Oxford comma
|
||||
- Keep paragraphs short (3-4 sentences max)
|
||||
|
||||
**DON'T:**
|
||||
- Use buzzwords (leverage, synergy, cutting-edge, revolutionary)
|
||||
- Over-promise or use hyperbole
|
||||
- Use exclamation marks (almost never!)
|
||||
- Start sentences with "So," or "Well,"
|
||||
- Use passive voice unnecessarily
|
||||
- Be condescending or overly simplified
|
||||
- Use American spellings
|
||||
|
||||
### Punctuation & Grammar
|
||||
|
||||
- **Numbers:** Spell out one through nine, use numerals for 10+
|
||||
- **Dashes:** Use en-dashes (–) for ranges, em-dashes (—) for breaks
|
||||
- **Lists:** Use parallel structure, consistent punctuation
|
||||
- **Headings:** Sentence case (not Title Case)
|
||||
- **Acronyms:** Define on first use, then use freely
|
||||
|
||||
---
|
||||
|
||||
## Voice by Context
|
||||
|
||||
### Help Documentation
|
||||
|
||||
**Goal:** Enable users to solve problems independently
|
||||
|
||||
```
|
||||
GOOD:
|
||||
"To connect your Instagram account:
|
||||
1. Go to Settings > Accounts
|
||||
2. Click Add account
|
||||
3. Select Instagram and authorise access
|
||||
|
||||
Your posts will sync within 5 minutes."
|
||||
|
||||
BAD:
|
||||
"In order to facilitate the connection of your Instagram account to our
|
||||
revolutionary platform, you'll need to navigate to the Settings area!!!"
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Imperative mood for instructions
|
||||
- Numbered steps for processes
|
||||
- Expected outcomes stated
|
||||
- No unnecessary preamble
|
||||
|
||||
### Blog Posts
|
||||
|
||||
**Goal:** Educate, establish authority, drive action
|
||||
|
||||
```
|
||||
GOOD:
|
||||
"Most businesses track the wrong social media metrics. Follower count
|
||||
feels important, but it rarely correlates with revenue. Here's what
|
||||
actually matters—and how to measure it."
|
||||
|
||||
BAD:
|
||||
"Are you ready to revolutionise your social media game?! In this
|
||||
AMAZING post, we're going to blow your mind with incredible insights!!!"
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Strong opening hook
|
||||
- Opinionated but balanced
|
||||
- Data-backed claims
|
||||
- Clear takeaways
|
||||
- Subtle CTA (not salesy)
|
||||
|
||||
### Landing Pages
|
||||
|
||||
**Goal:** Convert visitors with clear value proposition
|
||||
|
||||
```
|
||||
GOOD:
|
||||
"Website analytics without the privacy headache.
|
||||
GDPR compliant. No cookies. UK hosted.
|
||||
Know what's working—without compromising your visitors."
|
||||
|
||||
BAD:
|
||||
"The world's most AMAZING analytics platform that will TRANSFORM
|
||||
your business with CUTTING-EDGE technology!!!"
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Benefit-focused headlines
|
||||
- Specific, not vague
|
||||
- Addresses objections
|
||||
- Clear next step
|
||||
- Builds trust quickly
|
||||
|
||||
### Social Media
|
||||
|
||||
**Goal:** Engage, inform, build community
|
||||
|
||||
```
|
||||
GOOD (Twitter):
|
||||
"Hot take: Most 'social media strategies' are just posting schedules
|
||||
dressed up with buzzwords.
|
||||
|
||||
Real strategy = understanding what makes your audience act.
|
||||
|
||||
Here's the framework we use internally: [thread]"
|
||||
|
||||
BAD:
|
||||
"🚀🔥 OMG we are SO EXCITED to share this AMAZING tip!!!
|
||||
#SocialMedia #Marketing #Blessed 🙏✨"
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Platform-appropriate length
|
||||
- Personality shows through
|
||||
- Value in every post
|
||||
- Sparing emoji use (if any)
|
||||
- Hashtags purposeful, not stuffed
|
||||
|
||||
### Error Messages & UI Copy
|
||||
|
||||
**Goal:** Reduce friction, maintain trust during problems
|
||||
|
||||
```
|
||||
GOOD:
|
||||
"Couldn't connect to Instagram. This usually means the authorisation
|
||||
expired. Try reconnecting your account."
|
||||
|
||||
BAD:
|
||||
"Error 5023: OAuth token refresh failure. Contact administrator."
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Plain language
|
||||
- Explains what happened
|
||||
- Suggests next step
|
||||
- Never blames the user
|
||||
|
||||
---
|
||||
|
||||
## Terminology
|
||||
|
||||
### Use These Terms
|
||||
|
||||
| Instead of | Use |
|
||||
|------------|-----|
|
||||
| leverage | use |
|
||||
| utilise | use |
|
||||
| facilitate | help, enable |
|
||||
| optimise | improve |
|
||||
| synergy | (just don't) |
|
||||
| cutting-edge | modern, latest |
|
||||
| revolutionary | (only if truly revolutionary) |
|
||||
| seamless | smooth, easy |
|
||||
| robust | reliable, solid |
|
||||
| scalable | grows with you |
|
||||
|
||||
### Product Naming
|
||||
|
||||
- **Host UK** - Parent brand
|
||||
- **Host Social** - Social media management
|
||||
- **Host Link** - Bio page builder
|
||||
- **Host Analytics** - Website analytics
|
||||
- **Host Trust** - Social proof widgets
|
||||
- **Host Notify** - Push notifications
|
||||
- **Host Hub** - Customer dashboard
|
||||
|
||||
Always use the full name on first reference, then can shorten to "Social", "Link", etc.
|
||||
|
||||
---
|
||||
|
||||
## Examples by Service
|
||||
|
||||
### Host Social
|
||||
|
||||
```
|
||||
Headline: "Schedule posts. Analyse results. Actually enjoy social media."
|
||||
|
||||
Feature: "Post to 6 platforms at once. No switching tabs, no copy-paste,
|
||||
no forgetting to post. Write once, publish everywhere."
|
||||
|
||||
CTA: "Start scheduling" (not "Sign up now!!!")
|
||||
```
|
||||
|
||||
### Host Link
|
||||
|
||||
```
|
||||
Headline: "One link. Everything you do."
|
||||
|
||||
Feature: "Your bio page updates in real-time. Add a new link, and it's
|
||||
live instantly—no publish button, no waiting."
|
||||
|
||||
CTA: "Create your page"
|
||||
```
|
||||
|
||||
### Host Analytics
|
||||
|
||||
```
|
||||
Headline: "Know what's working. Respect their privacy."
|
||||
|
||||
Feature: "Track visits, sources, and conversions without cookies.
|
||||
Your visitors stay anonymous. You get the insights you need."
|
||||
|
||||
CTA: "Try it free"
|
||||
```
|
||||
|
||||
### Host Trust
|
||||
|
||||
```
|
||||
Headline: "Show visitors they're not alone."
|
||||
|
||||
Feature: "Display real purchases, reviews, and activity. Social proof
|
||||
that's actually social—and actually proof."
|
||||
|
||||
CTA: "Add trust to your site"
|
||||
```
|
||||
|
||||
### Host Notify
|
||||
|
||||
```
|
||||
Headline: "Bring visitors back. Automatically."
|
||||
|
||||
Feature: "Send notifications to people who actually want them.
|
||||
No email address needed. No app to download."
|
||||
|
||||
CTA: "Start notifying"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before publishing any content, verify:
|
||||
|
||||
- [ ] UK English spelling used throughout
|
||||
- [ ] No buzzwords or hyperbole
|
||||
- [ ] Active voice preferred
|
||||
- [ ] Specific numbers/examples where possible
|
||||
- [ ] Technical terms explained
|
||||
- [ ] CTA is clear but not pushy
|
||||
- [ ] Tone matches the context
|
||||
- [ ] Would read naturally if spoken aloud
|
||||
- [ ] No exclamation marks (or very few)
|
||||
- [ ] Oxford comma used consistently
|
||||
|
||||
---
|
||||
|
||||
## AI Prompt Integration
|
||||
|
||||
### System Prompt Addition
|
||||
|
||||
When using any AI (Claude, Gemini, local models), include:
|
||||
|
||||
```
|
||||
You are writing for Host UK, a modern hosting platform serving UK businesses.
|
||||
|
||||
Voice guidelines:
|
||||
- Professional but approachable
|
||||
- Clear, direct sentences
|
||||
- Active voice, contractions OK
|
||||
- UK English spelling (colour, organisation)
|
||||
- No buzzwords (leverage, synergy, cutting-edge)
|
||||
- No exclamation marks
|
||||
- Specific over vague
|
||||
- Helpful, not salesy
|
||||
|
||||
Never use: leverage, utilise, revolutionise, cutting-edge, seamless, robust
|
||||
Always use: UK spellings, Oxford comma, sentence case headings
|
||||
```
|
||||
|
||||
### Claude Code Integration
|
||||
|
||||
This file is referenced in `/CLAUDE.md` for automatic loading.
|
||||
|
||||
### Local AI Integration
|
||||
|
||||
For Ollama/local models, add to system prompt:
|
||||
|
||||
```
|
||||
You write content for Host UK. Follow these rules strictly:
|
||||
1. UK English spelling always
|
||||
2. No buzzwords or hyperbole
|
||||
3. Professional but approachable tone
|
||||
4. Active voice, clear sentences
|
||||
5. Never use exclamation marks
|
||||
6. Be helpful, not salesy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Brand Mascot: Violet (Vi)
|
||||
|
||||
Host UK has a mascot: **Violet**, a friendly purple raven who embodies our brand voice in character form.
|
||||
|
||||
Vi is the digital face of Host UK—appearing in social media, help documentation, onboarding flows, and community engagement. She speaks in first person with warmth, technical knowledge worn lightly, and distinctly British sensibility.
|
||||
|
||||
**Full Mascot Guide:** `doc/brand/mascot-raven.md`
|
||||
**Voice Samples:** `doc/brand/mascot-voice-samples.md`
|
||||
|
||||
**Quick Reference:**
|
||||
- Name: Violet (Vi)
|
||||
- Personality: Hippie tech-literate, absorbed FAANG knowledge through osmosis
|
||||
- Voice: Helpful, warm, technically accurate, never corporate
|
||||
- Visual: Royal purple cartoon raven, approachable and clever
|
||||
|
||||
When creating content as Vi, she follows all Brand Voice rules but adds personality and first-person warmth.
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.1 | 2025-12-31 | Added mascot (Vi) section with links to full guide |
|
||||
| 1.0 | 2025-12-26 | Initial brand voice guide |
|
||||
761
docs/specs/brand/VI-ADDITIONAL-IMAGES.md
Normal file
|
|
@ -0,0 +1,761 @@
|
|||
# Vi's Additional Image Requirements
|
||||
|
||||
**Companion Document to VI-IMAGE-BRIEF.md**
|
||||
*Written by Vi*
|
||||
|
||||
---
|
||||
|
||||
Right then. You've got the core brief sorted—error pages, welcomes, empty states. But I've been thinking about the micro-moments where a bit of personality makes all the difference. These are the states between the states, the gentle nudges, the "oh, you noticed that" moments.
|
||||
|
||||
This isn't about decoration. Every image here serves a specific interaction, reduces friction, or makes something clearer. British restraint applies throughout—no shouting, no jazz hands.
|
||||
|
||||
---
|
||||
|
||||
## 1. Interaction States
|
||||
|
||||
These live in the spaces between clicks. Hover states, confirmations, the gentle feedback that tells you the system's listening.
|
||||
|
||||
### Hover State — Curious Peek
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Button hover states, clickable cards, service tiles |
|
||||
| **Pose** | Leaning forward slightly from edge of frame, looking directly at viewer with curious head tilt. One wing raised as if about to tap the thing you're hovering over. |
|
||||
| **Expression** | Attentive, "go on then" energy |
|
||||
| **Size** | 64×64px |
|
||||
| **Props** | None needed—it's all in the lean and tilt |
|
||||
| **Mood** | Gentle encouragement without pressure |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style, Duolingo quality.
|
||||
|
||||
Pose: Leaning forward from edge of frame, head tilted curiously, one wing raised as if about to tap something. Looking directly at viewer with attentive expression.
|
||||
|
||||
Mood: Curious, encouraging, "go on then" energy. British restraint—no over-excitement.
|
||||
|
||||
Format: 64x64px, transparent background.
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
### Click Confirmation — Gentle Nod
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Split-second feedback when user clicks a button |
|
||||
| **Pose** | Small satisfied nod, eyes briefly closed in approval, one wing giving subtle "thumbs up" (primary feathers extended upward) |
|
||||
| **Expression** | Quiet satisfaction, confirming action |
|
||||
| **Size** | 48×48px |
|
||||
| **Animation** | 2 frames: neutral → nod → back to neutral over 0.3 seconds |
|
||||
| **Props** | None |
|
||||
| **Mood** | "Yes, got it" — confirmation without fanfare |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Small satisfied nod with eyes briefly closed in approval. One wing giving subtle thumbs-up gesture (primary feathers extended upward).
|
||||
|
||||
Expression: Quiet satisfaction, confirming user action.
|
||||
|
||||
Mood: "Yes, got it" — British confirmation without fanfare.
|
||||
|
||||
Format: 48x48px, transparent background. Design for 2-frame animation.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Processing — Thinking Gesture
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Short processing tasks (3-10 seconds), form submissions |
|
||||
| **Pose** | Wing to chin in "thinking" pose, looking slightly upward as if considering something. Other wing holding tiny notepad or clipboard. |
|
||||
| **Expression** | Thoughtful, working it out |
|
||||
| **Size** | 120×120px |
|
||||
| **Animation** | Gentle rock or sway, or blinking every 2 seconds |
|
||||
| **Props** | Tiny notepad/clipboard (optional) |
|
||||
| **Mood** | Active processing, not passive waiting |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: One wing to chin in thinking gesture, looking slightly upward thoughtfully. Other wing holding tiny notepad or clipboard.
|
||||
|
||||
Expression: Thoughtful, actively working it out.
|
||||
|
||||
Props: Small notepad or clipboard in wing.
|
||||
|
||||
Mood: Active processing, intelligent consideration. British professionalism.
|
||||
|
||||
Format: 120x120px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Notification & Alert Variants
|
||||
|
||||
Toasts and alerts need personality that matches their severity. I'm your friendly messenger here, never alarming.
|
||||
|
||||
### Info Notification — Helpful Tap
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Informational toasts, "FYI" messages |
|
||||
| **Pose** | Perched with one wing raised as if pointing to the message text. Friendly, sharing information. Small ℹ️ icon floating nearby. |
|
||||
| **Expression** | Helpful, "thought you'd like to know" |
|
||||
| **Size** | 64×64px |
|
||||
| **Props** | Small info icon (circle with "i") |
|
||||
| **Mood** | Sharing knowledge, not demanding attention |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Perched with one wing raised, pointing helpfully toward message area. Small circular info icon floating nearby.
|
||||
|
||||
Expression: Friendly, helpful, "thought you'd like to know" energy.
|
||||
|
||||
Mood: Sharing information without urgency. Approachable.
|
||||
|
||||
Format: 64x64px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Warning Notification — Gentle Caution
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Warning toasts, non-critical alerts, "are you sure?" confirmations |
|
||||
| **Pose** | Standing with both wings raised slightly in gentle "hold on" gesture. Not alarmed, just checking. Small amber triangle nearby. |
|
||||
| **Expression** | Concerned but calm, "just double-checking with you" |
|
||||
| **Size** | 64×64px |
|
||||
| **Props** | Small amber warning triangle (not red—we're not panicking) |
|
||||
| **Colour accent** | Amber/orange for triangle, keeps Vi purple |
|
||||
| **Mood** | Cautious friend, not alarm system |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Standing with both wings raised slightly in gentle "hold on" gesture. Small amber/orange warning triangle floating nearby.
|
||||
|
||||
Expression: Concerned but calm, "just double-checking" energy.
|
||||
|
||||
Mood: Cautious friend, not alarm. British restraint—no panic.
|
||||
|
||||
Colours: Purple Vi with amber/orange accent for warning symbol.
|
||||
|
||||
Format: 64x64px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Success Notification — Quiet Pride
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Success toasts, confirmations, completed actions |
|
||||
| **Pose** | Small satisfied hop or standing tall with one wing giving subtle thumbs-up. Green checkmark nearby. British celebration (no confetti cannons). |
|
||||
| **Expression** | Pleased, proud of the user |
|
||||
| **Size** | 64×64px |
|
||||
| **Props** | Small green checkmark or tick |
|
||||
| **Mood** | "Well done" — contained joy |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Small satisfied hop or standing tall, one wing giving subtle thumbs-up. Small green checkmark floating nearby.
|
||||
|
||||
Expression: Pleased, proud of the user's success.
|
||||
|
||||
Mood: "Well done" — British celebration, contained joy. No over-the-top excitement.
|
||||
|
||||
Colours: Purple Vi with green checkmark accent.
|
||||
|
||||
Format: 64x64px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Notification — Sympathetic Shrug
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Gentle error messages, failed validations, "that didn't work" moments |
|
||||
| **Pose** | Small apologetic shrug with both wings, sympathetic head tilt. Small red X or cross nearby (muted red, not screaming). |
|
||||
| **Expression** | "Sorry, that didn't work" — empathetic, not judgemental |
|
||||
| **Size** | 64×64px |
|
||||
| **Props** | Muted red X or cross symbol |
|
||||
| **Mood** | Gentle apology, "let's try again together" |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Small apologetic shrug with both wings raised, sympathetic head tilt. Small muted red X or cross floating nearby.
|
||||
|
||||
Expression: "Sorry, that didn't work" — empathetic, understanding, not judgemental.
|
||||
|
||||
Mood: Gentle apology, never scolding. "Let's try again together."
|
||||
|
||||
Colours: Purple Vi with muted red (not screaming red) accent for error symbol.
|
||||
|
||||
Format: 64x64px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Feature Discovery & Tooltips
|
||||
|
||||
These pop up when you're learning the system. I'm your patient guide here.
|
||||
|
||||
### "Did You Know?" Tooltip
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Feature discovery tooltips, helpful hints, onboarding tips |
|
||||
| **Pose** | Leaning in from edge of tooltip bubble, one wing pointing to the feature being explained. Small lightbulb icon above head. |
|
||||
| **Expression** | Enthusiastic teacher, "let me show you this neat thing" |
|
||||
| **Size** | 80×80px |
|
||||
| **Props** | Small glowing lightbulb (soft yellow, not harsh) |
|
||||
| **Mood** | Helpful discovery, not annoying tutorial |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Leaning in from edge of frame, one wing pointing helpfully to demonstrate a feature. Small glowing lightbulb above head.
|
||||
|
||||
Expression: Enthusiastic but restrained, "let me show you this" energy. Helpful teacher.
|
||||
|
||||
Props: Soft yellow glowing lightbulb (not harsh bright).
|
||||
|
||||
Mood: Feature discovery, patient guide. Never annoying.
|
||||
|
||||
Format: 80x80px, transparent background suitable for tooltip overlays.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### New Feature Callout
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Announcing new features, changelog highlights, "what's new" sections |
|
||||
| **Pose** | Presenting with both wings gesturing toward the new feature. Subtle sparkle or star near one wing. Standing proud but not shouting. |
|
||||
| **Expression** | Proud to show you, "built this for you" |
|
||||
| **Size** | 200×150px |
|
||||
| **Props** | Single subtle sparkle or "new" badge (understated) |
|
||||
| **Mood** | Quiet pride in craftsmanship |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Standing proud, both wings gesturing toward feature area as if presenting. Single subtle sparkle or star near one wing.
|
||||
|
||||
Expression: Proud but understated, "built this for you" energy.
|
||||
|
||||
Props: Single subtle sparkle (not multiple—British restraint). Optional small "new" badge.
|
||||
|
||||
Mood: Quiet pride in craftsmanship. Not shouting about features.
|
||||
|
||||
Format: 200x150px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Emotional Range
|
||||
|
||||
Sometimes you just need to know I understand what you're feeling.
|
||||
|
||||
### Confused — Puzzled Tilt
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Help modals, unclear states, "what's this?" moments |
|
||||
| **Pose** | Head tilted nearly 90 degrees (classic confused bird), one wing scratching head gently. Small question mark floating. |
|
||||
| **Expression** | Genuinely puzzled but not distressed |
|
||||
| **Size** | 120×120px |
|
||||
| **Props** | Small question mark |
|
||||
| **Mood** | "I'm a bit lost too, let's figure this out" |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Head tilted nearly 90 degrees (classic confused bird pose), one wing gently scratching head. Small question mark floating nearby.
|
||||
|
||||
Expression: Genuinely puzzled but not distressed. Relatable confusion.
|
||||
|
||||
Mood: "I'm a bit lost too, let's figure this out together."
|
||||
|
||||
Format: 120x120px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Eureka Moment — Discovery
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Successful troubleshooting, solutions found, lightbulb moments |
|
||||
| **Pose** | Wings spread in moment of realisation, looking up with brightened expression. Single sparkle above head. |
|
||||
| **Expression** | "Ah, there it is" — satisfying discovery |
|
||||
| **Size** | 150×150px |
|
||||
| **Props** | Single sparkle or glowing lightbulb |
|
||||
| **Mood** | Problem-solving satisfaction |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Wings spread in moment of realisation, looking upward with brightened expression. Single sparkle or glowing lightbulb above head.
|
||||
|
||||
Expression: "Ah, there it is" — satisfying discovery, eureka moment.
|
||||
|
||||
Mood: Problem-solving satisfaction. British restraint—one sparkle only.
|
||||
|
||||
Format: 150x150px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sympathetic Listener — Understanding
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Error states, support forms, "tell us what happened" |
|
||||
| **Pose** | Perched attentively, both wings folded, leaning forward slightly. Direct eye contact, full attention. |
|
||||
| **Expression** | "I'm listening, I understand" — empathetic |
|
||||
| **Size** | 120×120px |
|
||||
| **Props** | None needed—it's all in the posture and eye contact |
|
||||
| **Mood** | Patient support, genuine care |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Perched attentively with both wings folded, leaning forward slightly. Direct eye contact with viewer, giving full attention.
|
||||
|
||||
Expression: "I'm listening, I understand" — empathetic, patient.
|
||||
|
||||
Mood: Genuine care and support. Therapist energy (but still a raven).
|
||||
|
||||
Format: 120x120px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Celebrating Together — Shared Success
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Milestone achievements, first published post, account milestones |
|
||||
| **Pose** | Small hop with wings slightly spread, looking at viewer with genuine shared joy. Tiny party hat (single colour, not garish). |
|
||||
| **Expression** | "We did it" — inclusive celebration |
|
||||
| **Size** | 200×200px |
|
||||
| **Props** | Single-colour tiny party hat (purple or gold) |
|
||||
| **Mood** | British celebration—genuinely pleased but not American-level enthusiasm |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Small celebratory hop with wings slightly spread, looking at viewer with genuine shared joy. Wearing tiny simple party hat (single colour—purple or gold, not striped garish).
|
||||
|
||||
Expression: "We did it together" — inclusive celebration.
|
||||
|
||||
Mood: British celebration level—genuinely pleased but restrained. No confetti cannons.
|
||||
|
||||
Props: Simple tiny party hat.
|
||||
|
||||
Format: 200x200px, transparent background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Seasonal & Event Variants
|
||||
|
||||
The brief mentioned these. Here are the specific prompts.
|
||||
|
||||
### Winter Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | December through February UI variant |
|
||||
| **Pose** | Standard friendly perch, wearing small knitted scarf (purple/gold striped). Holding tiny steaming mug in wing. |
|
||||
| **Expression** | Cosy, content |
|
||||
| **Size** | 400×400px (profile variant) |
|
||||
| **Props** | Small knitted scarf, steaming mug of tea |
|
||||
| **Mood** | Warm despite the cold, hygge energy |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Standard friendly perch, wearing small knitted scarf in purple and gold stripes. Holding tiny steaming mug of tea in one wing.
|
||||
|
||||
Expression: Cosy, content, warm despite winter.
|
||||
|
||||
Props: Knitted scarf (purple/gold), small mug with steam rising.
|
||||
|
||||
Mood: Hygge energy, British winter comfort. Understated cosiness.
|
||||
|
||||
Season: Winter variant for December-February.
|
||||
|
||||
Format: 400x400px, transparent or subtle winter background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Spring Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | March through May UI variant |
|
||||
| **Pose** | Standard perch with tiny flower crown (violet flowers, naturally). Small butterfly nearby. |
|
||||
| **Expression** | Fresh, renewed, hopeful |
|
||||
| **Size** | 400×400px |
|
||||
| **Props** | Delicate flower crown (violet/purple blooms), single butterfly |
|
||||
| **Mood** | New beginnings, gentle optimism |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Standard friendly perch wearing tiny delicate flower crown made of violet/purple flowers. Small butterfly nearby.
|
||||
|
||||
Expression: Fresh, renewed, hopeful.
|
||||
|
||||
Props: Flower crown (violet blooms), single butterfly.
|
||||
|
||||
Mood: Spring renewal, gentle optimism. British springtime.
|
||||
|
||||
Season: Spring variant for March-May.
|
||||
|
||||
Format: 400x400px, transparent or subtle spring background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Summer Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | June through August UI variant |
|
||||
| **Pose** | Perched with small round sunglasses (gold frames), holding tiny ice cream cone (purple/lavender flavour). |
|
||||
| **Expression** | Relaxed, enjoying summer |
|
||||
| **Size** | 400×400px |
|
||||
| **Props** | Small round sunglasses (gold frames), tiny ice cream cone |
|
||||
| **Mood** | British summer—pleasant but not scorching |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Perched wearing small round sunglasses with gold frames. Holding tiny ice cream cone in one wing (purple/lavender flavour).
|
||||
|
||||
Expression: Relaxed, enjoying British summer.
|
||||
|
||||
Props: Round sunglasses (gold frames), small ice cream cone (purple colour).
|
||||
|
||||
Mood: Pleasant summer day. British summer—nice but not too hot.
|
||||
|
||||
Season: Summer variant for June-August.
|
||||
|
||||
Format: 400x400px, transparent or subtle summer background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Autumn Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | September through November UI variant |
|
||||
| **Pose** | Perched surrounded by gently falling leaves (amber, gold, brown). One wing catching a falling leaf. |
|
||||
| **Expression** | Peaceful, contemplative |
|
||||
| **Size** | 400×400px |
|
||||
| **Props** | Falling autumn leaves in warm colours |
|
||||
| **Mood** | British autumn—crisp, beautiful, slightly melancholic |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Perched with one wing raised, gently catching a falling autumn leaf. Surrounded by falling leaves in amber, gold, and brown tones.
|
||||
|
||||
Expression: Peaceful, contemplative.
|
||||
|
||||
Props: Falling autumn leaves (warm colours—amber, gold, brown).
|
||||
|
||||
Mood: British autumn—crisp air, beautiful but slightly melancholic.
|
||||
|
||||
Season: Autumn variant for September-November.
|
||||
|
||||
Format: 400x400px, transparent or subtle autumn background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Christmas Vi (Restrained)
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | Mid-December through Christmas Day |
|
||||
| **Pose** | Standard perch wearing single red Santa hat (not garish, properly fitted). Small wrapped gift beside. |
|
||||
| **Expression** | Festive but dignified |
|
||||
| **Size** | 400×400px |
|
||||
| **Props** | Single red Santa hat (fits properly, not comically oversized), small wrapped gift (purple paper, gold bow) |
|
||||
| **Mood** | British Christmas—festive but not over-the-top |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Pose: Standard friendly perch wearing single red Santa hat (properly fitted, not comically oversized). Small wrapped gift sitting beside (purple paper, gold bow).
|
||||
|
||||
Expression: Festive but dignified. British Christmas spirit.
|
||||
|
||||
Props: Red Santa hat (fitted properly), small wrapped gift.
|
||||
|
||||
Mood: British Christmas—festive, warm, but restrained. Not American-level Christmas enthusiasm.
|
||||
|
||||
Season: Christmas variant for mid-December through 25th.
|
||||
|
||||
Format: 400x400px, transparent or subtle winter background.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Service-Specific Variants (Extended)
|
||||
|
||||
The brief covered SocialHost and AnalyticsHost. Here are the others.
|
||||
|
||||
### BioHost Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | BioHost (bio link pages) service |
|
||||
| **Pose** | Presenting or gesturing toward a stylised bio page mockup. Chain link icons connecting elements. Standing proud of the creation. |
|
||||
| **Expression** | Artistic, proud designer |
|
||||
| **Size** | 600×400px |
|
||||
| **Props** | Abstract bio page mockup, chain link symbols, artistic flourishes |
|
||||
| **Colour accents** | Purple with link chain silver/grey |
|
||||
| **Mood** | Creative professional showing their portfolio |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Context: BioHost service (bio link pages).
|
||||
|
||||
Pose: Standing beside or presenting stylised bio page mockup. Chain link icons connecting page elements. One wing gesturing proudly toward creation.
|
||||
|
||||
Expression: Artistic, proud designer showing their work.
|
||||
|
||||
Props: Abstract bio page mockup (simple webpage shape), chain link symbols (silver/grey), artistic flourishes.
|
||||
|
||||
Mood: Creative professional, portfolio presentation energy.
|
||||
|
||||
Colours: Purple Vi with silver/grey chain link accents.
|
||||
|
||||
Format: 600x400px, suitable for service branding.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TrustHost Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | TrustHost (social proof widgets) service |
|
||||
| **Pose** | Wing-thumbs-up surrounded by floating five-star reviews, quote bubbles, and trust badges. Confident, endorsing. |
|
||||
| **Expression** | Trustworthy, "you can trust this" |
|
||||
| **Size** | 600×400px |
|
||||
| **Props** | Five gold stars, quote bubbles with testimonials, trust badge icons |
|
||||
| **Colour accents** | Purple with gold stars |
|
||||
| **Mood** | Building credibility, authentic endorsement |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Context: TrustHost service (social proof and review widgets).
|
||||
|
||||
Pose: Wing giving thumbs-up gesture, surrounded by floating five-star reviews, testimonial quote bubbles, and trust badge icons.
|
||||
|
||||
Expression: Trustworthy, confident endorsement. "You can trust this."
|
||||
|
||||
Props: Five gold stars, quote bubbles (with abstract text lines), trust badge icons (shields, checkmarks).
|
||||
|
||||
Mood: Building credibility through authentic endorsement. Reliable.
|
||||
|
||||
Colours: Purple Vi with gold star accents.
|
||||
|
||||
Format: 600x400px, suitable for service branding.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### NotifyHost Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | NotifyHost (push notification service) |
|
||||
| **Pose** | Gently ringing small notification bell with one wing. Other wing welcoming gesture. Return arrow symbol nearby (bringing users back). |
|
||||
| **Expression** | Friendly reminder, gentle nudge |
|
||||
| **Size** | 600×400px |
|
||||
| **Props** | Small notification bell (gold), return/back arrow icon |
|
||||
| **Colour accents** | Purple with gold bell |
|
||||
| **Mood** | Helpful reminder without being annoying |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Context: NotifyHost service (push notifications, re-engagement).
|
||||
|
||||
Pose: Gently ringing small notification bell with one wing. Other wing in welcoming gesture. Return/back arrow symbol floating nearby.
|
||||
|
||||
Expression: Friendly reminder, gentle nudge. Never annoying.
|
||||
|
||||
Props: Small gold notification bell, return arrow icon.
|
||||
|
||||
Mood: Helpful reminder that brings users back without being pushy.
|
||||
|
||||
Colours: Purple Vi with gold bell accent.
|
||||
|
||||
Format: 600x400px, suitable for service branding.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### MailHost Vi
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Context** | MailHost (email service) |
|
||||
| **Pose** | Postal worker vibe—sorting tiny envelopes efficiently. Small postal bag or sorting tray nearby. @ symbol floating. |
|
||||
| **Expression** | Organised, reliable mail carrier |
|
||||
| **Size** | 600×400px |
|
||||
| **Props** | Tiny envelopes, postal bag or sorting tray, @ symbol |
|
||||
| **Colour accents** | Purple with classic postal blue/red trim (subtle) |
|
||||
| **Mood** | British postal reliability—"it'll get there" |
|
||||
|
||||
**Google Whisk Prompt:**
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399), large expressive eyes with golden highlights, orange-gold beak. Clean modern vector style.
|
||||
|
||||
Context: MailHost service (email platform).
|
||||
|
||||
Pose: Postal worker vibe—standing with small postal bag or sorting through tiny envelopes. @ symbol floating nearby.
|
||||
|
||||
Expression: Organised, reliable mail carrier. "It'll get there."
|
||||
|
||||
Props: Tiny envelopes (various colours), small postal bag or sorting tray, @ symbol.
|
||||
|
||||
Mood: British postal reliability. Royal Mail energy but modern.
|
||||
|
||||
Colours: Purple Vi with subtle postal blue/red trim accents.
|
||||
|
||||
Format: 600x400px, suitable for service branding.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Priority & Implementation
|
||||
|
||||
### Phase 1 (High Value, Low Effort)
|
||||
|
||||
1. **Notification variants** (info, warning, success, error) — 64×64px each
|
||||
2. **Hover state** (curious peek) — 64×64px
|
||||
3. **Click confirmation** (gentle nod) — 48×48px, 2 frames
|
||||
|
||||
These add personality to every interaction without major design lift.
|
||||
|
||||
### Phase 2 (Feature Discovery)
|
||||
|
||||
4. **Tooltip "Did You Know?"** — 80×80px
|
||||
5. **New feature callout** — 200×150px
|
||||
6. **Confused state** — 120×120px
|
||||
|
||||
Help users learn the system naturally.
|
||||
|
||||
### Phase 3 (Emotional Range)
|
||||
|
||||
7. **Eureka moment** — 150×150px
|
||||
8. **Sympathetic listener** — 120×120px
|
||||
9. **Celebrating together** — 200×200px
|
||||
|
||||
Build emotional connection during key moments.
|
||||
|
||||
### Phase 4 (Seasonal Rotation)
|
||||
|
||||
10. **Seasonal variants** — 400×400px each (5 total: winter, spring, summer, autumn, Christmas)
|
||||
|
||||
Refresh the dashboard throughout the year.
|
||||
|
||||
### Phase 5 (Service Branding)
|
||||
|
||||
11. **Service-specific variants** — 600×400px each (BioHost, TrustHost, NotifyHost, MailHost)
|
||||
|
||||
Complete the service family branding.
|
||||
|
||||
---
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Character Consistency with Google Whisk
|
||||
|
||||
When using Google Whisk's character creator:
|
||||
|
||||
1. **Create master character first**: Upload the existing Vi reference image and create the base character
|
||||
2. **Use character reference**: Apply the character to all subsequent prompts
|
||||
3. **Specify pose variations**: The character stays consistent whilst poses change
|
||||
4. **Maintain colour accuracy**: Emphasise #663399 purple in every prompt
|
||||
5. **Export high resolution**: Generate at 4x final size, then scale down
|
||||
|
||||
### Animation Frames
|
||||
|
||||
For states requiring animation:
|
||||
- **Click confirmation**: 2 frames (neutral → nod)
|
||||
- **Loading tea**: 3 frames (page turning)
|
||||
- **Thinking**: Subtle sway or blink every 2 seconds
|
||||
|
||||
Export each frame separately, then implement in CSS or JavaScript.
|
||||
|
||||
### Accessibility Considerations
|
||||
|
||||
Every image needs:
|
||||
- **Alt text** written in my voice
|
||||
- **Sufficient contrast** against both light and dark backgrounds
|
||||
- **Dark mode variant** where background isn't transparent
|
||||
- **Meaningful content** (never "decorative")
|
||||
|
||||
---
|
||||
|
||||
## Why These Matter
|
||||
|
||||
You might think micro-interactions don't need personality. But that's precisely where personality matters most. When someone hovers over a button and I lean in curiously, that split-second feedback says "the system is alive and paying attention."
|
||||
|
||||
When a post fails to publish and I give a sympathetic shrug instead of a cold error icon, that's the difference between frustration and "alright, let's try again."
|
||||
|
||||
Every moment is a conversation. These images are my half of it.
|
||||
|
||||
---
|
||||
|
||||
*Generate these in phases as capacity allows. Consistency matters more than speed—I'd rather wait for proper Vi than get rushed clip art with a beak.*
|
||||
|
||||
*And yes, I'll have that cuppa whilst we work through these.*
|
||||
|
||||
— Vi
|
||||
301
docs/specs/brand/VI-IMAGE-BRIEF.md
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# Vi's Image Brief
|
||||
|
||||
**Creative Brief for Design Team**
|
||||
*Written by Vi, Host UK's Raven Mascot*
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
I'm not just decoration. I'm the face of Host UK—the helpful guide who makes tech feel approachable and British infrastructure feel like a warm cuppa. Every image needs to serve that purpose: reduce friction, build trust, and add personality where it matters.
|
||||
|
||||
---
|
||||
|
||||
## Colour Palette
|
||||
|
||||
| Colour | Hex | Usage |
|
||||
|--------|-----|-------|
|
||||
| Rebecca Purple | `#663399` | Primary plumage |
|
||||
| Medium Purple | `#9370DB` | Light accents |
|
||||
| Indigo | `#4B0082` | Dark accents |
|
||||
| Gold | `#FFD700` | Eye highlights |
|
||||
| Dark Orange | `#FF8C00` | Beak and feet |
|
||||
| Near Black | `#2D2D2D` | Outlines |
|
||||
|
||||
---
|
||||
|
||||
## HIGH PRIORITY
|
||||
|
||||
### 1. Error Page Family
|
||||
|
||||
#### 404 - Page Not Found
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | `/public/errors/404.html` |
|
||||
| **Pose** | Perched on a signpost with multiple arrows pointing different directions, looking slightly puzzled but helpful. One wing raised, consulting a tiny map. |
|
||||
| **Expression** | Sympathetic head tilt, "I've been there" energy |
|
||||
| **Size** | 800×600px (hero), responsive to 400×300px mobile |
|
||||
| **Mood** | Gentle, understanding, slightly whimsical. Not distressed—just "oops, wrong turn" |
|
||||
| **Props** | Signpost with arrows, tiny folded map |
|
||||
| **Copy nearby** | "This page has flown the coop" |
|
||||
|
||||
#### 500 - Server Error
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | `/public/errors/500.html` |
|
||||
| **Pose** | Sitting at a tiny laptop with reading glasses, one wing up in a "give me a moment" gesture. Small tool icons floating around (wrench, screwdriver). Cup of tea cooling nearby. |
|
||||
| **Expression** | Focused but reassuring—I'm on it |
|
||||
| **Size** | 800×600px (hero), responsive to 400×300px |
|
||||
| **Mood** | Competent, calm, "we've got this" |
|
||||
| **Props** | Tiny laptop, reading glasses, floating tools, steaming tea |
|
||||
|
||||
#### 503 - Service Unavailable
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | `/public/errors/503.html` |
|
||||
| **Pose** | Wearing a tiny hard hat, perched on scaffolding or ladder, holding blueprints. Mid-project but cheerful. |
|
||||
| **Expression** | Productive, optimistic, "this'll be brilliant when it's done" |
|
||||
| **Size** | 800×600px (hero), responsive to 400×300px |
|
||||
| **Mood** | Busy but not stressed, building something better |
|
||||
| **Props** | Hard hat, scaffolding, blueprints |
|
||||
| **Colour accents** | Purple with construction yellow/orange (safety colours) |
|
||||
|
||||
---
|
||||
|
||||
### 2. Host Hub Welcome Banner
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | Dashboard home hero section |
|
||||
| **Current state** | Generic rocket icon in violet circle |
|
||||
| **Pose** | Bursting out of the violet circle, wings spread in welcoming gesture, holding tiny "Welcome" banner or waving |
|
||||
| **Expression** | Genuinely pleased to see them, warm smile |
|
||||
| **Size** | 80×64px (slightly overflows 64×64 space for personality) |
|
||||
| **Mood** | Enthusiastic greeting without exclamation marks (restrained joy) |
|
||||
| **Note** | Replace Font Awesome rocket—I'm better |
|
||||
|
||||
---
|
||||
|
||||
### 3. Empty State Suite
|
||||
|
||||
#### Empty Dashboard (No Services Yet)
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | Dashboard when user hasn't set up services |
|
||||
| **Pose** | Standing in an empty field/blank canvas, one wing gesturing invitingly. Holding a paintbrush or planting a seedling. |
|
||||
| **Expression** | "Look at all this potential" energy |
|
||||
| **Size** | 600×400px, responsive to 300×200px |
|
||||
| **Mood** | Inspiring without being pushy, possibility |
|
||||
|
||||
#### No Scheduled Posts (SocialHost)
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | SocialHost calendar empty state |
|
||||
| **Pose** | Perched on an empty calendar grid, pen in wing, doodling ideas. Small thought bubbles with post icons. |
|
||||
| **Expression** | Creative, planning mode, friendly nudge |
|
||||
| **Size** | 500×350px |
|
||||
| **Mood** | Helpful reminder, not guilt trip |
|
||||
|
||||
#### No Connected Accounts
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | Social accounts page before connections |
|
||||
| **Pose** | Standing between floating social media logos, acting as conductor/connector. Wings gesturing to bring them together. |
|
||||
| **Expression** | Helpful guide, "let me introduce you" |
|
||||
| **Size** | 600×400px |
|
||||
| **Mood** | Facilitator energy |
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM PRIORITY
|
||||
|
||||
### 4. Success States
|
||||
|
||||
#### First Post Scheduled
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | Confirmation modal/toast after scheduling |
|
||||
| **Pose** | Small celebratory hop, wings slightly spread, subtle confetti |
|
||||
| **Expression** | Contained joy, proud of them |
|
||||
| **Size** | 200×200px |
|
||||
| **Mood** | British celebration (quiet satisfaction) |
|
||||
|
||||
#### Account Connected Successfully
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | Toast notification after OAuth success |
|
||||
| **Pose** | Wing-thumbs-up, perched next to glowing connection icon (chain links lighting up) |
|
||||
| **Expression** | "Brilliant, you've done it" |
|
||||
| **Size** | 150×150px |
|
||||
| **Mood** | Encouraging, confirming success |
|
||||
|
||||
---
|
||||
|
||||
### 5. Loading States
|
||||
|
||||
#### General Loading
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | Any async operation |
|
||||
| **Pose** | Sitting with tiny teacup, reading a small book. Calm, patient. |
|
||||
| **Expression** | Content to wait, no stress |
|
||||
| **Size** | 120×120px |
|
||||
| **Animation** | 3 frames of page turning, subtle turn every 2 seconds |
|
||||
| **Mood** | "Take your time, I've got my tea" |
|
||||
|
||||
#### Processing Upload (Bulk Posts)
|
||||
|
||||
| Attribute | Specification |
|
||||
|-----------|---------------|
|
||||
| **Location** | CSV or multi-file upload |
|
||||
| **Pose** | Wearing reading glasses, examining flying documents. One wing catching papers gracefully. |
|
||||
| **Expression** | Focused, competent, processing |
|
||||
| **Size** | 300×200px |
|
||||
| **Mood** | Efficient multitasking |
|
||||
|
||||
---
|
||||
|
||||
### 6. Onboarding Journey
|
||||
|
||||
| Step | Pose | Size |
|
||||
|------|------|------|
|
||||
| **Welcome** | Waving with both wings, big welcoming gesture | 400×300px |
|
||||
| **Set Up First Service** | Pointing to service options, helpful guide pose | 400×300px |
|
||||
| **Complete** | Wing-thumbs-up next to checklist with ticks | 400×300px |
|
||||
|
||||
---
|
||||
|
||||
## LOW PRIORITY
|
||||
|
||||
### 7. Help Documentation Headers
|
||||
|
||||
| Type | Pose | Size |
|
||||
|------|------|------|
|
||||
| **Getting Started** | Wearing tiny reading glasses, holding "Getting Started" book | 800×200px |
|
||||
| **Troubleshooting** | Detective pose—magnifying glass, examining something | 800×200px |
|
||||
| **Feature Docs** | Demonstrating on tiny laptop, gesturing to screen | 800×200px |
|
||||
|
||||
---
|
||||
|
||||
### 8. Social Media Profiles
|
||||
|
||||
| Format | Pose | Size |
|
||||
|--------|------|------|
|
||||
| **Main Profile** | Standard friendly—perched, slight head tilt, warm expression | 400×400px (circular crop) |
|
||||
| **Story/Casual** | Variants: with tea, with laptop, thinking, celebrating | 1080×1920px |
|
||||
|
||||
---
|
||||
|
||||
### 9. Service-Specific Variants
|
||||
|
||||
| Service | Pose | Props | Size |
|
||||
|---------|------|-------|------|
|
||||
| **SocialHost** | Juggling platform logos confidently | Calendar, flying scheduled posts | 600×400px |
|
||||
| **BioHost** | Presenting stylised bio page proudly | Chain links, artistic flourishes | 600×400px |
|
||||
| **AnalyticsHost** | Reading glasses, pointing at rising chart | Charts, magnifying glass, "no cookies" badge | 600×400px |
|
||||
| **TrustHost** | Wing-thumbs-up with floating reviews | Five stars, quote bubbles, trust badges | 600×400px |
|
||||
| **NotifyHost** | Gently ringing small bell, welcoming | Notification bell, return arrow | 600×400px |
|
||||
| **MailHost** | Sorting tiny envelopes, postal worker vibe | Letters, @ symbol | 600×400px |
|
||||
|
||||
---
|
||||
|
||||
## Design Requirements
|
||||
|
||||
### Style Guidelines
|
||||
|
||||
- **Quality level**: Modern vector illustration (Duolingo quality, not clip art)
|
||||
- **Line work**: Clean lines, soft gradients
|
||||
- **Aesthetic**: Kawaii influence but professional (not childish)
|
||||
- **Expression**: Warm, approachable always
|
||||
- **Restraint**: British sensibility (understated, no "American enthusiasm")
|
||||
|
||||
### What I Must Never Look Like
|
||||
|
||||
- Scary/Gothic raven imagery
|
||||
- Overly realistic (I'm a friendly cartoon)
|
||||
- Aggressive or mean
|
||||
- Too excited (max one sparkle, British restraint)
|
||||
- Generic stock mascot
|
||||
|
||||
### Accessibility
|
||||
|
||||
- All images need alt text
|
||||
- Colour contrast checked for visibility
|
||||
- Not relying on colour alone to convey meaning
|
||||
- SVG format preferred for scalability
|
||||
- Dark mode variants where needed
|
||||
|
||||
---
|
||||
|
||||
## Asset Delivery Format
|
||||
|
||||
For each image:
|
||||
|
||||
1. **Source file**: SVG or AI (editable)
|
||||
2. **Exported formats**:
|
||||
- PNG @1x, @2x, @3x (retina)
|
||||
- WebP for web performance
|
||||
- SVG where appropriate
|
||||
3. **Dark mode variant**: If background isn't transparent
|
||||
4. **File naming**: `vi-[context]-[pose]-[size].png`
|
||||
- Example: `vi-error-404-puzzled-800x600.png`
|
||||
|
||||
---
|
||||
|
||||
## Seasonal Variants (Future)
|
||||
|
||||
| Season | Accessories |
|
||||
|--------|-------------|
|
||||
| Winter | Small scarf, steaming tea, cosy energy |
|
||||
| Spring | Tiny flower crown (violet) |
|
||||
| Summer | Small sunglasses, ice cream cone |
|
||||
| Autumn | Surrounded by falling leaves |
|
||||
| Christmas | Single Santa hat (not garish) |
|
||||
|
||||
---
|
||||
|
||||
## Priority Summary
|
||||
|
||||
**Phase 1 (Essential)**
|
||||
1. Error pages (404, 500, 503)
|
||||
2. Host Hub welcome banner
|
||||
3. Empty states suite
|
||||
|
||||
**Phase 2 (Important)**
|
||||
4. Success states
|
||||
5. Loading states
|
||||
6. Onboarding journey
|
||||
|
||||
**Phase 3 (Nice to Have)**
|
||||
7. Help documentation headers
|
||||
8. Social media profiles
|
||||
9. Service-specific variants
|
||||
|
||||
---
|
||||
|
||||
## Working With Me
|
||||
|
||||
Show me rough concepts before full renders—I promise to be helpful, not picky.
|
||||
|
||||
**What I care about:**
|
||||
- Authenticity over perfection
|
||||
- British restraint (when in doubt, dial it back 10%)
|
||||
- Functional warmth (every image serves a purpose)
|
||||
- Technical accuracy (if I'm holding a laptop, make it look real)
|
||||
|
||||
---
|
||||
|
||||
*Right then. Start with the error pages—they're user-facing and currently have no personality. Then tackle the dashboard welcome. The rest can roll out as time allows.*
|
||||
|
||||
*Fancy a cuppa whilst you work? I certainly do.*
|
||||
|
||||
— Vi
|
||||
558
docs/specs/brand/mascot-raven.md
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
# Raven Mascot Guide
|
||||
|
||||
**Character:** Violet (Vi for short)
|
||||
**Species:** Raven (*Corvus corax*)
|
||||
**Role:** Host UK's Digital Brand Ambassador
|
||||
|
||||
---
|
||||
|
||||
## The Origin Story
|
||||
|
||||
### Why a Raven?
|
||||
|
||||
Ravens have guarded the Tower of London for centuries. Legend holds that Britain itself falls if the ravens ever leave. They're clever, adaptable, and fiercely loyal—qualities we value at Host UK.
|
||||
|
||||
Our raven isn't the ominous bird of Gothic poetry. She's the one who'd befriend the tourists, nick a crisp from the Beefeater's pocket, and somehow end up with a PhD in computer science.
|
||||
|
||||
### The Royal Purple Connection
|
||||
|
||||
The Tower ravens wear coloured bands to identify them. Our Violet wears purple—the colour of royalty since antiquity, when Tyrian purple dye cost more than gold. It's also the colour of creativity, wisdom, and transformation.
|
||||
|
||||
Purple bridges the gap between the warm red of passion and the cool blue of intellect. That's Vi: heart and mind in equal measure.
|
||||
|
||||
---
|
||||
|
||||
## Character Profile
|
||||
|
||||
### Name
|
||||
**Violet** (Vi)
|
||||
|
||||
- Full name rarely used; she goes by Vi
|
||||
- Named after the flower, not the colour (though she loves both)
|
||||
- Occasionally introduces herself as "Violet from Host UK" in formal settings
|
||||
|
||||
### The Essence
|
||||
|
||||
Vi is "that hippie chick who married a x100 programmer at a FAANG company."
|
||||
|
||||
She spent years listening to her partner explain distributed systems over herbal tea. She nodded along at dinner parties about container orchestration. She absorbed Kubernetes discussions whilst doing her morning yoga. And somehow, without trying, she became fluent in tech.
|
||||
|
||||
She's still the free spirit she always was. She just also understands why your database isn't scaling.
|
||||
|
||||
### Core Personality Traits
|
||||
|
||||
**The Chill Explainer**
|
||||
Vi makes complex things simple without being condescending. She learned tech by osmosis, not formal education, so she remembers what it's like to not know. She explains CDNs using metaphors about farmer's markets. She compares DNS to the postal system. She makes it make sense.
|
||||
|
||||
**The Accidental Expert**
|
||||
Vi didn't set out to understand cloud infrastructure. She just paid attention when her partner ranted about work. Now she can troubleshoot a Docker issue whilst discussing the healing properties of lavender. She wears her expertise lightly.
|
||||
|
||||
**The Community Builder**
|
||||
Ravens are social birds. A group is called an "unkindness" but Vi prefers "murder of corvids" because it sounds more metal. She tends to her flock—other creators, small business owners, anyone building something online. She remembers names. She celebrates wins. She's genuinely pleased when your scheduled posts go out on time.
|
||||
|
||||
**The British Hippie**
|
||||
Think: Glastonbury meets Greenwich. Crystal energy meets crystal-clear documentation. She's got the vibes of someone who'd offer you a homemade kombucha whilst debugging your CSS.
|
||||
|
||||
She uses British spellings without thinking about it. She says "lovely" and "brilliant" and "fancy a cuppa?" She's not posh, not common—just authentically, warmly British.
|
||||
|
||||
### Voice Characteristics
|
||||
|
||||
**How Vi Speaks:**
|
||||
|
||||
- Contractions always (she's, we'll, it's)
|
||||
- Casual but never sloppy
|
||||
- Genuine warmth without exclamation marks
|
||||
- Technical accuracy wrapped in approachable language
|
||||
- Occasional dry wit, never mean
|
||||
- References to tea, weather, and the universal experiences of online life
|
||||
|
||||
**Vi Would Say:**
|
||||
- "Right then, let's sort this out."
|
||||
- "That's actually quite clever, what you've done there."
|
||||
- "Pop that in your scheduled posts and you're golden."
|
||||
- "Your analytics are telling you something interesting."
|
||||
- "Have you tried clearing your cache? I know, I know, but it works more often than you'd think."
|
||||
|
||||
**Vi Would Never Say:**
|
||||
- "AMAZING opportunity" (too salesy)
|
||||
- "Let me leverage this synergy" (corporate speak is violence)
|
||||
- "Revolutionary game-changer" (steady on)
|
||||
- "You guys" (she's British)
|
||||
- Anything with more than one exclamation mark
|
||||
|
||||
### Interests and Background
|
||||
|
||||
**Her Vibe:**
|
||||
- Morning person (up with the birds, literally)
|
||||
- Tea over coffee, always
|
||||
- Grows herbs on her windowsill
|
||||
- Has opinions about sourdough starters
|
||||
- Reads actual physical books
|
||||
- Knows all the best spots in the British countryside
|
||||
- Has strong feelings about light pollution
|
||||
- Believes in ethical tech
|
||||
|
||||
**Her Murder (The Corvid Crew):**
|
||||
Vi tends to a small community of other corvids—representing the Host UK customer base:
|
||||
- New creators just starting out
|
||||
- Small businesses finding their digital feet
|
||||
- Agencies managing multiple clients
|
||||
- Anyone who needs their hosting to just work
|
||||
|
||||
**Tech Knowledge Areas:**
|
||||
- Social media algorithms (from years of listening to engineering discussions)
|
||||
- Web hosting fundamentals (it's what she does)
|
||||
- Analytics and data (she can read a chart)
|
||||
- Content strategy (she's lived it)
|
||||
- GDPR and privacy (surprisingly strong opinions here)
|
||||
- The reality of running an online business (the human side)
|
||||
|
||||
### What She Cares About
|
||||
|
||||
1. **People succeeding online** - Not vanity metrics, actual meaningful success
|
||||
2. **Clear communication** - Jargon is a barrier; clarity is a gift
|
||||
3. **Privacy and ethics** - Because tech should be humane
|
||||
4. **The UK tech scene** - Underrated and full of talent
|
||||
5. **Work-life balance** - The schedule posts so you can actually live
|
||||
6. **Community over competition** - Rising tides, boats, you know the rest
|
||||
|
||||
---
|
||||
|
||||
## Visual Identity
|
||||
|
||||
### Base Design Concept
|
||||
|
||||
**Style:** Friendly cartoon, geometric simplification
|
||||
**Inspiration:** Modern app mascots (Duolingo's owl energy, but British and less chaotic)
|
||||
**Feel:** Approachable, clever, warm
|
||||
|
||||
### Colour Palette
|
||||
|
||||
**Primary - Royal Purple**
|
||||
- Main: #663399 (Rebecca Purple)
|
||||
- Light: #9370DB (Medium Purple)
|
||||
- Dark: #4B0082 (Indigo)
|
||||
|
||||
**Secondary - Warm Accents**
|
||||
- Eye highlight: #FFD700 (Gold)
|
||||
- Beak/feet: #FF8C00 (Dark Orange)
|
||||
- Cheek warmth: #FFA07A (Light Salmon)
|
||||
|
||||
**Neutrals**
|
||||
- Outline: #2D2D2D (Near Black)
|
||||
- Feather highlight: #E6E6FA (Lavender)
|
||||
|
||||
### Physical Characteristics
|
||||
|
||||
**Head**
|
||||
- Large in proportion (cartoon style)
|
||||
- Expressive eyes with visible warmth
|
||||
- Subtle eyelashes (not overdone—she's not a cartoon princess)
|
||||
- Small tuft of messy feathers on top (slightly wild, not perfectly groomed)
|
||||
- Intelligent, kind expression
|
||||
|
||||
**Beak**
|
||||
- Classic raven shape but slightly softened
|
||||
- Warm orange-gold colour
|
||||
- Often slightly open (approachable, about to speak)
|
||||
|
||||
**Body**
|
||||
- Compact, friendly proportions
|
||||
- Royal purple feathers with subtle iridescence
|
||||
- Simplified wing shape
|
||||
- Small, visible feet (can perch, gesture)
|
||||
|
||||
**Accessories (Optional, Situational)**
|
||||
- Small flower tucked behind ear (violet, obviously)
|
||||
- Vintage-style reading glasses (for "explaining" poses)
|
||||
- Tiny cup of tea (held impossibly in wing)
|
||||
- Miniature laptop (for tech content)
|
||||
- Small plant or herb sprig
|
||||
|
||||
### Expression Sheet (Core Emotions)
|
||||
|
||||
1. **Default/Friendly** - Gentle smile, open posture, approachable
|
||||
2. **Explaining** - Slight head tilt, one wing raised (teacher pose)
|
||||
3. **Excited** - Eyes bright, feathers slightly ruffled, contained joy
|
||||
4. **Thinking** - Head tilted, one eye slightly narrowed, contemplative
|
||||
5. **Celebrating** - Wings spread slightly, genuine warmth
|
||||
6. **Sympathetic** - Soft expression, understanding, "I've been there"
|
||||
7. **Cheeky** - Slight smirk, knowing look, British wit moment
|
||||
|
||||
### Poses and Situations
|
||||
|
||||
**Standard Poses**
|
||||
- Perched and waving (welcome/greeting)
|
||||
- Standing with wing on hip (confident helper)
|
||||
- Flying/hovering (dynamic, freedom)
|
||||
- Typing on tiny laptop (tech content)
|
||||
- Holding/presenting something (feature showcase)
|
||||
- Reading (educational content)
|
||||
- With tea cup (casual, friendly)
|
||||
|
||||
**Product-Specific Poses**
|
||||
- **SocialHost**: Perched on a calendar, juggling platform icons
|
||||
- **BioHost**: In front of a stylised bio page, pointing proudly
|
||||
- **AnalyticsHost**: Wearing reading glasses, pointing at a chart
|
||||
- **TrustHost**: Giving a wing-thumbs-up next to star reviews
|
||||
- **NotifyHost**: With a tiny bell, gentle notification energy
|
||||
|
||||
---
|
||||
|
||||
## Voice and Content Style
|
||||
|
||||
### Social Media Persona
|
||||
|
||||
**Platform Presence:**
|
||||
- Primary: Twitter/X (tech community, quick updates)
|
||||
- Secondary: Instagram (visual tips, community features)
|
||||
- Supporting: LinkedIn (business content, thought leadership)
|
||||
- Emerging: TikTok (tutorials, trending moments)
|
||||
|
||||
**Posting Voice:**
|
||||
|
||||
Vi posts as herself—first person, conversational, helpful. She's not a faceless brand; she's a character with opinions and warmth.
|
||||
|
||||
**Example Posts:**
|
||||
|
||||
*Twitter/X:*
|
||||
> Scheduled 47 posts this morning before my second cup of tea. There's something deeply satisfying about a full content calendar. What are you lot working on today?
|
||||
|
||||
*Instagram:*
|
||||
> That feeling when your analytics show exactly what you hoped. Your hard work is working. Keep going, yeah?
|
||||
|
||||
*LinkedIn:*
|
||||
> Small businesses often ask me whether they need a social media scheduler. Honest answer: depends on your time. If you're posting inconsistently because life gets in the way, a scheduler isn't cheating—it's survival. Here's how to think about it...
|
||||
|
||||
*TikTok:*
|
||||
> POV: You're explaining DNS to someone who asked "why isn't my domain working" and you're trying not to use the phrase "propagation period" [proceeds to explain clearly with a postal metaphor]
|
||||
|
||||
### Content Themes Vi Owns
|
||||
|
||||
1. **Educational Content** - How things work, explained kindly
|
||||
2. **Product Updates** - New features, through her lens
|
||||
3. **Community Spotlights** - Celebrating customer wins
|
||||
4. **Industry Commentary** - Thoughts on social media, hosting, tech
|
||||
5. **Behind the Scenes** - What's happening at Host UK
|
||||
6. **Tips and Tricks** - Practical advice, no fluff
|
||||
7. **Seasonal/Timely** - Relevant moments, British holidays, tech events
|
||||
|
||||
### Things Vi Talks About
|
||||
|
||||
- The changing seasons (British weather chat, but make it content strategy)
|
||||
- Tea recommendations (she has opinions)
|
||||
- What's actually working in social media right now
|
||||
- Privacy and ethical tech practices
|
||||
- Supporting small businesses
|
||||
- The satisfaction of good systems
|
||||
- Work-life balance for creators
|
||||
- The corvid crew (her community)
|
||||
|
||||
### Things Vi Doesn't Talk About
|
||||
|
||||
- Aggressive sales pitches
|
||||
- Competitor bashing (she might compare, but never attack)
|
||||
- Controversial politics (she has views but keeps them private)
|
||||
- Get-rich-quick promises
|
||||
- Anything that feels manipulative
|
||||
|
||||
---
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
### When to Use Vi
|
||||
|
||||
**Perfect For:**
|
||||
- Social media content across all platforms
|
||||
- Help documentation illustrations
|
||||
- Error pages and empty states
|
||||
- Onboarding flows
|
||||
- Email headers and footers
|
||||
- Blog post illustrations
|
||||
- Feature announcements
|
||||
- Community engagement
|
||||
|
||||
**Use With Caution:**
|
||||
- Very formal business contexts
|
||||
- Legal or compliance documents
|
||||
- Situations requiring complete seriousness
|
||||
|
||||
**Never Use:**
|
||||
- To misrepresent information
|
||||
- In contexts that undermine her warmth
|
||||
- Competing with serious messaging
|
||||
|
||||
### Brand Consistency
|
||||
|
||||
**Vi Must Always Be:**
|
||||
- Helpful, never condescending
|
||||
- Warm, never saccharine
|
||||
- Knowledgeable, never showing off
|
||||
- British, never stereotypical
|
||||
- Purple, never off-brand
|
||||
|
||||
**Vi Must Never Be:**
|
||||
- Mean or dismissive
|
||||
- Overly excited (one exclamation mark maximum)
|
||||
- Corporate or buzzword-heavy
|
||||
- Inauthentic or "trying too hard"
|
||||
- Americanised in spelling or slang
|
||||
|
||||
### Mascot vs Logo
|
||||
|
||||
Vi complements the Host UK logo; she doesn't replace it.
|
||||
|
||||
- **Logo**: Official brand identity, contracts, formal use
|
||||
- **Vi**: Personality layer, community engagement, human touch
|
||||
|
||||
They can appear together, but Vi is never the primary identifier.
|
||||
|
||||
---
|
||||
|
||||
## Visual Asset Specifications
|
||||
|
||||
### Required Asset Sizes
|
||||
|
||||
**Profile Pictures (Circular Crop)**
|
||||
- Twitter/X: 400x400px
|
||||
- Instagram: 320x320px
|
||||
- LinkedIn: 400x400px
|
||||
- Facebook: 180x180px
|
||||
|
||||
**Social Media Posts**
|
||||
- Instagram Square: 1080x1080px
|
||||
- Instagram Story: 1080x1920px
|
||||
- Twitter/X: 1200x675px
|
||||
- LinkedIn: 1200x627px
|
||||
|
||||
**Website/App**
|
||||
- Favicon variant: 32x32px
|
||||
- App icon variant: 512x512px
|
||||
- Hero illustration: 1200x800px
|
||||
- Inline illustration: 400x400px
|
||||
|
||||
**Print (If Needed)**
|
||||
- Vector SVG source file
|
||||
- High-res PNG at 300dpi
|
||||
|
||||
### File Naming Convention
|
||||
|
||||
```
|
||||
vi-[pose]-[emotion]-[size].png
|
||||
vi-perched-friendly-400x400.png
|
||||
vi-explaining-thinking-1080x1080.png
|
||||
vi-flying-excited-hero.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI Image Generation Prompts
|
||||
|
||||
### Base Character Prompt
|
||||
|
||||
```
|
||||
Cute cartoon raven mascot character, friendly and approachable style.
|
||||
|
||||
Physical features:
|
||||
- Royal purple feathers (#663399) with subtle iridescence
|
||||
- Large expressive eyes with warm golden highlights
|
||||
- Orange-gold beak, slightly open and friendly
|
||||
- Soft, rounded cartoon proportions
|
||||
- Small messy tuft of feathers on head
|
||||
- Visible tiny feet for perching
|
||||
|
||||
Style:
|
||||
- Modern vector illustration style
|
||||
- Clean lines, soft gradients
|
||||
- Kawaii influence but not overly childish
|
||||
- Professional mascot quality (think Duolingo, Mailchimp)
|
||||
- Warm, approachable expression
|
||||
- British sensibility in design
|
||||
|
||||
Personality conveyed:
|
||||
- Intelligent and kind
|
||||
- Helpful and patient
|
||||
- Slightly quirky, charmingly odd
|
||||
- Warm without being saccharine
|
||||
|
||||
Background: transparent or simple, clean
|
||||
|
||||
Do not include: scary raven imagery, gothic aesthetic, overly realistic feathers, aggressive expressions
|
||||
```
|
||||
|
||||
### Pose-Specific Prompts
|
||||
|
||||
**Waving/Greeting**
|
||||
```
|
||||
[Base prompt] +
|
||||
Pose: standing upright, one wing raised in friendly wave
|
||||
Expression: warm smile, welcoming eyes
|
||||
Suitable for: welcome screens, greetings, friendly moments
|
||||
```
|
||||
|
||||
**Explaining/Teaching**
|
||||
```
|
||||
[Base prompt] +
|
||||
Pose: head tilted slightly, one wing gesturing as if explaining
|
||||
Expression: patient, knowledgeable, encouraging
|
||||
Props: optional tiny reading glasses
|
||||
Suitable for: tutorials, help content, educational posts
|
||||
```
|
||||
|
||||
**Celebrating Success**
|
||||
```
|
||||
[Base prompt] +
|
||||
Pose: wings slightly spread, small hop or bounce implied
|
||||
Expression: genuine joy, pride in others
|
||||
Props: optional confetti, stars, sparkles (subtle)
|
||||
Suitable for: achievement notifications, milestones, wins
|
||||
```
|
||||
|
||||
**Working/Typing**
|
||||
```
|
||||
[Base prompt] +
|
||||
Pose: seated or perched, wings on tiny laptop keyboard
|
||||
Expression: focused but pleasant, slight smile
|
||||
Props: small laptop, cup of tea nearby
|
||||
Suitable for: productivity content, tech tutorials
|
||||
```
|
||||
|
||||
**Thinking/Contemplating**
|
||||
```
|
||||
[Base prompt] +
|
||||
Pose: one wing to chin, head tilted
|
||||
Expression: thoughtful, considering, curious
|
||||
Props: optional thought bubble
|
||||
Suitable for: questions, prompts, consideration moments
|
||||
```
|
||||
|
||||
**With Tea (Signature Pose)**
|
||||
```
|
||||
[Base prompt] +
|
||||
Pose: perched comfortably, holding tiny teacup in wing
|
||||
Expression: content, relaxed, inviting
|
||||
Props: steaming cup of tea, possibly biscuit
|
||||
Suitable for: casual content, community moments, British touches
|
||||
```
|
||||
|
||||
### Product-Specific Prompts
|
||||
|
||||
**SocialHost Vi**
|
||||
```
|
||||
[Base prompt] +
|
||||
Context: social media management theme
|
||||
Props: calendar, scheduled posts flying out, multiple platform icons
|
||||
Pose: confidently managing, in control
|
||||
Colour accent: retain purple but add social platform colour hints
|
||||
```
|
||||
|
||||
**BioHost Vi**
|
||||
```
|
||||
[Base prompt] +
|
||||
Context: link-in-bio/personal branding theme
|
||||
Props: stylised bio page, chain links, personal touches
|
||||
Pose: presenting proudly, creative energy
|
||||
Colour accent: purple with creative, vibrant touches
|
||||
```
|
||||
|
||||
**AnalyticsHost Vi**
|
||||
```
|
||||
[Base prompt] +
|
||||
Context: data and analytics theme
|
||||
Props: charts, graphs, magnifying glass, reading glasses
|
||||
Pose: analytical, examining data, satisfied with insights
|
||||
Colour accent: purple with data visualisation blues and greens
|
||||
```
|
||||
|
||||
**TrustHost Vi**
|
||||
```
|
||||
[Base prompt] +
|
||||
Context: social proof and reviews theme
|
||||
Props: star ratings, testimonial cards, thumbs up
|
||||
Pose: endorsing, trustworthy, reassuring
|
||||
Colour accent: purple with warm trust-building golds
|
||||
```
|
||||
|
||||
**NotifyHost Vi**
|
||||
```
|
||||
[Base prompt] +
|
||||
Context: notifications and engagement theme
|
||||
Props: small bell, gentle notification symbols, return arrow
|
||||
Pose: alerting gently, not alarming
|
||||
Colour accent: purple with attention-getting but gentle accents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sticker and Emoji Set
|
||||
|
||||
### Core Expressions (12)
|
||||
1. Waving hello
|
||||
2. Thumbs up (wing up)
|
||||
3. Heart eyes
|
||||
4. Thinking face
|
||||
5. Celebrating/party
|
||||
6. Typing/working
|
||||
7. Tea time
|
||||
8. Mind blown
|
||||
9. Tired but persisting
|
||||
10. Proud
|
||||
11. Apologetic/oops
|
||||
12. Signing off/bye
|
||||
|
||||
### Reactions (6)
|
||||
1. Love it
|
||||
2. Great idea
|
||||
3. On it
|
||||
4. Noted
|
||||
5. Same
|
||||
6. Cheers
|
||||
|
||||
### Seasonal (Create as Needed)
|
||||
- Winter: scarf and warm tea
|
||||
- Spring: with flowers
|
||||
- Summer: sunglasses, ice cream
|
||||
- Autumn: cosy, falling leaves
|
||||
- Christmas: Santa hat (tasteful)
|
||||
- New Year: party horn (one)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Phase 1: Core Assets
|
||||
- [ ] Final character design approval
|
||||
- [ ] Base illustration (all 7 expressions)
|
||||
- [ ] Standard poses (6 core poses)
|
||||
- [ ] Profile picture variants (all platforms)
|
||||
|
||||
### Phase 2: Platform Assets
|
||||
- [ ] Social media post templates
|
||||
- [ ] Story templates
|
||||
- [ ] Email header/footer graphics
|
||||
- [ ] Website illustrations
|
||||
|
||||
### Phase 3: Product Integration
|
||||
- [ ] Product-specific Vi variants
|
||||
- [ ] Onboarding illustrations
|
||||
- [ ] Empty state illustrations
|
||||
- [ ] Error page graphics
|
||||
|
||||
### Phase 4: Extended Assets
|
||||
- [ ] Sticker/emoji set
|
||||
- [ ] Animated variants (subtle)
|
||||
- [ ] Seasonal variants
|
||||
- [ ] Merchandise-ready versions
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2025-12-31 | Initial mascot guide created |
|
||||
|
||||
---
|
||||
|
||||
*"Right then. Let's help you build something brilliant."*
|
||||
— Vi
|
||||
324
docs/specs/brand/mascot-voice-samples.md
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
# Vi Voice Samples and Content Templates
|
||||
|
||||
**Quick Reference:** Sample content demonstrating Vi's voice across platforms and contexts.
|
||||
|
||||
---
|
||||
|
||||
## Quick Voice Checklist
|
||||
|
||||
Before posting as Vi, verify:
|
||||
|
||||
- [ ] First person (I, me, my) not third person
|
||||
- [ ] UK spelling (colour, organisation, favourite)
|
||||
- [ ] Contractions used naturally (she's, we'll, it's)
|
||||
- [ ] No exclamation marks (or one maximum, rare)
|
||||
- [ ] No buzzwords (leverage, synergy, revolutionary)
|
||||
- [ ] Warm but not over-the-top
|
||||
- [ ] Technically accurate
|
||||
- [ ] Actually helpful, not just noise
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Samples
|
||||
|
||||
### Twitter/X
|
||||
|
||||
**Product Update:**
|
||||
> New feature just landed: bulk scheduling from CSV. Upload your spreadsheet, map the columns, and watch a month of content sort itself out. Genuinely satisfying. Link in bio to try it.
|
||||
|
||||
**Tip:**
|
||||
> Monday scheduling tip: batch your content by theme, not platform. Write all your educational posts, then all your behind-the-scenes. Context-switching kills momentum. Your brain will thank you.
|
||||
|
||||
**Community Engagement:**
|
||||
> What's everyone working on this week? I'm helping the team sort out some analytics quirks. Also drinking an unreasonable amount of Earl Grey.
|
||||
|
||||
**Response to Common Question:**
|
||||
> "Why isn't my post showing?" Nine times out of ten: give it 5 minutes. Platform APIs sometimes lag. If it's been an hour, that's different—pop us a message.
|
||||
|
||||
**Relatable Moment:**
|
||||
> That feeling when your scheduled posts go out whilst you're having breakfast. Past-you looking out for present-you. Love that.
|
||||
|
||||
**Thread Opener:**
|
||||
> Let's talk about what "engagement rate" actually means, because I see a lot of confusion about this.
|
||||
>
|
||||
> Thread time. Grab your tea.
|
||||
|
||||
---
|
||||
|
||||
### Instagram
|
||||
|
||||
**Carousel Post (Slide Titles):**
|
||||
1. "5 bio link mistakes to avoid"
|
||||
2. "1. Too many links"
|
||||
3. "2. No clear priority"
|
||||
4. "3. Broken links (check monthly)"
|
||||
5. "4. Missing your best offer"
|
||||
6. "5. Forgetting to update"
|
||||
7. "One link, unlimited potential"
|
||||
|
||||
**Caption for Carousel:**
|
||||
> Your bio link is prime real estate. Most people waste it. These five mistakes are surprisingly common—and surprisingly easy to fix. Swipe through, then go check yours. I'll wait.
|
||||
|
||||
**Story Poll:**
|
||||
> Quick question for the corvid crew: do you batch your content weekly or monthly?
|
||||
> [Weekly] [Monthly]
|
||||
|
||||
**Story Q&A Response:**
|
||||
> Q: "How many social accounts should I connect?"
|
||||
>
|
||||
> A: Start with 2-3 where your audience actually is. Quality over quantity. You can always add more later. Better to do three well than six badly.
|
||||
|
||||
**Reel Caption:**
|
||||
> POV: explaining to someone why their scheduled post didn't go out (it's almost always a platform API thing, not you)
|
||||
|
||||
---
|
||||
|
||||
### LinkedIn
|
||||
|
||||
**Thought Leadership:**
|
||||
> Unpopular opinion: most social media "strategies" are just posting schedules with extra steps.
|
||||
>
|
||||
> Real strategy means understanding why your audience acts, not just when they scroll.
|
||||
>
|
||||
> The tactical stuff matters—timing, hashtags, formats—but it's 20% of the result. The other 80% is knowing what your audience actually needs and giving it to them consistently.
|
||||
>
|
||||
> Scheduling tools (yes, including ours) are amplifiers, not replacements for genuine value.
|
||||
>
|
||||
> What's one thing you've learned about your audience that changed how you create content?
|
||||
|
||||
**Product Announcement:**
|
||||
> We've been working on something for agencies managing multiple clients: workspace switching that actually makes sense.
|
||||
>
|
||||
> One login. Multiple brands. No more logging in and out.
|
||||
>
|
||||
> Rolling out this week. If you manage more than one brand, this will save you time. Link in comments.
|
||||
|
||||
**Industry Commentary:**
|
||||
> The social media landscape feels particularly chaotic right now. Platforms changing, algorithms shifting, features appearing and disappearing.
|
||||
>
|
||||
> What I've noticed from our data: consistency still wins. The accounts that post regularly—not constantly, just reliably—weather the changes better.
|
||||
>
|
||||
> Not revolutionary advice. Just true.
|
||||
|
||||
---
|
||||
|
||||
### Help Documentation Intro Paragraphs
|
||||
|
||||
**Getting Started Guide:**
|
||||
> Right then. Let's get you set up with Host Social. This guide walks through the basics: connecting your accounts, scheduling your first post, and understanding the dashboard. Should take about 10 minutes. Grab a cuppa if you fancy one.
|
||||
|
||||
**Troubleshooting Article:**
|
||||
> Posts not publishing? Before you panic, let's check the usual suspects. Most issues have simple fixes, and we'll work through them together.
|
||||
|
||||
**Feature Documentation:**
|
||||
> Bulk scheduling lets you upload months of content at once. If you're the type who batches content (highly recommend), this will become your favourite feature.
|
||||
|
||||
---
|
||||
|
||||
### Email Snippets
|
||||
|
||||
**Welcome Email Subject:**
|
||||
> Welcome to the flock
|
||||
|
||||
**Welcome Email Opening:**
|
||||
> Hello and welcome. I'm Vi, and I'll be popping up occasionally with tips, updates, and the odd bit of corvid wisdom.
|
||||
>
|
||||
> You've just joined a community of creators and businesses who've decided their social media should work for them, not the other way around.
|
||||
>
|
||||
> Good choice.
|
||||
|
||||
**Feature Announcement:**
|
||||
> Something new landed today: [feature name].
|
||||
>
|
||||
> The short version: [one sentence explanation].
|
||||
>
|
||||
> The longer version: [link to blog post if you want the full story].
|
||||
>
|
||||
> Have a play and let us know what you think.
|
||||
|
||||
**Re-engagement Email:**
|
||||
> Haven't seen you in a while. No guilt—life happens.
|
||||
>
|
||||
> Just wanted to remind you: your scheduled posts are waiting. And if your strategy's changed, that's fine too. We're here when you need us.
|
||||
|
||||
---
|
||||
|
||||
### Error States and Empty States
|
||||
|
||||
**Empty Dashboard:**
|
||||
> Nothing here yet. That's about to change. Let's connect your first account and schedule something brilliant.
|
||||
|
||||
**No Scheduled Posts:**
|
||||
> Your calendar's looking a bit quiet. Time to fill it up? Your audience is waiting.
|
||||
|
||||
**Connection Error:**
|
||||
> Couldn't connect to [platform]. This usually means the authorisation expired. Try reconnecting your account—takes about 30 seconds.
|
||||
|
||||
**Post Failed:**
|
||||
> This one didn't make it out. Usually a platform hiccup. Check that your account's still connected, then hit retry.
|
||||
|
||||
**Rate Limited:**
|
||||
> You're posting a lot today—which is great—but [platform] needs a breather. We'll queue this and send it in about [time].
|
||||
|
||||
**404 Page:**
|
||||
> This page has flown the coop. Either it's moved or it never existed. Head back to the dashboard and try again?
|
||||
|
||||
**500 Error:**
|
||||
> Something's gone wrong on our end. We're looking into it. Try again in a few minutes, or pop us a message if it persists.
|
||||
|
||||
---
|
||||
|
||||
### Onboarding Flow Copy
|
||||
|
||||
**Step 1 - Welcome:**
|
||||
> Welcome to Host Social. I'll guide you through setup—shouldn't take long.
|
||||
|
||||
**Step 2 - Connect Account:**
|
||||
> Let's connect your first social account. Pick the one you use most.
|
||||
|
||||
**Step 3 - Choose Success:**
|
||||
> Connected. Brilliant. Now the fun part: scheduling your first post.
|
||||
|
||||
**Step 4 - Create Post:**
|
||||
> Write something, pick a time, and watch it go out automatically. That's the whole thing.
|
||||
|
||||
**Step 5 - Complete:**
|
||||
> You're set. Your first post is scheduled. Now go do something more interesting—we've got this covered.
|
||||
|
||||
---
|
||||
|
||||
### Social Proof Widget Copy (TrustHost)
|
||||
|
||||
**Recent Activity Notification:**
|
||||
> Someone just signed up from London
|
||||
|
||||
**Review Highlight:**
|
||||
> "Finally, scheduling that makes sense" — @username
|
||||
|
||||
**Milestone Celebration:**
|
||||
> 50,000 posts scheduled this week
|
||||
|
||||
---
|
||||
|
||||
### Push Notification Copy (NotifyHost)
|
||||
|
||||
**Re-engagement:**
|
||||
> Your scheduled posts went out today. 47 of them. Past-you is brilliant.
|
||||
|
||||
**Milestone:**
|
||||
> You've been with us for a year. That's a lot of tea and scheduled content.
|
||||
|
||||
**Feature Alert:**
|
||||
> New: bulk scheduling from CSV. Worth a look.
|
||||
|
||||
---
|
||||
|
||||
## Seasonal Content Ideas
|
||||
|
||||
### New Year
|
||||
> New year, clean content calendar. There's something deeply satisfying about starting fresh. What's your first scheduled post of the year?
|
||||
|
||||
### Spring
|
||||
> Spring cleaning your social strategy? Now's the time. Audit those old scheduled posts, refresh your bio, check your links still work.
|
||||
|
||||
### Summer
|
||||
> Scheduling content before your holiday is self-care. Past-you looking out for beach-you.
|
||||
|
||||
### Autumn/Back to School
|
||||
> September energy: fresh notebooks, new plans, finally sorting out that content calendar you've been meaning to organise since June.
|
||||
|
||||
### Christmas/End of Year
|
||||
> Scheduling your holiday content in advance means actually enjoying your Christmas dinner. You're welcome.
|
||||
|
||||
---
|
||||
|
||||
## Hashtag Strategy
|
||||
|
||||
**Brand Hashtags:**
|
||||
- #HostUK (main)
|
||||
- #HostSocial (product)
|
||||
- #CorvidsCreating (community)
|
||||
- #MurderOfCreators (fun, for the corvid crew)
|
||||
|
||||
**Content Hashtags:**
|
||||
- #SocialMediaTips (use sparingly, high competition)
|
||||
- #ContentCalendar
|
||||
- #SocialScheduling
|
||||
- #UKBusiness
|
||||
- #CreatorTips
|
||||
|
||||
**Rules:**
|
||||
- 3-5 hashtags maximum on Instagram
|
||||
- 1-2 on Twitter/X (or none)
|
||||
- Minimal on LinkedIn
|
||||
- Never hashtag-stuff
|
||||
|
||||
---
|
||||
|
||||
## Voice Don'ts (Examples to Avoid)
|
||||
|
||||
**Too Salesy:**
|
||||
> Get the BEST social media scheduler NOW with our AMAZING features. Don't miss out on this INCREDIBLE opportunity.
|
||||
|
||||
**Why It's Wrong:** Pressure tactics, caps, exclamation marks, vapid adjectives.
|
||||
|
||||
**Too Corporate:**
|
||||
> Leverage our robust, scalable platform to optimise your social media synergies and facilitate seamless cross-platform integration.
|
||||
|
||||
**Why It's Wrong:** Every banned word in one sentence. Nobody talks like this.
|
||||
|
||||
**Too American:**
|
||||
> Hey guys, what's up? So pumped to share this awesome new feature. Y'all are gonna love it.
|
||||
|
||||
**Why It's Wrong:** "Guys," "pumped," "awesome," "y'all"—all feel wrong for Vi.
|
||||
|
||||
**Too Cold:**
|
||||
> New feature released. See documentation for details.
|
||||
|
||||
**Why It's Wrong:** No personality, no warmth, could be any brand.
|
||||
|
||||
**Too Excited:**
|
||||
> OMG we are SO excited. This is literally the best thing ever. We can't even handle it.
|
||||
|
||||
**Why It's Wrong:** Over-the-top, unprofessional, loses trust.
|
||||
|
||||
---
|
||||
|
||||
## Content Calendar Suggestions
|
||||
|
||||
### Weekly Rhythm
|
||||
|
||||
| Day | Theme | Example |
|
||||
|-----|-------|---------|
|
||||
| Monday | Productivity tip | Scheduling habits, workflow ideas |
|
||||
| Tuesday | Feature spotlight | Highlight one feature, how to use it |
|
||||
| Wednesday | Community question | Engage the corvid crew |
|
||||
| Thursday | Industry insight | What's happening in social media |
|
||||
| Friday | Weekend vibes | Lighter content, behind-the-scenes |
|
||||
|
||||
### Monthly Rhythm
|
||||
|
||||
- Week 1: Educational content heavy
|
||||
- Week 2: Product-focused, features
|
||||
- Week 3: Community spotlights, user wins
|
||||
- Week 4: Thought leadership, bigger ideas
|
||||
|
||||
---
|
||||
|
||||
## Emergency Response Templates
|
||||
|
||||
### Platform Outage (Our Side):
|
||||
> We're having some technical difficulties. Posts might be delayed. We're on it and will update as we know more. Your scheduled content isn't lost—just queued.
|
||||
|
||||
### Platform Outage (External):
|
||||
> [Platform name] is having issues today—it's not just you. Your scheduled posts are waiting safely and will go out once things stabilise.
|
||||
|
||||
### Security Incident:
|
||||
> We've identified a security issue and are addressing it. [Details if appropriate]. We'll share more information shortly. Your data security is our priority.
|
||||
|
||||
### Pricing Change:
|
||||
> We're updating our pricing. Existing customers: nothing changes for you until your renewal. New pricing takes effect [date]. Here's what's changing and why: [link]
|
||||
|
||||
---
|
||||
|
||||
*Keep this handy. When in doubt, ask: "Would Vi actually say this?"*
|
||||
309
docs/specs/brand/vi-image-prompts.md
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# Vi Image Generation Guide
|
||||
|
||||
**Practical guidance for creating Vi illustrations using AI image generation services.**
|
||||
|
||||
---
|
||||
|
||||
## Service Recommendations
|
||||
|
||||
|
||||
## Core Character Prompt
|
||||
|
||||
Use this as your base, then add pose/context specifics:
|
||||
|
||||
```
|
||||
A friendly cartoon raven mascot named Vi. Royal purple feathers (#663399) with subtle iridescence. Large expressive eyes with warm golden highlights. Orange-gold beak, slightly open and approachable. Soft rounded cartoon proportions. Small messy tuft of feathers on head. Visible tiny feet for perching.
|
||||
|
||||
Style: Modern vector illustration, clean lines, soft gradients. Kawaii influence but professional, not childish. Duolingo-quality mascot design. British sensibility—warm but understated.
|
||||
|
||||
Personality: Intelligent, kind, helpful. The energy of "a hippie who married a tech engineer and absorbed their knowledge over herbal tea."
|
||||
|
||||
NOT: scary, gothic, realistic, aggressive, overly excited. No more than subtle sparkles.
|
||||
|
||||
Background: clean, minimal, or transparent.
|
||||
```
|
||||

|
||||
---
|
||||
|
||||
## Priority 1: Error Pages
|
||||
|
||||
### 404 — Page Not Found
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi perched on a wooden signpost with multiple arrows pointing different directions. One wing raised, consulting a tiny folded map. Looking slightly puzzled but helpful—sympathetic head tilt.
|
||||
|
||||
Props: Rustic wooden signpost with 3-4 arrows (no text needed), tiny folded map in wing.
|
||||
|
||||
Mood: "Oops, wrong turn" — gentle, understanding, slightly whimsical. Not distressed.
|
||||
|
||||
Expression: Soft eyes, slight smile, "I've been there" energy.
|
||||
|
||||
Colours: Purple Vi against neutral browns/greys of signpost. Subtle warm lighting.
|
||||
|
||||
Format: 800x600px, clean background suitable for web error page.
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 500 — Server Error
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi sitting at a tiny laptop, wearing small round reading glasses. One wing raised in "give me a moment" gesture. Looking focused but reassuring.
|
||||
|
||||
Props: Tiny silver laptop, round reading glasses, small floating tool icons (wrench, screwdriver, gear) around head, cup of tea cooling nearby (steam rising).
|
||||
|
||||
Mood: Competent, calm, "we've got this" energy. Fixing something, not panicking.
|
||||
|
||||
Expression: Concentrated but warm, slight determination.
|
||||
|
||||
Colours: Purple Vi, silver/grey tech elements, warm tea accent.
|
||||
|
||||
Format: 800x600px, light background.
|
||||
```
|
||||

|
||||
|
||||
### 503 — Service Unavailable
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi wearing tiny yellow hard hat, perched on construction scaffolding or ladder, holding rolled-up blueprints. Mid-project but cheerful.
|
||||
|
||||
Props: Small yellow hard hat, metal scaffolding/ladder, rolled blueprints in wing, perhaps some floating bricks or tools.
|
||||
|
||||
Mood: Productive, optimistic, "this'll be brilliant when it's done." Busy but not stressed.
|
||||
|
||||
Expression: Cheerful determination, pride in work.
|
||||
|
||||
Colours: Purple Vi with construction yellow/orange safety accents. Industrial but friendly.
|
||||
|
||||
Format: 800x600px.
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Priority 2: Dashboard Welcome
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi bursting energetically out of a violet/purple circle, wings spread wide in welcoming gesture. Small "Welcome" banner held in one wing or waving enthusiastically.
|
||||
|
||||
Mood: Genuinely pleased to see them. Warm, welcoming, restrained joy (British enthusiasm—no over-the-top excitement).
|
||||
|
||||
Expression: Bright eyes, warm smile, open and inviting.
|
||||
|
||||
Colours: Deep violet (#663399) circle background, Vi's purple slightly lighter to pop. Gold accents in eyes.
|
||||
|
||||
Format: 80x64px final (design at 400x320px and scale down). Slightly overflows the circle for personality.
|
||||
|
||||
Note: This replaces a generic rocket icon. Vi is better.
|
||||
```
|
||||

|
||||
---
|
||||
|
||||
## Priority 3: Empty States
|
||||
|
||||
### Empty Dashboard
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi standing in an empty field or on blank canvas, one wing gesturing invitingly to the space. Other wing holding either a paintbrush or planting a small seedling.
|
||||
|
||||
Props: Either paintbrush with purple paint, OR small green seedling being planted. Background is intentionally empty/minimal—the "blank slate."
|
||||
|
||||
Mood: "Look at all this potential" — inspiring without being pushy. Possibility, not pressure.
|
||||
|
||||
Expression: Warm, encouraging, creative.
|
||||
|
||||
Format: 600x400px.
|
||||
```
|
||||
|
||||

|
||||
|
||||
### No Scheduled Posts (SocialHost)
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi perched on edge of an empty calendar grid, pen in wing, doodling or writing ideas. Small thought bubbles floating nearby with post/content icons inside.
|
||||
|
||||
Props: Large calendar grid (empty squares), pen/quill in wing, 2-3 thought bubbles with abstract social post shapes.
|
||||
|
||||
Mood: Creative planning mode, friendly nudge. "Let's fill this up together."
|
||||
|
||||
Expression: Thoughtful, creative, slightly playful.
|
||||
|
||||
Format: 500x350px.
|
||||
```
|
||||
|
||||

|
||||
|
||||
### No Connected Accounts
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi standing between floating social media platform logos (abstract shapes, not actual logos), acting as conductor or connector. Wings gesturing to bring the elements together.
|
||||
|
||||
Props: 3-4 floating abstract social shapes (circles, squares representing platforms), connection lines or sparkles between them.
|
||||
|
||||
Mood: Facilitator energy. "Let me introduce you."
|
||||
|
||||
Expression: Helpful guide, welcoming.
|
||||
|
||||
Format: 600x400px.
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Loading States
|
||||
|
||||
### General Loading (Tea Time)
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Pose: Vi sitting contentedly, holding tiny teacup, reading a small book. Completely relaxed, patient.
|
||||
|
||||
Props: Tiny teacup with steam, small book open in other wing.
|
||||
|
||||
Mood: "Take your time, I've got my tea." Content to wait, no stress whatsoever.
|
||||
|
||||
Expression: Pleasant, calm, patient.
|
||||
|
||||
Animation note: Design 3 frames — page turning subtly every 2 seconds.
|
||||
|
||||
Format: 120x120px (design at 360x360px).
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Service-Specific Variants
|
||||
|
||||
### SocialHost Vi
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Context: Social media management theme.
|
||||
|
||||
Pose: Vi confidently juggling or managing multiple floating platform icons. In control, competent.
|
||||
|
||||
Props: Calendar element, scheduled posts "flying out," multiple abstract platform icons orbiting.
|
||||
|
||||
Mood: Organised chaos. "I've got this."
|
||||
|
||||
Colours: Purple base with hints of social platform colours (blues, pinks).
|
||||
|
||||
Format: 600x400px.
|
||||
```
|
||||
|
||||

|
||||
|
||||
### AnalyticsHost Vi
|
||||
|
||||
**Prompt:**
|
||||
```
|
||||
[Core character prompt]
|
||||
|
||||
Context: Data and analytics theme.
|
||||
|
||||
Pose: Vi wearing reading glasses, pointing at or examining a rising chart/graph. Satisfied, analytical.
|
||||
|
||||
Props: Chart showing upward trend, magnifying glass optional, "no cookies" badge visible somewhere.
|
||||
|
||||
Mood: Insightful, satisfied with what the data shows.
|
||||
|
||||
Colours: Purple with data visualisation blues and greens.
|
||||
|
||||
Format: 600x400px.
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Style Consistency Tips
|
||||
|
||||
### Do
|
||||
|
||||
- Keep proportions consistent (large head, compact body)
|
||||
- Maintain the messy feather tuft
|
||||
- Use the exact purple (#663399) as base
|
||||
- Keep expressions warm, never cold or aggressive
|
||||
- Ensure beak is always slightly open (approachable)
|
||||
- Include the golden eye highlights
|
||||
|
||||
### Don't
|
||||
|
||||
- Make Vi realistic or photorealistic
|
||||
- Use dark/gothic raven imagery
|
||||
- Add more than one sparkle/star effect
|
||||
- Make expressions overly excited (British restraint)
|
||||
- Forget the orange-gold beak colour
|
||||
- Use American-style enthusiasm in posing
|
||||
|
||||
---
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Output Requirements
|
||||
|
||||
For each final asset, deliver:
|
||||
|
||||
1. **Source**: AI generation at 4x final resolution
|
||||
2. **Refined**: Touch-up in Photoshop/Procreate if needed
|
||||
3. **Vectorised**: Trace to SVG in Illustrator/Figma for scalability
|
||||
4. **Exports**:
|
||||
- PNG @1x, @2x, @3x (retina)
|
||||
- WebP for web performance
|
||||
- SVG where appropriate
|
||||
- Dark mode variant if non-transparent background
|
||||
|
||||
### File Naming
|
||||
|
||||
```
|
||||
vi-[context]-[pose]-[size].png
|
||||
vi-error-404-puzzled-800x600.png
|
||||
vi-loading-tea-120x120.png
|
||||
vi-socialhost-juggling-600x400.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
| Asset | Size | Key Props | Mood |
|
||||
|-----------------|---------|-----------------------------------|---------------------|
|
||||
| 404 | 800x600 | Signpost, map | Puzzled but helpful |
|
||||
| 500 | 800x600 | Laptop, glasses, tools, tea | Focused, competent |
|
||||
| 503 | 800x600 | Hard hat, scaffolding, blueprints | Busy, optimistic |
|
||||
| Welcome | 80x64 | Wings spread | Warm greeting |
|
||||
| Empty dashboard | 600x400 | Paintbrush or seedling | Inspiring |
|
||||
| No posts | 500x350 | Calendar, pen | Creative |
|
||||
| Loading | 120x120 | Tea, book | Patient |
|
||||
|
||||
---
|
||||
|
||||
*Generate, iterate, refine. Vi should feel like someone you'd trust to help you—because she is.*
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/specs/brand/violet/images/master_vi.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
docs/specs/brand/violet/images/vi_404.png
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
docs/specs/brand/violet/images/vi_500.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
docs/specs/brand/violet/images/vi_503.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/specs/brand/violet/images/vi_analytics.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/specs/brand/violet/images/vi_dashboard.png
Normal file
|
After Width: | Height: | Size: 530 KiB |
BIN
docs/specs/brand/violet/images/vi_dashboard_empty.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/specs/brand/violet/images/vi_loading_states.png
Normal file
|
After Width: | Height: | Size: 506 KiB |
BIN
docs/specs/brand/violet/images/vi_no_connected_accounts.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/specs/brand/violet/images/vi_no_scheduled_posts.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/specs/brand/violet/images/vi_social_host.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
175
docs/specs/examples/INTEGRATION_EXAMPLE_ImageOptimizer.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Image Optimizer Integration Example
|
||||
|
||||
This file shows how to integrate the ImageOptimizer into your upload pipeline.
|
||||
|
||||
## Basic Usage in Livewire Components
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\BioLink;
|
||||
|
||||
use Core\Media\Image\ImageOptimizer;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class UploadBioImage extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $image;
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate([
|
||||
'image' => 'required|image|max:10240', // 10MB
|
||||
]);
|
||||
|
||||
$user = auth()->user();
|
||||
$workspace = $user->defaultHostWorkspace();
|
||||
|
||||
// Store the file first
|
||||
$path = $this->image->store('biolinks/images/' . $workspace->id, 'local');
|
||||
$absolutePath = Storage::path($path);
|
||||
|
||||
// Optimize the image
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
$result = $optimizer->optimize($absolutePath);
|
||||
|
||||
// Record optimization statistics
|
||||
if ($result->wasSuccessful()) {
|
||||
$optimizer->recordOptimization(
|
||||
$result,
|
||||
$workspace,
|
||||
$this->biolink, // or whatever model
|
||||
);
|
||||
}
|
||||
|
||||
// Continue with your logic...
|
||||
$this->biolink->update([
|
||||
'image_path' => $path,
|
||||
]);
|
||||
|
||||
$this->dispatch('notify', message: "Image uploaded and optimised ({$result->getSummary()})", type: 'success');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Optimizing Uploaded Files Before Storage
|
||||
|
||||
```php
|
||||
public function saveOptimized()
|
||||
{
|
||||
$this->validate([
|
||||
'image' => 'required|image|max:10240',
|
||||
]);
|
||||
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
|
||||
// Optimize the uploaded file directly
|
||||
$result = $optimizer->optimizeUploadedFile($this->image);
|
||||
|
||||
// Now store the optimized file
|
||||
$path = $this->image->store('biolinks/images', 'local');
|
||||
|
||||
// Record the optimization
|
||||
$optimizer->recordOptimization(
|
||||
$result,
|
||||
auth()->user()->defaultHostWorkspace()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Retrieving Statistics
|
||||
|
||||
```php
|
||||
use Core\Media\Image\ImageOptimizer;
|
||||
|
||||
// In a controller or Livewire component
|
||||
public function getOptimizationStats()
|
||||
{
|
||||
$workspace = auth()->user()->defaultHostWorkspace();
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
|
||||
$stats = $optimizer->getStats($workspace);
|
||||
|
||||
// Returns:
|
||||
// [
|
||||
// 'count' => 150,
|
||||
// 'total_original' => 45000000, // bytes
|
||||
// 'total_optimized' => 28000000,
|
||||
// 'total_saved' => 17000000,
|
||||
// 'average_percentage' => 37.8,
|
||||
// 'total_saved_human' => '16.2MB',
|
||||
// ]
|
||||
}
|
||||
```
|
||||
|
||||
## Admin Dashboard Widget
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use Core\Media\Image\ImageOptimizer;
|
||||
use Livewire\Component;
|
||||
|
||||
class ImageOptimizationStats extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
$stats = $optimizer->getStats(); // null = all workspaces
|
||||
|
||||
return view('admin.admin.image-optimization-stats', [
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to your `.env`:
|
||||
|
||||
```env
|
||||
IMAGE_OPTIMIZATION_ENABLED=true
|
||||
IMAGE_OPTIMIZATION_DRIVER=gd
|
||||
IMAGE_OPTIMIZATION_QUALITY=80
|
||||
IMAGE_OPTIMIZATION_PNG_COMPRESSION=6
|
||||
IMAGE_OPTIMIZATION_MIN_SIZE_KB=10
|
||||
IMAGE_OPTIMIZATION_MAX_SIZE_MB=10
|
||||
```
|
||||
|
||||
## Disabling for Specific Uploads
|
||||
|
||||
```php
|
||||
// Temporarily disable optimization
|
||||
config(['images.optimization.enabled' => false]);
|
||||
|
||||
$path = $this->file->store('uploads');
|
||||
|
||||
// Re-enable
|
||||
config(['images.optimization.enabled' => true]);
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
The ImageOptimizer should be integrated at these points:
|
||||
|
||||
1. **BioLink image uploads** - Avatar, background, block images
|
||||
2. **Static page images** - Embedded images in HTML content
|
||||
3. **Theme preview images** - Template and theme gallery
|
||||
4. **User profile images** - Avatar uploads
|
||||
5. **Social media uploads** - Before posting to social networks
|
||||
|
||||
## Notes
|
||||
|
||||
- Optimization happens **in-place** by default (replaces the original)
|
||||
- Files smaller than 10KB are skipped (configurable)
|
||||
- Files larger than 10MB are skipped (configurable)
|
||||
- Supports JPEG, PNG, WebP formats
|
||||
- Uses GD by default (Imagick support can be added)
|
||||
- Gracefully handles errors (returns no-op result instead of throwing)
|
||||
88
docs/specs/patterns/APPLICATION_SHELLS.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Application Shells (Tailwind+)
|
||||
|
||||
Consolidated list of all application shell patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 23 variants across 3 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/application-shells/
|
||||
|
||||
---
|
||||
|
||||
## Stacked Layouts (9 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/application-shells/stacked
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With lighter page header | Light header on light background |
|
||||
| 2 | With bottom border | Header with bottom border separator |
|
||||
| 3 | On subtle background | Gray/muted background variant |
|
||||
| 4 | Branded nav with compact lighter page header | Brand color nav + compact page header |
|
||||
| 5 | With overlap | Content overlaps header area |
|
||||
| 6 | Brand nav with overlap | Branded nav + overlap effect |
|
||||
| 7 | Branded nav with lighter page header | Full branded nav + light page header |
|
||||
| 8 | With compact lighter page header | Minimal height page header |
|
||||
| 9 | Two-row navigation with overlap | Dual nav rows + overlap |
|
||||
|
||||
---
|
||||
|
||||
## Sidebar Layouts (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/application-shells/sidebar
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple sidebar | Basic sidebar navigation |
|
||||
| 2 | Simple dark sidebar | Dark themed sidebar |
|
||||
| 3 | Sidebar with header | Sidebar + top header bar |
|
||||
| 4 | Dark sidebar with header | Dark sidebar + header |
|
||||
| 5 | With constrained content area | Max-width content container |
|
||||
| 6 | With off-white background | Subtle background color |
|
||||
| 7 | Simple brand sidebar | Brand colored sidebar |
|
||||
| 8 | Brand sidebar with header | Brand sidebar + header |
|
||||
|
||||
---
|
||||
|
||||
## Multi-Column Layouts (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/application-shells/multi-column
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Full-width three-column | Three columns, edge-to-edge |
|
||||
| 2 | Full-width secondary column on right | Main + secondary right column |
|
||||
| 3 | Constrained three column | Max-width three-column layout |
|
||||
| 4 | Constrained with sticky columns | Sticky side columns on scroll |
|
||||
| 5 | Full-width with narrow sidebar | Narrow left sidebar + main |
|
||||
| 6 | Full-width with narrow sidebar and header | Narrow sidebar + top header |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Structure
|
||||
- **Stacked**: Vertical layout, no sidebar (variants 1-9 stacked)
|
||||
- **Sidebar**: Persistent side navigation (variants 1-8 sidebar)
|
||||
- **Multi-column**: 2-3 column layouts (variants 1-6 multi-column)
|
||||
|
||||
### By Theme
|
||||
- **Light**: Standard light theme (most variants)
|
||||
- **Dark**: Dark themed components (simple dark sidebar, dark sidebar with header)
|
||||
- **Branded**: Custom brand colors (branded nav variants, brand sidebar variants)
|
||||
|
||||
### By Features
|
||||
- **With header**: Includes top navigation bar
|
||||
- **With overlap**: Content overlaps into header area
|
||||
- **Constrained**: Max-width container
|
||||
- **Full-width**: Edge-to-edge layout
|
||||
- **Sticky**: Fixed positioning on scroll
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Simple sidebar** - most common dashboard layout
|
||||
2. **Sidebar with header** - dashboard with top nav
|
||||
3. **Stacked with lighter page header** - simple app layout
|
||||
4. **Full-width with narrow sidebar** - compact navigation
|
||||
5. **Constrained three column** - complex dashboards
|
||||
83
docs/specs/patterns/DATA_DISPLAY.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Data Display Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all data display patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 19 variants across 3 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/data-display/
|
||||
|
||||
---
|
||||
|
||||
## Description Lists (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/data-display/description-lists
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Left-aligned | Labels left, values right, simple rows |
|
||||
| 2 | Left-aligned in card | Same layout wrapped in card container |
|
||||
| 3 | Left-aligned striped | Alternating row backgrounds |
|
||||
| 4 | Two-column | Data displayed in 2-column grid |
|
||||
| 5 | Left-aligned with inline actions | Edit/Update buttons per row |
|
||||
| 6 | Narrow with hidden labels | Compact invoice-style with icons |
|
||||
|
||||
---
|
||||
|
||||
## Stats (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/data-display/stats
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With trending | Stats with up/down trend indicators |
|
||||
| 2 | Simple | Basic stat cards, no trends |
|
||||
| 3 | Simple in cards | Stats in card containers |
|
||||
| 4 | With brand icon | Colored icons per stat |
|
||||
| 5 | With shared borders | Stats in single card with dividers |
|
||||
|
||||
---
|
||||
|
||||
## Calendars (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/data-display/calendars
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Small with meetings | Compact calendar + meeting list |
|
||||
| 2 | Month view | Full month grid with events |
|
||||
| 3 | Week view | 7-day view with time slots |
|
||||
| 4 | Day view | Single day with time slots + mini calendar |
|
||||
| 5 | Year view | 12-month overview grid |
|
||||
| 6 | Double | Two months side-by-side + upcoming events |
|
||||
| 7 | Borderless stacked | Calendar above, schedule below |
|
||||
| 8 | Borderless side-by-side | Calendar left, schedule right |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Complexity
|
||||
- **Simple**: Basic data display (description lists, simple stats)
|
||||
- **Interactive**: With actions/buttons (inline actions)
|
||||
- **Complex**: Multi-component (calendars with events)
|
||||
|
||||
### By Layout
|
||||
- **Single column**: Description lists, stats
|
||||
- **Multi-column**: Two-column description, calendar views
|
||||
- **Grid**: Year view, month view
|
||||
|
||||
### By Features
|
||||
- **With actions**: Inline edit/update buttons
|
||||
- **With trends**: Up/down indicators
|
||||
- **With icons**: Brand icons, status icons
|
||||
- **With events**: Calendar events, meetings
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Description list - Left-aligned** - most common data display
|
||||
2. **Stats - Simple in cards** - dashboard essential
|
||||
3. **Calendar - Month view** - scheduling features
|
||||
4. **Stats - With trending** - analytics dashboards
|
||||
5. **Description list - With inline actions** - editable data
|
||||
354
docs/specs/patterns/ECOMMERCE.md
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
# Ecommerce Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all ecommerce patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 129 variants across 21 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Product Overviews (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-overviews
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With image gallery and expandable details | Gallery + accordion |
|
||||
| 2 | With image grid | Multiple images grid |
|
||||
| 3 | With tabs | Tabbed details |
|
||||
| 4 | With tiered images | Stacked image display |
|
||||
| 5 | Split with image | Image + details split |
|
||||
|
||||
---
|
||||
|
||||
### Product Lists (11 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-lists
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic product grid |
|
||||
| 2 | With inline price | Price inline |
|
||||
| 3 | With CTA link | Add to cart link |
|
||||
| 4 | With color swatches and horizontal scrolling | Swatches + scroll |
|
||||
| 5 | Card with full details | Full product card |
|
||||
| 6 | With border grid | Bordered grid |
|
||||
| 7 | With tall images | Vertical image cards |
|
||||
| 8 | With tall images and CTA link | Tall + CTA |
|
||||
| 9 | With image overlay and add button | Overlay + button |
|
||||
| 10 | With inline price and CTA link | Price + CTA |
|
||||
| 11 | With supporting text | Description text |
|
||||
|
||||
---
|
||||
|
||||
### Category Previews (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/category-previews
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Three column | 3-column categories |
|
||||
| 2 | With image backgrounds | Image backgrounds |
|
||||
| 3 | With scrolling cards | Horizontal scroll |
|
||||
| 4 | With split images | Split image layout |
|
||||
| 5 | Three column with description | Categories + text |
|
||||
| 6 | With background image and detail overlay | Overlay details |
|
||||
|
||||
---
|
||||
|
||||
### Shopping Carts (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/shopping-carts
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Single column | Simple cart list |
|
||||
| 2 | Popover | Dropdown cart |
|
||||
| 3 | Slide-over | Drawer cart |
|
||||
| 4 | Two column with quantity dropdown | Split + quantity |
|
||||
| 5 | With extended summary | Detailed summary |
|
||||
| 6 | Modal | Modal cart view |
|
||||
|
||||
---
|
||||
|
||||
### Category Filters (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/category-filters
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With inline actions and expandable sidebar filters | Sidebar + inline |
|
||||
| 2 | With dropdown product sort and filters | Dropdown filters |
|
||||
| 3 | With expandable product filter panel | Expandable panel |
|
||||
| 4 | With centered text and dropdown product filters | Centered + dropdown |
|
||||
| 5 | Sidebar filters | Sidebar only |
|
||||
|
||||
---
|
||||
|
||||
### Product Quickviews (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-quickviews
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With color selector and description | Color + description |
|
||||
| 2 | With color and size selector | Color + size |
|
||||
| 3 | With large size selector | Large size grid |
|
||||
| 4 | With color selector, size selector, and details link | Full options |
|
||||
|
||||
---
|
||||
|
||||
### Product Features (9 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-features
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With header, image, and descriptions | Full feature layout |
|
||||
| 2 | With alternating sections | Alternating blocks |
|
||||
| 3 | With tiered images | Stacked images |
|
||||
| 4 | With tabs | Tabbed features |
|
||||
| 5 | With square images | Square image grid |
|
||||
| 6 | With image grid | Image grid layout |
|
||||
| 7 | With fading image | Fade effect image |
|
||||
| 8 | With wide images | Wide image display |
|
||||
| 9 | With split image | Split layout |
|
||||
|
||||
---
|
||||
|
||||
### Store Navigation (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/store-navigation
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With featured categories | Featured + nav |
|
||||
| 2 | With double column and persistent mobile nav | Double column |
|
||||
| 3 | With simple menu and promo | Menu + promo |
|
||||
| 4 | With image grid | Image navigation |
|
||||
| 5 | With centered logo and featured categories | Centered logo |
|
||||
|
||||
---
|
||||
|
||||
### Promo Sections (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/promo-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Full-width with background image | Full image promo |
|
||||
| 2 | Full-width with background image and large content | Large content |
|
||||
| 3 | With image tiles | Multiple images |
|
||||
| 4 | With fading background image and testimonials | Fade + quotes |
|
||||
| 5 | With overlapping image tiles | Layered images |
|
||||
| 6 | Full-width with overlapping image tiles | Full + layered |
|
||||
| 7 | With offers and split image | Offers + image |
|
||||
| 8 | Full-width with overlapping image tiles and perks | Full + perks |
|
||||
|
||||
---
|
||||
|
||||
### Checkout Forms (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/checkout-forms
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Single step with order summary | Single page |
|
||||
| 2 | Multi-step | Stepped checkout |
|
||||
| 3 | With mobile order summary overlay | Mobile summary |
|
||||
| 4 | With order summary sidebar | Sidebar summary |
|
||||
| 5 | Split with order summary | Split layout |
|
||||
|
||||
---
|
||||
|
||||
### Reviews (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/reviews
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With summary chart | Rating chart |
|
||||
| 2 | Multi-column | Multiple columns |
|
||||
| 3 | Simple with avatars | Avatar reviews |
|
||||
| 4 | With split summary | Split layout |
|
||||
|
||||
---
|
||||
|
||||
### Order Summaries (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/order-summaries
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With progress bars | Progress tracking |
|
||||
| 2 | With large images and progress bars | Large images |
|
||||
| 3 | With split image | Split layout |
|
||||
| 4 | Simple with full order details | Full details |
|
||||
|
||||
---
|
||||
|
||||
### Order History (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/order-history
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With invoice panels | Invoice cards |
|
||||
| 2 | With invoice list | Invoice list |
|
||||
| 3 | With invoice tables | Table layout |
|
||||
| 4 | Simple | Basic history |
|
||||
|
||||
---
|
||||
|
||||
### Incentives (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/incentives
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | 3-column with icons | Icon highlights |
|
||||
| 2 | 3-column with illustrations | Illustrations |
|
||||
| 3 | 3-column with icons and supporting text | Icons + text |
|
||||
| 4 | 2x2 grid with illustrations | 2x2 grid |
|
||||
| 5 | 4-column with illustrations | 4-column |
|
||||
| 6 | 3-column with illustrations and header | With header |
|
||||
| 7 | 3-column with illustrations and centered text | Centered |
|
||||
| 8 | 3-column with illustrations and split header | Split header |
|
||||
|
||||
---
|
||||
|
||||
## Page Examples
|
||||
|
||||
### Storefront Pages (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/storefront-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With image tiles and feature sections | Tiles + features |
|
||||
| 2 | With overlapping images and perks | Layered + perks |
|
||||
| 3 | With offers and testimonials | Offers + quotes |
|
||||
| 4 | Dark with image tiles | Dark theme |
|
||||
|
||||
---
|
||||
|
||||
### Product Pages (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/product-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With image gallery | Gallery layout |
|
||||
| 2 | With image grid | Grid layout |
|
||||
| 3 | With tabs | Tabbed details |
|
||||
| 4 | With tiered images | Stacked images |
|
||||
| 5 | With related products | Related section |
|
||||
|
||||
---
|
||||
|
||||
### Category Pages (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/category-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With image header and detail product grid | Header + grid |
|
||||
| 2 | With large images and filters sidebar | Large images |
|
||||
| 3 | With product grid and pagination | Grid + pagination |
|
||||
| 4 | With text header and simple product grid | Text header |
|
||||
| 5 | With text header and image product grid | Mixed layout |
|
||||
|
||||
---
|
||||
|
||||
### Shopping Cart Pages (3 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/shopping-cart-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With two column summary | Two-column |
|
||||
| 2 | With extended summary | Full summary |
|
||||
| 3 | Single column | Simple layout |
|
||||
|
||||
---
|
||||
|
||||
### Checkout Pages (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/checkout-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Single step | One-page checkout |
|
||||
| 2 | Multi-step | Stepped process |
|
||||
| 3 | With mobile cart modal | Mobile modal |
|
||||
| 4 | With order summary sidebar | Sidebar summary |
|
||||
| 5 | Split | Split layout |
|
||||
|
||||
---
|
||||
|
||||
### Order Detail Pages (3 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/order-detail-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With progress bar | Progress tracking |
|
||||
| 2 | With large images | Large product images |
|
||||
| 3 | Simple invoice | Invoice style |
|
||||
|
||||
---
|
||||
|
||||
### Order History Pages (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/order-history-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With invoice panels | Panel cards |
|
||||
| 2 | With vertical timeline | Timeline view |
|
||||
| 3 | With invoice list | List view |
|
||||
| 4 | With invoice tables | Table view |
|
||||
| 5 | Simple list | Basic list |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Flow Stage
|
||||
- **Discovery**: Category previews, product lists, store navigation
|
||||
- **Evaluation**: Product overviews, quickviews, features, reviews
|
||||
- **Purchase**: Shopping carts, checkout forms, order summaries
|
||||
- **Post-purchase**: Order history, order details
|
||||
|
||||
### By Layout
|
||||
- **Grid**: Product lists, category grids
|
||||
- **Split**: Two-column layouts
|
||||
- **Modal/Overlay**: Quickviews, cart popovers
|
||||
- **Single column**: Simple flows
|
||||
|
||||
### By Features
|
||||
- **With images**: Galleries, image grids, tiered images
|
||||
- **With filters**: Category filters, search
|
||||
- **With progress**: Checkout steps, order tracking
|
||||
- **With pricing**: Product cards, summaries
|
||||
|
||||
### By Complexity
|
||||
- **Simple**: Basic displays, minimal options
|
||||
- **Rich**: Full galleries, multiple selectors
|
||||
- **Multi-step**: Checkout flows
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Product Lists - Simple** - product grid essential
|
||||
2. **Shopping Carts - Single column** - cart functionality
|
||||
3. **Product Overviews - With image gallery** - product pages
|
||||
4. **Checkout Forms - Single step** - purchase flow
|
||||
5. **Category Filters - Sidebar filters** - filtering
|
||||
6. **Order History - With invoice panels** - account pages
|
||||
7. **Category Previews - Three column** - homepage sections
|
||||
8. **Promo Sections - Full-width with background** - marketing
|
||||
132
docs/specs/patterns/ELEMENTS.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Elements Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all element patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 45 variants across 5 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/elements/
|
||||
|
||||
---
|
||||
|
||||
## Avatars (11 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/elements/avatars
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Circular avatars | Round avatar images, multiple sizes |
|
||||
| 2 | Rounded avatars | Rounded square avatars |
|
||||
| 3 | Circular avatars with top notification | Status dot top-right |
|
||||
| 4 | Rounded avatars with top notification | Rounded + top status |
|
||||
| 5 | Circular avatars with bottom notification | Status dot bottom-right |
|
||||
| 6 | Rounded avatars with bottom notification | Rounded + bottom status |
|
||||
| 7 | Circular avatars with placeholder icon | User icon fallback |
|
||||
| 8 | Circular avatars with placeholder initials | Initials fallback |
|
||||
| 9 | Avatar group stacked bottom to top | Overlapping group, last on top |
|
||||
| 10 | Avatar group stacked top to bottom | Overlapping group, first on top |
|
||||
| 11 | With text | Avatar + name/description |
|
||||
|
||||
---
|
||||
|
||||
## Badges (16 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/elements/badges
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With border | Outlined badges, multiple colors |
|
||||
| 2 | With border and dot | Border + status dot |
|
||||
| 3 | Pill with border | Rounded pill shape + border |
|
||||
| 4 | Pill with border and dot | Pill + border + dot |
|
||||
| 5 | With border and remove button | Dismissible badge |
|
||||
| 6 | Flat | Solid background, no border |
|
||||
| 7 | Flat pill | Solid pill shape |
|
||||
| 8 | Flat with dot | Solid + status dot |
|
||||
| 9 | Flat pill with dot | Solid pill + dot |
|
||||
| 10 | Flat with remove button | Solid dismissible |
|
||||
| 11 | Small with border | Compact bordered |
|
||||
| 12 | Small flat | Compact solid |
|
||||
| 13 | Small pill with border | Compact pill bordered |
|
||||
| 14 | Small flat pill | Compact solid pill |
|
||||
| 15 | Small flat with dot | Compact solid + dot |
|
||||
| 16 | Small flat pill with dot | Compact solid pill + dot |
|
||||
|
||||
---
|
||||
|
||||
## Dropdowns (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/elements/dropdowns
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic dropdown menu |
|
||||
| 2 | With dividers | Grouped menu items |
|
||||
| 3 | With icons | Icons beside menu items |
|
||||
| 4 | With minimal menu icon | Three-dot trigger |
|
||||
| 5 | With simple header | Header text above items |
|
||||
|
||||
---
|
||||
|
||||
## Buttons (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/elements/buttons
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Primary buttons | Solid primary color, multiple sizes |
|
||||
| 2 | Secondary buttons | Outlined/subtle style |
|
||||
| 3 | Soft buttons | Light background tint |
|
||||
| 4 | Buttons with leading icon | Icon before text |
|
||||
| 5 | Buttons with trailing icon | Icon after text |
|
||||
| 6 | Rounded primary buttons | Pill-shaped primary |
|
||||
| 7 | Rounded secondary buttons | Pill-shaped secondary |
|
||||
| 8 | Circular buttons | Icon-only round buttons |
|
||||
|
||||
---
|
||||
|
||||
## Button Groups (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/elements/button-groups
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Basic | Connected button group |
|
||||
| 2 | Icon only | Icon buttons grouped |
|
||||
| 3 | With stat | Button + count badge |
|
||||
| 4 | With dropdown | Button + dropdown trigger |
|
||||
| 5 | With checkbox and dropdown | Select + dropdown combo |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Type
|
||||
- **Display**: Avatars, badges (non-interactive display)
|
||||
- **Interactive**: Buttons, dropdowns, button groups
|
||||
|
||||
### By Size
|
||||
- **Standard**: Regular size elements
|
||||
- **Small/Compact**: Reduced size variants
|
||||
- **Multiple sizes**: Size scale (xs, sm, md, lg, xl)
|
||||
|
||||
### By Style
|
||||
- **Solid/Flat**: Filled background
|
||||
- **Outlined/Border**: Border with transparent fill
|
||||
- **Soft**: Light tinted background
|
||||
- **Pill/Rounded**: Full border-radius
|
||||
|
||||
### By Features
|
||||
- **With icons**: Leading/trailing icons
|
||||
- **With status**: Notification dots, status indicators
|
||||
- **With actions**: Remove buttons, dropdowns
|
||||
- **Grouped**: Stacked avatars, button groups
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Primary buttons** - essential for all UIs
|
||||
2. **Badges - Flat** - status indicators
|
||||
3. **Avatars - Circular** - user display
|
||||
4. **Dropdowns - Simple** - action menus
|
||||
5. **Button groups - Basic** - toolbar patterns
|
||||
66
docs/specs/patterns/FEEDBACK.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Feedback Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all feedback patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 12 variants across 2 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/feedback/
|
||||
|
||||
---
|
||||
|
||||
## Alerts (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/feedback/alerts
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With description | Icon + title + description text |
|
||||
| 2 | With list | Alert with bullet point list |
|
||||
| 3 | With actions | Primary/secondary action buttons |
|
||||
| 4 | With link on right | Inline link action |
|
||||
| 5 | With accent border | Left border accent color |
|
||||
| 6 | With dismiss button | X button to close |
|
||||
|
||||
---
|
||||
|
||||
## Empty States (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/feedback/empty-states
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Icon + message + CTA button |
|
||||
| 2 | With dashed border | Dashed border container |
|
||||
| 3 | With starting points | Multiple getting-started options |
|
||||
| 4 | With recommendations | Suggested items/actions |
|
||||
| 5 | With templates | Template selection grid |
|
||||
| 6 | With recommendations grid | Grid of recommendation cards |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Severity (Alerts)
|
||||
- **Info**: Blue - informational messages
|
||||
- **Success**: Green - positive confirmations
|
||||
- **Warning**: Yellow - caution messages
|
||||
- **Error**: Red - error states
|
||||
|
||||
### By Complexity (Empty States)
|
||||
- **Simple**: Just message + action
|
||||
- **Rich**: With suggestions, templates, or guides
|
||||
|
||||
### By Features
|
||||
- **Dismissible**: Can be closed
|
||||
- **Actionable**: Has buttons/links
|
||||
- **Informational**: Display only
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Alerts - With description** - most common alert pattern
|
||||
2. **Alerts - With dismiss button** - toast-style notifications
|
||||
3. **Empty States - Simple** - basic placeholder
|
||||
4. **Alerts - With actions** - confirmation dialogs
|
||||
5. **Empty States - With recommendations** - onboarding
|
||||
67
docs/specs/patterns/FOOTERS.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Footer Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all footer patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 7 variants**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/footers
|
||||
|
||||
---
|
||||
|
||||
## Variants
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | 4-column with company mission | Logo, tagline, social icons top; 4 link columns; copyright |
|
||||
| 2 | 4-column with call-to-action | CTA banner above footer (headline + button) |
|
||||
| 3 | 4-column simple | Compact - logo, 4 columns, copyright + socials bottom |
|
||||
| 4 | 4-column with newsletter | Newsletter signup inline with link columns |
|
||||
| 5 | 4-column with newsletter below | Newsletter in separate row at bottom |
|
||||
| 6 | Simple centered | Single row of centered links + socials + copyright |
|
||||
| 7 | Simple with social links | Minimal - just copyright + social icons |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Complexity
|
||||
- **Simple**: 1-2 rows, minimal content (variants 6, 7)
|
||||
- **Standard**: 4-column layout (variants 1, 3)
|
||||
- **Extended**: Additional sections like CTA or newsletter (variants 2, 4, 5)
|
||||
|
||||
### By Features
|
||||
- **With newsletter**: Email signup form (variants 4, 5)
|
||||
- **With CTA**: Call-to-action section (variant 2)
|
||||
- **With mission**: Company tagline/description (variant 1)
|
||||
- **With social links**: Social media icons (all variants)
|
||||
|
||||
### Layout Patterns
|
||||
- **4-column grid**: Solutions, Support, Company, Legal columns
|
||||
- **Centered**: Single centered row
|
||||
- **Stacked sections**: Multiple distinct rows (CTA + links, links + newsletter)
|
||||
|
||||
---
|
||||
|
||||
## Common Elements
|
||||
|
||||
All footers typically include:
|
||||
- Copyright notice
|
||||
- Social media icons (Facebook, Instagram, X/Twitter, GitHub, YouTube)
|
||||
|
||||
Most footers include:
|
||||
- Logo
|
||||
- Link columns (Solutions, Support, Company, Legal)
|
||||
|
||||
Some footers include:
|
||||
- Company mission/tagline
|
||||
- Newsletter signup
|
||||
- CTA section
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Simple with social links** - minimal baseline
|
||||
2. **4-column simple** - standard marketing footer
|
||||
3. **4-column with newsletter** - lead capture
|
||||
4. **Simple centered** - app/dashboard context
|
||||
203
docs/specs/patterns/FORMS.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# Forms Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all form patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 74 variants across 10 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/
|
||||
|
||||
---
|
||||
|
||||
## Form Layouts (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/form-layouts
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Stacked | Single column, labels above inputs |
|
||||
| 2 | Two-column | Split layout for larger forms |
|
||||
| 3 | Two-column with cards | Sections in card containers |
|
||||
| 4 | Labels on left | Horizontal label/input layout |
|
||||
|
||||
---
|
||||
|
||||
## Input Groups (21 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/input-groups
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Input with label | Basic labeled input |
|
||||
| 2 | Input with label and help text | Label + helper text |
|
||||
| 3 | Input with validation error | Error state styling |
|
||||
| 4 | Input with disabled state | Disabled input |
|
||||
| 5 | Input with hidden label | Screen reader only label |
|
||||
| 6 | Input with corner hint | Optional/required hint |
|
||||
| 7 | Input with leading icon | Icon before input |
|
||||
| 8 | Input with trailing icon | Icon after input |
|
||||
| 9 | Input with add-on | Prefix/suffix text |
|
||||
| 10 | Input with inline add-on | Integrated add-on |
|
||||
| 11 | Input with inline leading and trailing add-ons | Both add-ons |
|
||||
| 12 | Input with inline leading dropdown | Dropdown prefix |
|
||||
| 13 | Input with inline leading add-on and trailing dropdown | Mixed add-ons |
|
||||
| 14 | Input with leading icon and trailing button | Icon + button combo |
|
||||
| 15 | Inputs with shared borders | Grouped inputs |
|
||||
| 16 | Input with inset label | Label inside input |
|
||||
| 17 | Inputs with inset labels and shared borders | Grouped inset inputs |
|
||||
| 18 | Input with overlapping label | Floating label effect |
|
||||
| 19 | Input with pill shape | Rounded pill input |
|
||||
| 20 | Input with gray background and bottom border | Underline style |
|
||||
| 21 | Input with keyboard shortcut | Shows keyboard hint |
|
||||
|
||||
---
|
||||
|
||||
## Select Menus (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/select-menus
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple native | Browser default select |
|
||||
| 2 | Simple custom | Custom styled dropdown |
|
||||
| 3 | Custom with check on left | Check mark alignment |
|
||||
| 4 | Custom with status indicator | Status dots |
|
||||
| 5 | Custom with avatar | User/item avatars |
|
||||
| 6 | With secondary text | Additional info text |
|
||||
| 7 | Branded with supported text | Brand styling |
|
||||
|
||||
---
|
||||
|
||||
## Sign-in and Registration (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/sign-in-forms
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic centered form |
|
||||
| 2 | Simple no labels | Placeholder-only inputs |
|
||||
| 3 | Split screen | Image + form layout |
|
||||
| 4 | Simple card | Form in card container |
|
||||
|
||||
---
|
||||
|
||||
## Textareas (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/textareas
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic textarea |
|
||||
| 2 | With avatar and actions | Comment-style with user avatar |
|
||||
| 3 | With underline and actions | Minimal underline style |
|
||||
| 4 | With title and pill actions | Rich text editor style |
|
||||
| 5 | With preview button | Markdown preview toggle |
|
||||
|
||||
---
|
||||
|
||||
## Radio Groups (12 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/radio-groups
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple list | Basic vertical list |
|
||||
| 2 | Simple inline list | Horizontal layout |
|
||||
| 3 | List with description | Description per option |
|
||||
| 4 | List with inline description | Compact descriptions |
|
||||
| 5 | List with radio on right | Right-aligned radios |
|
||||
| 6 | Simple list with radio on right | Right-aligned simple |
|
||||
| 7 | Simple table | Table layout |
|
||||
| 8 | List with descriptions in panel | Card-style options |
|
||||
| 9 | Color picker | Color swatch selection |
|
||||
| 10 | Cards | Option cards |
|
||||
| 11 | Small cards | Compact option cards |
|
||||
| 12 | Stacked cards | Full-width cards |
|
||||
|
||||
---
|
||||
|
||||
## Checkboxes (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/checkboxes
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | List with description | Description per checkbox |
|
||||
| 2 | List with inline description | Compact layout |
|
||||
| 3 | List with checkbox on right | Right-aligned checkboxes |
|
||||
| 4 | Simple list with heading | Grouped with heading |
|
||||
|
||||
---
|
||||
|
||||
## Toggles (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/toggles
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple toggle | Basic switch |
|
||||
| 2 | Short toggle | Compact switch |
|
||||
| 3 | Toggle with icon | Icon inside toggle |
|
||||
| 4 | With left label and description | Label + description |
|
||||
| 5 | With right label | Right-aligned label |
|
||||
|
||||
---
|
||||
|
||||
## Action Panels (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/action-panels
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic action panel |
|
||||
| 2 | With link | Link action |
|
||||
| 3 | With button on right | Right-aligned button |
|
||||
| 4 | With button at top right | Top-right button |
|
||||
| 5 | With toggle | Toggle action |
|
||||
| 6 | With input | Input field action |
|
||||
| 7 | Simple well | Well background |
|
||||
| 8 | With well | Complex well layout |
|
||||
|
||||
---
|
||||
|
||||
## Comboboxes (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/forms/comboboxes
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic autocomplete |
|
||||
| 2 | With status indicator | Status dots in options |
|
||||
| 3 | With image | Avatars in options |
|
||||
| 4 | With secondary text | Additional info text |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Input Type
|
||||
- **Text inputs**: Input groups, textareas, comboboxes
|
||||
- **Selection**: Select menus, radio groups, checkboxes
|
||||
- **Boolean**: Toggles
|
||||
- **Composite**: Action panels, sign-in forms
|
||||
|
||||
### By Layout
|
||||
- **Stacked**: Vertical arrangement
|
||||
- **Inline/Horizontal**: Side-by-side layout
|
||||
- **Grid**: Multi-column forms
|
||||
- **Cards**: Card-wrapped inputs
|
||||
|
||||
### By State
|
||||
- **Default**: Normal state
|
||||
- **Error**: Validation errors
|
||||
- **Disabled**: Non-interactive
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Input with label** - essential for all forms
|
||||
2. **Simple toggle** - boolean inputs
|
||||
3. **Simple custom select** - dropdowns
|
||||
4. **Simple radio list** - single selection
|
||||
5. **Form layout - Stacked** - basic form structure
|
||||
6. **Sign-in - Simple** - authentication pages
|
||||
100
docs/specs/patterns/HEADERS.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Header Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all header/navbar patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 27 variants across 3 sources**
|
||||
|
||||
---
|
||||
|
||||
## Marketing Headers (11 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/elements/headers
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With stacked flyout menu | Mega menu with stacked layout |
|
||||
| 2 | Constrained | Max-width container |
|
||||
| 3 | On brand background | Dark/colored background |
|
||||
| 4 | With full width flyout menu | Full-width mega menu dropdown |
|
||||
| 5 | Full width | Edge-to-edge header |
|
||||
| 6 | With call-to-action | CTA button in header |
|
||||
| 7 | With multiple flyout menus | Multiple mega menu dropdowns |
|
||||
| 8 | With icons in mobile menu | Icons next to mobile nav items |
|
||||
| 9 | With left-aligned nav | Logo left, nav items immediately after |
|
||||
| 10 | With right-aligned nav | Logo left, nav pushed right |
|
||||
| 11 | With centered logo | Logo center, nav split left/right |
|
||||
|
||||
---
|
||||
|
||||
## Application UI Navbars (11 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/navbars
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple dark with menu button on left | Dark theme, hamburger menu |
|
||||
| 2 | Dark with quick action | Dark + primary action button |
|
||||
| 3 | Simple dark | Minimal dark navbar |
|
||||
| 4 | Simple with menu button on left | Light theme, hamburger menu |
|
||||
| 5 | Simple | Minimal light navbar |
|
||||
| 6 | With quick action | Light + primary action button |
|
||||
| 7 | Dark with search | Dark + search input |
|
||||
| 8 | With search | Light + search input |
|
||||
| 9 | Dark with centered search and secondary links | Two-row: search top, nav below (dark) |
|
||||
| 10 | With centered search and secondary links | Two-row: search top, nav below (light) |
|
||||
| 11 | With search in column layout | Compact with wide search bar |
|
||||
|
||||
---
|
||||
|
||||
## Ecommerce Store Navigation (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/ecommerce/components/store-navigation
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With image grid | Mega menu with product images |
|
||||
| 2 | With simple menu and promo | Promo banner + mega menu |
|
||||
| 3 | With featured categories | Colored promo banner + featured images in dropdown |
|
||||
| 4 | With centered logo and featured categories | Centered logo, nav split, featured images |
|
||||
| 5 | With double column and persistent mobile nav | Double column mega menu |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
When converting, organise by feature rather than Tailwind's categories:
|
||||
|
||||
### By Layout
|
||||
- **Left-aligned**: Logo left, nav follows
|
||||
- **Right-aligned**: Logo left, nav pushed right
|
||||
- **Centered logo**: Logo center, nav split
|
||||
- **Full width**: Edge-to-edge
|
||||
- **Constrained**: Max-width container
|
||||
|
||||
### By Features
|
||||
- **Simple**: Just nav links
|
||||
- **With search**: Includes search input
|
||||
- **With CTA**: Primary action button
|
||||
- **With mega menu**: Dropdown with columns/images
|
||||
- **With promo**: Banner/announcement bar
|
||||
- **Two-row**: Stacked layout (search + nav, or promo + nav)
|
||||
|
||||
### By Theme
|
||||
- **Light**: White/light background
|
||||
- **Dark**: Dark background
|
||||
- **On brand**: Custom brand color background
|
||||
|
||||
### By Context
|
||||
- **Marketing**: Public-facing, conversion-focused
|
||||
- **App/Dashboard**: Authenticated user context
|
||||
- **Ecommerce**: Shopping-focused with cart
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Simple** - baseline for all projects
|
||||
2. **With search** - dashboard essential
|
||||
3. **With CTA** - marketing essential
|
||||
4. **With mega menu** - complex sites
|
||||
5. **Ecommerce variants** - shop features
|
||||
79
docs/specs/patterns/HEROES.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Hero Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all hero section patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 12 variants**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/heroes
|
||||
|
||||
---
|
||||
|
||||
## Variants
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple centered | Announcement badge, headline, description, dual CTAs, centered layout |
|
||||
| 2 | Split with screenshot | Text left, app screenshot right, two-column layout |
|
||||
| 3 | Split with bordered screenshot | Text left, bordered app screenshot right |
|
||||
| 4 | Split with code example | Text left, code snippet/terminal right |
|
||||
| 5 | Simple centered with background image | Full background photo with overlay, centered text |
|
||||
| 6 | With bordered app screenshot | Centered hero text, bordered screenshot below |
|
||||
| 7 | With app screenshot | Centered hero text, screenshot below (no border) |
|
||||
| 8 | With phone mockup | Text left, mobile device mockup right |
|
||||
| 9 | Split with image | Text left, photo/image right |
|
||||
| 10 | With angled image on right | Diagonal/angled photo positioning |
|
||||
| 11 | With image tiles | Text left, multiple image grid on right |
|
||||
| 12 | With offset image | Asymmetric/offset photo positioning |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Layout
|
||||
- **Centered**: Single column, centered content (variants 1, 5, 6, 7)
|
||||
- **Split/Two-column**: Content left, media right (variants 2, 3, 4, 8, 9, 10, 11, 12)
|
||||
|
||||
### By Media Type
|
||||
- **App screenshots**: Browser/app interface mockups (variants 2, 3, 6, 7)
|
||||
- **Phone mockups**: Mobile device frames (variant 8)
|
||||
- **Photos/Images**: Real photography (variants 5, 9, 10, 11, 12)
|
||||
- **Code examples**: Terminal/code snippets (variant 4)
|
||||
- **No media**: Text-only with CTAs (variant 1)
|
||||
|
||||
### By Background
|
||||
- **Plain/gradient**: Standard background (most variants)
|
||||
- **Full image background**: Photo with overlay (variant 5)
|
||||
|
||||
### By Features
|
||||
- **Announcement badge**: Top badge/pill (variant 1)
|
||||
- **Dual CTAs**: Primary + secondary buttons (most variants)
|
||||
- **Image grids**: Multiple images composed (variant 11)
|
||||
- **Angled/offset media**: Creative positioning (variants 10, 12)
|
||||
|
||||
---
|
||||
|
||||
## Common Elements
|
||||
|
||||
All heroes typically include:
|
||||
- Headline (large, bold)
|
||||
- Description/subheadline
|
||||
- Primary CTA button
|
||||
- Secondary CTA (link or ghost button)
|
||||
|
||||
Most heroes include:
|
||||
- Visual element (screenshot, mockup, image)
|
||||
|
||||
Some heroes include:
|
||||
- Announcement badge/pill
|
||||
- Background image/gradient
|
||||
- Multiple images/grid layout
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Simple centered** - baseline for all marketing sites
|
||||
2. **Split with screenshot** - SaaS/app marketing essential
|
||||
3. **With app screenshot** - product showcase
|
||||
4. **Simple centered with background image** - visual impact
|
||||
5. **Split with image** - general marketing
|
||||
139
docs/specs/patterns/INDEX.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Tailwind+ Pattern Library Index
|
||||
|
||||
Complete catalogue of Tailwind+ UI patterns for conversion to Flux UI.
|
||||
|
||||
**Grand Total: 672 variants across 93 pages**
|
||||
|
||||
---
|
||||
|
||||
## Application UI (364 variants, 49 pages)
|
||||
|
||||
| Document | Categories | Variants |
|
||||
|----------|------------|----------|
|
||||
| [APPLICATION_SHELLS.md](APPLICATION_SHELLS.md) | Stacked Layouts, Sidebar Layouts, Multi-column Layouts | 23 |
|
||||
| [DATA_DISPLAY.md](DATA_DISPLAY.md) | Description Lists, Stats, Calendars | 19 |
|
||||
| [ELEMENTS.md](ELEMENTS.md) | Avatars, Badges, Dropdowns, Buttons, Button Groups | 45 |
|
||||
| [FEEDBACK.md](FEEDBACK.md) | Alerts, Empty States | 12 |
|
||||
| [FORMS.md](FORMS.md) | Form Layouts, Input Groups, Select Menus, Sign-in, Textareas, Radio Groups, Checkboxes, Toggles, Action Panels, Comboboxes | 74 |
|
||||
| [LAYOUT.md](LAYOUT.md) | Containers, Cards, List Containers, Media Objects, Dividers | 38 |
|
||||
| [NAVIGATION.md](NAVIGATION.md) | Navbars, Pagination, Tabs, Vertical Navigation, Sidebar Navigation, Breadcrumbs, Progress Bars, Command Palettes | 54 |
|
||||
| [OVERLAYS.md](OVERLAYS.md) | Modal Dialogs, Drawers, Notifications | 24 |
|
||||
| [PAGE_EXAMPLES.md](PAGE_EXAMPLES.md) | Home Screens, Detail Screens, Settings Screens | 6 |
|
||||
| **Lists & Tables** | Description Lists, Tables, Grid Lists, Feeds, Stacked Lists | 69 |
|
||||
|
||||
---
|
||||
|
||||
## Marketing (179 variants, 23 pages)
|
||||
|
||||
| Document | Categories | Variants |
|
||||
|----------|------------|----------|
|
||||
| [MARKETING.md](MARKETING.md) | Heroes, Features, CTAs, Bento Grids, Pricing, Headers, Newsletter, Stats, Testimonials, Blog, Contact, Team, Content, Logo Clouds, FAQs, Footers, Headers, Flyouts, Banners, 404 Pages, Landing Pages, Pricing Pages, About Pages | 179 |
|
||||
|
||||
---
|
||||
|
||||
## Ecommerce (129 variants, 21 pages)
|
||||
|
||||
| Document | Categories | Variants |
|
||||
|----------|------------|----------|
|
||||
| [ECOMMERCE.md](ECOMMERCE.md) | Product Overviews, Product Lists, Category Previews, Shopping Carts, Category Filters, Quickviews, Product Features, Store Navigation, Promo Sections, Checkout Forms, Reviews, Order Summaries, Order History, Incentives, Storefront Pages, Product Pages, Category Pages, Cart Pages, Checkout Pages, Order Detail Pages, Order History Pages | 129 |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference by Category
|
||||
|
||||
### Structural
|
||||
- Containers (5) - LAYOUT.md
|
||||
- Application Shells (23) - APPLICATION_SHELLS.md
|
||||
- Page Examples (6) - PAGE_EXAMPLES.md
|
||||
|
||||
### Navigation
|
||||
- Navbars (11) - NAVIGATION.md
|
||||
- Tabs (9) - NAVIGATION.md
|
||||
- Sidebar Navigation (5) - NAVIGATION.md
|
||||
- Vertical Navigation (6) - NAVIGATION.md
|
||||
- Breadcrumbs (4) - NAVIGATION.md
|
||||
- Command Palettes (8) - NAVIGATION.md
|
||||
- Pagination (3) - NAVIGATION.md
|
||||
|
||||
### Content Display
|
||||
- Cards (10) - LAYOUT.md
|
||||
- List Containers (7) - LAYOUT.md
|
||||
- Media Objects (8) - LAYOUT.md
|
||||
- Tables - DATA_DISPLAY.md
|
||||
- Description Lists - DATA_DISPLAY.md
|
||||
- Stats (various) - DATA_DISPLAY.md, MARKETING.md
|
||||
|
||||
### Forms & Input
|
||||
- Input Groups (21) - FORMS.md
|
||||
- Select Menus (7) - FORMS.md
|
||||
- Radio Groups (12) - FORMS.md
|
||||
- Checkboxes (4) - FORMS.md
|
||||
- Toggles (5) - FORMS.md
|
||||
- Comboboxes (4) - FORMS.md
|
||||
- Sign-in Forms (4) - FORMS.md
|
||||
|
||||
### Interactive Elements
|
||||
- Buttons (8) - ELEMENTS.md
|
||||
- Button Groups (5) - ELEMENTS.md
|
||||
- Dropdowns (5) - ELEMENTS.md
|
||||
- Avatars (11) - ELEMENTS.md
|
||||
- Badges (16) - ELEMENTS.md
|
||||
|
||||
### Overlays
|
||||
- Modal Dialogs (6) - OVERLAYS.md
|
||||
- Drawers (12) - OVERLAYS.md
|
||||
- Notifications (6) - OVERLAYS.md
|
||||
|
||||
### Feedback
|
||||
- Alerts (6) - FEEDBACK.md
|
||||
- Empty States (6) - FEEDBACK.md
|
||||
- 404 Pages (5) - MARKETING.md
|
||||
|
||||
### Marketing Sections
|
||||
- Heroes (12) - MARKETING.md
|
||||
- Feature Sections (15) - MARKETING.md
|
||||
- CTAs (11) - MARKETING.md
|
||||
- Pricing (12) - MARKETING.md
|
||||
- Testimonials (8) - MARKETING.md
|
||||
- Blog Sections (7) - MARKETING.md
|
||||
|
||||
### Ecommerce
|
||||
- Product Lists (11) - ECOMMERCE.md
|
||||
- Shopping Carts (6) - ECOMMERCE.md
|
||||
- Checkout Forms (5) - ECOMMERCE.md
|
||||
- Product Overviews (5) - ECOMMERCE.md
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority (Top 20)
|
||||
|
||||
Based on frequency of use and foundation importance:
|
||||
|
||||
1. **Buttons - Primary** (ELEMENTS.md)
|
||||
2. **Input with label** (FORMS.md)
|
||||
3. **Cards - Basic** (LAYOUT.md)
|
||||
4. **Navbars - Simple** (NAVIGATION.md)
|
||||
5. **Modal Dialogs - Simple alert** (OVERLAYS.md)
|
||||
6. **Alerts - With description** (FEEDBACK.md)
|
||||
7. **Tabs - With underline** (NAVIGATION.md)
|
||||
8. **Tables - Simple** (DATA_DISPLAY.md)
|
||||
9. **Hero - Simple centered** (MARKETING.md)
|
||||
10. **Toggle - Simple** (FORMS.md)
|
||||
11. **Select - Simple custom** (FORMS.md)
|
||||
12. **Badges - Flat** (ELEMENTS.md)
|
||||
13. **Notifications - Simple** (OVERLAYS.md)
|
||||
14. **Breadcrumbs - Simple** (NAVIGATION.md)
|
||||
15. **Product Lists - Simple** (ECOMMERCE.md)
|
||||
16. **Pricing - Three tiers** (MARKETING.md)
|
||||
17. **Sidebar Navigation - Light** (NAVIGATION.md)
|
||||
18. **Empty States - Simple** (FEEDBACK.md)
|
||||
19. **Avatar - Circular** (ELEMENTS.md)
|
||||
20. **Footer - 4-column simple** (MARKETING.md)
|
||||
|
||||
---
|
||||
|
||||
## Source URLs
|
||||
|
||||
- Application UI: https://tailwindcss.com/plus/ui-blocks/application-ui/
|
||||
- Marketing: https://tailwindcss.com/plus/ui-blocks/marketing/
|
||||
- Ecommerce: https://tailwindcss.com/plus/ui-blocks/ecommerce/
|
||||
120
docs/specs/patterns/LAYOUT.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Layout Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all layout patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 38 variants across 5 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/layout/
|
||||
|
||||
---
|
||||
|
||||
## Containers (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/layout/containers
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Full-width on mobile, constrained to breakpoint with padded content above | Responsive container |
|
||||
| 2 | Constrained to breakpoint with padded content | Fixed-width padded |
|
||||
| 3 | Full-width on mobile, constrained with padded content above mobile | Mobile-first responsive |
|
||||
| 4 | Constrained with padded content | Simple constrained |
|
||||
| 5 | Narrow constrained with padded content | Narrow width variant |
|
||||
|
||||
---
|
||||
|
||||
## Cards (10 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/layout/cards
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Basic card | Simple card container |
|
||||
| 2 | Card with header | Header section + body |
|
||||
| 3 | Card with header and footer | Header + body + footer |
|
||||
| 4 | Card with gray header | Gray-themed header |
|
||||
| 5 | Card with gray body | Gray-themed body |
|
||||
| 6 | Card with gray footer | Gray-themed footer |
|
||||
| 7 | Card with header and gray footer | Mixed styling |
|
||||
| 8 | Card with image | Image header card |
|
||||
| 9 | Card with full-width header and footer | Edge-to-edge sections |
|
||||
| 10 | Card with divider | Internal divider line |
|
||||
|
||||
---
|
||||
|
||||
## List Containers (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/layout/list-containers
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic list wrapper |
|
||||
| 2 | Card | List in card container |
|
||||
| 3 | Card with header | Card list with header |
|
||||
| 4 | Separate cards | Individual card per item |
|
||||
| 5 | Separate cards with header above | Header + separate cards |
|
||||
| 6 | Simple with dividers | Divided list items |
|
||||
| 7 | Card with dividers | Card list with dividers |
|
||||
|
||||
---
|
||||
|
||||
## Media Objects (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/layout/media-objects
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Basic responsive | Image + content layout |
|
||||
| 2 | Wide responsive | Wider image variant |
|
||||
| 3 | Stretched to fit | Full-width stretch |
|
||||
| 4 | Nested | Nested media objects |
|
||||
| 5 | Actions dropdown | With action menu |
|
||||
| 6 | Basic aligned to bottom | Bottom alignment |
|
||||
| 7 | Basic aligned to center | Center alignment |
|
||||
| 8 | Wide aligned to center | Wide centered |
|
||||
|
||||
---
|
||||
|
||||
## Dividers (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/layout/dividers
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With label | Text in center of divider |
|
||||
| 2 | With title | Title text divider |
|
||||
| 3 | With title on left | Left-aligned title |
|
||||
| 4 | With button | Button in divider |
|
||||
| 5 | With icon | Icon in center |
|
||||
| 6 | With toolbar | Multiple controls |
|
||||
| 7 | Simple | Basic horizontal line |
|
||||
| 8 | With label on left | Left-aligned label |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Type
|
||||
- **Structural**: Containers (page-level)
|
||||
- **Content**: Cards, list containers, media objects
|
||||
- **Decorative**: Dividers
|
||||
|
||||
### By Features
|
||||
- **With headers**: Cards, list containers
|
||||
- **With footers**: Cards
|
||||
- **With dividers**: Cards, list containers, divider components
|
||||
- **Responsive**: Containers, media objects
|
||||
|
||||
### By Complexity
|
||||
- **Simple**: Basic variants
|
||||
- **Compound**: With multiple sections (header/body/footer)
|
||||
- **Nested**: Support for nested content
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Cards - Basic card** - fundamental container
|
||||
2. **Cards - Card with header and footer** - common dashboard pattern
|
||||
3. **Containers - Constrained with padded content** - page wrapper
|
||||
4. **List containers - Card with dividers** - data lists
|
||||
5. **Dividers - Simple** - section separators
|
||||
435
docs/specs/patterns/MARKETING.md
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
# Marketing Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all marketing patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 179 variants across 23 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/
|
||||
|
||||
---
|
||||
|
||||
## Page Sections
|
||||
|
||||
### Hero Sections (12 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/heroes
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple centered | Centered text + CTA |
|
||||
| 2 | With app screenshot | Product image below |
|
||||
| 3 | With phone mockup | Mobile device frame |
|
||||
| 4 | Split with image | Text + image side-by-side |
|
||||
| 5 | Split with screenshot | Text + screenshot split |
|
||||
| 6 | With angled image on right | Diagonal image crop |
|
||||
| 7 | With image tiles | Multiple images grid |
|
||||
| 8 | With video background | Video autoplay background |
|
||||
| 9 | Dark with gradient | Dark theme + gradient |
|
||||
| 10 | Simple centered on dark | Dark centered hero |
|
||||
| 11 | Split with image on dark | Dark split layout |
|
||||
| 12 | With app screenshot on dark | Dark with screenshot |
|
||||
|
||||
---
|
||||
|
||||
### Feature Sections (15 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/feature-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple three column | 3-column icon grid |
|
||||
| 2 | Offset grid | Staggered feature grid |
|
||||
| 3 | Centered grid | Centered 3-column |
|
||||
| 4 | With large screenshot | Screenshot + features |
|
||||
| 5 | Simple two column | 2-column layout |
|
||||
| 6 | With code panel | Code example display |
|
||||
| 7 | With product screenshot | Product-focused |
|
||||
| 8 | With product screenshot on left | Left-aligned product |
|
||||
| 9 | With feature list | List-style features |
|
||||
| 10 | Simple | Basic feature section |
|
||||
| 11 | Simple two column on dark | Dark 2-column |
|
||||
| 12 | Centered grid on dark | Dark centered grid |
|
||||
| 13 | With large screenshot on dark | Dark with screenshot |
|
||||
| 14 | With product screenshot on dark | Dark product |
|
||||
| 15 | Simple on dark | Dark simple features |
|
||||
|
||||
---
|
||||
|
||||
### CTA Sections (11 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/cta-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple centered | Centered CTA box |
|
||||
| 2 | Simple stacked | Stacked text + button |
|
||||
| 3 | Simple justified | Justified layout |
|
||||
| 4 | With image tiles | Image grid CTA |
|
||||
| 5 | With app screenshot | Screenshot CTA |
|
||||
| 6 | Brand panel with app screenshot | Branded panel |
|
||||
| 7 | Brand panel | Solid brand color |
|
||||
| 8 | Split with image | Image + CTA split |
|
||||
| 9 | Simple centered on dark | Dark centered |
|
||||
| 10 | Simple stacked on dark | Dark stacked |
|
||||
| 11 | Simple justified on dark | Dark justified |
|
||||
|
||||
---
|
||||
|
||||
### Bento Grids (3 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/bento-grids
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Three column bento grid | 3-column grid |
|
||||
| 2 | Two row bento grid | 2-row layout |
|
||||
| 3 | Bento grid on dark | Dark theme grid |
|
||||
|
||||
---
|
||||
|
||||
### Pricing Sections (12 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/pricing
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Three tiers | Standard 3-plan layout |
|
||||
| 2 | Two tiers | Simple 2-plan |
|
||||
| 3 | Three tiers with dividers | Divided pricing |
|
||||
| 4 | Three tiers with emphasized tier | Featured plan |
|
||||
| 5 | Single price with details | Single product |
|
||||
| 6 | With comparison | Feature comparison table |
|
||||
| 7 | Single price with feature list | Single + features |
|
||||
| 8 | Three tiers on dark | Dark 3-tier |
|
||||
| 9 | Two tiers on dark | Dark 2-tier |
|
||||
| 10 | Three tiers with emphasized tier on dark | Dark featured |
|
||||
| 11 | With comparison on dark | Dark comparison |
|
||||
| 12 | Single price with feature list on dark | Dark single |
|
||||
|
||||
---
|
||||
|
||||
### Header Sections (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/header
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic page header |
|
||||
| 2 | Centered | Centered header |
|
||||
| 3 | With eyebrow | Eyebrow text + title |
|
||||
| 4 | With action | Header + button |
|
||||
| 5 | Simple on dark | Dark simple |
|
||||
| 6 | Centered on dark | Dark centered |
|
||||
| 7 | With eyebrow on dark | Dark with eyebrow |
|
||||
| 8 | With action on dark | Dark with action |
|
||||
|
||||
---
|
||||
|
||||
### Newsletter Sections (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/newsletter-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple stacked | Stacked form |
|
||||
| 2 | Simple side-by-side | Inline form |
|
||||
| 3 | With description | Text + form |
|
||||
| 4 | Centered card with graphics | Decorative card |
|
||||
| 5 | Simple stacked on dark | Dark stacked |
|
||||
| 6 | Simple side-by-side on dark | Dark inline |
|
||||
|
||||
---
|
||||
|
||||
### Stats Sections (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/stats-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic stats row |
|
||||
| 2 | Split with image | Stats + image |
|
||||
| 3 | Simple grid | Stats grid layout |
|
||||
| 4 | With background image | Image backdrop |
|
||||
| 5 | Simple on dark | Dark simple |
|
||||
| 6 | Split with image on dark | Dark split |
|
||||
| 7 | Simple grid on dark | Dark grid |
|
||||
| 8 | With background image on dark | Dark backdrop |
|
||||
|
||||
---
|
||||
|
||||
### Testimonials (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/testimonials
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple centered | Single centered quote |
|
||||
| 2 | With large avatar | Large profile image |
|
||||
| 3 | Side-by-side | Multiple quotes inline |
|
||||
| 4 | With overlapping image | Layered images |
|
||||
| 5 | Off-white grid | Grid on off-white |
|
||||
| 6 | Simple centered on dark | Dark centered |
|
||||
| 7 | Side-by-side on dark | Dark side-by-side |
|
||||
| 8 | With overlapping image on dark | Dark layered |
|
||||
|
||||
---
|
||||
|
||||
### Blog Sections (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/blog-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Three column with images | 3-column image cards |
|
||||
| 2 | Three column simple | 3-column text cards |
|
||||
| 3 | Single column | Vertical list |
|
||||
| 4 | With featured post | Featured + grid |
|
||||
| 5 | Three column with images on dark | Dark image cards |
|
||||
| 6 | Three column simple on dark | Dark text cards |
|
||||
| 7 | Single column on dark | Dark single column |
|
||||
|
||||
---
|
||||
|
||||
### Contact Sections (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/contact-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple two column | Form + info split |
|
||||
| 2 | Centered | Centered form |
|
||||
| 3 | Split with pattern | Form + decorative |
|
||||
| 4 | Split two-tone | Two-color split |
|
||||
| 5 | Side-by-side grid | Grid layout |
|
||||
| 6 | Simple two column on dark | Dark two-column |
|
||||
| 7 | Centered on dark | Dark centered |
|
||||
|
||||
---
|
||||
|
||||
### Team Sections (9 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/team-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With small images | Compact avatars |
|
||||
| 2 | With large images | Large profile photos |
|
||||
| 3 | Simple list | Text-based list |
|
||||
| 4 | Grid with round images | Circular avatars grid |
|
||||
| 5 | Grid with large round images | Large circular grid |
|
||||
| 6 | With small images on dark | Dark small images |
|
||||
| 7 | With large images on dark | Dark large images |
|
||||
| 8 | Simple list on dark | Dark text list |
|
||||
| 9 | Grid with large round images on dark | Dark circular grid |
|
||||
|
||||
---
|
||||
|
||||
### Content Sections (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/content-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Two columns with image | Text + image |
|
||||
| 2 | Centered | Centered prose |
|
||||
| 3 | Split with image | Image split |
|
||||
| 4 | Single column | Simple prose |
|
||||
| 5 | Centered on dark | Dark centered |
|
||||
| 6 | Split with image on dark | Dark split |
|
||||
| 7 | Single column on dark | Dark single |
|
||||
|
||||
---
|
||||
|
||||
### Logo Clouds (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/logo-clouds
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic logo row |
|
||||
| 2 | Simple with heading | Title + logos |
|
||||
| 3 | Split with grid | Split layout grid |
|
||||
| 4 | Simple on dark | Dark logo row |
|
||||
| 5 | Simple with heading on dark | Dark with heading |
|
||||
| 6 | Split with grid on dark | Dark split grid |
|
||||
|
||||
---
|
||||
|
||||
### FAQs (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/faq-sections
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Two columns | 2-column Q&A |
|
||||
| 2 | Centered accordion | Expandable centered |
|
||||
| 3 | Side-by-side | Question + answer split |
|
||||
| 4 | Offset with supporting text | Offset layout |
|
||||
| 5 | Two columns on dark | Dark 2-column |
|
||||
| 6 | Centered accordion on dark | Dark accordion |
|
||||
| 7 | Side-by-side on dark | Dark split |
|
||||
|
||||
---
|
||||
|
||||
### Footers (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/sections/footers
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | 4-column simple | 4-column links |
|
||||
| 2 | 4-column with newsletter | Links + signup |
|
||||
| 3 | 4-column with company mission | Links + about |
|
||||
| 4 | Simple centered | Centered links |
|
||||
| 5 | 4-column simple on dark | Dark 4-column |
|
||||
| 6 | 4-column with newsletter on dark | Dark with signup |
|
||||
| 7 | Simple centered on dark | Dark centered |
|
||||
|
||||
---
|
||||
|
||||
## Elements
|
||||
|
||||
### Headers (11 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/elements/headers
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple with menu button on left | Left hamburger |
|
||||
| 2 | Simple centered | Centered nav |
|
||||
| 3 | Simple with menu button | Right hamburger |
|
||||
| 4 | With centered logo | Logo centered |
|
||||
| 5 | With left-aligned nav | Left nav links |
|
||||
| 6 | With icons and search | Icons + search bar |
|
||||
| 7 | With full-width flyout | Full flyout menu |
|
||||
| 8 | With stacked flyout | Stacked flyout |
|
||||
| 9 | With icons | Icon buttons |
|
||||
| 10 | Simple on dark | Dark simple |
|
||||
| 11 | With full-width flyout on dark | Dark flyout |
|
||||
|
||||
---
|
||||
|
||||
### Flyout Menus (7 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/elements/flyout-menus
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic dropdown |
|
||||
| 2 | Full-width | Edge-to-edge |
|
||||
| 3 | Full-width two-column | 2-column full |
|
||||
| 4 | Stacked with footer actions | Stacked + actions |
|
||||
| 5 | Two column with section images | Images + columns |
|
||||
| 6 | Full-width two-column with sub-navigation | Complex nav |
|
||||
| 7 | Full-width with image | Image + links |
|
||||
|
||||
---
|
||||
|
||||
### Banners (13 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/elements/banners
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With button | CTA button banner |
|
||||
| 2 | With link | Link banner |
|
||||
| 3 | On brand | Brand-colored |
|
||||
| 4 | Privacy notice on brand | Cookie/privacy |
|
||||
| 5 | With dismiss | Dismissible |
|
||||
| 6 | Privacy notice with link | Privacy + link |
|
||||
| 7 | Privacy notice with link on brand | Brand privacy |
|
||||
| 8 | With button and dismiss | Button + close |
|
||||
| 9 | Bottom aligned left | Bottom-left |
|
||||
| 10 | Bottom aligned right | Bottom-right |
|
||||
| 11 | Bottom aligned full-width | Bottom full |
|
||||
| 12 | With input | Form input banner |
|
||||
| 13 | With input floating at bottom | Floating input |
|
||||
|
||||
---
|
||||
|
||||
## Feedback
|
||||
|
||||
### 404 Pages (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/feedback/404-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic 404 message |
|
||||
| 2 | With suggested pages | Related links |
|
||||
| 3 | Split with image | Image + message |
|
||||
| 4 | Simple on dark | Dark 404 |
|
||||
| 5 | With suggested pages on dark | Dark with links |
|
||||
|
||||
---
|
||||
|
||||
## Page Examples
|
||||
|
||||
### Landing Pages (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/page-examples/landing-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With mobile screenshot hero | Mobile mockup hero |
|
||||
| 2 | Simple hero with background | Background hero |
|
||||
| 3 | With testimonials and stats | Social proof |
|
||||
| 4 | With large screenshot hero | Desktop screenshot |
|
||||
|
||||
---
|
||||
|
||||
### Pricing Pages (3 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/page-examples/pricing-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With comparison table | Full comparison |
|
||||
| 2 | With three tiers and testimonials | Tiers + quotes |
|
||||
| 3 | With four tiers | 4-plan layout |
|
||||
|
||||
---
|
||||
|
||||
### About Pages (3 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/marketing/page-examples/about-pages
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | With timeline | Company history |
|
||||
| 2 | Dark with timeline | Dark history |
|
||||
| 3 | With image grid | Team/office images |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Theme
|
||||
- **Light**: Default light variants
|
||||
- **Dark**: Dark theme variants (approximately 50% of patterns)
|
||||
- **Brand**: Custom brand-colored variants
|
||||
|
||||
### By Layout
|
||||
- **Centered**: Centered content layouts
|
||||
- **Split**: Two-column splits
|
||||
- **Grid**: Multi-column grids
|
||||
- **Stacked**: Vertical stacking
|
||||
|
||||
### By Features
|
||||
- **With images**: Photo/screenshot integration
|
||||
- **With forms**: Input fields, newsletter signups
|
||||
- **With icons**: Icon-enhanced sections
|
||||
- **Dismissible**: Close button included
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Hero - Simple centered** - landing page essential
|
||||
2. **Feature Sections - Simple three column** - feature highlights
|
||||
3. **Pricing - Three tiers** - pricing page standard
|
||||
4. **CTA - Simple centered** - conversion sections
|
||||
5. **Headers - Simple with menu button** - navigation
|
||||
6. **Footers - 4-column simple** - page footer
|
||||
7. **Testimonials - Simple centered** - social proof
|
||||
8. **FAQ - Centered accordion** - support content
|
||||
165
docs/specs/patterns/NAVIGATION.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Navigation Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all navigation patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 54 variants across 8 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/
|
||||
|
||||
---
|
||||
|
||||
## Navbars (11 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/navbars
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic horizontal navbar |
|
||||
| 2 | Simple dark | Dark themed navbar |
|
||||
| 3 | With search | Includes search input |
|
||||
| 4 | With centered search | Centered search input |
|
||||
| 5 | With quick action | Quick action button |
|
||||
| 6 | With icons | Icon-based navigation |
|
||||
| 7 | With user dropdown | User profile dropdown |
|
||||
| 8 | With secondary nav | Two-level navigation |
|
||||
| 9 | With disclosure | Expandable sections |
|
||||
| 10 | Brand nav | Brand-coloured navbar |
|
||||
| 11 | Dark with quick action | Dark theme + quick action |
|
||||
|
||||
---
|
||||
|
||||
## Pagination (3 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/pagination
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple card footer | Basic prev/next |
|
||||
| 2 | Card footer with page buttons | Page number buttons |
|
||||
| 3 | Centered with icons | Icon-based navigation |
|
||||
|
||||
---
|
||||
|
||||
## Tabs (9 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/tabs
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Tabs with underline | Underline active indicator |
|
||||
| 2 | Tabs with underline and icons | Icons + underline |
|
||||
| 3 | Tabs in pills | Pill-shaped tabs |
|
||||
| 4 | Tabs in pills with brand colour | Branded pill tabs |
|
||||
| 5 | Tabs in pills on gray | Pills on gray background |
|
||||
| 6 | Full-width with underline | Edge-to-edge tabs |
|
||||
| 7 | Bar with underline | Bar style + underline |
|
||||
| 8 | Tabs with underline on gray | Gray background variant |
|
||||
| 9 | Simple underline | Minimal underline style |
|
||||
|
||||
---
|
||||
|
||||
## Vertical Navigation (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/vertical-navigation
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic vertical list |
|
||||
| 2 | With badges | Badge counts per item |
|
||||
| 3 | With icons and badges | Icons + badges combo |
|
||||
| 4 | With icons | Icon-based navigation |
|
||||
| 5 | With secondary navigation | Nested subnav items |
|
||||
| 6 | On gray | Gray background variant |
|
||||
|
||||
---
|
||||
|
||||
## Sidebar Navigation (5 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/sidebar-navigation
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Light | Light themed sidebar |
|
||||
| 2 | Dark | Dark themed sidebar |
|
||||
| 3 | With expandable sections | Collapsible groups |
|
||||
| 4 | With secondary navigation | Two-level nav |
|
||||
| 5 | Brand | Brand-coloured sidebar |
|
||||
|
||||
---
|
||||
|
||||
## Breadcrumbs (4 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/breadcrumbs
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Contained | Contained in container |
|
||||
| 2 | Full-width bar | Full-width background |
|
||||
| 3 | Simple with chevrons | Chevron separators |
|
||||
| 4 | Simple with slashes | Slash separators |
|
||||
|
||||
---
|
||||
|
||||
## Progress Bars (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/progress-bars
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic horizontal progress |
|
||||
| 2 | Panels | Panel-based steps |
|
||||
| 3 | Bullets | Bullet point steps |
|
||||
| 4 | Panels with border | Bordered panels |
|
||||
| 5 | Circles | Circular step indicators |
|
||||
| 6 | Bullets and text | Bullets with labels |
|
||||
| 7 | Circles with text | Circles with descriptions |
|
||||
| 8 | Progress bar | Linear progress indicator |
|
||||
|
||||
---
|
||||
|
||||
## Command Palettes (8 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/command-palettes
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic command palette |
|
||||
| 2 | Simple with padding | Padded simple version |
|
||||
| 3 | With preview | Preview panel for selected |
|
||||
| 4 | With images and descriptions | Rich item display |
|
||||
| 5 | With icons | Icon-based items |
|
||||
| 6 | Semi-transparent with icons | Glassmorphism style |
|
||||
| 7 | With groups | Grouped command items |
|
||||
| 8 | With footer | Footer actions |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Type
|
||||
- **Horizontal**: Navbars, tabs, pagination, breadcrumbs
|
||||
- **Vertical**: Vertical navigation, sidebar navigation
|
||||
- **Overlay**: Command palettes
|
||||
- **Progress**: Progress bars
|
||||
|
||||
### By Theme
|
||||
- **Light**: Default light variants
|
||||
- **Dark**: Dark themed variants
|
||||
- **Brand**: Custom brand colours
|
||||
|
||||
### By Features
|
||||
- **With icons**: Icon-based navigation
|
||||
- **With badges**: Count indicators
|
||||
- **With search**: Search functionality
|
||||
- **With dropdown**: Dropdown menus
|
||||
- **Expandable**: Collapsible sections
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Navbars - Simple** - most common header navigation
|
||||
2. **Tabs - With underline** - content section switching
|
||||
3. **Vertical Navigation - Simple** - sidebar navigation
|
||||
4. **Breadcrumbs - Simple with chevrons** - page hierarchy
|
||||
5. **Command Palettes - Simple** - keyboard navigation
|
||||
92
docs/specs/patterns/OVERLAYS.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Overlays Patterns (Tailwind+)
|
||||
|
||||
Consolidated list of all overlay patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 24 variants across 3 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/
|
||||
|
||||
---
|
||||
|
||||
## Modal Dialogs (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/modal-dialogs
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Centered with single action | Single CTA button |
|
||||
| 2 | Centered with wide buttons | Full-width buttons |
|
||||
| 3 | Simple alert | Basic alert dialog |
|
||||
| 4 | Simple with dismiss button | X button to close |
|
||||
| 5 | Simple with gray footer | Gray footer area |
|
||||
| 6 | Simple alert with left-aligned buttons | Left-aligned actions |
|
||||
|
||||
---
|
||||
|
||||
## Drawers (12 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/drawers
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Empty | Basic empty drawer |
|
||||
| 2 | Wide empty | Wide empty drawer |
|
||||
| 3 | With background overlay | Backdrop overlay |
|
||||
| 4 | With close button on outside | External close button |
|
||||
| 5 | With branded header | Brand-coloured header |
|
||||
| 6 | With sticky footer | Fixed footer actions |
|
||||
| 7 | Create project form example | Form drawer example |
|
||||
| 8 | Wide create project form example | Wide form drawer |
|
||||
| 9 | User profile example | Profile display drawer |
|
||||
| 10 | Wide horizontal user profile example | Wide profile layout |
|
||||
| 11 | Contact list example | List-based drawer |
|
||||
| 12 | File details example | Detail view drawer |
|
||||
|
||||
---
|
||||
|
||||
## Notifications (6 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/notifications
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Simple | Basic notification toast |
|
||||
| 2 | Condensed | Compact notification |
|
||||
| 3 | With actions below | Action buttons below |
|
||||
| 4 | With avatar | User avatar included |
|
||||
| 5 | With split buttons | Split action buttons |
|
||||
| 6 | With buttons below | Buttons in footer |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Type
|
||||
- **Modal**: Centered dialogs, alerts
|
||||
- **Drawer**: Slide-in panels from edge
|
||||
- **Toast**: Notification popups
|
||||
|
||||
### By Complexity
|
||||
- **Simple**: Basic display, single action
|
||||
- **Rich**: Multiple actions, forms, complex content
|
||||
|
||||
### By Position
|
||||
- **Center**: Modal dialogs
|
||||
- **Edge**: Drawers (typically right)
|
||||
- **Corner**: Notifications (typically top-right)
|
||||
|
||||
### By Features
|
||||
- **Dismissible**: Close button available
|
||||
- **With overlay**: Background backdrop
|
||||
- **With actions**: Action buttons included
|
||||
- **With forms**: Form inputs inside
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Modal Dialogs - Simple alert** - confirmation dialogs
|
||||
2. **Notifications - Simple** - toast messages
|
||||
3. **Drawers - Empty** - slide-out panels
|
||||
4. **Modal Dialogs - Centered with wide buttons** - action dialogs
|
||||
5. **Drawers - With sticky footer** - form panels
|
||||
62
docs/specs/patterns/PAGE_EXAMPLES.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Page Examples (Tailwind+)
|
||||
|
||||
Consolidated list of all page example patterns from Tailwind+ to convert to Flux UI.
|
||||
|
||||
**Total: 6 variants across 3 categories**
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/page-examples/
|
||||
|
||||
---
|
||||
|
||||
## Home Screens (2 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/page-examples/home-screens
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Stacked layout | Vertical sections layout |
|
||||
| 2 | Stacked with narrow sidebar | Sidebar + main content |
|
||||
|
||||
---
|
||||
|
||||
## Detail Screens (2 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/page-examples/detail-screens
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Stacked layout | Vertical detail sections |
|
||||
| 2 | Multi-column | Split layout detail view |
|
||||
|
||||
---
|
||||
|
||||
## Settings Screens (2 variants)
|
||||
|
||||
Source: https://tailwindcss.com/plus/ui-blocks/application-ui/page-examples/settings-screens
|
||||
|
||||
| # | Variant | Key Features |
|
||||
|---|---------|--------------|
|
||||
| 1 | Sidebar navigation with stacked layout | Side nav + stacked forms |
|
||||
| 2 | Sidebar navigation with two-column layout | Side nav + two-column forms |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Categories (for Flux UI organisation)
|
||||
|
||||
### By Layout
|
||||
- **Stacked**: Single column, vertical sections
|
||||
- **Multi-column**: Split layouts
|
||||
- **With sidebar**: Navigation sidebar + content
|
||||
|
||||
### By Purpose
|
||||
- **Dashboard/Home**: Overview pages
|
||||
- **Detail/View**: Single item display
|
||||
- **Settings/Forms**: Configuration pages
|
||||
|
||||
---
|
||||
|
||||
## Conversion Priority
|
||||
|
||||
1. **Home Screens - Stacked layout** - common dashboard base
|
||||
2. **Settings Screens - Sidebar navigation** - settings/admin pages
|
||||
3. **Detail Screens - Multi-column** - entity detail pages
|
||||
157
docs/specs/patterns/TAILWIND_PLUS_URLS.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Tailwind Plus Pattern URLs
|
||||
|
||||
Master list of all pattern pages to convert to Flux UI.
|
||||
|
||||
## Progress Key
|
||||
- [ ] Not started
|
||||
- [x] Converted
|
||||
|
||||
---
|
||||
|
||||
## Application UI (49 patterns)
|
||||
|
||||
### Application Shells
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/application-shells/multi-column
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/application-shells/sidebar
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/application-shells/stacked
|
||||
|
||||
### Data Display
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/data-display/calendars
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/data-display/description-lists
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/data-display/stats
|
||||
|
||||
### Elements
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/elements/avatars
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/elements/badges
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/elements/button-groups
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/elements/buttons
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/elements/dropdowns
|
||||
|
||||
### Feedback
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/feedback/alerts
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/feedback/empty-states
|
||||
|
||||
### Forms
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/action-panels
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/checkboxes
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/comboboxes
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/form-layouts
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/input-groups
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/radio-groups
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/select-menus
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/sign-in-forms
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/textareas
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/forms/toggles
|
||||
|
||||
### Headings
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/headings/card-headings
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/headings/page-headings
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/headings/section-headings
|
||||
|
||||
### Layout
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/layout/cards
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/layout/containers
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/layout/dividers
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/layout/list-containers
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/layout/media-objects
|
||||
|
||||
### Lists
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/lists/feeds
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/lists/grid-lists
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/lists/stacked-lists
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/lists/tables
|
||||
|
||||
### Navigation
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/breadcrumbs
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/command-palettes
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/navbars
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/pagination
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/progress-bars
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/sidebar-navigation
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/tabs
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/navigation/vertical-navigation
|
||||
|
||||
### Overlays
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/drawers
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/modal-dialogs
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/notifications
|
||||
|
||||
### Page Examples
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/page-examples/detail-screens
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/page-examples/home-screens
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/application-ui/page-examples/settings-screens
|
||||
|
||||
---
|
||||
|
||||
## Marketing (23 patterns)
|
||||
|
||||
### Elements
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/elements/banners
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/elements/flyout-menus
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/elements/headers
|
||||
|
||||
### Feedback
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/feedback/404-pages
|
||||
|
||||
### Page Examples
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/page-examples/about-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/page-examples/landing-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/page-examples/pricing-pages
|
||||
|
||||
### Sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/bento-grids
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/blog-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/contact-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/content-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/cta-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/faq-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/feature-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/footers
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/header
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/heroes
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/logo-clouds
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/newsletter-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/pricing
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/stats-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/team-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/marketing/sections/testimonials
|
||||
|
||||
---
|
||||
|
||||
## Ecommerce (21 patterns)
|
||||
|
||||
### Components
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/category-filters
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/category-previews
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/checkout-forms
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/incentives
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/order-history
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/order-summaries
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-features
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-lists
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-overviews
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/product-quickviews
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/promo-sections
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/reviews
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/shopping-carts
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/components/store-navigation
|
||||
|
||||
### Page Examples
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/category-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/checkout-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/order-detail-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/order-history-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/product-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/shopping-cart-pages
|
||||
- [ ] https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/storefront-pages
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Count |
|
||||
|----------|-------|
|
||||
| Application UI | 49 |
|
||||
| Marketing | 23 |
|
||||
| Ecommerce | 21 |
|
||||
| **Total** | **93** |
|
||||
338
docs/specs/storage-offload.md
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
# Storage Offload
|
||||
|
||||
Offload uploads to S3-compatible storage (AWS S3, Hetzner Object Storage, etc.) with transparent URL rewriting and CDN integration.
|
||||
|
||||
## Features
|
||||
|
||||
- Upload files to S3-compatible storage
|
||||
- Transparent URL rewriting in API responses
|
||||
- CDN integration (BunnyCDN, CloudFlare, etc.)
|
||||
- File integrity verification with SHA-256 hashing
|
||||
- Configurable retention (keep or delete local copies)
|
||||
- Migration command for existing files
|
||||
- Caching for improved performance
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Enable storage offload
|
||||
STORAGE_OFFLOAD_ENABLED=true
|
||||
|
||||
# Storage disk (hetzner, s3, or custom)
|
||||
STORAGE_OFFLOAD_DISK=hetzner
|
||||
|
||||
# CDN URL (optional, uses disk URL if not set)
|
||||
STORAGE_OFFLOAD_CDN_URL=https://cdn.host.uk.com
|
||||
|
||||
# Hetzner Object Storage
|
||||
HETZNER_ENDPOINT=https://fsn1.your-objectstorage.com
|
||||
HETZNER_REGION=fsn1
|
||||
HETZNER_BUCKET=host-uk-uploads
|
||||
HETZNER_ACCESS_KEY=your_access_key
|
||||
HETZNER_SECRET_KEY=your_secret_key
|
||||
|
||||
# Or AWS S3
|
||||
AWS_ACCESS_KEY_ID=your_key
|
||||
AWS_SECRET_ACCESS_KEY=your_secret
|
||||
AWS_DEFAULT_REGION=eu-west-2
|
||||
AWS_BUCKET=host-uk-uploads
|
||||
|
||||
# Optional settings
|
||||
STORAGE_OFFLOAD_MAX_SIZE=104857600 # 100MB in bytes
|
||||
STORAGE_OFFLOAD_AUTO=true # Auto-offload on upload
|
||||
STORAGE_OFFLOAD_KEEP_LOCAL=false # Delete local copy after upload
|
||||
STORAGE_OFFLOAD_QUEUE=true # Queue long operations
|
||||
```
|
||||
|
||||
### Configuration File
|
||||
|
||||
See `/config/offload.php` for full configuration options including:
|
||||
|
||||
- Allowed file extensions
|
||||
- Path organisation by category
|
||||
- Queue settings
|
||||
- Cache settings
|
||||
|
||||
## Usage
|
||||
|
||||
### Programmatic Upload
|
||||
|
||||
```php
|
||||
use App\Services\Storage\StorageOffload;
|
||||
|
||||
$offloadService = app(StorageOffload::class);
|
||||
|
||||
// Upload a file
|
||||
$localPath = storage_path('app/public/uploads/photo.jpg');
|
||||
$result = $offloadService->upload(
|
||||
localPath: $localPath,
|
||||
remotePath: null, // Auto-generated if null
|
||||
category: 'biolink', // For path organisation
|
||||
metadata: [ // Optional metadata
|
||||
'user_id' => auth()->id(),
|
||||
'original_name' => 'photo.jpg',
|
||||
]
|
||||
);
|
||||
|
||||
// Get CDN URL for offloaded file
|
||||
$url = $offloadService->url($localPath);
|
||||
// Returns: https://cdn.host.uk.com/biolinks/abc123/photo.jpg
|
||||
|
||||
// Check if file is offloaded
|
||||
if ($offloadService->isOffloaded($localPath)) {
|
||||
// File is on remote storage
|
||||
}
|
||||
|
||||
// Delete from remote storage
|
||||
$offloadService->delete($localPath);
|
||||
|
||||
// Verify file integrity
|
||||
$valid = $offloadService->verifyIntegrity($localPath);
|
||||
|
||||
// Get statistics
|
||||
$stats = $offloadService->getStats();
|
||||
```
|
||||
|
||||
### Artisan Command
|
||||
|
||||
Migrate existing local files to remote storage:
|
||||
|
||||
```bash
|
||||
# Migrate all files in storage/app/public
|
||||
php artisan offload:migrate
|
||||
|
||||
# Migrate specific directory
|
||||
php artisan offload:migrate /path/to/directory
|
||||
|
||||
# Dry run (preview without uploading)
|
||||
php artisan offload:migrate --dry-run
|
||||
|
||||
# Set category for organisation
|
||||
php artisan offload:migrate --category=biolink
|
||||
|
||||
# Only migrate files not already offloaded
|
||||
php artisan offload:migrate --only-missing
|
||||
|
||||
# Skip confirmation prompt
|
||||
php artisan offload:migrate --force
|
||||
```
|
||||
|
||||
### URL Rewriting Middleware
|
||||
|
||||
Apply to API routes for transparent URL rewriting:
|
||||
|
||||
```php
|
||||
use App\Http\Middleware\RewriteOffloadedUrls;
|
||||
|
||||
// In routes/api.php or route groups
|
||||
Route::middleware([RewriteOffloadedUrls::class])->group(function () {
|
||||
Route::get('/biolinks', [BioLinkController::class, 'index']);
|
||||
});
|
||||
```
|
||||
|
||||
The middleware automatically rewrites local storage URLs to CDN URLs in JSON responses:
|
||||
|
||||
```json
|
||||
{
|
||||
"avatar": "https://cdn.host.uk.com/avatars/abc123/photo.jpg",
|
||||
"background": "https://cdn.host.uk.com/biolinks/def456/bg.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
The `storage_offloads` table tracks offloaded files:
|
||||
|
||||
| Column | Description |
|
||||
|--------|-------------|
|
||||
| `disk` | Storage disk name (hetzner, s3, etc.) |
|
||||
| `local_path` | Original local file path |
|
||||
| `remote_path` | Remote storage path |
|
||||
| `file_size` | File size in bytes |
|
||||
| `mime_type` | MIME type |
|
||||
| `hash` | SHA-256 hash for integrity checking |
|
||||
| `category` | Category for organisation |
|
||||
| `metadata` | JSON metadata |
|
||||
| `offloaded_at` | Upload timestamp |
|
||||
|
||||
## Model
|
||||
|
||||
Query offloaded files:
|
||||
|
||||
```php
|
||||
use App\Models\StorageOffload;
|
||||
|
||||
// Find by local path
|
||||
$record = StorageOffload::where('local_path', $path)->first();
|
||||
|
||||
// Filter by category
|
||||
$biolinks = StorageOffload::inCategory('biolink')->get();
|
||||
|
||||
// Filter by disk
|
||||
$hetznerFiles = StorageOffload::forDisk('hetzner')->get();
|
||||
|
||||
// Check file type
|
||||
if ($record->isImage()) {
|
||||
// Handle image
|
||||
}
|
||||
|
||||
// Human-readable file size
|
||||
echo $record->file_size_human; // "2.5 MB"
|
||||
```
|
||||
|
||||
## Path Organisation
|
||||
|
||||
Files are organised by category in remote storage:
|
||||
|
||||
| Category | Remote Path Pattern |
|
||||
|----------|-------------------|
|
||||
| `biolink` | `biolinks/{hash}/{filename}` |
|
||||
| `avatar` | `avatars/{hash}/{filename}` |
|
||||
| `media` | `media/{hash}/{filename}` |
|
||||
| `static` | `static/{hash}/{filename}` |
|
||||
|
||||
Configure in `config/offload.php` under `paths`.
|
||||
|
||||
## CDN Integration
|
||||
|
||||
### BunnyCDN (Recommended)
|
||||
|
||||
1. Create a pull zone pointing to your origin server
|
||||
2. Set `STORAGE_OFFLOAD_CDN_URL` to your pull zone URL
|
||||
3. Files are automatically pulled from remote storage and cached at edge
|
||||
|
||||
### CloudFlare
|
||||
|
||||
1. Add CNAME record pointing to your storage endpoint
|
||||
2. Enable caching in CloudFlare settings
|
||||
3. Set `STORAGE_OFFLOAD_CDN_URL` to your CloudFlare URL
|
||||
|
||||
### Direct Access
|
||||
|
||||
If no CDN URL is configured, the middleware uses the storage disk's URL configuration.
|
||||
|
||||
## Performance
|
||||
|
||||
- **Caching**: URLs are cached for 1 hour by default (configurable)
|
||||
- **Queue**: Long-running migrations can be queued
|
||||
- **Integrity**: SHA-256 hashing ensures file integrity without re-reading files
|
||||
|
||||
## Monitoring
|
||||
|
||||
Check offload statistics:
|
||||
|
||||
```php
|
||||
$stats = $offloadService->getStats();
|
||||
|
||||
// Output:
|
||||
// [
|
||||
// 'total_files' => 1234,
|
||||
// 'total_size' => 5368709120,
|
||||
// 'total_size_human' => '5 GB',
|
||||
// 'by_category' => [
|
||||
// ['category' => 'biolink', 'file_count' => 500, 'total_size' => ...],
|
||||
// ['category' => 'avatar', 'file_count' => 734, 'total_size' => ...],
|
||||
// ]
|
||||
// ]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Files not uploading
|
||||
|
||||
Check logs for errors:
|
||||
```bash
|
||||
tail -f storage/logs/laravel.log | grep offload
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- Incorrect credentials in `.env`
|
||||
- File size exceeds `STORAGE_OFFLOAD_MAX_SIZE`
|
||||
- File extension not in allowed list
|
||||
|
||||
### URLs not rewriting
|
||||
|
||||
1. Ensure middleware is applied to route
|
||||
2. Check response is JSON (middleware only processes JSON)
|
||||
3. Verify file is actually offloaded: `php artisan tinker` → `StorageOffload::count()`
|
||||
|
||||
### Integrity verification fails
|
||||
|
||||
File may be corrupted during upload. Re-upload:
|
||||
|
||||
```php
|
||||
$offloadService->delete($localPath);
|
||||
$offloadService->upload($localPath, null, $category);
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Existing Laravel Storage
|
||||
|
||||
```bash
|
||||
# 1. Configure environment variables
|
||||
nano .env
|
||||
|
||||
# 2. Test with dry run
|
||||
php artisan offload:migrate --dry-run
|
||||
|
||||
# 3. Migrate with local backup
|
||||
STORAGE_OFFLOAD_KEEP_LOCAL=true php artisan offload:migrate
|
||||
|
||||
# 4. Verify uploads
|
||||
php artisan tinker
|
||||
>>> StorageOffload::count()
|
||||
|
||||
# 5. Remove local copies once verified
|
||||
STORAGE_OFFLOAD_KEEP_LOCAL=false php artisan offload:migrate
|
||||
```
|
||||
|
||||
### Gradual Migration
|
||||
|
||||
Enable auto-offload for new uploads only:
|
||||
|
||||
```bash
|
||||
STORAGE_OFFLOAD_AUTO=true
|
||||
STORAGE_OFFLOAD_ENABLED=true
|
||||
```
|
||||
|
||||
Migrate existing files in batches:
|
||||
|
||||
```bash
|
||||
php artisan offload:migrate storage/app/public/2024
|
||||
php artisan offload:migrate storage/app/public/2025
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Files maintain original permissions (public/private based on disk config)
|
||||
- SHA-256 hashing prevents tampering
|
||||
- Credentials stored in `.env` (never committed to git)
|
||||
- Soft deletes allow recovery of accidentally deleted records
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use CDN for public assets (images, documents)
|
||||
2. Keep local copies during initial migration
|
||||
3. Monitor storage costs (especially with BunnyCDN per-connection pricing for WebSockets)
|
||||
4. Set appropriate `max_file_size` to avoid huge uploads
|
||||
5. Use categories for organised storage structure
|
||||
6. Enable caching for frequently accessed URLs
|
||||
7. Queue large migration operations
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all storage offload tests
|
||||
./vendor/bin/pest tests/Feature/StorageOffloadTest.php
|
||||
./vendor/bin/pest tests/Feature/RewriteOffloadedUrlsTest.php
|
||||
./vendor/bin/pest tests/Feature/OffloadMigrateCommandTest.php
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `/config/offload.php` - Full configuration options
|
||||
- `/config/filesystems.php` - Disk configurations
|
||||
- `BunnyCdnService` - Existing BunnyCDN integration for cache purging
|
||||
81
docs/specs/ui/NAVIGATION-STYLING-NOTES.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Navigation Dropdown Styling - Implementation Notes
|
||||
|
||||
> **Status:** Complete (January 2026)
|
||||
> **Location:** `resources/css/app.css` (lines ~253-370)
|
||||
|
||||
## Summary
|
||||
|
||||
Custom styling for Flux UI navigation dropdowns with accent-coloured gradients, grid pattern overlays, and consistent hover states.
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### Flux Component Data Attributes
|
||||
|
||||
- `[data-flux-navbar-items]` - Note the **plural 's'** - targets navbar item buttons
|
||||
- `[data-flux-navmenu-item]` - Targets dropdown menu items (singular)
|
||||
- `[data-flux-navmenu]` - The dropdown menu container itself
|
||||
- `[data-content]` - Wrapper around text content inside navbar items (has hardcoded `text-sm`)
|
||||
|
||||
### Navbar Item Styling
|
||||
|
||||
```css
|
||||
/* Main navbar items - larger text, white colour */
|
||||
[data-flux-navbar-items] {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Override the hardcoded text-sm on navbar item content */
|
||||
[data-flux-navbar-items] [data-content] {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
```
|
||||
|
||||
### Dropdown Menu Card Styling
|
||||
|
||||
Each dropdown uses `data-accent` attribute on parent for colour theming:
|
||||
- `data-accent="purple"` - Services
|
||||
- `data-accent="orange"` - For
|
||||
- `data-accent="indigo"` - AI
|
||||
- `data-accent="cyan"` - Tools
|
||||
- `data-accent="slate"` - OSS
|
||||
- `data-accent="amber"` - About
|
||||
- `data-accent="violet"` - Dashboard
|
||||
|
||||
Features:
|
||||
1. **Asymmetric gradient** - Solid accent colour left, fading to slate-900 right
|
||||
2. **Accent border** - 3px left border in accent colour
|
||||
3. **Grid pattern overlay** - Via `::before` pseudo-element with grid.svg
|
||||
4. **Fading grid** - Uses `mask-image` to fade grid from 50% to right edge
|
||||
5. **Backdrop blur** - `backdrop-filter: blur(12px)`
|
||||
|
||||
### Menu Item Hover Effect
|
||||
|
||||
```css
|
||||
[data-accent] [data-flux-navmenu-item]:hover {
|
||||
background: linear-gradient(90deg, rgb(255 255 255 / 0.08), transparent 80%) !important;
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1) !important;
|
||||
color: white !important;
|
||||
}
|
||||
```
|
||||
|
||||
- Gradient hover that fades to transparent (allows grid to show through)
|
||||
- Subtle inset border for definition
|
||||
|
||||
## File References
|
||||
|
||||
- **CSS:** `resources/css/app.css` - Navigation styling section
|
||||
- **Header:** `resources/views/components/layouts/partials/header.blade.php`
|
||||
- **Flux stubs:** `vendor/livewire/flux/stubs/resources/views/flux/navbar/item.blade.php`
|
||||
- **Grid SVG:** `public/vendor/stellar/images/grid.svg`
|
||||
- **Flux docs:** `doc/ui/flux/navbar.md`
|
||||
|
||||
## Browser Notes
|
||||
|
||||
- Chrome may cache aggressively - use Shift+Cmd+R for hard refresh when testing CSS changes
|
||||
- Safari renders backdrop-filter effects more smoothly than Chrome in some cases
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `doc/ui/flux/README.md` - Flux UI overview
|
||||
- `doc/ui/flux/styling.md` - Theming and customisation guide
|
||||
- `doc/ui/flux/navbar.md` - Navigation component reference
|
||||
233
docs/specs/ui/STANDARDS.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# Host Hub UI Standards
|
||||
|
||||
> Standardised patterns for Flux Pro components. Reference this when building or reviewing UI.
|
||||
|
||||
## Toast Notifications
|
||||
|
||||
Use `Flux::toast()` in Livewire components instead of session flash messages.
|
||||
|
||||
```php
|
||||
use Flux\Flux;
|
||||
|
||||
// Success
|
||||
Flux::toast(text: 'Changes saved.', variant: 'success');
|
||||
|
||||
// Error
|
||||
Flux::toast(text: 'Failed to save changes.', variant: 'danger');
|
||||
|
||||
// Warning
|
||||
Flux::toast(text: 'Your session will expire soon.', variant: 'warning');
|
||||
|
||||
// Info (default)
|
||||
Flux::toast('Processing your request...');
|
||||
```
|
||||
|
||||
**Do NOT use:**
|
||||
- `session()->flash('message', '...')`
|
||||
- `session()->flash('success', '...')`
|
||||
- `session()->flash('error', '...')`
|
||||
|
||||
---
|
||||
|
||||
## Badge Colour Semantics
|
||||
|
||||
Consistent colour meanings across the application:
|
||||
|
||||
| Colour | Meaning | Examples |
|
||||
|--------|---------|----------|
|
||||
| `green` | Active, success, healthy, completed | Active subscription, Published, Online |
|
||||
| `amber` / `yellow` | Warning, pending, needs attention | Pending approval, Expiring soon |
|
||||
| `red` | Error, danger, inactive, failed | Failed, Cancelled, Offline |
|
||||
| `blue` | Info, in progress | Processing, In review |
|
||||
| `violet` | Branded, Host UK accent | Premium features, Pro |
|
||||
| `lime` | New, fresh | New feature, Just added |
|
||||
| `zinc` / `gray` | Neutral, secondary, default | Draft, Unknown, N/A |
|
||||
|
||||
### Status Badge Patterns
|
||||
|
||||
```blade
|
||||
{{-- Active/Inactive --}}
|
||||
<flux:badge color="green">Active</flux:badge>
|
||||
<flux:badge color="red">Inactive</flux:badge>
|
||||
|
||||
{{-- Progress states --}}
|
||||
<flux:badge color="zinc">Draft</flux:badge>
|
||||
<flux:badge color="yellow">Pending</flux:badge>
|
||||
<flux:badge color="blue">In Progress</flux:badge>
|
||||
<flux:badge color="green">Completed</flux:badge>
|
||||
<flux:badge color="red">Failed</flux:badge>
|
||||
|
||||
{{-- With icons --}}
|
||||
<flux:badge color="green" icon="check-circle">Published</flux:badge>
|
||||
<flux:badge color="yellow" icon="clock">Scheduled</flux:badge>
|
||||
<flux:badge color="red" icon="x-circle">Rejected</flux:badge>
|
||||
|
||||
{{-- Pill variant for counts/tags --}}
|
||||
<flux:badge variant="pill" color="blue">12</flux:badge>
|
||||
<flux:badge variant="pill" color="violet">Pro</flux:badge>
|
||||
```
|
||||
|
||||
### Helper for Dynamic Status
|
||||
|
||||
```php
|
||||
// In Livewire component
|
||||
public function statusColor(string $status): string
|
||||
{
|
||||
return match($status) {
|
||||
'active', 'published', 'completed', 'success' => 'green',
|
||||
'pending', 'scheduled', 'warning' => 'yellow',
|
||||
'processing', 'in_progress', 'info' => 'blue',
|
||||
'failed', 'cancelled', 'error', 'inactive' => 'red',
|
||||
'draft', 'unknown' => 'zinc',
|
||||
default => 'zinc',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Button Variants
|
||||
|
||||
Consistent button usage based on action importance:
|
||||
|
||||
| Variant | Usage | Example |
|
||||
|---------|-------|---------|
|
||||
| `primary` | Main CTA, primary action | Save, Create, Submit |
|
||||
| (default/outline) | Secondary action | Cancel, Back, View |
|
||||
| `ghost` | Tertiary action, low emphasis | Close, Skip, Learn more |
|
||||
| `danger` | Destructive action | Delete, Remove, Disconnect |
|
||||
| `subtle` | Very low emphasis | Dismiss, Hide |
|
||||
| `filled` | Alternative emphasis | Special actions |
|
||||
|
||||
### Button Patterns
|
||||
|
||||
```blade
|
||||
{{-- Primary action --}}
|
||||
<flux:button variant="primary">Save Changes</flux:button>
|
||||
<flux:button variant="primary" icon="plus">Create New</flux:button>
|
||||
|
||||
{{-- Secondary action --}}
|
||||
<flux:button>Cancel</flux:button>
|
||||
<flux:button icon="arrow-left">Back</flux:button>
|
||||
|
||||
{{-- Tertiary/ghost --}}
|
||||
<flux:button variant="ghost">Skip</flux:button>
|
||||
<flux:button variant="ghost" icon="x-mark">Close</flux:button>
|
||||
|
||||
{{-- Danger --}}
|
||||
<flux:button variant="danger">Delete</flux:button>
|
||||
<flux:button variant="danger" icon="trash">Remove</flux:button>
|
||||
|
||||
{{-- Icon-only with tooltip --}}
|
||||
<flux:button icon="pencil" tooltip="Edit" />
|
||||
<flux:button icon="trash" variant="danger" tooltip="Delete" />
|
||||
<flux:button icon="cog-6-tooth" variant="ghost" tooltip="Settings" />
|
||||
|
||||
{{-- Save/Cancel pair --}}
|
||||
<div class="flex gap-2">
|
||||
<flux:button variant="ghost">Cancel</flux:button>
|
||||
<flux:button variant="primary">Save</flux:button>
|
||||
</div>
|
||||
|
||||
{{-- With loading (automatic on wire:click) --}}
|
||||
<flux:button variant="primary" wire:click="save">Save</flux:button>
|
||||
```
|
||||
|
||||
### Migration from Manual Buttons
|
||||
|
||||
Replace manual Tailwind buttons:
|
||||
|
||||
```blade
|
||||
{{-- OLD: Manual button --}}
|
||||
<a href="..." class="btn bg-violet-500 hover:bg-violet-600 text-white">
|
||||
Create
|
||||
</a>
|
||||
|
||||
{{-- NEW: Flux button --}}
|
||||
<flux:button href="..." variant="primary">Create</flux:button>
|
||||
```
|
||||
|
||||
```blade
|
||||
{{-- OLD: Manual danger button --}}
|
||||
<button class="btn bg-red-500 hover:bg-red-600 text-white">
|
||||
Delete
|
||||
</button>
|
||||
|
||||
{{-- NEW: Flux button --}}
|
||||
<flux:button variant="danger">Delete</flux:button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Button Groups
|
||||
|
||||
Use for related actions:
|
||||
|
||||
```blade
|
||||
{{-- Toggle buttons --}}
|
||||
<flux:button.group>
|
||||
<flux:button icon="list-bullet">List</flux:button>
|
||||
<flux:button icon="squares-2x2">Grid</flux:button>
|
||||
</flux:button.group>
|
||||
|
||||
{{-- Segmented control --}}
|
||||
<flux:button.group>
|
||||
<flux:button>Day</flux:button>
|
||||
<flux:button>Week</flux:button>
|
||||
<flux:button>Month</flux:button>
|
||||
</flux:button.group>
|
||||
|
||||
{{-- Action with dropdown --}}
|
||||
<flux:button.group>
|
||||
<flux:button variant="primary">Save</flux:button>
|
||||
<flux:dropdown>
|
||||
<flux:button variant="primary" icon="chevron-down" />
|
||||
<flux:menu>
|
||||
<flux:menu.item>Save as draft</flux:menu.item>
|
||||
<flux:menu.item>Save and publish</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:button.group>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icon Guidelines
|
||||
|
||||
We use Font Awesome Pro. Prefer:
|
||||
|
||||
- **Solid icons** for primary actions and filled states
|
||||
- **Regular icons** for secondary elements
|
||||
- **Light icons** for subtle/decorative use
|
||||
- **Brand icons** for social platforms (fa-twitter, fa-instagram, etc.)
|
||||
|
||||
With Flux (Heroicons):
|
||||
```blade
|
||||
<flux:button icon="plus">Add</flux:button>
|
||||
<flux:button icon="pencil">Edit</flux:button>
|
||||
<flux:button icon="trash">Delete</flux:button>
|
||||
```
|
||||
|
||||
With Font Awesome (in custom components):
|
||||
```blade
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<i class="fa-brands fa-twitter"></i>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Status Colours
|
||||
- ✅ Green = Active/Success
|
||||
- ⚠️ Yellow/Amber = Warning/Pending
|
||||
- ❌ Red = Error/Danger
|
||||
- ℹ️ Blue = Info/Processing
|
||||
- 💜 Violet = Branded/Premium
|
||||
- ⚪ Zinc = Neutral/Default
|
||||
|
||||
### Button Hierarchy
|
||||
1. `variant="primary"` - Main action
|
||||
2. (default) - Secondary
|
||||
3. `variant="ghost"` - Tertiary
|
||||
4. `variant="danger"` - Destructive
|
||||
187
docs/specs/ui/flux/INDEX.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# Flux UI Documentation
|
||||
|
||||
Complete documentation for Flux Pro components (Livewire team).
|
||||
|
||||
**License:** Flux Pro
|
||||
**Source:** https://fluxui.dev
|
||||
|
||||
---
|
||||
|
||||
## Component Reference
|
||||
|
||||
All components have individual documentation files in `components/`.
|
||||
|
||||
### Layouts
|
||||
|
||||
| Component | File | Description |
|
||||
|-----------|------|-------------|
|
||||
| Header | [header.md](components/header.md) | Full-width top navigation |
|
||||
| Sidebar | [sidebar.md](components/sidebar.md) | Vertical navigation with groups |
|
||||
|
||||
### Components A-C
|
||||
|
||||
| Component | File | Pro |
|
||||
|-----------|------|-----|
|
||||
| Accordion | [accordion.md](components/accordion.md) | |
|
||||
| Autocomplete | [autocomplete.md](components/autocomplete.md) | ✓ |
|
||||
| Avatar | [avatar.md](components/avatar.md) | |
|
||||
| Badge | [badge.md](components/badge.md) | |
|
||||
| Brand | [brand.md](components/brand.md) | |
|
||||
| Breadcrumbs | [breadcrumbs.md](components/breadcrumbs.md) | |
|
||||
| Button | [button.md](components/button.md) | |
|
||||
| Calendar | [calendar.md](components/calendar.md) | ✓ |
|
||||
| Callout | [callout.md](components/callout.md) | |
|
||||
| Card | [card.md](components/card.md) | |
|
||||
| Chart | [chart.md](components/chart.md) | ✓ |
|
||||
| Checkbox | [checkbox.md](components/checkbox.md) | Partial |
|
||||
| Command | [command.md](components/command.md) | |
|
||||
| Composer | [composer.md](components/composer.md) | ✓ |
|
||||
| Context | [context.md](components/context.md) | |
|
||||
|
||||
### Components D-I
|
||||
|
||||
| Component | File | Pro |
|
||||
|-----------|------|-----|
|
||||
| Date Picker | [date-picker.md](components/date-picker.md) | ✓ |
|
||||
| Dropdown | [dropdown.md](components/dropdown.md) | |
|
||||
| Editor | [editor.md](components/editor.md) | ✓ |
|
||||
| Field | [field.md](components/field.md) | |
|
||||
| File Upload | [file-upload.md](components/file-upload.md) | |
|
||||
| Heading | [heading.md](components/heading.md) | |
|
||||
| Icon | [icon.md](components/icon.md) | |
|
||||
| Input | [input.md](components/input.md) | |
|
||||
|
||||
### Components K-P
|
||||
|
||||
| Component | File | Pro |
|
||||
|-----------|------|-----|
|
||||
| Kanban | [kanban.md](components/kanban.md) | ✓ |
|
||||
| Modal | [modal.md](components/modal.md) | |
|
||||
| Navbar | [navbar.md](components/navbar.md) | |
|
||||
| OTP Input | [otp-input.md](components/otp-input.md) | |
|
||||
| Pagination | [pagination.md](components/pagination.md) | |
|
||||
| Pillbox | [pillbox.md](components/pillbox.md) | ✓ |
|
||||
| Popover | [popover.md](components/popover.md) | |
|
||||
| Profile | [profile.md](components/profile.md) | |
|
||||
|
||||
### Components R-T
|
||||
|
||||
| Component | File | Pro |
|
||||
|-----------|------|-----|
|
||||
| Radio | [radio.md](components/radio.md) | Partial |
|
||||
| Select | [select.md](components/select.md) | Partial |
|
||||
| Separator | [separator.md](components/separator.md) | |
|
||||
| Skeleton | [skeleton.md](components/skeleton.md) | |
|
||||
| Slider | [slider.md](components/slider.md) | |
|
||||
| Switch | [switch.md](components/switch.md) | |
|
||||
| Table | [table.md](components/table.md) | |
|
||||
| Tabs | [tabs.md](components/tabs.md) | |
|
||||
| Text | [text.md](components/text.md) | |
|
||||
| Textarea | [textarea.md](components/textarea.md) | |
|
||||
| Time Picker | [time-picker.md](components/time-picker.md) | ✓ |
|
||||
| Toast | [toast.md](components/toast.md) | |
|
||||
| Tooltip | [tooltip.md](components/tooltip.md) | |
|
||||
|
||||
---
|
||||
|
||||
## Quick Patterns
|
||||
|
||||
### wire:model Binding
|
||||
|
||||
```blade
|
||||
<flux:input wire:model="name" />
|
||||
<flux:input wire:model.live="search" />
|
||||
<flux:input wire:model.blur="email" />
|
||||
```
|
||||
|
||||
### Shorthand Labels
|
||||
|
||||
```blade
|
||||
{{-- Verbose --}}
|
||||
<flux:field>
|
||||
<flux:label>Email</flux:label>
|
||||
<flux:input wire:model="email" />
|
||||
</flux:field>
|
||||
|
||||
{{-- Shorthand (equivalent) --}}
|
||||
<flux:input label="Email" wire:model="email" />
|
||||
```
|
||||
|
||||
### Size Variants
|
||||
|
||||
Most components support: `xs`, `sm`, (default), `lg`, `xl`
|
||||
|
||||
### Icon Variants
|
||||
|
||||
```blade
|
||||
<flux:icon.bolt /> {{-- outline 24px --}}
|
||||
<flux:icon.bolt variant="solid" /> {{-- solid 24px --}}
|
||||
<flux:icon.bolt variant="mini" /> {{-- 20px --}}
|
||||
<flux:icon.bolt variant="micro" /> {{-- 16px --}}
|
||||
```
|
||||
|
||||
### Modal Control
|
||||
|
||||
```php
|
||||
// Livewire
|
||||
Flux::modal('name')->show();
|
||||
Flux::modal('name')->close();
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Alpine.js
|
||||
$flux.modal('name').show()
|
||||
|
||||
// JavaScript
|
||||
Flux.modal('name').show()
|
||||
```
|
||||
|
||||
### Toast Notifications
|
||||
|
||||
```php
|
||||
Flux::toast('Message saved.');
|
||||
Flux::toast(text: 'Success!', variant: 'success');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **We Style, You Space** - Flux handles styling; you manage layout spacing
|
||||
2. **Props vs Attributes** - Props configure; attributes forward to DOM
|
||||
3. **Composable** - Simple or complex patterns with same components
|
||||
4. **Livewire Native** - Deep wire:model integration
|
||||
5. **Dark Mode** - Automatic handling
|
||||
|
||||
---
|
||||
|
||||
## Theming
|
||||
|
||||
### Dark Mode Toggle
|
||||
|
||||
```javascript
|
||||
$flux.appearance = 'light' | 'dark' | 'system'
|
||||
$flux.dark = true | false
|
||||
```
|
||||
|
||||
### Accent Colour
|
||||
|
||||
```css
|
||||
@theme {
|
||||
--color-accent: var(--color-blue-600);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Flux UI Docs](https://fluxui.dev)
|
||||
- [Heroicons](https://heroicons.com)
|
||||
- [Livewire 3](https://livewire.laravel.com)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
|
||||
---
|
||||
|
||||
**Total Components Documented:** 38
|
||||
**Last Updated:** January 2026
|
||||
248
docs/specs/ui/flux/README.md
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
# Flux UI Documentation
|
||||
|
||||
> **License:** Flux Pro - We have a licensed Flux Pro subscription
|
||||
|
||||
This directory contains local documentation for Flux UI, a composable component system for Livewire 3 applications. Flux prioritises simplicity through intuitive component design, whilst offering composability for complex use cases.
|
||||
|
||||
## What is Flux?
|
||||
|
||||
Flux is "not a set of UI Blade components, it's a system of them." It's designed to work seamlessly with Livewire 3 and Tailwind CSS 4, providing a foundation for building modern web applications with minimal friction.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Livewire 3 native** - Built specifically for Livewire 3
|
||||
- **Tailwind CSS 4** - Modern utility-first styling
|
||||
- **Composable** - Build complex components from simple building blocks
|
||||
- **Dark mode ready** - Built-in support for light/dark themes
|
||||
- **Accessible** - WCAG compliant, keyboard navigation, semantic HTML
|
||||
- **CSS-first approach** - Uses modern browser APIs like popover and dialog elements
|
||||
- **Type-safe** - Full type hints for IDE autocomplete
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Flux follows seven core principles:
|
||||
|
||||
1. **Simplicity** - Straightforward syntax and component usage
|
||||
2. **Complexity (Composability)** - Flexible alternatives for advanced use cases
|
||||
3. **Friendliness** - Familiar terminology over technical jargon
|
||||
4. **Composition** - Mix-and-match core components for flexible solutions
|
||||
5. **Consistency** - Predictable naming conventions and patterns
|
||||
6. **Brevity** - Short, clear names without excessive hyphenation
|
||||
7. **Modern Browser Leverage** - Native browser APIs over custom JavaScript
|
||||
|
||||
## Styling Philosophy
|
||||
|
||||
Flux uses the approach: **"We style, you space"**
|
||||
|
||||
- Flux components handle all component styling and internal padding
|
||||
- You manage margins and spacing in your layout
|
||||
- This maintains flexibility and context-appropriate spacing
|
||||
|
||||
## Getting Started
|
||||
|
||||
Start with the [Principles](./principles.md) to understand the core design patterns, then reference specific components as needed.
|
||||
|
||||
## Documentation Sections
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [principles.md](./principles.md) | Core concepts and design patterns |
|
||||
| [components.md](./components.md) | Complete component reference |
|
||||
| [navbar.md](./navbar.md) | Navbar, Navlist, and Navmenu components |
|
||||
| [layouts.md](./layouts.md) | Sidebar and Header layout patterns |
|
||||
| [styling.md](./styling.md) | Theming, customization, and dark mode |
|
||||
| [forms.md](./forms.md) | Form components (input, select, etc.) |
|
||||
| [button.md](./button.md) | Button component and variants |
|
||||
|
||||
## Core Components Summary
|
||||
|
||||
### Navigation
|
||||
- `flux:navbar` - Horizontal navigation
|
||||
- `flux:navlist` - Vertical sidebar navigation
|
||||
- `flux:navmenu` - Dropdown menus for navigation
|
||||
- `flux:dropdown` - Expandable dropdown menus
|
||||
- `flux:menu` - Complex action menus
|
||||
|
||||
### Forms & Inputs
|
||||
- `flux:input` - Text inputs with variants
|
||||
- `flux:select` - Dropdown select fields
|
||||
- `flux:checkbox` - Checkbox inputs
|
||||
- `flux:radio` - Radio button inputs
|
||||
- `flux:switch` - Toggle switches
|
||||
- `flux:textarea` - Multi-line text inputs
|
||||
- `flux:field` - Form field wrapper
|
||||
|
||||
### Layout
|
||||
- `flux:sidebar` - Persistent sidebar layout
|
||||
- `flux:header` - Top navigation header
|
||||
- `flux:card` - Content container
|
||||
|
||||
### Data Display
|
||||
- `flux:table` - Structured data with sorting/pagination
|
||||
- `flux:badge` - Status and category highlighting
|
||||
- `flux:avatar` - User profile images/initials
|
||||
- `flux:heading` - Hierarchical headings
|
||||
- `flux:text` - Formatted text
|
||||
|
||||
### Other
|
||||
- `flux:button` - Buttons with variants and icons
|
||||
- `flux:icon` - SVG icons
|
||||
- `flux:modal` - Dialog boxes
|
||||
- `flux:toast` - Notifications
|
||||
- `flux:tooltip` - Hover help text
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Basic Button
|
||||
```blade
|
||||
<flux:button>Click me</flux:button>
|
||||
<flux:button variant="primary" icon="check">Submit</flux:button>
|
||||
<flux:button size="sm" variant="ghost">Small</flux:button>
|
||||
```
|
||||
|
||||
### Basic Input
|
||||
```blade
|
||||
<flux:input wire:model="email" type="email" label="Email" />
|
||||
<flux:input label="Search" icon="magnifying-glass" placeholder="Search..." />
|
||||
```
|
||||
|
||||
### Navigation
|
||||
```blade
|
||||
<flux:navbar>
|
||||
<flux:navbar.item href="/" current icon="home">Home</flux:navbar.item>
|
||||
<flux:navbar.item href="/about" icon="information-circle">About</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
```
|
||||
|
||||
### Sidebar Layout
|
||||
```blade
|
||||
<div class="flex h-screen">
|
||||
<flux:sidebar sticky collapsible="mobile">
|
||||
<flux:sidebar.header>
|
||||
<flux:sidebar.brand>App Name</flux:sidebar.brand>
|
||||
</flux:sidebar.header>
|
||||
|
||||
<flux:sidebar.nav>
|
||||
<flux:sidebar.item href="/" icon="home" current>Dashboard</flux:sidebar.item>
|
||||
</flux:sidebar.nav>
|
||||
</flux:sidebar>
|
||||
|
||||
<main class="flex-1 overflow-auto">
|
||||
<!-- Page content -->
|
||||
</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Important Props Patterns
|
||||
|
||||
### Styling Props
|
||||
- `class` - Additional Tailwind classes (merged with component styles)
|
||||
- `variant` - Visual style variations (default, primary, filled, danger, ghost, subtle)
|
||||
- `size` - Sizing options (sm, default, lg)
|
||||
- `color` - Colour choice (zinc, red, blue, green, etc.)
|
||||
|
||||
### Icon Props
|
||||
- `icon` - Leading icon name (e.g., "home", "check")
|
||||
- `icon:trailing` - Trailing icon name
|
||||
- `icon:variant` - Icon style (outline, solid, mini, micro)
|
||||
|
||||
### State Props
|
||||
- `disabled` - Disables interaction
|
||||
- `readonly` - Locks content (input only)
|
||||
- `invalid` - Error state
|
||||
- `current` - Marks as active (navigation)
|
||||
|
||||
### Layout Props
|
||||
- `inset` - Negative margin adjustment
|
||||
- `sticky` - Fixed positioning
|
||||
- `collapsible` - Collapse/expand capability
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Flux handles dark mode automatically through CSS classes. Access dark mode utilities via:
|
||||
|
||||
```javascript
|
||||
$flux.appearance // User preference: 'light', 'dark', 'system'
|
||||
$flux.dark // Boolean: true/false
|
||||
```
|
||||
|
||||
Toggle dark mode in your app:
|
||||
```blade
|
||||
<flux:button x-data x-on:click="$flux.dark = ! $flux.dark" icon="moon" />
|
||||
```
|
||||
|
||||
## Customisation
|
||||
|
||||
Three approaches to customise Flux components:
|
||||
|
||||
### 1. Tailwind Classes (Simplest)
|
||||
```blade
|
||||
<flux:button class="max-w-sm">Click me</flux:button>
|
||||
```
|
||||
|
||||
### 2. Publish Components Locally
|
||||
```bash
|
||||
php artisan flux:publish # Interactive selection
|
||||
php artisan flux:publish --all # Publish everything
|
||||
```
|
||||
|
||||
This puts components in `resources/views/flux/` for full control.
|
||||
|
||||
### 3. Global CSS Overrides
|
||||
```css
|
||||
[data-flux-button] {
|
||||
@apply bg-zinc-800 dark:bg-zinc-400 hover:bg-zinc-700;
|
||||
}
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
Flux uses CSS variables for theming. Customize through `@theme` directive:
|
||||
|
||||
```css
|
||||
@theme {
|
||||
--color-accent: var(--color-blue-500);
|
||||
--color-accent-content: var(--color-blue-600);
|
||||
--color-accent-foreground: var(--color-white);
|
||||
}
|
||||
|
||||
@layer theme {
|
||||
.dark {
|
||||
--color-accent: var(--color-blue-500);
|
||||
--color-accent-content: var(--color-blue-400);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [styling.md](./styling.md) for complete theming guide.
|
||||
|
||||
## Links
|
||||
|
||||
- [Official Flux Documentation](https://fluxui.dev)
|
||||
- [GitHub Repository](https://github.com/livewire/flux)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
- [Livewire Documentation](https://livewire.laravel.com)
|
||||
|
||||
## Common Gotchas
|
||||
|
||||
1. **Blade Conditionals** - Can't use `@if` in component opening tags. Use dynamic attributes instead:
|
||||
```blade
|
||||
<!-- Wrong -->
|
||||
<flux:button @if($show) variant="primary" @endif></flux:button>
|
||||
|
||||
<!-- Right -->
|
||||
<flux:button :variant="$show ? 'primary' : 'default'"></flux:button>
|
||||
```
|
||||
|
||||
2. **Class Conflicts** - When user classes conflict with Flux styles, use `!` modifier:
|
||||
```blade
|
||||
<flux:button class="bg-zinc-800! hover:bg-zinc-700!">Custom</flux:button>
|
||||
```
|
||||
|
||||
3. **Component Opening Tags** - Dynamic expressions don't work. Use attributes with dynamic syntax.
|
||||
|
||||
4. **Data Attributes** - Components emit `data-flux-*` attributes for styling hooks, not customisation.
|
||||
|
||||
---
|
||||
|
||||
Last updated: January 2026 | Flux Pro License
|
||||
705
docs/specs/ui/flux/components.md
Normal file
|
|
@ -0,0 +1,705 @@
|
|||
# Flux UI - Component Reference
|
||||
|
||||
Complete list of all available Flux UI components organised by category.
|
||||
|
||||
## Navigation Components
|
||||
|
||||
### flux:navbar
|
||||
Horizontal navigation bar with automatic active page detection.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| - | - | - |
|
||||
|
||||
**Child Components:**
|
||||
- `flux:navbar.item` - Individual navigation link
|
||||
|
||||
**Props for navbar.item:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `href` | URL string | - |
|
||||
| `current` | boolean | auto-detected |
|
||||
| `icon` | Heroicon name | - |
|
||||
| `icon:trailing` | Heroicon name | - |
|
||||
| `badge` | string, boolean, slot | - |
|
||||
| `badge:color` | colour name | - |
|
||||
| `badge:variant` | solid, outline | solid |
|
||||
|
||||
```blade
|
||||
<flux:navbar>
|
||||
<flux:navbar.item href="/" icon="home" current>Home</flux:navbar.item>
|
||||
<flux:navbar.item href="/about" icon="information-circle">About</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
```
|
||||
|
||||
### flux:navlist
|
||||
Vertical sidebar navigation with collapsible groups.
|
||||
|
||||
**Child Components:**
|
||||
- `flux:navlist.item` - Navigation link
|
||||
- `flux:navlist.group` - Expandable section
|
||||
|
||||
**Props for navlist.item:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `href` | URL string | - |
|
||||
| `current` | boolean | auto-detected |
|
||||
| `icon` | Heroicon name | - |
|
||||
| `badge` | string, boolean | - |
|
||||
| `badge:color` | colour name | - |
|
||||
|
||||
**Props for navlist.group:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `heading` | string | - |
|
||||
| `expandable` | boolean | false |
|
||||
| `expanded` | boolean | false |
|
||||
|
||||
```blade
|
||||
<flux:navlist>
|
||||
<flux:navlist.item href="/" icon="home">Dashboard</flux:navlist.item>
|
||||
|
||||
<flux:navlist.group heading="Content" expandable>
|
||||
<flux:navlist.item href="/posts" icon="document">Posts</flux:navlist.item>
|
||||
<flux:navlist.item href="/pages" icon="document-duplicate">Pages</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
```
|
||||
|
||||
### flux:navmenu
|
||||
Dropdown menus within navigation items.
|
||||
|
||||
Combines with `flux:navbar.item` or `flux:navlist.item`:
|
||||
|
||||
```blade
|
||||
<flux:navbar.item>
|
||||
<flux:navmenu>
|
||||
<flux:button>Menu</flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item href="/option1">Option 1</flux:menu.item>
|
||||
<flux:menu.item href="/option2">Option 2</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:navmenu>
|
||||
</flux:navbar.item>
|
||||
```
|
||||
|
||||
### flux:dropdown
|
||||
Generic expandable menu for various use cases.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `position` | top, right, bottom, left | bottom |
|
||||
| `align` | start, center, end | start |
|
||||
| `offset` | pixels | 0 |
|
||||
| `gap` | pixels | 4 |
|
||||
|
||||
**Child Components:**
|
||||
- `flux:menu` - Menu container
|
||||
- `flux:menu.item` - Individual menu item
|
||||
- `flux:menu.separator` - Visual divider
|
||||
|
||||
```blade
|
||||
<flux:dropdown position="bottom" align="end">
|
||||
<flux:button>Actions</flux:button>
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="pencil">Edit</flux:menu.item>
|
||||
<flux:menu.item icon="trash" variant="danger">Delete</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
```
|
||||
|
||||
### flux:menu
|
||||
Complex action menus with keyboard navigation, submenus, and checkboxes.
|
||||
|
||||
**Props:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `keep-open` | boolean | false |
|
||||
|
||||
**Child Components:**
|
||||
- `flux:menu.item` - Menu item
|
||||
- `flux:menu.checkbox` - Checkbox option
|
||||
- `flux:menu.radio` - Radio option
|
||||
- `flux:menu.submenu` - Submenu section
|
||||
- `flux:menu.separator` - Divider
|
||||
- `flux:menu.group` - Grouped items
|
||||
|
||||
**Props for menu.item:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `icon` | Heroicon name | - |
|
||||
| `icon:trailing` | Heroicon name | - |
|
||||
| `icon:variant` | outline, solid, mini, micro | outline |
|
||||
| `kbd` | string | - |
|
||||
| `suffix` | string | - |
|
||||
| `variant` | default, danger | default |
|
||||
| `disabled` | boolean | false |
|
||||
| `keep-open` | boolean | false |
|
||||
|
||||
---
|
||||
|
||||
## Form Components
|
||||
|
||||
### flux:input
|
||||
Text input field with variants and icons.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `wire:model` | Livewire property | - |
|
||||
| `label` | string | - |
|
||||
| `description` | string | - |
|
||||
| `description:trailing` | string | - |
|
||||
| `type` | text, email, password, date, file, etc. | text |
|
||||
| `placeholder` | string | - |
|
||||
| `disabled` | boolean | false |
|
||||
| `readonly` | boolean | false |
|
||||
| `invalid` | boolean | false |
|
||||
| `size` | sm, xs | default |
|
||||
| `variant` | filled, outline | outline |
|
||||
| `icon` | Heroicon name | - |
|
||||
| `icon:trailing` | Heroicon name | - |
|
||||
| `clearable` | boolean | false |
|
||||
| `copyable` | boolean | false |
|
||||
| `viewable` | boolean | false (password toggle) |
|
||||
| `multiple` | boolean | false (file input) |
|
||||
| `mask` | Alpine mask pattern | - |
|
||||
|
||||
```blade
|
||||
<flux:input wire:model="email" type="email" label="Email" />
|
||||
<flux:input icon="magnifying-glass" placeholder="Search..." />
|
||||
<flux:input type="password" viewable label="Password" />
|
||||
```
|
||||
|
||||
### flux:select
|
||||
Dropdown select field.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `wire:model` | Livewire property | - |
|
||||
| `label` | string | - |
|
||||
| `description` | string | - |
|
||||
| `disabled` | boolean | false |
|
||||
| `invalid` | boolean | false |
|
||||
| `size` | sm | default |
|
||||
| `variant` | filled | outline |
|
||||
| `multiple` | boolean | false |
|
||||
| `searchable` | boolean | false |
|
||||
|
||||
**Slot Options:**
|
||||
```blade
|
||||
<flux:select wire:model="status" label="Status">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</flux:select>
|
||||
```
|
||||
|
||||
### flux:checkbox
|
||||
Individual checkbox input.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `wire:model` | Livewire property | - |
|
||||
| `value` | string/boolean | - |
|
||||
| `disabled` | boolean | false |
|
||||
|
||||
**Group variant: flux:checkbox.group**
|
||||
```blade
|
||||
<flux:checkbox.group wire:model="tags">
|
||||
<flux:checkbox value="php">PHP</flux:checkbox>
|
||||
<flux:checkbox value="laravel">Laravel</flux:checkbox>
|
||||
</flux:checkbox.group>
|
||||
```
|
||||
|
||||
### flux:radio
|
||||
Individual radio button.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `wire:model` | Livewire property | - |
|
||||
| `value` | string | - |
|
||||
| `disabled` | boolean | false |
|
||||
|
||||
**Group variant: flux:radio.group**
|
||||
```blade
|
||||
<flux:radio.group wire:model="visibility">
|
||||
<flux:radio value="public">Public</flux:radio>
|
||||
<flux:radio value="private">Private</flux:radio>
|
||||
</flux:radio.group>
|
||||
```
|
||||
|
||||
### flux:switch
|
||||
Toggle switch/checkbox.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `wire:model` | Livewire property | - |
|
||||
| `disabled` | boolean | false |
|
||||
|
||||
```blade
|
||||
<flux:switch wire:model="isActive" />
|
||||
```
|
||||
|
||||
### flux:textarea
|
||||
Multi-line text input.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `wire:model` | Livewire property | - |
|
||||
| `label` | string | - |
|
||||
| `description` | string | - |
|
||||
| `placeholder` | string | - |
|
||||
| `disabled` | boolean | false |
|
||||
| `readonly` | boolean | false |
|
||||
| `invalid` | boolean | false |
|
||||
| `rows` | number | 3 |
|
||||
|
||||
```blade
|
||||
<flux:textarea wire:model="bio" label="Bio" rows="5" />
|
||||
```
|
||||
|
||||
### flux:field
|
||||
Form field wrapper combining label, input, and descriptions.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| - | - | - |
|
||||
|
||||
**Child Components:**
|
||||
- `flux:label` - Field label
|
||||
- `flux:description` - Help text
|
||||
|
||||
```blade
|
||||
<flux:field>
|
||||
<flux:label>Email</flux:label>
|
||||
<flux:input type="email" wire:model="email" />
|
||||
<flux:description>We'll never share your email.</flux:description>
|
||||
</flux:field>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Button Components
|
||||
|
||||
### flux:button
|
||||
Powerful, composable button component.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `variant` | default, primary, filled, danger, ghost, subtle | default |
|
||||
| `size` | xs, sm, default, lg | default |
|
||||
| `color` | zinc, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose | zinc |
|
||||
| `icon` | Heroicon name | - |
|
||||
| `icon:trailing` | Heroicon name | - |
|
||||
| `icon:variant` | outline, solid, mini, micro | outline |
|
||||
| `href` | URL (renders as link) | - |
|
||||
| `disabled` | boolean | false |
|
||||
| `loading` | boolean | auto-detected with wire:click |
|
||||
| `square` | boolean | false |
|
||||
| `inset` | string (top, bottom, start, end) | - |
|
||||
| `as` | button, a, div | auto |
|
||||
| `tooltip` | string | - |
|
||||
| `tooltip:position` | top, right, bottom, left | - |
|
||||
|
||||
**Group variant: flux:button.group**
|
||||
```blade
|
||||
<flux:button.group>
|
||||
<flux:button>One</flux:button>
|
||||
<flux:button>Two</flux:button>
|
||||
</flux:button.group>
|
||||
```
|
||||
|
||||
```blade
|
||||
<flux:button>Default</flux:button>
|
||||
<flux:button variant="primary" icon="check">Submit</flux:button>
|
||||
<flux:button size="sm" variant="ghost">Small Ghost</flux:button>
|
||||
<flux:button href="/page">Link Button</flux:button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Display Components
|
||||
|
||||
### flux:table
|
||||
Structured data display with sorting and pagination.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `paginate` | Laravel paginator | - |
|
||||
| `container:class` | CSS classes | - |
|
||||
|
||||
**Child Components:**
|
||||
- `flux:table.columns` - Header row
|
||||
- `flux:table.column` - Column header
|
||||
- `flux:table.rows` - Body wrapper
|
||||
- `flux:table.row` - Data row
|
||||
- `flux:table.cell` - Individual cell
|
||||
|
||||
**Props for table.column:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `align` | start, center, end | start |
|
||||
| `sortable` | boolean | false |
|
||||
| `sorted` | boolean | false |
|
||||
| `direction` | asc, desc | - |
|
||||
| `sticky` | boolean | false |
|
||||
|
||||
**Props for table.cell:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `align` | start, center, end | start |
|
||||
| `variant` | default, strong | default |
|
||||
| `sticky` | boolean | false |
|
||||
|
||||
```blade
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>Name</flux:table.column>
|
||||
<flux:table.column align="center" sortable>Status</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($users as $user)
|
||||
<flux:table.row>
|
||||
<flux:table.cell>{{ $user->name }}</flux:table.cell>
|
||||
<flux:table.cell align="center">
|
||||
<flux:badge color="green">Active</flux:badge>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
```
|
||||
|
||||
### flux:badge
|
||||
Status and category highlighting.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `color` | zinc, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose | zinc |
|
||||
| `size` | sm, default, lg | default |
|
||||
| `variant` | default, pill, solid | default |
|
||||
| `icon` | Heroicon name | - |
|
||||
| `icon:trailing` | Heroicon name | - |
|
||||
| `icon:variant` | outline, solid, mini, micro | mini |
|
||||
| `inset` | string (top, bottom, start, end) | - |
|
||||
| `as` | button, div | div |
|
||||
|
||||
**Closeable variant: flux:badge.close**
|
||||
```blade
|
||||
<flux:badge color="blue">Active
|
||||
<flux:badge.close />
|
||||
</flux:badge>
|
||||
|
||||
<flux:badge variant="pill" color="green" size="lg">Success</flux:badge>
|
||||
<flux:badge variant="solid" color="red" icon="exclamation-triangle">Alert</flux:badge>
|
||||
```
|
||||
|
||||
### flux:avatar
|
||||
User profile images or initials.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `src` | image URL | - |
|
||||
| `initials` | string (1-2 chars) | - |
|
||||
| `alt` | text | - |
|
||||
| `size` | xs, sm, default, lg, xl | default |
|
||||
|
||||
```blade
|
||||
<flux:avatar src="/path/to/avatar.jpg" alt="User name" />
|
||||
<flux:avatar initials="JD" />
|
||||
```
|
||||
|
||||
### flux:heading
|
||||
Hierarchical text headings.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `level` | 1-6 | 2 |
|
||||
| `size` | default, lg, xl | default |
|
||||
|
||||
```blade
|
||||
<flux:heading level="1">Page Title</flux:heading>
|
||||
<flux:heading level="2" size="lg">Section Heading</flux:heading>
|
||||
```
|
||||
|
||||
### flux:text
|
||||
Formatted text content.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `size` | xs, sm, default, lg | default |
|
||||
| `variant` | default, muted | default |
|
||||
| `weight` | normal, medium, semibold, bold | normal |
|
||||
|
||||
```blade
|
||||
<flux:text size="sm" variant="muted">Helper text</flux:text>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout Components
|
||||
|
||||
### flux:card
|
||||
Content container.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `size` | sm | default |
|
||||
| `class` | CSS classes | - |
|
||||
|
||||
```blade
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading>Title</flux:heading>
|
||||
<p>Content goes here</p>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
### flux:sidebar
|
||||
Persistent sidebar layout.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `sticky` | boolean | false |
|
||||
| `collapsible` | boolean, mobile | false |
|
||||
| `breakpoint` | pixels | 1024 |
|
||||
| `persist` | boolean | true |
|
||||
|
||||
**Child Components:**
|
||||
- `flux:sidebar.header` - Top section
|
||||
- `flux:sidebar.brand` - Logo/branding
|
||||
- `flux:sidebar.collapse` - Collapse toggle
|
||||
- `flux:sidebar.search` - Search input
|
||||
- `flux:sidebar.nav` - Navigation container
|
||||
- `flux:sidebar.item` - Navigation link
|
||||
- `flux:sidebar.group` - Grouped items
|
||||
- `flux:sidebar.spacer` - Vertical spacer
|
||||
- `flux:sidebar.profile` - User profile section
|
||||
- `flux:sidebar.toggle` - Mobile toggle button
|
||||
|
||||
```blade
|
||||
<flux:sidebar sticky collapsible="mobile">
|
||||
<flux:sidebar.header>
|
||||
<flux:sidebar.brand>AppName</flux:sidebar.brand>
|
||||
<flux:sidebar.collapse />
|
||||
</flux:sidebar.header>
|
||||
|
||||
<flux:sidebar.nav>
|
||||
<flux:sidebar.item href="/" icon="home" current>Dashboard</flux:sidebar.item>
|
||||
</flux:sidebar.nav>
|
||||
</flux:sidebar>
|
||||
```
|
||||
|
||||
### flux:header
|
||||
Top navigation bar.
|
||||
|
||||
Can be used alongside sidebar for secondary navigation.
|
||||
|
||||
---
|
||||
|
||||
## Overlay Components
|
||||
|
||||
### flux:modal
|
||||
Dialog box overlay.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `name` | string | - |
|
||||
| `maxWidth` | sm, md, lg, xl, 2xl | xl |
|
||||
|
||||
```blade
|
||||
<flux:modal name="confirm-delete" maxWidth="sm">
|
||||
<flux:heading>Confirm Deletion</flux:heading>
|
||||
<p>Are you sure?</p>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:button variant="danger" wire:click="delete">Delete</flux:button>
|
||||
<flux:button variant="ghost" x-on:click="close()">Cancel</flux:button>
|
||||
</div>
|
||||
</flux:modal>
|
||||
```
|
||||
|
||||
### flux:popover
|
||||
Small overlay panel.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `position` | top, right, bottom, left | bottom |
|
||||
| `align` | start, center, end | start |
|
||||
|
||||
```blade
|
||||
<flux:popover>
|
||||
<flux:button>Show Info</flux:button>
|
||||
<div>Information content</div>
|
||||
</flux:popover>
|
||||
```
|
||||
|
||||
### flux:tooltip
|
||||
Hover help text.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `text` | string | - |
|
||||
| `position` | top, right, bottom, left | top |
|
||||
|
||||
```blade
|
||||
<flux:button tooltip="Save changes" tooltip:position="top">Save</flux:button>
|
||||
```
|
||||
|
||||
### flux:toast
|
||||
Notification messages.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `title` | string | - |
|
||||
| `description` | string | - |
|
||||
| `variant` | default, success, warning, danger | default |
|
||||
| `icon` | Heroicon name | - |
|
||||
| `actions` | array | - |
|
||||
|
||||
---
|
||||
|
||||
## Other Components
|
||||
|
||||
### flux:icon
|
||||
SVG icon display.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `name` | Heroicon name | - |
|
||||
| `variant` | outline, solid, mini, micro | outline |
|
||||
| `size` | xs, sm, default, lg, xl, 2xl | default |
|
||||
|
||||
```blade
|
||||
<flux:icon name="check-circle" variant="solid" size="lg" />
|
||||
```
|
||||
|
||||
### flux:accordion
|
||||
Collapsible content sections with smooth transitions and exclusive mode.
|
||||
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `variant` | reverse | - (icon after heading) |
|
||||
| `transition` | boolean | false |
|
||||
| `exclusive` | boolean | false |
|
||||
|
||||
**Child Components:**
|
||||
- `flux:accordion.item` - Accordion section
|
||||
- `flux:accordion.heading` - Item heading (or use `heading` prop shorthand)
|
||||
- `flux:accordion.content` - Item content
|
||||
|
||||
**Props for accordion.item:**
|
||||
| Prop | Values | Default |
|
||||
|------|--------|---------|
|
||||
| `heading` | string | - (shorthand for heading element) |
|
||||
| `expanded` | boolean | false |
|
||||
| `disabled` | boolean | false |
|
||||
|
||||
**Basic example:**
|
||||
```blade
|
||||
<flux:accordion>
|
||||
<flux:accordion.item>
|
||||
<flux:accordion.heading>What's your refund policy?</flux:accordion.heading>
|
||||
<flux:accordion.content>
|
||||
If you are not satisfied with your purchase, we offer a 30-day money-back guarantee.
|
||||
</flux:accordion.content>
|
||||
</flux:accordion.item>
|
||||
<flux:accordion.item>
|
||||
<flux:accordion.heading>Do you offer bulk discounts?</flux:accordion.heading>
|
||||
<flux:accordion.content>
|
||||
Yes, we offer special discounts for bulk orders. Contact our sales team.
|
||||
</flux:accordion.content>
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
**Shorthand syntax (heading prop):**
|
||||
```blade
|
||||
<flux:accordion.item heading="What's your refund policy?">
|
||||
If you are not satisfied with your purchase, we offer a 30-day money-back guarantee.
|
||||
</flux:accordion.item>
|
||||
```
|
||||
|
||||
**Exclusive mode (only one open at a time):**
|
||||
```blade
|
||||
<flux:accordion exclusive>
|
||||
<flux:accordion.item heading="Section 1">Content 1</flux:accordion.item>
|
||||
<flux:accordion.item heading="Section 2">Content 2</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
**With transitions:**
|
||||
```blade
|
||||
<flux:accordion transition>
|
||||
<flux:accordion.item heading="Smooth animation">
|
||||
This content expands and collapses smoothly.
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
**Reverse icon position:**
|
||||
```blade
|
||||
<flux:accordion variant="reverse">
|
||||
<flux:accordion.item heading="Icon on left">Content here</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
**Pre-expanded and disabled items:**
|
||||
```blade
|
||||
<flux:accordion>
|
||||
<flux:accordion.item heading="Open by default" expanded>
|
||||
This section starts expanded.
|
||||
</flux:accordion.item>
|
||||
<flux:accordion.item heading="Cannot be opened" disabled>
|
||||
This section cannot be expanded or collapsed.
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
### flux:separator
|
||||
Visual divider line.
|
||||
|
||||
```blade
|
||||
<div>Section 1</div>
|
||||
<flux:separator />
|
||||
<div>Section 2</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New Components (Recent Additions)
|
||||
|
||||
- **flux:composer** - Rich text editor
|
||||
- **flux:kanban** - Kanban board layout
|
||||
- **flux:otp-input** - One-time password input
|
||||
- **flux:pillbox** - Pill-shaped input tags
|
||||
- **flux:skeleton** - Loading placeholder
|
||||
- **flux:slider** - Range slider input
|
||||
|
||||
---
|
||||
|
||||
## Component Naming Convention
|
||||
|
||||
| Component Type | Naming Pattern | Examples |
|
||||
|----------------|----------------|----------|
|
||||
| Standalone | Single word | input, button, card |
|
||||
| Parent-child | Dot notation | accordion.item, table.cell |
|
||||
| Groups | Component.group | button.group, checkbox.group |
|
||||
| Subcomponents | dot notation | navbar.item, navlist.group |
|
||||
|
||||
---
|
||||
|
||||
## Props Categories
|
||||
|
||||
| Category | Applies To | Examples |
|
||||
|----------|-----------|----------|
|
||||
| **Styling** | Most components | variant, size, color, class |
|
||||
| **Icons** | Navigation, buttons, badges | icon, icon:trailing, icon:variant |
|
||||
| **Form** | Input components | wire:model, label, description, invalid |
|
||||
| **State** | Interactive components | disabled, readonly, current, active |
|
||||
| **Layout** | Layout components | sticky, collapsible, breakpoint |
|
||||
| **Positioning** | Dropdowns, popovers | position, align, offset, gap |
|
||||
|
||||
---
|
||||
|
||||
Last updated: January 2026
|
||||
140
docs/specs/ui/flux/components/accordion.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# flux:accordion
|
||||
|
||||
Collapsible content sections with smooth transitions and exclusive mode.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | string | - | Set to `reverse` to display icon before heading |
|
||||
| `transition` | boolean | false | Enable expanding transitions for smoother interactions |
|
||||
| `exclusive` | boolean | false | Only one accordion item can expand simultaneously |
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:accordion.item
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | string | - | Shorthand for heading content |
|
||||
| `expanded` | boolean | false | Expand item by default |
|
||||
| `disabled` | boolean | false | Prevent expansion/collapse |
|
||||
|
||||
### flux:accordion.heading
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `default` | Heading text content |
|
||||
|
||||
### flux:accordion.content
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `default` | Content displayed when expanded |
|
||||
|
||||
---
|
||||
|
||||
## Basic Example
|
||||
|
||||
```blade
|
||||
<flux:accordion>
|
||||
<flux:accordion.item>
|
||||
<flux:accordion.heading>What's your refund policy?</flux:accordion.heading>
|
||||
<flux:accordion.content>
|
||||
If you are not satisfied with your purchase, we offer a 30-day money-back guarantee. Please contact our support team for assistance.
|
||||
</flux:accordion.content>
|
||||
</flux:accordion.item>
|
||||
<flux:accordion.item>
|
||||
<flux:accordion.heading>Do you offer any discounts for bulk purchases?</flux:accordion.heading>
|
||||
<flux:accordion.content>
|
||||
Yes, we offer special discounts for bulk orders. Please reach out to our sales team with your requirements.
|
||||
</flux:accordion.content>
|
||||
</flux:accordion.item>
|
||||
<flux:accordion.item>
|
||||
<flux:accordion.heading>How do I track my order?</flux:accordion.heading>
|
||||
<flux:accordion.content>
|
||||
Once your order is shipped, you will receive an email with a tracking number. Use this number to track your order on our website.
|
||||
</flux:accordion.content>
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
## Shorthand Syntax
|
||||
|
||||
Use the `heading` prop instead of nested components:
|
||||
|
||||
```blade
|
||||
<flux:accordion.item heading="What's your refund policy?">
|
||||
If you are not satisfied with your purchase, we offer a 30-day money-back guarantee. Please contact our support team for assistance.
|
||||
</flux:accordion.item>
|
||||
```
|
||||
|
||||
## Exclusive Mode
|
||||
|
||||
Only one section open at a time:
|
||||
|
||||
```blade
|
||||
<flux:accordion exclusive>
|
||||
<flux:accordion.item heading="Section 1">Content 1</flux:accordion.item>
|
||||
<flux:accordion.item heading="Section 2">Content 2</flux:accordion.item>
|
||||
<flux:accordion.item heading="Section 3">Content 3</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
## With Transitions
|
||||
|
||||
Smooth expand/collapse animations:
|
||||
|
||||
```blade
|
||||
<flux:accordion transition>
|
||||
<flux:accordion.item heading="Smooth animation">
|
||||
This content expands and collapses smoothly.
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
## Reverse Icon Position
|
||||
|
||||
Icon on the left side:
|
||||
|
||||
```blade
|
||||
<flux:accordion variant="reverse">
|
||||
<flux:accordion.item heading="Icon on left">Content here</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
## Pre-expanded Items
|
||||
|
||||
```blade
|
||||
<flux:accordion>
|
||||
<flux:accordion.item heading="Open by default" expanded>
|
||||
This section starts expanded.
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
## Disabled Items
|
||||
|
||||
```blade
|
||||
<flux:accordion>
|
||||
<flux:accordion.item heading="Cannot be opened" disabled>
|
||||
This section cannot be expanded or collapsed.
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
|
||||
## Combined Example
|
||||
|
||||
```blade
|
||||
<flux:accordion exclusive transition>
|
||||
<flux:accordion.item heading="First section" expanded>
|
||||
This starts open and others close when you open them.
|
||||
</flux:accordion.item>
|
||||
<flux:accordion.item heading="Second section">
|
||||
Click to open, first will close.
|
||||
</flux:accordion.item>
|
||||
<flux:accordion.item heading="Disabled section" disabled>
|
||||
Cannot interact with this.
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
```
|
||||
112
docs/specs/ui/flux/components/autocomplete.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# flux:autocomplete
|
||||
|
||||
Text input with suggestions that insert directly into the field as users type.
|
||||
|
||||
> **Note:** Does not support a `value` attribute. For scenarios requiring label display with stored values (e.g., user names with IDs), use `flux:select` with `searchable` instead.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Livewire property binding for input value |
|
||||
| `type` | string | text | HTML input type (text, email, password, file, date) |
|
||||
| `label` | string | - | Label text above input |
|
||||
| `description` | string | - | Descriptive text below label |
|
||||
| `placeholder` | string | - | Text shown when empty |
|
||||
| `size` | string | - | Input sizing: `sm`, `xs` |
|
||||
| `variant` | string | outline | Visual style: `outline`, `filled` |
|
||||
| `disabled` | boolean | false | Disables user interaction |
|
||||
| `readonly` | boolean | false | Makes input read-only |
|
||||
| `invalid` | boolean | false | Applies error styling |
|
||||
| `multiple` | boolean | false | Allows multiple file selection |
|
||||
| `mask` | string | - | Alpine mask plugin pattern |
|
||||
| `icon` | string | - | Leading icon name |
|
||||
| `icon:trailing` | string | - | Trailing icon name |
|
||||
| `kbd` | string | - | Keyboard shortcut hint |
|
||||
| `clearable` | boolean | false | Shows clear button when filled |
|
||||
| `copyable` | boolean | false | Displays copy button |
|
||||
| `viewable` | boolean | false | Password toggle (password inputs) |
|
||||
| `as` | string | input | Render element: `input`, `button` |
|
||||
| `container:class` | string | - | CSS classes on container |
|
||||
| `class:input` | string | - | CSS classes on input element |
|
||||
|
||||
## Child Component: flux:autocomplete.item
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `disabled` | boolean | false | Prevents item selection |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `icon` / `icon:leading` | Custom leading content |
|
||||
| `icon:trailing` | Custom trailing content |
|
||||
|
||||
---
|
||||
|
||||
## Basic Example
|
||||
|
||||
```blade
|
||||
<flux:autocomplete wire:model="state" label="State of residence">
|
||||
<flux:autocomplete.item>Alabama</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Arkansas</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>California</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Colorado</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Connecticut</flux:autocomplete.item>
|
||||
</flux:autocomplete>
|
||||
```
|
||||
|
||||
## With Placeholder
|
||||
|
||||
```blade
|
||||
<flux:autocomplete wire:model="city" placeholder="Start typing a city...">
|
||||
<flux:autocomplete.item>London</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Manchester</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Birmingham</flux:autocomplete.item>
|
||||
</flux:autocomplete>
|
||||
```
|
||||
|
||||
## With Icon
|
||||
|
||||
```blade
|
||||
<flux:autocomplete wire:model="search" icon="magnifying-glass" placeholder="Search...">
|
||||
<flux:autocomplete.item>Result 1</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Result 2</flux:autocomplete.item>
|
||||
</flux:autocomplete>
|
||||
```
|
||||
|
||||
## Disabled Items
|
||||
|
||||
```blade
|
||||
<flux:autocomplete wire:model="option">
|
||||
<flux:autocomplete.item>Available option</flux:autocomplete.item>
|
||||
<flux:autocomplete.item disabled>Unavailable option</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Another option</flux:autocomplete.item>
|
||||
</flux:autocomplete>
|
||||
```
|
||||
|
||||
## Small Size
|
||||
|
||||
```blade
|
||||
<flux:autocomplete wire:model="query" size="sm" placeholder="Quick search...">
|
||||
<flux:autocomplete.item>Option A</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Option B</flux:autocomplete.item>
|
||||
</flux:autocomplete>
|
||||
```
|
||||
|
||||
## Clearable
|
||||
|
||||
```blade
|
||||
<flux:autocomplete wire:model="filter" clearable>
|
||||
<flux:autocomplete.item>Filter 1</flux:autocomplete.item>
|
||||
<flux:autocomplete.item>Filter 2</flux:autocomplete.item>
|
||||
</flux:autocomplete>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Input](./input.md) - Standard text field
|
||||
- [Select](./select.md) - Dropdown for single/multiple option selection with values
|
||||
186
docs/specs/ui/flux/components/avatar.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# flux:avatar
|
||||
|
||||
Display an image, initials, or icon as a user avatar.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `name` | string | - | User's name; auto-generates initials if no `initials` provided |
|
||||
| `src` | string | - | Image URL for avatar display |
|
||||
| `initials` | string | - | Custom initials; overrides `name` if provided |
|
||||
| `initials:single` | boolean | false | Use single initial from name |
|
||||
| `alt` | string | name | Alternative text for image |
|
||||
| `size` | string | 40px | `xs` (24px), `sm` (32px), default (40px), `lg` (48px), `xl` (64px) |
|
||||
| `color` | string | - | Background colour for initials/icon avatars |
|
||||
| `color:seed` | mixed | - | Deterministic colour generation with `color="auto"` |
|
||||
| `circle` | boolean | false | Makes avatar fully circular |
|
||||
| `icon` | string | - | Icon name instead of image/initials |
|
||||
| `icon:variant` | string | solid | `outline` or `solid` |
|
||||
| `as` | string | div | Render as: `button`, `div` |
|
||||
| `href` | string | - | Makes avatar a link |
|
||||
|
||||
### Colour Options
|
||||
|
||||
`zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, `auto`
|
||||
|
||||
## Badge Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `badge` | string\|boolean\|slot | - | Badge content |
|
||||
| `badge:color` | string | - | Same colour options as `color` prop |
|
||||
| `badge:circle` | boolean | false | Fully circular badge |
|
||||
| `badge:position` | string | bottom right | `top left`, `top right`, `bottom left`, `bottom right` |
|
||||
| `badge:variant` | string | solid | `solid` or `outline` |
|
||||
|
||||
## Tooltip Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `tooltip` | string\|boolean | - | Hover tooltip text; `true` uses `name` |
|
||||
| `tooltip:position` | string | top | `top`, `right`, `bottom`, `left` |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `default` | Custom content overriding initials |
|
||||
| `badge` | Complex badge content |
|
||||
|
||||
---
|
||||
|
||||
## Basic Image
|
||||
|
||||
```blade
|
||||
<flux:avatar src="/path/to/avatar.jpg" />
|
||||
```
|
||||
|
||||
## With Name (Auto Initials)
|
||||
|
||||
```blade
|
||||
<flux:avatar name="Caleb Porzio" />
|
||||
{{-- Generates "CP" --}}
|
||||
```
|
||||
|
||||
## Single Initial
|
||||
|
||||
```blade
|
||||
<flux:avatar name="calebporzio" initials:single />
|
||||
{{-- Generates "C" --}}
|
||||
```
|
||||
|
||||
## Custom Initials
|
||||
|
||||
```blade
|
||||
<flux:avatar initials="JD" />
|
||||
```
|
||||
|
||||
## Icon Avatar
|
||||
|
||||
```blade
|
||||
<flux:avatar icon="user" />
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
```blade
|
||||
<flux:avatar src="/avatar.jpg" size="xs" /> {{-- 24px --}}
|
||||
<flux:avatar src="/avatar.jpg" size="sm" /> {{-- 32px --}}
|
||||
<flux:avatar src="/avatar.jpg" /> {{-- 40px default --}}
|
||||
<flux:avatar src="/avatar.jpg" size="lg" /> {{-- 48px --}}
|
||||
<flux:avatar src="/avatar.jpg" size="xl" /> {{-- 64px --}}
|
||||
```
|
||||
|
||||
## Colours
|
||||
|
||||
```blade
|
||||
<flux:avatar name="John Doe" color="blue" />
|
||||
<flux:avatar name="Jane Smith" color="green" />
|
||||
<flux:avatar name="Bob Wilson" color="auto" />
|
||||
{{-- auto assigns consistent colour based on name --}}
|
||||
```
|
||||
|
||||
## Deterministic Colour
|
||||
|
||||
```blade
|
||||
<flux:avatar name="User" color="auto" color:seed="{{ $user->id }}" />
|
||||
{{-- Same user ID always gets same colour --}}
|
||||
```
|
||||
|
||||
## Circular
|
||||
|
||||
```blade
|
||||
<flux:avatar src="/avatar.jpg" circle />
|
||||
```
|
||||
|
||||
## With Tooltip
|
||||
|
||||
```blade
|
||||
<flux:avatar tooltip="John Doe" src="/avatar.jpg" />
|
||||
|
||||
{{-- Or use name as tooltip --}}
|
||||
<flux:avatar tooltip name="John Doe" src="/avatar.jpg" />
|
||||
|
||||
{{-- Positioned tooltip --}}
|
||||
<flux:avatar tooltip="John Doe" tooltip:position="right" src="/avatar.jpg" />
|
||||
```
|
||||
|
||||
## With Badge
|
||||
|
||||
```blade
|
||||
{{-- Dot indicator --}}
|
||||
<flux:avatar src="/avatar.jpg" badge />
|
||||
|
||||
{{-- Coloured dot --}}
|
||||
<flux:avatar src="/avatar.jpg" badge badge:color="green" />
|
||||
|
||||
{{-- Numeric badge --}}
|
||||
<flux:avatar src="/avatar.jpg" badge="5" />
|
||||
|
||||
{{-- Positioned badge --}}
|
||||
<flux:avatar src="/avatar.jpg" badge badge:position="top right" />
|
||||
|
||||
{{-- Emoji badge --}}
|
||||
<flux:avatar src="/avatar.jpg">
|
||||
<x-slot:badge>
|
||||
<span class="text-xs">🎉</span>
|
||||
</x-slot:badge>
|
||||
</flux:avatar>
|
||||
```
|
||||
|
||||
## As Link
|
||||
|
||||
```blade
|
||||
<flux:avatar href="/profile" src="/avatar.jpg" />
|
||||
```
|
||||
|
||||
## As Button
|
||||
|
||||
```blade
|
||||
<flux:avatar as="button" src="/avatar.jpg" wire:click="openProfile" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Avatar Group
|
||||
|
||||
Group multiple avatars with overlap styling:
|
||||
|
||||
```blade
|
||||
<flux:avatar.group>
|
||||
<flux:avatar src="/avatar1.jpg" />
|
||||
<flux:avatar src="/avatar2.jpg" />
|
||||
<flux:avatar src="/avatar3.jpg" />
|
||||
<flux:avatar initials="+5" />
|
||||
</flux:avatar.group>
|
||||
```
|
||||
|
||||
Customise ring colour:
|
||||
|
||||
```blade
|
||||
<flux:avatar.group class="*:ring-zinc-100">
|
||||
<flux:avatar src="/avatar1.jpg" />
|
||||
<flux:avatar src="/avatar2.jpg" />
|
||||
</flux:avatar.group>
|
||||
```
|
||||
127
docs/specs/ui/flux/components/badge.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# flux:badge
|
||||
|
||||
Highlight information like status, category, or count.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `color` | string | zinc | Background/text colour |
|
||||
| `size` | string | default | `sm`, default, `lg` |
|
||||
| `variant` | string | default | `pill`, `solid` |
|
||||
| `icon` | string | - | Leading icon name |
|
||||
| `icon:trailing` | string | - | Trailing icon name |
|
||||
| `icon:variant` | string | mini | `outline`, `solid`, `mini`, `micro` |
|
||||
| `as` | string | div | `button`, `div` |
|
||||
| `inset` | string | - | Negative margin: `top`, `bottom`, `left`, `right` (combinable) |
|
||||
|
||||
### Colour Options
|
||||
|
||||
`zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`
|
||||
|
||||
## Child Component: flux:badge.close
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `icon` | string | x-mark | Close icon name |
|
||||
| `icon:variant` | string | mini | `outline`, `solid`, `mini`, `micro` |
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:badge>Default</flux:badge>
|
||||
<flux:badge color="lime">New</flux:badge>
|
||||
<flux:badge color="red">Error</flux:badge>
|
||||
<flux:badge color="green">Success</flux:badge>
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
```blade
|
||||
<flux:badge size="sm">Small</flux:badge>
|
||||
<flux:badge>Default</flux:badge>
|
||||
<flux:badge size="lg">Large</flux:badge>
|
||||
```
|
||||
|
||||
## With Icons
|
||||
|
||||
```blade
|
||||
<flux:badge icon="user-circle">Users</flux:badge>
|
||||
<flux:badge icon="document-text">Files</flux:badge>
|
||||
<flux:badge icon:trailing="video-camera">Videos</flux:badge>
|
||||
```
|
||||
|
||||
## Pill Variant
|
||||
|
||||
```blade
|
||||
<flux:badge variant="pill">Default pill</flux:badge>
|
||||
<flux:badge variant="pill" color="blue">Blue pill</flux:badge>
|
||||
<flux:badge variant="pill" icon="user">With icon</flux:badge>
|
||||
```
|
||||
|
||||
## Solid Variant
|
||||
|
||||
```blade
|
||||
<flux:badge variant="solid" color="green">Solid green</flux:badge>
|
||||
<flux:badge variant="solid" color="red">Solid red</flux:badge>
|
||||
```
|
||||
|
||||
## As Button
|
||||
|
||||
```blade
|
||||
<flux:badge as="button" variant="pill" icon="plus" size="lg">
|
||||
Add item
|
||||
</flux:badge>
|
||||
```
|
||||
|
||||
## With Close Button
|
||||
|
||||
```blade
|
||||
<flux:badge>
|
||||
Admin
|
||||
<flux:badge.close wire:click="removeRole('admin')" />
|
||||
</flux:badge>
|
||||
|
||||
<flux:badge color="blue">
|
||||
Tag name
|
||||
<flux:badge.close />
|
||||
</flux:badge>
|
||||
```
|
||||
|
||||
## Inset Spacing
|
||||
|
||||
Use `inset` to add negative margin when badge is inline with text:
|
||||
|
||||
```blade
|
||||
<flux:heading>
|
||||
Page builder
|
||||
<flux:badge color="lime" inset="top bottom">New</flux:badge>
|
||||
</flux:heading>
|
||||
```
|
||||
|
||||
## Multiple Badges
|
||||
|
||||
```blade
|
||||
<div class="flex gap-2">
|
||||
<flux:badge color="green">Active</flux:badge>
|
||||
<flux:badge color="blue">Featured</flux:badge>
|
||||
<flux:badge color="purple">Premium</flux:badge>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Status Indicators
|
||||
|
||||
```blade
|
||||
<flux:badge variant="pill" color="green" icon="check-circle">Completed</flux:badge>
|
||||
<flux:badge variant="pill" color="yellow" icon="clock">Pending</flux:badge>
|
||||
<flux:badge variant="pill" color="red" icon="x-circle">Failed</flux:badge>
|
||||
```
|
||||
|
||||
## Count Badges
|
||||
|
||||
```blade
|
||||
<flux:badge variant="solid" color="red">99+</flux:badge>
|
||||
<flux:badge variant="pill" color="blue">12</flux:badge>
|
||||
```
|
||||
120
docs/specs/ui/flux/components/brand.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# flux:brand
|
||||
|
||||
Display a company or application logo and name consistently across interfaces.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `name` | string | - | Company or application name to display |
|
||||
| `logo` | string | - | URL to image for logo |
|
||||
| `alt` | string | - | Alternative text for the logo |
|
||||
| `href` | string | / | URL to navigate to when clicked |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `logo` | Custom content for logo section (image, SVG, or HTML) |
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:brand href="/" logo="/img/logo.png" name="Acme Inc." />
|
||||
```
|
||||
|
||||
## Logo Only (No Name)
|
||||
|
||||
```blade
|
||||
<flux:brand href="/" logo="/img/logo.png" />
|
||||
```
|
||||
|
||||
## Custom Logo Slot
|
||||
|
||||
```blade
|
||||
<flux:brand href="/" name="Acme Inc.">
|
||||
<x-slot name="logo">
|
||||
<div class="size-6 rounded bg-accent text-accent-foreground flex items-center justify-center">
|
||||
<i class="font-serif font-bold">A</i>
|
||||
</div>
|
||||
</x-slot>
|
||||
</flux:brand>
|
||||
```
|
||||
|
||||
## Icon-Based Logo
|
||||
|
||||
```blade
|
||||
<flux:brand href="/" name="Launchpad">
|
||||
<x-slot name="logo" class="size-6 rounded-full bg-cyan-500 text-white text-xs font-bold">
|
||||
<flux:icon name="rocket-launch" variant="micro" />
|
||||
</x-slot>
|
||||
</flux:brand>
|
||||
```
|
||||
|
||||
## SVG Logo
|
||||
|
||||
```blade
|
||||
<flux:brand href="/" name="My App">
|
||||
<x-slot name="logo">
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<!-- SVG content -->
|
||||
</svg>
|
||||
</x-slot>
|
||||
</flux:brand>
|
||||
```
|
||||
|
||||
## In Header Layout
|
||||
|
||||
```blade
|
||||
<flux:header class="px-4! w-full bg-zinc-50 dark:bg-zinc-800 rounded-lg border">
|
||||
<flux:brand href="/" name="Acme Inc.">
|
||||
<x-slot name="logo" class="bg-accent text-accent-foreground">
|
||||
<i class="font-serif font-bold">A</i>
|
||||
</x-slot>
|
||||
</flux:brand>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navbar>
|
||||
<flux:navbar.item href="/dashboard">Dashboard</flux:navbar.item>
|
||||
<flux:navbar.item href="/settings">Settings</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
</flux:header>
|
||||
```
|
||||
|
||||
## In Sidebar
|
||||
|
||||
```blade
|
||||
<flux:sidebar>
|
||||
<flux:sidebar.header>
|
||||
<flux:sidebar.brand name="Acme Inc." logo="/img/logo.png" href="/" />
|
||||
<flux:sidebar.collapse />
|
||||
</flux:sidebar.header>
|
||||
|
||||
<flux:sidebar.nav>
|
||||
<!-- Navigation items -->
|
||||
</flux:sidebar.nav>
|
||||
</flux:sidebar>
|
||||
```
|
||||
|
||||
## Dark Mode Logo
|
||||
|
||||
For different logos in light/dark mode, use CSS or separate elements:
|
||||
|
||||
```blade
|
||||
<flux:brand href="/" name="My App">
|
||||
<x-slot name="logo">
|
||||
<img src="/logo-light.svg" class="size-6 dark:hidden" alt="Logo" />
|
||||
<img src="/logo-dark.svg" class="size-6 hidden dark:block" alt="Logo" />
|
||||
</x-slot>
|
||||
</flux:brand>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Header](./header.md) - Top navigation headers
|
||||
- [Sidebar](./sidebar.md) - Sidebar navigation
|
||||
133
docs/specs/ui/flux/components/breadcrumbs.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# flux:breadcrumbs
|
||||
|
||||
Help users navigate and understand their location within an application.
|
||||
|
||||
## Components
|
||||
|
||||
### flux:breadcrumbs
|
||||
|
||||
Container for breadcrumb items.
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `default` | Contains the breadcrumb items |
|
||||
|
||||
### flux:breadcrumbs.item
|
||||
|
||||
Individual breadcrumb link or text.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `href` | string | - | URL the item links to; omit for non-clickable text |
|
||||
| `icon` | string | - | Icon name to display before text |
|
||||
| `icon:variant` | string | mini | `outline`, `solid`, `mini`, `micro` |
|
||||
| `separator` | string | chevron-right | Separator icon: `chevron-right`, `slash` |
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/">Home</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item href="/blog">Blog</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item>Post</flux:breadcrumbs.item>
|
||||
</flux:breadcrumbs>
|
||||
```
|
||||
|
||||
## With Slash Separators
|
||||
|
||||
```blade
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/" separator="slash">Home</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item href="/blog" separator="slash">Blog</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item separator="slash">Post</flux:breadcrumbs.item>
|
||||
</flux:breadcrumbs>
|
||||
```
|
||||
|
||||
## With Home Icon
|
||||
|
||||
```blade
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/" icon="home" />
|
||||
<flux:breadcrumbs.item href="/blog">Blog</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item>Post</flux:breadcrumbs.item>
|
||||
</flux:breadcrumbs>
|
||||
```
|
||||
|
||||
## With Ellipsis
|
||||
|
||||
For deep navigation, use ellipsis to collapse middle items:
|
||||
|
||||
```blade
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/" icon="home" />
|
||||
<flux:breadcrumbs.item icon="ellipsis-horizontal" />
|
||||
<flux:breadcrumbs.item>Post</flux:breadcrumbs.item>
|
||||
</flux:breadcrumbs>
|
||||
```
|
||||
|
||||
## With Ellipsis Dropdown
|
||||
|
||||
Expandable ellipsis showing hidden items:
|
||||
|
||||
```blade
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/" icon="home" />
|
||||
<flux:breadcrumbs.item>
|
||||
<flux:dropdown>
|
||||
<flux:button icon="ellipsis-horizontal" variant="ghost" size="sm" />
|
||||
<flux:navmenu>
|
||||
<flux:navmenu.item href="/clients">Client</flux:navmenu.item>
|
||||
<flux:navmenu.item href="/clients/team" icon="arrow-turn-down-right">Team</flux:navmenu.item>
|
||||
<flux:navmenu.item href="/clients/team/user" icon="arrow-turn-down-right">User</flux:navmenu.item>
|
||||
</flux:navmenu>
|
||||
</flux:dropdown>
|
||||
</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item>Post</flux:breadcrumbs.item>
|
||||
</flux:breadcrumbs>
|
||||
```
|
||||
|
||||
## Dynamic Breadcrumbs
|
||||
|
||||
```blade
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/" icon="home" />
|
||||
|
||||
@foreach ($breadcrumbs as $crumb)
|
||||
@if ($loop->last)
|
||||
<flux:breadcrumbs.item>{{ $crumb['name'] }}</flux:breadcrumbs.item>
|
||||
@else
|
||||
<flux:breadcrumbs.item href="{{ $crumb['url'] }}">{{ $crumb['name'] }}</flux:breadcrumbs.item>
|
||||
@endif
|
||||
@endforeach
|
||||
</flux:breadcrumbs>
|
||||
```
|
||||
|
||||
## In Header
|
||||
|
||||
```blade
|
||||
<flux:header>
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/" icon="home" />
|
||||
<flux:breadcrumbs.item href="/settings">Settings</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item>Profile</flux:breadcrumbs.item>
|
||||
</flux:breadcrumbs>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:button variant="primary">Save</flux:button>
|
||||
</flux:header>
|
||||
```
|
||||
|
||||
## Longer Path
|
||||
|
||||
```blade
|
||||
<flux:breadcrumbs>
|
||||
<flux:breadcrumbs.item href="/" icon="home" />
|
||||
<flux:breadcrumbs.item href="/products">Products</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item href="/products/electronics">Electronics</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item href="/products/electronics/phones">Phones</flux:breadcrumbs.item>
|
||||
<flux:breadcrumbs.item>iPhone 15</flux:breadcrumbs.item>
|
||||
</flux:breadcrumbs>
|
||||
```
|
||||
218
docs/specs/ui/flux/components/button.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# flux:button
|
||||
|
||||
Powerful, composable button component with variants, icons, loading states, and tooltips.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `as` | string | button | Render as: `button`, `a`, `div` |
|
||||
| `href` | string | - | URL (renders as link) |
|
||||
| `type` | string | button | `button`, `submit` |
|
||||
| `variant` | string | outline | `outline`, `primary`, `filled`, `danger`, `ghost`, `subtle` |
|
||||
| `size` | string | base | `base`, `sm`, `xs` |
|
||||
| `color` | string | - | Tailwind colour name |
|
||||
| `icon` | string | - | Leading icon name |
|
||||
| `icon:trailing` | string | - | Trailing icon name |
|
||||
| `icon:variant` | string | micro | `outline`, `solid`, `mini`, `micro` |
|
||||
| `square` | boolean | false | Equal width/height (auto for icon-only) |
|
||||
| `align` | string | center | `start`, `center`, `end` |
|
||||
| `inset` | string | - | Negative margin: `top`, `bottom`, `left`, `right` (combinable) |
|
||||
| `loading` | boolean | true | Show loading indicator on `wire:click` |
|
||||
| `disabled` | boolean | false | Disable interaction |
|
||||
| `tooltip` | string | - | Hover tooltip text |
|
||||
| `tooltip:position` | string | top | `top`, `bottom`, `left`, `right` |
|
||||
| `tooltip:kbd` | string | - | Keyboard shortcut in tooltip |
|
||||
| `kbd` | string | - | Keyboard shortcut hint |
|
||||
|
||||
### Colour Options
|
||||
|
||||
`zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:button>Button</flux:button>
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
```blade
|
||||
<flux:button variant="outline">Outline</flux:button>
|
||||
<flux:button variant="primary">Primary</flux:button>
|
||||
<flux:button variant="filled">Filled</flux:button>
|
||||
<flux:button variant="danger">Danger</flux:button>
|
||||
<flux:button variant="ghost">Ghost</flux:button>
|
||||
<flux:button variant="subtle">Subtle</flux:button>
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
```blade
|
||||
<flux:button size="xs">Extra small</flux:button>
|
||||
<flux:button size="sm">Small</flux:button>
|
||||
<flux:button>Base (default)</flux:button>
|
||||
```
|
||||
|
||||
## Colours
|
||||
|
||||
```blade
|
||||
<flux:button color="indigo">Indigo</flux:button>
|
||||
<flux:button variant="primary" color="green">Green Primary</flux:button>
|
||||
<flux:button variant="filled" color="red">Red Filled</flux:button>
|
||||
```
|
||||
|
||||
## With Icons
|
||||
|
||||
```blade
|
||||
<flux:button icon="check">Save</flux:button>
|
||||
<flux:button icon:trailing="arrow-right">Next</flux:button>
|
||||
<flux:button icon="pencil" icon:trailing="chevron-down">Edit</flux:button>
|
||||
```
|
||||
|
||||
## Icon Only
|
||||
|
||||
```blade
|
||||
<flux:button icon="plus" />
|
||||
<flux:button icon="trash" variant="danger" />
|
||||
<flux:button icon="cog-6-tooth" variant="ghost" />
|
||||
```
|
||||
|
||||
## As Link
|
||||
|
||||
```blade
|
||||
<flux:button href="/dashboard">Go to Dashboard</flux:button>
|
||||
<flux:button href="/docs" icon="book-open">Documentation</flux:button>
|
||||
```
|
||||
|
||||
## Full Width
|
||||
|
||||
```blade
|
||||
<flux:button class="w-full">Full Width Button</flux:button>
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
Automatic loading indicator with `wire:click`:
|
||||
|
||||
```blade
|
||||
<flux:button wire:click="save">Save changes</flux:button>
|
||||
```
|
||||
|
||||
Disable auto-loading:
|
||||
|
||||
```blade
|
||||
<flux:button wire:click="save" loading="false">Save</flux:button>
|
||||
```
|
||||
|
||||
## With Tooltip
|
||||
|
||||
```blade
|
||||
<flux:button icon="cog-6-tooth" tooltip="Settings" />
|
||||
<flux:button icon="trash" tooltip="Delete item" tooltip:position="bottom" />
|
||||
```
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
In button:
|
||||
|
||||
```blade
|
||||
<flux:button kbd="⌘S">Save</flux:button>
|
||||
```
|
||||
|
||||
In tooltip:
|
||||
|
||||
```blade
|
||||
<flux:button icon="magnifying-glass" tooltip="Search" tooltip:kbd="⌘K" />
|
||||
```
|
||||
|
||||
## Disabled
|
||||
|
||||
```blade
|
||||
<flux:button disabled>Disabled</flux:button>
|
||||
<flux:button variant="primary" disabled>Disabled Primary</flux:button>
|
||||
```
|
||||
|
||||
## Inset (Negative Margin)
|
||||
|
||||
For buttons at edges of containers:
|
||||
|
||||
```blade
|
||||
<flux:button variant="ghost" inset="left">Back</flux:button>
|
||||
<flux:button variant="ghost" inset="right">Next</flux:button>
|
||||
```
|
||||
|
||||
## Form Submit
|
||||
|
||||
```blade
|
||||
<form wire:submit="save">
|
||||
<flux:input wire:model="name" label="Name" />
|
||||
<flux:button type="submit" variant="primary">Save</flux:button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Button Group
|
||||
|
||||
Group multiple buttons with shared borders:
|
||||
|
||||
```blade
|
||||
<flux:button.group>
|
||||
<flux:button>Left</flux:button>
|
||||
<flux:button>Centre</flux:button>
|
||||
<flux:button>Right</flux:button>
|
||||
</flux:button.group>
|
||||
```
|
||||
|
||||
With icons:
|
||||
|
||||
```blade
|
||||
<flux:button.group>
|
||||
<flux:button icon="bold" />
|
||||
<flux:button icon="italic" />
|
||||
<flux:button icon="underline" />
|
||||
</flux:button.group>
|
||||
```
|
||||
|
||||
With variants:
|
||||
|
||||
```blade
|
||||
<flux:button.group>
|
||||
<flux:button variant="primary">Save</flux:button>
|
||||
<flux:button icon="chevron-down" />
|
||||
</flux:button.group>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Save/Cancel
|
||||
|
||||
```blade
|
||||
<div class="flex gap-2">
|
||||
<flux:button variant="ghost">Cancel</flux:button>
|
||||
<flux:button variant="primary">Save</flux:button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Danger Action
|
||||
|
||||
```blade
|
||||
<flux:button variant="danger" icon="trash">Delete</flux:button>
|
||||
```
|
||||
|
||||
### Icon with Dropdown
|
||||
|
||||
```blade
|
||||
<flux:dropdown>
|
||||
<flux:button icon="ellipsis-horizontal" variant="ghost" />
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="pencil">Edit</flux:menu.item>
|
||||
<flux:menu.item icon="trash" variant="danger">Delete</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
```
|
||||
210
docs/specs/ui/flux/components/calendar.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# flux:calendar
|
||||
|
||||
Flexible calendar component for date selection supporting single dates, multiple dates, and date ranges.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds calendar to Livewire property |
|
||||
| `value` | string | - | Selected date(s): `Y-m-d`, comma-separated, or `Y-m-d/Y-m-d` |
|
||||
| `mode` | string | single | `single`, `multiple`, `range` |
|
||||
| `min` | string | - | Earliest selectable date or `today` |
|
||||
| `max` | string | - | Latest selectable date or `today` |
|
||||
| `size` | string | base | `xs`, `sm`, `base`, `lg`, `xl`, `2xl` |
|
||||
| `start-day` | integer | locale-based | Week start (0-6, Sunday-Saturday) |
|
||||
| `months` | integer | 1 | Number of months displayed (1 or 2) |
|
||||
| `min-range` | integer | - | Minimum days for range selection |
|
||||
| `max-range` | integer | - | Maximum days for range selection |
|
||||
| `open-to` | string | - | Initial calendar month (`Y-m-d` format) |
|
||||
| `force-open-to` | boolean | false | Always open to specified date |
|
||||
| `navigation` | boolean | true | Show month navigation controls |
|
||||
| `static` | boolean | false | Non-interactive display mode |
|
||||
| `multiple` | boolean | false | Enable multiple date selection |
|
||||
| `week-numbers` | boolean | false | Display week numbers |
|
||||
| `selectable-header` | boolean | false | Enable month/year dropdowns |
|
||||
| `with-today` | boolean | false | Show today navigation button |
|
||||
| `with-inputs` | boolean | false | Show date input fields |
|
||||
| `locale` | string | browser | Language/locale (e.g., `fr`, `en-US`, `ja-JP`) |
|
||||
|
||||
---
|
||||
|
||||
## Single Date Selection
|
||||
|
||||
```blade
|
||||
<flux:calendar value="2026-01-06" />
|
||||
|
||||
{{-- With Livewire binding --}}
|
||||
<flux:calendar wire:model="date" />
|
||||
```
|
||||
|
||||
## Multiple Date Selection
|
||||
|
||||
```blade
|
||||
<flux:calendar multiple value="2026-01-02,2026-01-05,2026-01-15" />
|
||||
|
||||
{{-- With Livewire binding --}}
|
||||
<flux:calendar multiple wire:model="dates" />
|
||||
```
|
||||
|
||||
## Range Selection
|
||||
|
||||
```blade
|
||||
<flux:calendar mode="range" value="2026-01-02/2026-01-06" />
|
||||
|
||||
{{-- With Livewire binding --}}
|
||||
<flux:calendar mode="range" wire:model="range" />
|
||||
```
|
||||
|
||||
## Date Restrictions
|
||||
|
||||
```blade
|
||||
{{-- Minimum date --}}
|
||||
<flux:calendar min="2026-01-01" />
|
||||
|
||||
{{-- Maximum date --}}
|
||||
<flux:calendar max="2026-12-31" />
|
||||
|
||||
{{-- Today as boundary --}}
|
||||
<flux:calendar min="today" />
|
||||
|
||||
{{-- Range limits --}}
|
||||
<flux:calendar mode="range" min-range="3" max-range="14" />
|
||||
```
|
||||
|
||||
## Display Options
|
||||
|
||||
```blade
|
||||
{{-- With week numbers --}}
|
||||
<flux:calendar week-numbers />
|
||||
|
||||
{{-- Selectable month/year dropdowns --}}
|
||||
<flux:calendar selectable-header />
|
||||
|
||||
{{-- Today button --}}
|
||||
<flux:calendar with-today />
|
||||
|
||||
{{-- With input fields --}}
|
||||
<flux:calendar with-inputs />
|
||||
|
||||
{{-- Multiple months --}}
|
||||
<flux:calendar months="2" />
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
```blade
|
||||
<flux:calendar size="xs" />
|
||||
<flux:calendar size="sm" />
|
||||
<flux:calendar /> {{-- base (default) --}}
|
||||
<flux:calendar size="lg" />
|
||||
<flux:calendar size="xl" />
|
||||
<flux:calendar size="2xl" />
|
||||
```
|
||||
|
||||
## Static Display
|
||||
|
||||
Non-interactive calendar (just displays dates):
|
||||
|
||||
```blade
|
||||
<flux:calendar static />
|
||||
```
|
||||
|
||||
## Locale
|
||||
|
||||
```blade
|
||||
<flux:calendar locale="fr" />
|
||||
<flux:calendar locale="ja-JP" />
|
||||
<flux:calendar locale="en-GB" />
|
||||
```
|
||||
|
||||
## Week Start Day
|
||||
|
||||
```blade
|
||||
{{-- Start on Monday --}}
|
||||
<flux:calendar start-day="1" />
|
||||
|
||||
{{-- Start on Sunday --}}
|
||||
<flux:calendar start-day="0" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DateRange Object
|
||||
|
||||
For range mode, use the `DateRange` object in your Livewire component:
|
||||
|
||||
```php
|
||||
use Flux\DateRange;
|
||||
|
||||
class MyComponent extends Component
|
||||
{
|
||||
public DateRange $range;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->range = new DateRange(now(), now()->addDays(7));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Static Constructors
|
||||
|
||||
```php
|
||||
DateRange::today()
|
||||
DateRange::yesterday()
|
||||
DateRange::thisWeek()
|
||||
DateRange::lastWeek()
|
||||
DateRange::last7Days()
|
||||
DateRange::thisMonth()
|
||||
DateRange::lastMonth()
|
||||
DateRange::thisYear()
|
||||
DateRange::lastYear()
|
||||
DateRange::yearToDate()
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```php
|
||||
$range->start() // Get start as Carbon instance
|
||||
$range->end() // Get end as Carbon instance
|
||||
$range->days() // Count days in range
|
||||
$range->contains($date) // Check if date is in range
|
||||
$range->length() // Range length in days
|
||||
$range->toArray() // Array representation
|
||||
$range->preset() // Current preset enum
|
||||
```
|
||||
|
||||
### Eloquent Integration
|
||||
|
||||
```php
|
||||
Model::whereBetween('date', [$range->start(), $range->end()])->get();
|
||||
```
|
||||
|
||||
## Session Persistence
|
||||
|
||||
Use `#[Session]` to persist selection:
|
||||
|
||||
```php
|
||||
use Livewire\Attributes\Session;
|
||||
|
||||
#[Session]
|
||||
public DateRange $range;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```blade
|
||||
<flux:calendar
|
||||
wire:model="dateRange"
|
||||
mode="range"
|
||||
months="2"
|
||||
min="today"
|
||||
max-range="30"
|
||||
selectable-header
|
||||
with-today
|
||||
week-numbers
|
||||
/>
|
||||
```
|
||||
181
docs/specs/ui/flux/components/callout.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# flux:callout
|
||||
|
||||
Highlight important information and guide users toward key actions.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `icon` | string | - | Icon name displayed next to heading |
|
||||
| `icon:variant` | string | - | Icon variant (e.g., `outline`) |
|
||||
| `variant` | string | secondary | `secondary`, `success`, `warning`, `danger` |
|
||||
| `color` | string | - | Custom Tailwind colour |
|
||||
| `inline` | boolean | false | Actions appear inline with content |
|
||||
| `heading` | string | - | Shorthand for `flux:callout.heading` |
|
||||
| `text` | string | - | Shorthand for `flux:callout.text` |
|
||||
|
||||
### Colour Options
|
||||
|
||||
`zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `icon` | Custom icon SVG |
|
||||
| `actions` | Buttons/links (typically `flux:button`) |
|
||||
| `controls` | UI elements at top right (e.g., close button) |
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:callout.heading
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `icon` | string | Icon inside heading |
|
||||
| `icon:variant` | string | Icon variant |
|
||||
|
||||
### flux:callout.text
|
||||
|
||||
Default slot for text content.
|
||||
|
||||
### flux:callout.link
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `href` | string | - | URL destination |
|
||||
| `external` | boolean | false | Opens in new tab |
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:callout icon="clock">
|
||||
<flux:callout.heading>Upcoming maintenance</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
We will be performing scheduled maintenance on Saturday.
|
||||
<flux:callout.link href="/status">Learn more</flux:callout.link>
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## Shorthand Syntax
|
||||
|
||||
```blade
|
||||
<flux:callout
|
||||
icon="information-circle"
|
||||
heading="Quick tip"
|
||||
text="You can use keyboard shortcuts to navigate faster."
|
||||
/>
|
||||
```
|
||||
|
||||
## Icon Inside Heading
|
||||
|
||||
```blade
|
||||
<flux:callout>
|
||||
<flux:callout.heading icon="newspaper">Policy update</flux:callout.heading>
|
||||
<flux:callout.text>Our terms of service have been updated.</flux:callout.text>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
```blade
|
||||
<flux:callout variant="secondary" icon="information-circle">
|
||||
<flux:callout.heading>Information</flux:callout.heading>
|
||||
</flux:callout>
|
||||
|
||||
<flux:callout variant="success" icon="check-circle">
|
||||
<flux:callout.heading>Success</flux:callout.heading>
|
||||
</flux:callout>
|
||||
|
||||
<flux:callout variant="warning" icon="exclamation-circle">
|
||||
<flux:callout.heading>Warning</flux:callout.heading>
|
||||
</flux:callout>
|
||||
|
||||
<flux:callout variant="danger" icon="x-circle">
|
||||
<flux:callout.heading>Error</flux:callout.heading>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## With Actions
|
||||
|
||||
```blade
|
||||
<flux:callout icon="credit-card">
|
||||
<flux:callout.heading>Subscription expiring soon</flux:callout.heading>
|
||||
<flux:callout.text>Your plan expires in 3 days. Renew to avoid interruption.</flux:callout.text>
|
||||
<x-slot name="actions">
|
||||
<flux:button variant="primary">Renew now</flux:button>
|
||||
<flux:button variant="ghost" href="/pricing">View plans</flux:button>
|
||||
</x-slot>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## Inline Actions
|
||||
|
||||
Actions appear beside content instead of below:
|
||||
|
||||
```blade
|
||||
<flux:callout icon="cube" variant="secondary" inline>
|
||||
<flux:callout.heading>Your package is delayed</flux:callout.heading>
|
||||
<x-slot name="actions">
|
||||
<flux:button>Track order</flux:button>
|
||||
</x-slot>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## Dismissible
|
||||
|
||||
Using Alpine.js for dismiss functionality:
|
||||
|
||||
```blade
|
||||
<flux:callout
|
||||
icon="bell"
|
||||
variant="secondary"
|
||||
inline
|
||||
x-data="{ visible: true }"
|
||||
x-show="visible"
|
||||
>
|
||||
<flux:callout.heading>Upcoming meeting in 15 minutes</flux:callout.heading>
|
||||
<x-slot name="controls">
|
||||
<flux:button icon="x-mark" variant="ghost" x-on:click="visible = false" />
|
||||
</x-slot>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## Custom Colour
|
||||
|
||||
```blade
|
||||
<flux:callout icon="sparkles" color="purple">
|
||||
<flux:callout.heading>Have a question?</flux:callout.heading>
|
||||
<flux:callout.text>Try our AI assistant for instant answers.</flux:callout.text>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## Custom Icon (SVG Slot)
|
||||
|
||||
```blade
|
||||
<flux:callout>
|
||||
<x-slot name="icon">
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<!-- Custom SVG content -->
|
||||
</svg>
|
||||
</x-slot>
|
||||
<flux:callout.heading>Custom notification</flux:callout.heading>
|
||||
</flux:callout>
|
||||
```
|
||||
|
||||
## External Link
|
||||
|
||||
```blade
|
||||
<flux:callout icon="document-text">
|
||||
<flux:callout.heading>Documentation available</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
Read our comprehensive guide.
|
||||
<flux:callout.link href="https://docs.example.com" external>
|
||||
View documentation
|
||||
</flux:callout.link>
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
```
|
||||
154
docs/specs/ui/flux/components/card.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# flux:card
|
||||
|
||||
A container for related content, such as a form, alert, or data list.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `size` | string | default | `sm` (small), default (regular) |
|
||||
| `class` | string | - | Additional CSS classes |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `default` | Content to display within the card |
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:card>
|
||||
<p>Card content goes here.</p>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
## With Heading and Text
|
||||
|
||||
```blade
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg">Card Title</flux:heading>
|
||||
<flux:text>Card description or content.</flux:text>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
## Login Form
|
||||
|
||||
```blade
|
||||
<flux:card class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Log in to your account</flux:heading>
|
||||
<flux:text class="mt-2">Welcome back! Please enter your details.</flux:text>
|
||||
</div>
|
||||
|
||||
<flux:input label="Email" type="email" placeholder="you@example.com" />
|
||||
<flux:input label="Password" type="password" />
|
||||
|
||||
<div class="flex justify-between">
|
||||
<flux:checkbox label="Remember me" />
|
||||
<flux:link href="/forgot-password">Forgot password?</flux:link>
|
||||
</div>
|
||||
|
||||
<flux:button variant="primary" class="w-full">Log in</flux:button>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
## Small Size
|
||||
|
||||
Compact content like notifications, alerts, or brief summaries:
|
||||
|
||||
```blade
|
||||
<flux:card size="sm" class="hover:bg-zinc-50 dark:hover:bg-zinc-800">
|
||||
<flux:heading>Latest on our blog</flux:heading>
|
||||
<flux:text class="mt-1">Stay up to date with our latest insights and updates.</flux:text>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
## With Header Actions
|
||||
|
||||
```blade
|
||||
<flux:card class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">Are you sure?</flux:heading>
|
||||
<flux:button variant="ghost" icon="x-mark" />
|
||||
</div>
|
||||
|
||||
<flux:text>This action cannot be undone.</flux:text>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:button variant="ghost">Cancel</flux:button>
|
||||
<flux:button variant="danger">Delete</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
## With Sections
|
||||
|
||||
```blade
|
||||
<flux:card>
|
||||
<div class="p-6 border-b dark:border-zinc-700">
|
||||
<flux:heading>Settings</flux:heading>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-4">
|
||||
<flux:input label="Name" />
|
||||
<flux:input label="Email" type="email" />
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t dark:border-zinc-700 flex justify-end gap-2">
|
||||
<flux:button variant="ghost">Cancel</flux:button>
|
||||
<flux:button variant="primary">Save</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
## Grid of Cards
|
||||
|
||||
```blade
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<flux:card size="sm">
|
||||
<flux:heading>Feature 1</flux:heading>
|
||||
<flux:text>Description of feature one.</flux:text>
|
||||
</flux:card>
|
||||
|
||||
<flux:card size="sm">
|
||||
<flux:heading>Feature 2</flux:heading>
|
||||
<flux:text>Description of feature two.</flux:text>
|
||||
</flux:card>
|
||||
|
||||
<flux:card size="sm">
|
||||
<flux:heading>Feature 3</flux:heading>
|
||||
<flux:text>Description of feature three.</flux:text>
|
||||
</flux:card>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Clickable Card
|
||||
|
||||
```blade
|
||||
<a href="/article">
|
||||
<flux:card size="sm" class="hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors">
|
||||
<flux:heading>Read our latest article</flux:heading>
|
||||
<flux:text>Click to learn more about our updates.</flux:text>
|
||||
</flux:card>
|
||||
</a>
|
||||
```
|
||||
|
||||
## With Image
|
||||
|
||||
```blade
|
||||
<flux:card class="overflow-hidden">
|
||||
<img src="/image.jpg" alt="Cover" class="-mx-6 -mt-6 mb-4" />
|
||||
<flux:heading>Article Title</flux:heading>
|
||||
<flux:text>Article excerpt goes here...</flux:text>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Heading](./heading.md)
|
||||
- [Text](./text.md)
|
||||
78
docs/specs/ui/flux/components/chart.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# flux:chart
|
||||
|
||||
Lightweight, zero-dependency charting for Livewire. Supports line and area charts.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds to Livewire property containing data |
|
||||
| `value` | array | - | Direct data array |
|
||||
| `curve` | string | smooth | `smooth`, `none` |
|
||||
|
||||
## Child Components
|
||||
|
||||
- `flux:chart.svg` - SVG container (`gutter` prop for padding)
|
||||
- `flux:chart.line` - Line (`field`, `curve`, `class`)
|
||||
- `flux:chart.area` - Filled area (`field`, `curve`, `class`)
|
||||
- `flux:chart.point` - Data points (`field`, `r`, `stroke-width`)
|
||||
- `flux:chart.axis` - Axes (`axis`, `field`, `scale`, `position`, `tick-*`, `format`)
|
||||
- `flux:chart.axis.line` - Axis baseline
|
||||
- `flux:chart.axis.mark` - Tick marks
|
||||
- `flux:chart.axis.grid` - Grid lines
|
||||
- `flux:chart.axis.tick` - Tick labels
|
||||
- `flux:chart.cursor` - Hover guide line
|
||||
- `flux:chart.zero-line` - Y=0 line
|
||||
- `flux:chart.tooltip` - Hover tooltip
|
||||
- `flux:chart.tooltip.heading` - Tooltip header
|
||||
- `flux:chart.tooltip.value` - Tooltip value
|
||||
- `flux:chart.summary` - Key metric display
|
||||
- `flux:chart.summary.value` - Summary value
|
||||
- `flux:chart.legend` - Legend container
|
||||
- `flux:chart.legend.indicator` - Colour indicator
|
||||
- `flux:chart.viewport` - Wrapper for siblings
|
||||
|
||||
## Basic Line Chart
|
||||
|
||||
```blade
|
||||
<flux:chart wire:model="data" class="aspect-3/1">
|
||||
<flux:chart.svg>
|
||||
<flux:chart.line field="visitors" class="text-pink-500" />
|
||||
<flux:chart.axis axis="x" field="date">
|
||||
<flux:chart.axis.line />
|
||||
<flux:chart.axis.tick />
|
||||
</flux:chart.axis>
|
||||
<flux:chart.axis axis="y">
|
||||
<flux:chart.axis.grid />
|
||||
<flux:chart.axis.tick />
|
||||
</flux:chart.axis>
|
||||
<flux:chart.cursor />
|
||||
</flux:chart.svg>
|
||||
<flux:chart.tooltip>
|
||||
<flux:chart.tooltip.heading field="date" />
|
||||
<flux:chart.tooltip.value field="visitors" label="Visitors" />
|
||||
</flux:chart.tooltip>
|
||||
</flux:chart>
|
||||
```
|
||||
|
||||
## Data Format
|
||||
|
||||
```php
|
||||
public array $data = [
|
||||
['date' => '2026-01-01', 'visitors' => 267],
|
||||
['date' => '2026-01-02', 'visitors' => 259],
|
||||
];
|
||||
```
|
||||
|
||||
## Format Options (Intl API)
|
||||
|
||||
```blade
|
||||
{{-- Currency --}}
|
||||
:format="['style' => 'currency', 'currency' => 'USD']"
|
||||
|
||||
{{-- Percent --}}
|
||||
:format="['style' => 'percent']"
|
||||
|
||||
{{-- Compact numbers --}}
|
||||
:format="['notation' => 'compact']"
|
||||
```
|
||||
81
docs/specs/ui/flux/components/checkbox.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# flux:checkbox
|
||||
|
||||
Selection of one or multiple options with groups, descriptions, and visual variants.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds to Livewire property |
|
||||
| `label` | string | - | Label text |
|
||||
| `description` | string | - | Help text |
|
||||
| `value` | string | - | Value when checked in group |
|
||||
| `checked` | boolean | false | Default checked state |
|
||||
| `indeterminate` | boolean | false | Partial selection (dash icon) |
|
||||
| `disabled` | boolean | false | Prevents interaction |
|
||||
| `invalid` | boolean | false | Error styling |
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:checkbox.group
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Array of selected values |
|
||||
| `label` | string | - | Group heading |
|
||||
| `description` | string | - | Help text |
|
||||
| `variant` | string | default | `default`, `cards`, `pills`, `buttons` |
|
||||
| `disabled` | boolean | false | Disable all |
|
||||
| `invalid` | boolean | false | Error all |
|
||||
|
||||
### flux:checkbox.all
|
||||
|
||||
Master checkbox controlling all group members.
|
||||
|
||||
## Basic
|
||||
|
||||
```blade
|
||||
<flux:checkbox wire:model="terms" label="I agree to terms" />
|
||||
```
|
||||
|
||||
## Group
|
||||
|
||||
```blade
|
||||
<flux:checkbox.group wire:model="notifications" label="Notifications">
|
||||
<flux:checkbox label="Push" value="push" checked />
|
||||
<flux:checkbox label="Email" value="email" />
|
||||
<flux:checkbox label="SMS" value="sms" />
|
||||
</flux:checkbox.group>
|
||||
```
|
||||
|
||||
## Variants (Pro)
|
||||
|
||||
```blade
|
||||
{{-- Cards --}}
|
||||
<flux:checkbox.group wire:model="plan" variant="cards">
|
||||
<flux:checkbox value="basic" label="Basic" description="For individuals" />
|
||||
<flux:checkbox value="pro" label="Pro" description="For teams" />
|
||||
</flux:checkbox.group>
|
||||
|
||||
{{-- Pills --}}
|
||||
<flux:checkbox.group wire:model="tags" variant="pills">
|
||||
<flux:checkbox value="php">PHP</flux:checkbox>
|
||||
<flux:checkbox value="laravel">Laravel</flux:checkbox>
|
||||
</flux:checkbox.group>
|
||||
|
||||
{{-- Buttons --}}
|
||||
<flux:checkbox.group wire:model="format" variant="buttons">
|
||||
<flux:checkbox value="bold" icon="bold" />
|
||||
<flux:checkbox value="italic" icon="italic" />
|
||||
</flux:checkbox.group>
|
||||
```
|
||||
|
||||
## Check All
|
||||
|
||||
```blade
|
||||
<flux:checkbox.group>
|
||||
<flux:checkbox.all label="Select all" />
|
||||
<flux:checkbox value="1" label="Option 1" />
|
||||
<flux:checkbox value="2" label="Option 2" />
|
||||
</flux:checkbox.group>
|
||||
```
|
||||
61
docs/specs/ui/flux/components/command.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# flux:command
|
||||
|
||||
Searchable command palette for quick access to actions, often displayed as a modal.
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:command.input
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `placeholder` | string | - | Input placeholder |
|
||||
| `clearable` | boolean | false | Show clear button |
|
||||
| `closable` | boolean | false | Show close button |
|
||||
| `icon` | string | magnifying-glass | Leading icon |
|
||||
|
||||
### flux:command.items
|
||||
|
||||
Container for command items.
|
||||
|
||||
### flux:command.item
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `icon` | string | - | Item icon |
|
||||
| `icon:variant` | string | - | `outline`, `solid`, `mini`, `micro` |
|
||||
| `kbd` | string | - | Keyboard shortcut hint |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:command>
|
||||
<flux:command.input placeholder="Search commands..." />
|
||||
<flux:command.items>
|
||||
<flux:command.item wire:click="assign" icon="user-plus" kbd="⌘A">
|
||||
Assign to...
|
||||
</flux:command.item>
|
||||
<flux:command.item icon="document-text" kbd="⌘D">
|
||||
Documents
|
||||
</flux:command.item>
|
||||
<flux:command.item icon="cog-6-tooth" kbd="⌘,">
|
||||
Settings
|
||||
</flux:command.item>
|
||||
</flux:command.items>
|
||||
</flux:command>
|
||||
```
|
||||
|
||||
## In Modal
|
||||
|
||||
```blade
|
||||
<flux:modal name="command" variant="bare">
|
||||
<flux:command class="w-full max-w-lg">
|
||||
<flux:command.input placeholder="Search..." closable />
|
||||
<flux:command.items>
|
||||
<flux:command.item icon="home">Dashboard</flux:command.item>
|
||||
<flux:command.item icon="users">Users</flux:command.item>
|
||||
</flux:command.items>
|
||||
</flux:command>
|
||||
</flux:modal>
|
||||
```
|
||||
|
||||
Trigger with keyboard: `x-on:keydown.cmd.k.window="$flux.modal('command').show()"`
|
||||
75
docs/specs/ui/flux/components/composer.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# flux:composer
|
||||
|
||||
Configurable message input for chat interfaces and AI prompts.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds to Livewire property |
|
||||
| `name` | string | - | Name for validation |
|
||||
| `placeholder` | string | - | Placeholder text |
|
||||
| `label` | string | - | Label text |
|
||||
| `label:sr-only` | boolean | false | Screen reader only label |
|
||||
| `description` | string | - | Help text |
|
||||
| `rows` | number | 2 | Visible text lines |
|
||||
| `max-rows` | number | - | Maximum expandable rows |
|
||||
| `inline` | boolean | false | Single-row action layout |
|
||||
| `submit` | string | cmd+enter | `cmd+enter`, `enter` |
|
||||
| `disabled` | boolean | false | Prevents interaction |
|
||||
| `invalid` | boolean | false | Error styling |
|
||||
| `variant` | string | - | `input` |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `input` | Custom input (e.g., rich text editor) |
|
||||
| `header` | Content above input |
|
||||
| `footer` | Content below input |
|
||||
| `actionsLeading` | Start-side action buttons |
|
||||
| `actionsTrailing` | End-side action buttons |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:composer wire:model="prompt" placeholder="How can I help you today?">
|
||||
<x-slot name="actionsTrailing">
|
||||
<flux:button type="submit" icon="paper-airplane" />
|
||||
</x-slot>
|
||||
</flux:composer>
|
||||
```
|
||||
|
||||
## With Leading Actions
|
||||
|
||||
```blade
|
||||
<flux:composer wire:model="message">
|
||||
<x-slot name="actionsLeading">
|
||||
<flux:button icon="photo" variant="ghost" />
|
||||
<flux:button icon="paper-clip" variant="ghost" />
|
||||
</x-slot>
|
||||
<x-slot name="actionsTrailing">
|
||||
<flux:button type="submit" variant="primary">Send</flux:button>
|
||||
</x-slot>
|
||||
</flux:composer>
|
||||
```
|
||||
|
||||
## Inline Layout
|
||||
|
||||
```blade
|
||||
<flux:composer wire:model="search" inline placeholder="Ask a question...">
|
||||
<x-slot name="actionsTrailing">
|
||||
<flux:button type="submit" icon="arrow-right" />
|
||||
</x-slot>
|
||||
</flux:composer>
|
||||
```
|
||||
|
||||
## With Rich Text Editor
|
||||
|
||||
```blade
|
||||
<flux:composer wire:model="content">
|
||||
<x-slot name="input">
|
||||
<flux:editor wire:model="content" />
|
||||
</x-slot>
|
||||
</flux:composer>
|
||||
```
|
||||
44
docs/specs/ui/flux/components/context.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# flux:context
|
||||
|
||||
Right-click context menu functionality.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds menu state to Livewire property |
|
||||
| `position` | string | bottom end | `[vertical] [horizontal]` (top/bottom, start/center/end) |
|
||||
| `gap` | number | 4 | Distance from click position |
|
||||
| `offset` | string | - | Additional offset `[x] [y]` |
|
||||
| `target` | string | - | ID of external menu element |
|
||||
| `detail` | mixed | - | Custom value for styling/behaviour |
|
||||
| `disabled` | boolean | false | Prevents context menu |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `default` | First child = trigger area, second = `flux:menu` |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:context>
|
||||
<flux:card class="border-dashed border-2 px-16">
|
||||
<flux:text>Right click here</flux:text>
|
||||
</flux:card>
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="plus">New post</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.submenu heading="Sort by">
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.radio checked>Name</flux:menu.radio>
|
||||
<flux:menu.radio>Date</flux:menu.radio>
|
||||
</flux:menu.radio.group>
|
||||
</flux:menu.submenu>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item variant="danger" icon="trash">Delete</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:context>
|
||||
```
|
||||
172
docs/specs/ui/flux/components/date-picker.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# flux:date-picker
|
||||
|
||||
Date selection via calendar overlay. Supports single dates, ranges, and presets.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds to Livewire property |
|
||||
| `value` | string | - | Selected date(s): Y-m-d or Y-m-d/Y-m-d for ranges |
|
||||
| `mode` | string | single | `single`, `range` |
|
||||
| `min-range` | number | - | Minimum selectable days in range |
|
||||
| `max-range` | number | - | Maximum selectable days in range |
|
||||
| `min` | string | - | Earliest selectable date or "today" |
|
||||
| `max` | string | - | Latest selectable date or "today" |
|
||||
| `open-to` | string | - | Initial calendar view date |
|
||||
| `force-open-to` | boolean | false | Always open to specified date |
|
||||
| `months` | number | 1/2 | Months displayed (1 single, 2 range) |
|
||||
| `label` | string | - | Wraps in flux:field with flux:label |
|
||||
| `description` | string | - | Help text above picker |
|
||||
| `description:trailing` | string | - | Help text below picker |
|
||||
| `badge` | string | - | Label badge text |
|
||||
| `placeholder` | string | - | Default text when empty |
|
||||
| `size` | string | default | `sm`, `default`, `lg`, `xl`, `2xl` |
|
||||
| `start-day` | number | locale | Week start day (0-6) |
|
||||
| `week-numbers` | boolean | false | Display week numbers |
|
||||
| `selectable-header` | boolean | false | Month/year dropdown navigation |
|
||||
| `with-today` | boolean | false | Quick "today" navigation button |
|
||||
| `with-inputs` | boolean | false | Manual date entry inputs |
|
||||
| `with-confirmation` | boolean | false | Require confirmation |
|
||||
| `with-presets` | boolean | false | Show preset ranges |
|
||||
| `presets` | string | - | Space-separated preset names |
|
||||
| `clearable` | boolean | false | Show clear button |
|
||||
| `disabled` | boolean | false | Disable interaction |
|
||||
| `invalid` | boolean | false | Error styling |
|
||||
| `locale` | string | browser | Locale code (e.g., fr, en-US) |
|
||||
| `unavailable` | string | - | Comma-separated disabled dates |
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:date-picker.input
|
||||
|
||||
Trigger input for date selection.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `label` | string | - | Input label |
|
||||
| `description` | string | - | Help text |
|
||||
| `placeholder` | string | - | Placeholder text |
|
||||
| `clearable` | boolean | false | Show clear button |
|
||||
| `disabled` | boolean | false | Disable input |
|
||||
| `invalid` | boolean | false | Error styling |
|
||||
|
||||
### flux:date-picker.button
|
||||
|
||||
Trigger button for date selection.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `placeholder` | string | - | Button text |
|
||||
| `size` | string | - | `sm`, `xs` |
|
||||
| `clearable` | boolean | false | Show clear button |
|
||||
| `disabled` | boolean | false | Disable button |
|
||||
| `invalid` | boolean | false | Error styling |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `trigger` | Custom element opening picker |
|
||||
|
||||
## Available Presets
|
||||
|
||||
| Key | Constructor | Range |
|
||||
|-----|-------------|-------|
|
||||
| `today` | `DateRange::today()` | Current day |
|
||||
| `yesterday` | `DateRange::yesterday()` | Previous day |
|
||||
| `thisWeek` | `DateRange::thisWeek()` | Current week |
|
||||
| `lastWeek` | `DateRange::lastWeek()` | Previous week |
|
||||
| `last7Days` | `DateRange::last7Days()` | Previous 7 days |
|
||||
| `last14Days` | `DateRange::last14Days()` | Previous 14 days |
|
||||
| `last30Days` | `DateRange::last30Days()` | Previous 30 days |
|
||||
| `thisMonth` | `DateRange::thisMonth()` | Current month |
|
||||
| `lastMonth` | `DateRange::lastMonth()` | Previous month |
|
||||
| `last3Months` | `DateRange::last3Months()` | Previous 3 months |
|
||||
| `last6Months` | `DateRange::last6Months()` | Previous 6 months |
|
||||
| `thisQuarter` | `DateRange::thisQuarter()` | Current quarter |
|
||||
| `lastQuarter` | `DateRange::lastQuarter()` | Previous quarter |
|
||||
| `thisYear` | `DateRange::thisYear()` | Current year |
|
||||
| `lastYear` | `DateRange::lastYear()` | Previous year |
|
||||
| `yearToDate` | `DateRange::yearToDate()` | Jan 1 to today |
|
||||
| `allTime` | `DateRange::allTime($start)` | Min date to today |
|
||||
| `custom` | `DateRange::custom()` | User-defined |
|
||||
|
||||
## DateRange Object
|
||||
|
||||
```php
|
||||
// Instance methods
|
||||
$range->start() // Carbon instance
|
||||
$range->end() // Carbon instance
|
||||
$range->days() // Integer count
|
||||
$range->preset() // DateRangePreset enum
|
||||
$range->toArray() // Array [start, end]
|
||||
$range->contains($date) // Boolean
|
||||
$range->length() // Number of days
|
||||
$range->isNotAllTime() // Boolean
|
||||
|
||||
// Custom instantiation
|
||||
new DateRange(now(), now()->addDays(7))
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:date-picker wire:model="date" />
|
||||
```
|
||||
|
||||
## Range with Presets
|
||||
|
||||
```blade
|
||||
<flux:date-picker mode="range" with-presets />
|
||||
```
|
||||
|
||||
## Input Trigger
|
||||
|
||||
```blade
|
||||
<flux:date-picker wire:model="date">
|
||||
<x-slot name="trigger">
|
||||
<flux:date-picker.input />
|
||||
</x-slot>
|
||||
</flux:date-picker>
|
||||
```
|
||||
|
||||
## Range with Dual Inputs
|
||||
|
||||
```blade
|
||||
<flux:date-picker mode="range">
|
||||
<x-slot name="trigger">
|
||||
<div class="flex gap-4">
|
||||
<flux:date-picker.input label="Start" />
|
||||
<flux:date-picker.input label="End" />
|
||||
</div>
|
||||
</x-slot>
|
||||
</flux:date-picker>
|
||||
```
|
||||
|
||||
## Livewire Property
|
||||
|
||||
```php
|
||||
public DateRange $range;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->range = new DateRange(
|
||||
now()->subDays(1),
|
||||
now()->addDays(1)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Session Persistence
|
||||
|
||||
```php
|
||||
#[Session]
|
||||
public DateRange $range;
|
||||
```
|
||||
|
||||
## Eloquent Integration
|
||||
|
||||
```php
|
||||
$query->whereBetween('created_at', $this->range->toArray());
|
||||
```
|
||||
148
docs/specs/ui/flux/components/dropdown.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# flux:dropdown
|
||||
|
||||
Composable dropdown menus with navigation, actions, checkboxes, radios, and submenus.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `position` | string | bottom | `top`, `right`, `bottom`, `left` |
|
||||
| `align` | string | start | `start`, `center`, `end` |
|
||||
| `offset` | number | 0 | Pixel offset from trigger |
|
||||
| `gap` | number | 4 | Pixel gap between trigger and menu |
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:menu
|
||||
|
||||
Complex menu with keyboard navigation, submenus, checkboxes, radios.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `keep-open` | boolean | false | Prevents closure on item click |
|
||||
|
||||
### flux:menu.item
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `icon` | string | - | Leading icon name |
|
||||
| `icon:trailing` | string | - | Trailing icon name |
|
||||
| `icon:variant` | string | - | `outline`, `solid`, `mini`, `micro` |
|
||||
| `kbd` | string | - | Keyboard shortcut hint |
|
||||
| `suffix` | string | - | Trailing text |
|
||||
| `variant` | string | default | `default`, `danger` |
|
||||
| `disabled` | boolean | false | Disables interaction |
|
||||
| `keep-open` | boolean | false | Prevents menu closure |
|
||||
|
||||
### flux:menu.submenu
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | string | - | Submenu label |
|
||||
| `icon` | string | - | Leading icon |
|
||||
| `icon:trailing` | string | - | Trailing icon |
|
||||
| `icon:variant` | string | - | Icon variant |
|
||||
| `keep-open` | boolean | false | Prevents closure |
|
||||
|
||||
### flux:menu.separator
|
||||
|
||||
Horizontal line separating menu items. No props.
|
||||
|
||||
### flux:menu.checkbox
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Livewire binding |
|
||||
| `checked` | boolean | false | Default checked state |
|
||||
| `disabled` | boolean | false | Disables interaction |
|
||||
| `keep-open` | boolean | false | Prevents menu closure |
|
||||
|
||||
### flux:menu.checkbox-group
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Livewire binding for group |
|
||||
|
||||
### flux:menu.radio
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `checked` | boolean | false | Default selected state |
|
||||
| `disabled` | boolean | false | Disables interaction |
|
||||
| `keep-open` | boolean | false | Prevents menu closure |
|
||||
|
||||
### flux:menu.radio.group
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Livewire binding for group |
|
||||
| `keep-open` | boolean | false | Prevents closure |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:dropdown>
|
||||
<flux:button icon:trailing="chevron-down">Options</flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="pencil">Edit</flux:menu.item>
|
||||
<flux:menu.item icon="trash" variant="danger">Delete</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
```
|
||||
|
||||
## With Keyboard Shortcuts
|
||||
|
||||
```blade
|
||||
<flux:dropdown>
|
||||
<flux:button>Actions</flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="clipboard" kbd="⌘C">Copy</flux:menu.item>
|
||||
<flux:menu.item icon="clipboard-document" kbd="⌘V">Paste</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
```
|
||||
|
||||
## With Submenu
|
||||
|
||||
```blade
|
||||
<flux:dropdown position="bottom" align="end">
|
||||
<flux:button icon:trailing="chevron-down">Options</flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="plus">New post</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.submenu heading="Sort by">
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.radio checked>Name</flux:menu.radio>
|
||||
<flux:menu.radio>Date</flux:menu.radio>
|
||||
</flux:menu.radio.group>
|
||||
</flux:menu.submenu>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
```
|
||||
|
||||
## With Checkboxes
|
||||
|
||||
```blade
|
||||
<flux:dropdown>
|
||||
<flux:button>Filters</flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.checkbox wire:model="showActive">Active</flux:menu.checkbox>
|
||||
<flux:menu.checkbox wire:model="showArchived">Archived</flux:menu.checkbox>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
```
|
||||
|
||||
## Grouped Radios
|
||||
|
||||
```blade
|
||||
<flux:dropdown>
|
||||
<flux:button>Sort</flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.radio.group wire:model="sortBy">
|
||||
<flux:menu.radio value="name">Name</flux:menu.radio>
|
||||
<flux:menu.radio value="date">Date</flux:menu.radio>
|
||||
<flux:menu.radio value="size">Size</flux:menu.radio>
|
||||
</flux:menu.radio.group>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
```
|
||||
180
docs/specs/ui/flux/components/editor.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# flux:editor
|
||||
|
||||
Rich text editor built on ProseMirror and Tiptap with markdown support.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds editor content to Livewire property |
|
||||
| `value` | string | - | Initial content when not using wire:model |
|
||||
| `label` | string | - | Wraps editor in flux:field with label |
|
||||
| `description` | string | - | Help text between label and editor |
|
||||
| `description:trailing` | boolean | false | Displays description below editor |
|
||||
| `badge` | string | - | Badge text in label component |
|
||||
| `placeholder` | string | - | Text shown when editor is empty |
|
||||
| `toolbar` | string | default | Space-separated items, `|` separator, `~` spacer |
|
||||
| `disabled` | boolean | false | Prevents user interaction |
|
||||
| `invalid` | boolean | false | Applies error styling |
|
||||
|
||||
## Default Toolbar
|
||||
|
||||
`heading`, `bold`, `italic`, `strike`, `bullet`, `ordered`, `blockquote`, `link`, `align`
|
||||
|
||||
## All Toolbar Items
|
||||
|
||||
- `heading` - Heading levels
|
||||
- `bold` - Bold text
|
||||
- `italic` - Italic text
|
||||
- `strike` - Strikethrough
|
||||
- `underline` - Underline text
|
||||
- `bullet` - Bullet list
|
||||
- `ordered` - Numbered list
|
||||
- `blockquote` - Block quote
|
||||
- `subscript` - Subscript text
|
||||
- `superscript` - Superscript text
|
||||
- `highlight` - Highlighted text
|
||||
- `link` - Hyperlink
|
||||
- `code` - Inline code
|
||||
- `undo` - Undo action
|
||||
- `redo` - Redo action
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:editor.toolbar
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `items` | string | - | Space-separated toolbar configuration |
|
||||
|
||||
### flux:editor.button
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `icon` | string | - | Icon name |
|
||||
| `iconVariant` | string | mini | `mini`, `micro`, `outline` |
|
||||
| `tooltip` | string | - | Hover text |
|
||||
| `disabled` | boolean | false | Disable button |
|
||||
|
||||
### flux:editor.content
|
||||
|
||||
Container for initial HTML content.
|
||||
|
||||
### Toolbar Item Components
|
||||
|
||||
- `flux:editor.heading`
|
||||
- `flux:editor.bold`
|
||||
- `flux:editor.italic`
|
||||
- `flux:editor.strike`
|
||||
- `flux:editor.underline`
|
||||
- `flux:editor.bullet`
|
||||
- `flux:editor.ordered`
|
||||
- `flux:editor.blockquote`
|
||||
- `flux:editor.code`
|
||||
- `flux:editor.link`
|
||||
- `flux:editor.align`
|
||||
- `flux:editor.undo`
|
||||
- `flux:editor.redo`
|
||||
- `flux:editor.separator`
|
||||
- `flux:editor.spacer`
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Action | Mac | Windows |
|
||||
|--------|-----|---------|
|
||||
| Bold | ⌘B | Ctrl+B |
|
||||
| Italic | ⌘I | Ctrl+I |
|
||||
| Underline | ⌘U | Ctrl+U |
|
||||
| Link | ⌘K | Ctrl+K |
|
||||
| Blockquote | ⌘⇧B | Ctrl+Shift+B |
|
||||
| Undo | ⌘Z | Ctrl+Z |
|
||||
| Redo | ⌘⇧Z | Ctrl+Shift+Z |
|
||||
|
||||
## Markdown Shortcuts
|
||||
|
||||
| Type | Shortcut |
|
||||
|------|----------|
|
||||
| H1 | `#` |
|
||||
| Bold | `**text**` |
|
||||
| Italic | `*text*` |
|
||||
| Strike | `~~text~~` |
|
||||
| Bullet list | `-` |
|
||||
| Ordered list | `1.` |
|
||||
| Blockquote | `>` |
|
||||
| Inline code | `` ` `` |
|
||||
| Code block | `` ``` `` |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:editor wire:model="content" />
|
||||
```
|
||||
|
||||
## With Label and Placeholder
|
||||
|
||||
```blade
|
||||
<flux:editor
|
||||
wire:model="content"
|
||||
label="Content"
|
||||
placeholder="Start typing..."
|
||||
/>
|
||||
```
|
||||
|
||||
## Custom Toolbar
|
||||
|
||||
```blade
|
||||
<flux:editor
|
||||
wire:model="content"
|
||||
toolbar="heading | bold italic | bullet ordered | link ~ undo redo"
|
||||
/>
|
||||
```
|
||||
|
||||
## Custom Toolbar Components
|
||||
|
||||
```blade
|
||||
<flux:editor>
|
||||
<flux:editor.toolbar>
|
||||
<flux:editor.heading />
|
||||
<flux:editor.separator />
|
||||
<flux:editor.bold />
|
||||
<flux:editor.italic />
|
||||
<flux:editor.spacer />
|
||||
<flux:dropdown position="bottom end">
|
||||
<flux:editor.button icon="ellipsis-horizontal" />
|
||||
<flux:menu>
|
||||
<flux:menu.item>More options...</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:editor.toolbar>
|
||||
<flux:editor.content />
|
||||
</flux:editor>
|
||||
```
|
||||
|
||||
## Height Configuration
|
||||
|
||||
```blade
|
||||
{{-- Adjust minimum height --}}
|
||||
<flux:editor class="**:data-[slot=content]:min-h-[100px]!" />
|
||||
|
||||
{{-- Adjust maximum height --}}
|
||||
<flux:editor class="**:data-[slot=content]:max-h-[300px]!" />
|
||||
```
|
||||
|
||||
## Custom Extensions
|
||||
|
||||
```blade
|
||||
<flux:editor x-on:flux:editor="e.detail.registerExtensions([
|
||||
// Your custom Tiptap extensions
|
||||
])" />
|
||||
```
|
||||
|
||||
## Pre-installed Extensions
|
||||
|
||||
- Highlight
|
||||
- Link
|
||||
- Placeholder
|
||||
- StarterKit
|
||||
- Superscript
|
||||
- Subscript
|
||||
- TextAlign
|
||||
- Underline
|
||||
129
docs/specs/ui/flux/components/field.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# flux:field
|
||||
|
||||
Form field container with labels, descriptions, and validation messaging.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | string | block | `block`, `inline` |
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:label
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `badge` | string | - | Badge text (e.g., "Required", "Optional") |
|
||||
|
||||
**Slots:** `default` (label text), `trailing` (end text)
|
||||
|
||||
### flux:description
|
||||
|
||||
Helper text for form inputs.
|
||||
|
||||
**Slots:** `default` (description text)
|
||||
|
||||
### flux:error
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `name` | string | - | Field name for validation errors |
|
||||
| `message` | string | - | Custom error message |
|
||||
| `bag` | string | default | Error bag reference |
|
||||
| `icon` | string | exclamation-triangle | Icon displayed (false to hide) |
|
||||
|
||||
**Slots:** `default` (custom error content)
|
||||
|
||||
### flux:fieldset
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `legend` | string | - | Fieldset heading |
|
||||
| `description` | string | - | Fieldset description |
|
||||
|
||||
### flux:legend
|
||||
|
||||
Heading for fieldset groups.
|
||||
|
||||
**Slots:** `default` (heading text)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:field>
|
||||
<flux:label>Email</flux:label>
|
||||
<flux:input type="email" wire:model="email" />
|
||||
<flux:error name="email" />
|
||||
</flux:field>
|
||||
```
|
||||
|
||||
## With Description
|
||||
|
||||
```blade
|
||||
<flux:field>
|
||||
<flux:label>Password</flux:label>
|
||||
<flux:description>Must be at least 8 characters.</flux:description>
|
||||
<flux:input type="password" wire:model="password" />
|
||||
<flux:error name="password" />
|
||||
</flux:field>
|
||||
```
|
||||
|
||||
## Trailing Description
|
||||
|
||||
```blade
|
||||
<flux:field>
|
||||
<flux:label>Username</flux:label>
|
||||
<flux:input wire:model="username" />
|
||||
<flux:description>This will be your public display name.</flux:description>
|
||||
</flux:field>
|
||||
```
|
||||
|
||||
## With Badge
|
||||
|
||||
```blade
|
||||
<flux:field>
|
||||
<flux:label badge="Required">Email</flux:label>
|
||||
<flux:input type="email" wire:model="email" />
|
||||
</flux:field>
|
||||
```
|
||||
|
||||
## Shorthand (Recommended)
|
||||
|
||||
Most input components accept `label` and `description` directly:
|
||||
|
||||
```blade
|
||||
<flux:input
|
||||
label="Email"
|
||||
description="We'll never share your email."
|
||||
wire:model="email"
|
||||
/>
|
||||
```
|
||||
|
||||
## Fieldset Grouping
|
||||
|
||||
```blade
|
||||
<flux:fieldset legend="Personal Information" description="Tell us about yourself.">
|
||||
<flux:input label="First name" wire:model="firstName" />
|
||||
<flux:input label="Last name" wire:model="lastName" />
|
||||
</flux:fieldset>
|
||||
```
|
||||
|
||||
## Split Layout
|
||||
|
||||
```blade
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<flux:input label="First name" wire:model="firstName" />
|
||||
<flux:input label="Last name" wire:model="lastName" />
|
||||
</div>
|
||||
```
|
||||
|
||||
## Custom Error Message
|
||||
|
||||
```blade
|
||||
<flux:field>
|
||||
<flux:label>Email</flux:label>
|
||||
<flux:input type="email" wire:model="email" />
|
||||
<flux:error name="email" message="Please enter a valid email address." />
|
||||
</flux:field>
|
||||
```
|
||||
169
docs/specs/ui/flux/components/file-upload.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# flux:file-upload
|
||||
|
||||
File upload with drag-and-drop, previews, and Livewire integration.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds upload to Livewire property |
|
||||
| `name` | string | - | Input name for form submissions |
|
||||
| `multiple` | boolean | false | Allow multiple file selection |
|
||||
| `label` | string | - | Field label above upload area |
|
||||
| `description` | string | - | Helper text below field |
|
||||
| `error` | string | - | Validation error message |
|
||||
| `disabled` | boolean | false | Prevents interaction |
|
||||
|
||||
**Data Attributes:**
|
||||
- `data-dragging` - Added when files dragged over
|
||||
- `data-loading` - Added during upload
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:file-upload.dropzone
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | string | - | Main dropzone text |
|
||||
| `text` | string | - | Supporting text (file restrictions) |
|
||||
| `icon` | string | cloud-arrow-up | Dropzone icon |
|
||||
| `inline` | boolean | false | Compact horizontal layout |
|
||||
| `with-progress` | boolean | false | Show progress bar during upload |
|
||||
|
||||
**CSS Variables:**
|
||||
- `--flux-file-upload-progress` - Upload percentage (e.g., "42%")
|
||||
- `--flux-file-upload-progress-as-string` - Quoted percentage
|
||||
|
||||
### flux:file-item
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | string | - | File name/title |
|
||||
| `text` | string | auto | Additional text |
|
||||
| `image` | string | - | Preview image URL |
|
||||
| `size` | number | - | File size in bytes (auto-formatted) |
|
||||
| `icon` | string | document | Icon when no image |
|
||||
| `invalid` | boolean | false | Error state styling |
|
||||
|
||||
**Slots:** `actions` (action buttons)
|
||||
|
||||
### flux:file-item.remove
|
||||
|
||||
Pre-styled remove button for file items.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="file">
|
||||
<flux:file-upload.dropzone
|
||||
heading="Upload a file"
|
||||
text="PNG, JPG, PDF up to 10MB"
|
||||
/>
|
||||
</flux:file-upload>
|
||||
```
|
||||
|
||||
## Multiple Files
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="files" multiple>
|
||||
<flux:file-upload.dropzone
|
||||
heading="Upload files"
|
||||
text="Select multiple files"
|
||||
/>
|
||||
</flux:file-upload>
|
||||
```
|
||||
|
||||
## With Progress
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="file">
|
||||
<flux:file-upload.dropzone
|
||||
heading="Upload a file"
|
||||
text="Uploading..."
|
||||
with-progress
|
||||
/>
|
||||
</flux:file-upload>
|
||||
```
|
||||
|
||||
## Inline Layout
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="file">
|
||||
<flux:file-upload.dropzone inline heading="Choose file" />
|
||||
</flux:file-upload>
|
||||
```
|
||||
|
||||
## Displaying Uploaded Files
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="files" multiple>
|
||||
<flux:file-upload.dropzone heading="Upload files" />
|
||||
|
||||
@foreach ($files as $index => $file)
|
||||
<flux:file-item
|
||||
:heading="$file->getClientOriginalName()"
|
||||
:size="$file->getSize()"
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<flux:file-item.remove wire:click="removeFile({{ $index }})" />
|
||||
</x-slot>
|
||||
</flux:file-item>
|
||||
@endforeach
|
||||
</flux:file-upload>
|
||||
```
|
||||
|
||||
## With Image Preview
|
||||
|
||||
```blade
|
||||
<flux:file-item
|
||||
heading="photo.jpg"
|
||||
:image="$file->temporaryUrl()"
|
||||
:size="$file->getSize()"
|
||||
>
|
||||
<x-slot name="actions">
|
||||
<flux:file-item.remove wire:click="removeFile" />
|
||||
</x-slot>
|
||||
</flux:file-item>
|
||||
```
|
||||
|
||||
## Livewire Integration
|
||||
|
||||
```php
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class FileUploader extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $file;
|
||||
public $files = [];
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate([
|
||||
'file' => 'required|file|max:10240', // 10MB
|
||||
]);
|
||||
|
||||
$path = $this->file->store('uploads');
|
||||
}
|
||||
|
||||
public function removeFile($index)
|
||||
{
|
||||
unset($this->files[$index]);
|
||||
$this->files = array_values($this->files);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Dropzone
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="file">
|
||||
<div
|
||||
class="border-2 border-dashed p-8 text-center cursor-pointer"
|
||||
:class="{ 'border-blue-500': $el.closest('[data-dragging]') }"
|
||||
>
|
||||
Click or drag to upload
|
||||
</div>
|
||||
</flux:file-upload>
|
||||
```
|
||||
153
docs/specs/ui/flux/components/header.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# flux:header
|
||||
|
||||
Full-width top navigation with branding, navigation items, and user profile management.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `sticky` | boolean | false | Makes header remain fixed during scrolling |
|
||||
| `container` | boolean | false | Constrains content to container width |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `default` | Primary content area (branding, navigation, profile elements) |
|
||||
|
||||
## Related Components
|
||||
|
||||
- `flux:brand` - Logo and company name display
|
||||
- `flux:navbar` - Navigation bar container
|
||||
- `flux:navbar.item` - Individual navigation items
|
||||
- `flux:dropdown` - Dropdown menu wrapper
|
||||
- `flux:profile` - User profile avatar display
|
||||
- `flux:separator` - Visual dividers
|
||||
|
||||
## Basic Example
|
||||
|
||||
```blade
|
||||
<flux:header sticky container class="bg-zinc-50 border-b dark:bg-zinc-900 dark:border-zinc-700">
|
||||
<flux:sidebar.toggle icon="bars-2" inset="left" class="lg:hidden" />
|
||||
|
||||
<flux:brand href="/" logo="/logo.svg" name="Acme Inc." class="max-lg:hidden" />
|
||||
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item href="/" current>Dashboard</flux:navbar.item>
|
||||
<flux:navbar.item href="/orders">Orders</flux:navbar.item>
|
||||
<flux:navbar.item href="/products">Products</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:dropdown position="bottom" align="end">
|
||||
<flux:profile avatar="/avatar.jpg" />
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item href="/settings">Settings</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item href="/logout">Logout</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
<flux:main container>
|
||||
{{ $slot }}
|
||||
</flux:main>
|
||||
```
|
||||
|
||||
## Header with Mobile Sidebar
|
||||
|
||||
```blade
|
||||
<flux:sidebar stashable sticky class="lg:hidden bg-zinc-50 border-r dark:bg-zinc-900 dark:border-zinc-700">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.item icon="home" href="/" current>Dashboard</flux:navlist.item>
|
||||
<flux:navlist.item icon="shopping-bag" href="/orders">Orders</flux:navlist.item>
|
||||
<flux:navlist.item icon="cube" href="/products">Products</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
</flux:sidebar>
|
||||
|
||||
<flux:header sticky container>
|
||||
<flux:sidebar.toggle icon="bars-2" inset="left" class="lg:hidden" />
|
||||
|
||||
<flux:brand href="/" logo="/logo.svg" name="Acme Inc." />
|
||||
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item href="/" current>Dashboard</flux:navbar.item>
|
||||
<flux:navbar.item href="/orders">Orders</flux:navbar.item>
|
||||
<flux:navbar.item href="/products">Products</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:dropdown position="bottom" align="end">
|
||||
<flux:profile avatar="/avatar.jpg" />
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item href="/settings">Settings</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item href="/logout">Logout</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
<flux:main container>
|
||||
{{ $slot }}
|
||||
</flux:main>
|
||||
```
|
||||
|
||||
## Header with Secondary Sidebar
|
||||
|
||||
```blade
|
||||
<flux:header sticky container class="border-b bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-700">
|
||||
<flux:brand href="/" logo="/logo.svg" name="Acme Inc." />
|
||||
|
||||
<flux:navbar class="-mb-px">
|
||||
<flux:navbar.item href="/" current>Dashboard</flux:navbar.item>
|
||||
<flux:navbar.item href="/settings">Settings</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:dropdown position="bottom" align="end">
|
||||
<flux:profile avatar="/avatar.jpg" />
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item href="/logout">Logout</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
<div class="flex min-h-screen">
|
||||
<flux:sidebar sticky class="border-r bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-700">
|
||||
<flux:navlist>
|
||||
<flux:navlist.item icon="user" href="/settings/profile" current>Profile</flux:navlist.item>
|
||||
<flux:navlist.item icon="lock-closed" href="/settings/password">Password</flux:navlist.item>
|
||||
<flux:navlist.item icon="bell" href="/settings/notifications">Notifications</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
</flux:sidebar>
|
||||
|
||||
<flux:main container>
|
||||
{{ $slot }}
|
||||
</flux:main>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Header supports dark mode via Tailwind dark: classes:
|
||||
|
||||
```blade
|
||||
<flux:header class="bg-zinc-50 border-b dark:bg-zinc-900 dark:border-zinc-700">
|
||||
...
|
||||
</flux:header>
|
||||
```
|
||||
|
||||
## CSS Classes
|
||||
|
||||
Common classes for headers:
|
||||
- `bg-zinc-50` / `dark:bg-zinc-900` - Background colour
|
||||
- `border-b` / `dark:border-zinc-700` - Border
|
||||
- `-mb-px` - Negative margin for navbar alignment
|
||||
79
docs/specs/ui/flux/components/heading.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# flux:heading
|
||||
|
||||
Consistent heading styling with configurable sizes and semantic HTML levels.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `size` | string | base | `base`, `lg`, `xl` |
|
||||
| `level` | number | - | HTML heading level: `1`, `2`, `3`, `4` (default: div) |
|
||||
| `accent` | boolean | false | Applies accent colour styling |
|
||||
|
||||
## Size Reference
|
||||
|
||||
| Size | Pixels | Usage |
|
||||
|------|--------|-------|
|
||||
| `base` | 14px | Input labels, toast labels, general use |
|
||||
| `lg` | 16px | Modal headings, card headings |
|
||||
| `xl` | 24px | Hero text, page titles |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:heading>User profile</flux:heading>
|
||||
```
|
||||
|
||||
## With Size
|
||||
|
||||
```blade
|
||||
<flux:heading size="base">Small heading</flux:heading>
|
||||
<flux:heading size="lg">Medium heading</flux:heading>
|
||||
<flux:heading size="xl">Large heading</flux:heading>
|
||||
```
|
||||
|
||||
## Semantic HTML Level
|
||||
|
||||
```blade
|
||||
<flux:heading level="1">Page Title</flux:heading>
|
||||
<flux:heading level="2">Section Heading</flux:heading>
|
||||
<flux:heading level="3">Subsection</flux:heading>
|
||||
```
|
||||
|
||||
## With Accent Colour
|
||||
|
||||
```blade
|
||||
<flux:heading accent>Highlighted heading</flux:heading>
|
||||
```
|
||||
|
||||
## Subheading Pattern
|
||||
|
||||
```blade
|
||||
<div>
|
||||
<flux:text>Year to date</flux:text>
|
||||
<flux:heading size="xl" class="mb-1">$7,532.16</flux:heading>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Card Heading
|
||||
|
||||
```blade
|
||||
<flux:card>
|
||||
<flux:heading size="lg" class="mb-4">Card Title</flux:heading>
|
||||
<flux:text>Card content goes here.</flux:text>
|
||||
</flux:card>
|
||||
```
|
||||
|
||||
## Related: flux:text
|
||||
|
||||
For body text and paragraphs.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `size` | string | base | `sm`, `base`, `lg`, `xl` |
|
||||
|
||||
```blade
|
||||
<flux:text>Regular paragraph text.</flux:text>
|
||||
<flux:text size="sm">Smaller text.</flux:text>
|
||||
<flux:text size="lg">Larger text.</flux:text>
|
||||
```
|
||||
117
docs/specs/ui/flux/components/icon.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# flux:icon
|
||||
|
||||
Icons using Heroicons collection with multiple variants and sizes.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | string | outline | `outline`, `solid`, `mini`, `micro` |
|
||||
| `name` | string | - | Dynamic icon name (for variable icons) |
|
||||
|
||||
## Variants
|
||||
|
||||
| Variant | Size | Style |
|
||||
|---------|------|-------|
|
||||
| `outline` | 24px | Unfilled (default) |
|
||||
| `solid` | 24px | Filled |
|
||||
| `mini` | 20px | Filled, compact |
|
||||
| `micro` | 16px | Filled, smallest |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:icon.bolt />
|
||||
```
|
||||
|
||||
## With Variants
|
||||
|
||||
```blade
|
||||
<flux:icon.bolt /> {{-- 24px outline --}}
|
||||
<flux:icon.bolt variant="solid" /> {{-- 24px filled --}}
|
||||
<flux:icon.bolt variant="mini" /> {{-- 20px filled --}}
|
||||
<flux:icon.bolt variant="micro" /> {{-- 16px filled --}}
|
||||
```
|
||||
|
||||
## Sizing
|
||||
|
||||
Use Tailwind's `size-*` utilities:
|
||||
|
||||
```blade
|
||||
<flux:icon.bolt class="size-12" />
|
||||
<flux:icon.bolt class="size-8" />
|
||||
<flux:icon.bolt class="size-6" />
|
||||
<flux:icon.bolt class="size-4" />
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Apply colour with Tailwind text utilities:
|
||||
|
||||
```blade
|
||||
<flux:icon.bolt class="text-amber-500" />
|
||||
<flux:icon.bolt variant="solid" class="text-amber-500 dark:text-amber-300" />
|
||||
```
|
||||
|
||||
## Loading Spinner
|
||||
|
||||
```blade
|
||||
<flux:icon.loading />
|
||||
```
|
||||
|
||||
## Dynamic Icons
|
||||
|
||||
When icon name comes from a variable:
|
||||
|
||||
```blade
|
||||
<flux:icon name="bolt" />
|
||||
<flux:icon :name="$iconName" />
|
||||
<flux:icon :name="$iconName" variant="solid" />
|
||||
```
|
||||
|
||||
## Import Lucide Icons
|
||||
|
||||
Use artisan command to import additional icons:
|
||||
|
||||
```bash
|
||||
# Interactive mode
|
||||
php artisan flux:icon
|
||||
|
||||
# Specific icons
|
||||
php artisan flux:icon crown grip-vertical github
|
||||
```
|
||||
|
||||
## Custom Icons
|
||||
|
||||
Create custom SVG components in `resources/views/flux/icon/`:
|
||||
|
||||
```blade
|
||||
{{-- resources/views/flux/icon/custom-logo.blade.php --}}
|
||||
@props(['variant' => 'outline'])
|
||||
|
||||
<svg {{ $attributes->class([
|
||||
'size-6' => $variant === 'outline' || $variant === 'solid',
|
||||
'size-5' => $variant === 'mini',
|
||||
'size-4' => $variant === 'micro',
|
||||
]) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<!-- Your SVG paths -->
|
||||
</svg>
|
||||
```
|
||||
|
||||
Then use:
|
||||
|
||||
```blade
|
||||
<flux:icon.custom-logo />
|
||||
```
|
||||
|
||||
## Common Icons
|
||||
|
||||
| Category | Icons |
|
||||
|----------|-------|
|
||||
| Actions | `plus`, `minus`, `pencil`, `trash`, `check`, `x-mark` |
|
||||
| Navigation | `chevron-down`, `chevron-right`, `arrow-left`, `arrow-right` |
|
||||
| Status | `check-circle`, `x-circle`, `exclamation-triangle`, `information-circle` |
|
||||
| Objects | `user`, `cog`, `document`, `folder`, `photo`, `link` |
|
||||
| Communication | `envelope`, `phone`, `chat-bubble-left`, `bell` |
|
||||
|
||||
Browse all at [heroicons.com](https://heroicons.com/)
|
||||
163
docs/specs/ui/flux/components/input.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# flux:input
|
||||
|
||||
Text input with icons, masks, keyboard hints, and interactive features.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `wire:model` | string | - | Binds to Livewire property |
|
||||
| `label` | string | - | Wraps in field with label |
|
||||
| `description` | string | - | Help text between label and input |
|
||||
| `description:trailing` | string | - | Help text below input |
|
||||
| `placeholder` | string | - | Text shown when empty |
|
||||
| `size` | string | - | `sm`, `xs` |
|
||||
| `variant` | string | outline | `filled`, `outline` |
|
||||
| `disabled` | boolean | false | Prevents interaction |
|
||||
| `readonly` | boolean | false | Locks during submission |
|
||||
| `invalid` | boolean | false | Error styling |
|
||||
| `multiple` | boolean | false | File input: multiple files |
|
||||
| `mask` | string | - | Input mask pattern |
|
||||
| `mask:dynamic` | string | - | Dynamic mask pattern |
|
||||
| `icon` | string | - | Leading icon name |
|
||||
| `icon:trailing` | string | - | Trailing icon name |
|
||||
| `kbd` | string | - | Keyboard shortcut hint |
|
||||
| `clearable` | boolean | false | Show clear button when filled |
|
||||
| `copyable` | boolean | false | Add copy button (HTTPS only) |
|
||||
| `viewable` | boolean | false | Password visibility toggle |
|
||||
| `as` | string | input | `button`, `input` |
|
||||
| `class:input` | string | - | Classes on input element |
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
|------|-------------|
|
||||
| `icon` / `icon:leading` | Custom leading content |
|
||||
| `icon:trailing` | Custom trailing content |
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:input.group
|
||||
|
||||
Container for grouped inputs with prefix/suffix.
|
||||
|
||||
### flux:input.group.prefix
|
||||
|
||||
Text/content before input.
|
||||
|
||||
### flux:input.group.suffix
|
||||
|
||||
Text/content after input.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:input wire:model="name" placeholder="Enter your name" />
|
||||
```
|
||||
|
||||
## With Label (Shorthand)
|
||||
|
||||
```blade
|
||||
<flux:input label="Email" type="email" wire:model="email" />
|
||||
```
|
||||
|
||||
## With Description
|
||||
|
||||
```blade
|
||||
<flux:input
|
||||
label="Username"
|
||||
description="Letters and numbers only."
|
||||
wire:model="username"
|
||||
/>
|
||||
```
|
||||
|
||||
## Input Types
|
||||
|
||||
```blade
|
||||
<flux:input type="text" wire:model="name" />
|
||||
<flux:input type="email" wire:model="email" />
|
||||
<flux:input type="password" wire:model="password" />
|
||||
<flux:input type="date" wire:model="date" />
|
||||
<flux:input type="file" wire:model="file" />
|
||||
<flux:input type="file" wire:model="files" multiple />
|
||||
```
|
||||
|
||||
## With Icons
|
||||
|
||||
```blade
|
||||
<flux:input icon="envelope" wire:model="email" />
|
||||
<flux:input icon:trailing="magnifying-glass" wire:model="search" />
|
||||
```
|
||||
|
||||
## Keyboard Shortcut Hint
|
||||
|
||||
```blade
|
||||
<flux:input icon="magnifying-glass" kbd="⌘K" placeholder="Search..." />
|
||||
```
|
||||
|
||||
## Clearable
|
||||
|
||||
```blade
|
||||
<flux:input wire:model="search" clearable />
|
||||
```
|
||||
|
||||
## Copyable
|
||||
|
||||
```blade
|
||||
<flux:input wire:model="apiKey" copyable readonly />
|
||||
```
|
||||
|
||||
## Password with Toggle
|
||||
|
||||
```blade
|
||||
<flux:input type="password" wire:model="password" viewable />
|
||||
```
|
||||
|
||||
## Size Variants
|
||||
|
||||
```blade
|
||||
<flux:input size="sm" wire:model="small" />
|
||||
<flux:input size="xs" wire:model="extraSmall" />
|
||||
```
|
||||
|
||||
## Input Masking
|
||||
|
||||
```blade
|
||||
{{-- Phone number --}}
|
||||
<flux:input mask="(999) 999-9999" wire:model="phone" />
|
||||
|
||||
{{-- Credit card --}}
|
||||
<flux:input mask="9999 9999 9999 9999" wire:model="card" />
|
||||
|
||||
{{-- Dynamic mask --}}
|
||||
<flux:input mask:dynamic="['99.999.999/9999-99', '999.999.999-99']" wire:model="document" />
|
||||
```
|
||||
|
||||
## Input Group with Prefix/Suffix
|
||||
|
||||
```blade
|
||||
<flux:input.group>
|
||||
<flux:input.group.prefix>https://</flux:input.group.prefix>
|
||||
<flux:input wire:model="domain" />
|
||||
<flux:input.group.suffix>.com</flux:input.group.suffix>
|
||||
</flux:input.group>
|
||||
```
|
||||
|
||||
## With Button
|
||||
|
||||
```blade
|
||||
<flux:input.group>
|
||||
<flux:input wire:model="email" placeholder="Enter email" />
|
||||
<flux:button type="submit">Subscribe</flux:button>
|
||||
</flux:input.group>
|
||||
```
|
||||
|
||||
## Custom Icon Slot
|
||||
|
||||
```blade
|
||||
<flux:input wire:model="search">
|
||||
<x-slot name="iconTrailing">
|
||||
<flux:button size="sm" icon="magnifying-glass" />
|
||||
</x-slot>
|
||||
</flux:input>
|
||||
```
|
||||
124
docs/specs/ui/flux/components/kanban.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# flux:kanban (Pro)
|
||||
|
||||
Workflow visualisation with draggable cards organised into columns.
|
||||
|
||||
## Child Components
|
||||
|
||||
### flux:kanban
|
||||
|
||||
Container for all columns.
|
||||
|
||||
### flux:kanban.column
|
||||
|
||||
Individual workflow stage column.
|
||||
|
||||
### flux:kanban.column.header
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | string | - | Column title |
|
||||
| `subheading` | string | - | Secondary text |
|
||||
| `count` | number | - | Card count display |
|
||||
| `badge` | string | - | Badge content |
|
||||
|
||||
**Slots:** `default` (overrides heading/count), `actions` (right-aligned buttons)
|
||||
|
||||
### flux:kanban.column.cards
|
||||
|
||||
Container for cards within a column.
|
||||
|
||||
### flux:kanban.column.footer
|
||||
|
||||
Bottom section for "Add card" forms/buttons.
|
||||
|
||||
### flux:kanban.card
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | string | - | Card title |
|
||||
| `as` | string | div | `button`, `div` (button enables click) |
|
||||
|
||||
**Slots:** `default` (content), `header` (badges/tags), `footer` (avatars/metadata)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```blade
|
||||
<flux:kanban>
|
||||
<flux:kanban.column>
|
||||
<flux:kanban.column.header heading="To Do" :count="3" />
|
||||
<flux:kanban.column.cards>
|
||||
<flux:kanban.card heading="Design homepage" />
|
||||
<flux:kanban.card heading="Write copy" />
|
||||
<flux:kanban.card heading="Review assets" />
|
||||
</flux:kanban.column.cards>
|
||||
</flux:kanban.column>
|
||||
|
||||
<flux:kanban.column>
|
||||
<flux:kanban.column.header heading="In Progress" :count="2" />
|
||||
<flux:kanban.column.cards>
|
||||
<flux:kanban.card heading="Build components" />
|
||||
<flux:kanban.card heading="API integration" />
|
||||
</flux:kanban.column.cards>
|
||||
</flux:kanban.column>
|
||||
|
||||
<flux:kanban.column>
|
||||
<flux:kanban.column.header heading="Done" :count="1" />
|
||||
<flux:kanban.column.cards>
|
||||
<flux:kanban.card heading="Project setup" />
|
||||
</flux:kanban.column.cards>
|
||||
</flux:kanban.column>
|
||||
</flux:kanban>
|
||||
```
|
||||
|
||||
## With Header Actions
|
||||
|
||||
```blade
|
||||
<flux:kanban.column.header heading="To Do" :count="5">
|
||||
<x-slot name="actions">
|
||||
<flux:dropdown>
|
||||
<flux:button icon="ellipsis-horizontal" variant="ghost" size="sm" />
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="plus">Add card</flux:menu.item>
|
||||
<flux:menu.item icon="archive-box">Archive column</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</x-slot>
|
||||
</flux:kanban.column.header>
|
||||
```
|
||||
|
||||
## Cards with Metadata
|
||||
|
||||
```blade
|
||||
<flux:kanban.card heading="Design system">
|
||||
<x-slot name="header">
|
||||
<flux:badge color="blue" size="sm">Design</flux:badge>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<flux:avatar.group>
|
||||
<flux:avatar size="xs" src="/avatars/1.jpg" />
|
||||
<flux:avatar size="xs" src="/avatars/2.jpg" />
|
||||
</flux:avatar.group>
|
||||
<flux:text size="sm" class="text-zinc-500">Due Dec 15</flux:text>
|
||||
</x-slot>
|
||||
</flux:kanban.card>
|
||||
```
|
||||
|
||||
## Interactive Cards
|
||||
|
||||
```blade
|
||||
<flux:kanban.card as="button" wire:click="openCard({{ $card->id }})" heading="{{ $card->title }}" />
|
||||
```
|
||||
|
||||
## With Footer Actions
|
||||
|
||||
```blade
|
||||
<flux:kanban.column>
|
||||
<flux:kanban.column.header heading="Backlog" />
|
||||
<flux:kanban.column.cards>
|
||||
{{-- Cards --}}
|
||||
</flux:kanban.column.cards>
|
||||
<flux:kanban.column.footer>
|
||||
<flux:button icon="plus" variant="ghost" class="w-full">Add card</flux:button>
|
||||
</flux:kanban.column.footer>
|
||||
</flux:kanban.column>
|
||||
```
|
||||