Add skills documentation and misc updates

Add form value persistence across cache revalidation re-renders

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-29 04:38:06 +00:00
parent 432d167dda
commit 1b46c5270c
21 changed files with 5540 additions and 323 deletions

View File

@@ -410,74 +410,33 @@ The process involves creating Action classes with @route decorators and converti
**Note**: SPA pages are the preferred standard. Use Blade only for SEO-critical public pages or authentication flows.
```blade
@rsx_id('Frontend_Index') {{-- Every view starts with this --}}
<body class="{{ rsx_body_class() }}"> {{-- Adds view class --}}
```
Pattern recognition:
- `@rsx_id('View_Name')` - required first line
- `rsx_body_class()` - adds view class for CSS scoping
- `@rsx_page_data()` - pass data to JS
- `on_app_ready()` with page guard for JS (fires for ALL pages in bundle)
**NO inline styles, scripts, or event handlers** - Use companion `.scss` and `.js` files.
**jqhtml components** work fully in Blade (no slots).
**Detailed guidance in `blade-views` skill** - auto-activates when building Blade pages.
### SCSS Component-First Architecture
---
SCSS in `rsx/app/` and `rsx/theme/components/` must wrap in a single component class matching the jqhtml/blade file. Components auto-render with `class="Component_Name"` on root. `rsx/lib/` is non-visual. `rsx/theme/` (outside components/) holds primitives, variables, Bootstrap overrides.
## SCSS ARCHITECTURE
**BEM**: Child classes use component's exact name as prefix. `.Component_Name { &__element }` → HTML: `<div class="Component_Name__element">` (no kebab-case).
**Component-first**: Every styled element is a component with scoped SCSS. No generic classes scattered across files.
**Variables**: `rsx/theme/variables.scss` - must be included before directory includes in bundles. Multiple SCSS files can target same component if primary file exists.
**Scoping rules**:
- Files in `rsx/app/` and `rsx/theme/components/` **must** wrap in single component class
- Wrapper class matches JS class or Blade `@rsx_id`
- BEM children use PascalCase: `.Component_Name__element` (NOT kebab-case)
Details: `php artisan rsx:man scss`
**Responsive breakpoints** (Bootstrap defaults do NOT work):
- Tier 1: `mobile` (0-1023px), `desktop` (1024px+)
- Tier 2: `phone`, `tablet`, `desktop-sm`, `desktop-md`, `desktop-lg`, `desktop-xl`
- SCSS: `@include mobile { }`, `@include desktop-xl { }`
- Classes: `.col-mobile-12`, `.d-tablet-none`, `.mobile-only`
- JS: `Responsive.is_mobile()`, `Responsive.is_phone()`
### Responsive Breakpoints
RSX replaces Bootstrap breakpoints with semantic names. **Bootstrap defaults (col-md-6, d-lg-none) do NOT work.**
| Tier 1 | Range | Tier 2 | Range |
|--------|-------|--------|-------|
| `mobile` | 0-1023px | `phone` | 0-799px |
| `desktop` | 1024px+ | `tablet` | 800-1023px |
| | | `desktop-sm` | 1024-1199px |
| | | `desktop-md` | 1200-1639px |
| | | `desktop-lg` | 1640-2199px |
| | | `desktop-xl` | 2200px+ |
**SCSS**: `@include mobile { }`, `@include desktop { }`, `@include phone { }`, `@include desktop-xl { }`, etc.
**Classes**: `.col-mobile-6`, `.d-desktop-none`, `.mobile-only`, `.hide-tablet`
**JS**: `Responsive.is_mobile()`, `Responsive.is_phone()`, `Responsive.is_desktop_xl()`, etc.
Details: `php artisan rsx:man responsive`
### JavaScript for Blade Pages
Unlike SPA actions (which use component lifecycle), Blade pages use static `on_app_ready()` with a page guard:
```javascript
class My_Page { // Matches @rsx_id('My_Page')
static on_app_ready() {
if (!$('.My_Page').exists()) return; // Guard required - fires for ALL pages in bundle
// Page code here
}
}
```
### Passing Data to JavaScript
Use `@rsx_page_data` for page-specific data needed by JavaScript (IDs, config, etc.):
```blade
@rsx_page_data(['user_id' => $user->id, 'mode' => 'edit'])
@section('content')
<!-- Your HTML -->
@endsection
```
Access in JavaScript:
```javascript
const user_id = window.rsxapp.page_data.user_id;
```
Use when data doesn't belong in DOM attributes. Multiple calls merge together.
**Detailed guidance in `scss` skill** - auto-activates when styling components.
---
@@ -732,174 +691,27 @@ this.$sid('result_container').component('My_Component', {
## FORM COMPONENTS
**Form fields** (`<Rsx_Form>` with `$data`, `$controller`, `$method`):
```blade
<Rsx_Form $data="{!! json_encode($form_data) !!}" $controller="Controller" $method="save">
<Form_Field $name="email" $label="Email" $required=true>
<Text_Input $type="email" />
</Form_Field>
Forms use `<Rsx_Form>` with `$data`, `$controller`, `$method` for automatic data binding. Key components: `Form_Field`, `Form_Field_Hidden`, input types (`Text_Input`, `Select_Input`, `Checkbox_Input`).
<Form_Field_Hidden $name="id" />
</Rsx_Form>
```
Pattern recognition:
- `vals()` dual-mode method for get/set
- `Form_Utils.apply_form_errors()` for validation
- Action loads data, Controller saves it
- `$disabled=true` still returns values (unlike HTML)
- **Form_Field** - Standard formatted field with label, errors, help text
- **Form_Field_Hidden** - Single-tag hidden input (extends Form_Field_Abstract)
- **Form_Field_Abstract** - Base class for custom formatting (advanced)
**Disabled fields**: Use `$disabled=true` attribute on input components to disable fields. Unlike standard HTML, disabled fields still return values via `vals()` (useful for read-only data that should be submitted).
```blade
<Text_Input $type="email" $disabled=true />
<Select_Input $options="{!! json_encode($options) !!}" $disabled=true />
<Checkbox_Input $label="Subscribe" $disabled=true />
```
**Form component classes** use the **vals() dual-mode pattern**:
```javascript
class My_Form extends Component {
vals(values) {
if (values) {
// Setter - populate form
this.$sid('name').val(values.name || '');
return null;
} else {
// Getter - extract values
return {name: this.$sid('name').val()};
}
}
}
```
**Validation**: `Form_Utils.apply_form_errors(form.$, errors)` - Matches by `name` attribute.
### Form Conventions (Action/Controller Pattern)
Forms follow a load/save pattern mirroring traditional Laravel: Action loads data, Controller saves it.
```javascript
// Action: on_create() sets defaults, on_load() fetches for edit mode
on_create() {
this.data.form_data = { title: '', status_id: Model.STATUS_ACTIVE };
this.data.is_edit = !!this.args.id;
}
async on_load() {
if (!this.data.is_edit) return;
const record = await My_Model.fetch(this.args.id);
this.data.form_data = { id: record.id, title: record.title };
}
```
```php
// Controller: save() receives all form values, validates, persists
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
if (empty($params['title'])) return response_form_error('Validation failed', ['title' => 'Required']);
$record = $params['id'] ? My_Model::find($params['id']) : new My_Model();
$record->title = $params['title'];
$record->save();
return ['redirect' => Rsx::Route('View_Action', $record->id)];
}
```
**Key principles**: form_data must be serializable (plain objects, no models) | Keep load/save in same controller for field alignment | on_load() loads data, on_ready() is UI-only
Details: `php artisan rsx:man form_conventions`
### Polymorphic Form Fields
For fields that can reference multiple model types (e.g., an Activity linked to either a Contact or Project), use JSON-encoded polymorphic values.
```php
use App\RSpade\Core\Polymorphic_Field_Helper;
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
Contact_Model::class,
Project_Model::class,
]);
if ($error = $eventable->validate('Please select an entity')) {
$errors['eventable'] = $error;
}
$model->eventable_type = $eventable->model;
$model->eventable_id = $eventable->id;
```
Client submits: `{"model":"Contact_Model","id":123}`. Always use `Model::class` for the whitelist.
Details: `php artisan rsx:man polymorphic`
**Detailed guidance in `forms` skill** - auto-activates when building forms.
---
## MODALS
### Built-in Dialog Types
Built-in dialogs: `Modal.alert()`, `Modal.confirm()`, `Modal.prompt()`, `Modal.select()`, `Modal.error()`.
| Method | Returns | Description |
|--------|---------|-------------|
| `Modal.alert(body)` | `void` | Simple notification |
| `Modal.alert(title, body, buttonLabel?)` | `void` | Alert with title |
| `Modal.confirm(body)` | `boolean` | Yes/no confirmation |
| `Modal.confirm(title, body, confirmLabel?, cancelLabel?)` | `boolean` | Confirmation with labels |
| `Modal.prompt(body)` | `string\|false` | Text input |
| `Modal.prompt(title, body, default?, multiline?)` | `string\|false` | Prompt with options |
| `Modal.select(body, options)` | `string\|false` | Dropdown selection |
| `Modal.select(title, body, options, default?, placeholder?)` | `string\|false` | Select with options |
| `Modal.error(error, title?)` | `void` | Error with red styling |
| `Modal.unclosable(title, body)` | `void` | Modal user cannot close |
Form modals: `Modal.form({title, component, on_submit})` - component must implement `vals()`.
```javascript
await Modal.alert("File saved");
if (await Modal.confirm("Delete?")) { /* confirmed */ }
const name = await Modal.prompt("Enter name:");
const choice = await Modal.select("Choose:", [{value: 'a', label: 'A'}, {value: 'b', label: 'B'}]);
await Modal.error("Something went wrong");
```
Reusable modals: Extend `Modal_Abstract`, implement static `show()`.
### Form Modals
```javascript
const result = await Modal.form({
title: "Edit User",
component: "User_Form",
component_args: {data: user},
on_submit: async (form) => {
const response = await User_Controller.save(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false; // Keep open
}
return response.data; // Close and return
}
});
```
Form component must implement `vals()` and include `<div $sid="error_container"></div>`.
### Modal Classes
For complex/reusable modals, create dedicated classes:
```javascript
class Add_User_Modal extends Modal_Abstract {
static async show() {
return await Modal.form({...}) || false;
}
}
// Usage
const user = await Add_User_Modal.show();
if (user) {
grid.reload();
await Next_Modal.show(user.id);
}
```
Pattern: Extend `Modal_Abstract`, implement static `show()`, return data or `false`.
Details: `php artisan rsx:man modals`
**Detailed guidance in `modals` skill** - auto-activates when building modals.
---
@@ -934,46 +746,15 @@ User_Model::create(['email' => $email]);
### Enums
**CRITICAL: Read `php artisan rsx:man enum` for complete documentation before implementing.**
Integer-backed enums with model-level mapping. Define in `$enums` array with `constant`, `label`, and custom properties.
Integer-backed enums with model-level mapping to constants, labels, and custom properties. Uses BEM-style double underscore naming for magic properties.
Pattern recognition:
- BEM-style access: `$model->status_id__label`, `$model->status_id__badge`
- JS methods: `Model.status_id__enum()`, `Model.status_id__enum_select()`
- Static constants: `Model::STATUS_ACTIVE`
- Use BIGINT columns, run `rsx:migrate:document_models` after changes
```php
class Project_Model extends Rsx_Model_Abstract {
public static $enums = [
'status_id' => [
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success', 'order' => 1],
2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false, 'order' => 99],
],
];
}
// Usage - BEM-style: field__property (double underscore)
$project->status_id = Project_Model::STATUS_ACTIVE;
echo $project->status_id__label; // "Active"
echo $project->status_id__badge; // "bg-success" (custom property)
```
**Special properties**:
- `order` - Sort position in dropdowns (default: 0, lower first)
- `selectable` - Include in dropdown options (default: true). Non-selectable items excluded from `field__enum_select()` but still shown if current value.
**JavaScript static methods** (BEM-style double underscore):
```js
// Get all enum data
Project_Model.status_id__enum()
// Get specific enum's metadata by ID
Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE).badge // "bg-success"
Project_Model.status_id__enum(2).selectable // false
// Other methods
Project_Model.status_id__enum_select() // For dropdowns
Project_Model.status_id__enum_labels() // id => label map
Project_Model.status_id__enum_ids() // Array of valid IDs
```
**Migration:** Use BIGINT for enum columns, TINYINT(1) for booleans. Run `rsx:migrate:document_models` after adding enums.
**Detailed guidance in `model-enums` skill** - auto-activates when implementing enums.
### Model Fetch
@@ -1033,27 +814,13 @@ No `IF EXISTS`, no `information_schema` queries, no fallbacks. Know current stat
Files upload UNATTACHED → validate → assign via API. Session-based validation prevents cross-user file assignment.
```php
// Controller: Assign uploaded file
$attachment = File_Attachment_Model::find_by_key($params['photo_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
$attachment->attach_to($user, 'profile_photo'); // Single (replaces)
$attachment->add_to($project, 'documents'); // Multiple (adds)
}
Pattern recognition:
- `File_Attachment_Model::find_by_key()` + `can_user_assign_this_file()`
- `attach_to()` (single/replaces) vs `add_to()` (multiple/adds)
- `get_attachment()` / `get_attachments()` for retrieval
- `get_url()`, `get_download_url()`, `get_thumbnail_url()`
// Model: Retrieve attachments
$photo = $user->get_attachment('profile_photo');
$documents = $project->get_attachments('documents');
// Display
$photo->get_thumbnail_url('cover', 128, 128);
$photo->get_url();
$photo->get_download_url();
```
**Endpoints:** `POST /_upload`, `GET /_download/:key`, `GET /_thumbnail/:key/:type/:width/:height`
Details: `php artisan rsx:man file_upload`
**Detailed guidance in `file-attachments` skill** - auto-activates when handling uploads.
---
@@ -1180,48 +947,15 @@ Sessions persist 365 days. Never implement "Remember Me".
**Two Classes - Strict Separation**: `Rsx_Time` (datetimes with timezone) | `Rsx_Date` (calendar dates, no timezone)
**String-Based Philosophy**: RSpade uses ISO strings, not Carbon objects. Dates are `"2025-12-24"`, datetimes are `"2025-12-24T15:30:00-06:00"`. Same format in PHP, JavaScript, JSON, database queries. No object serialization issues.
**String-Based**: ISO strings, not Carbon. Never use `$casts` with `'date'`/`'datetime'` - blocked by `rsx:check`.
**Model Casts**: `Rsx_Model_Abstract` auto-applies `Rsx_Date_Cast` (DATE columns) and `Rsx_DateTime_Cast` (DATETIME columns). Never define `$casts` with `'date'`, `'datetime'`, or `'timestamp'` - these use Carbon and are blocked by `rsx:check`.
Pattern recognition:
- `Rsx_Time::now()`, `Rsx_Time::format()`, `Rsx_Time::relative()`
- `Rsx_Date::today()`, `Rsx_Date::format()`, `Rsx_Date::is_past()`
- `Rsx_Time::to_database()` for UTC storage
- Functions throw if wrong type passed
### Rsx_Time - Moments in Time
```php
use App\RSpade\Core\Time\Rsx_Time;
Rsx_Time::now(); // Current time in user's timezone
Rsx_Time::now_iso(); // ISO 8601 format: 2025-12-24T15:30:00-06:00
Rsx_Time::format($datetime); // "Dec 24, 2025 3:30 PM"
Rsx_Time::format_short($datetime); // "Dec 24, 3:30 PM"
Rsx_Time::to_database($datetime); // UTC for storage
Rsx_Time::get_user_timezone(); // User's timezone or default
```
```javascript
Rsx_Time.now(); // Current moment (timezone-aware)
Rsx_Time.format(datetime); // Formatted for display
Rsx_Time.relative(datetime); // "2 hours ago", "in 3 days"
```
### Rsx_Date - Calendar Dates
```php
use App\RSpade\Core\Time\Rsx_Date;
Rsx_Date::today(); // "2025-12-24" (user's timezone)
Rsx_Date::format($date); // "Dec 24, 2025"
Rsx_Date::is_today($date); // Boolean
Rsx_Date::is_past($date); // Boolean
```
**Key Principle**: Functions throw if wrong type passed (datetime to date function or vice versa).
### Server Time Sync
Client time syncs automatically via rsxapp data on page load and AJAX responses. No manual sync required.
### User Timezone
Stored in `login_users.timezone` (IANA format). Falls back to `config('rsx.datetime.default_timezone')`.
Details: `php artisan rsx:man time`
**Detailed guidance in `date-time` skill** - auto-activates when working with dates/times.
---
@@ -1321,6 +1055,33 @@ Run `rsx:check` before commits. Enforces naming, prohibits animations on non-act
---
## SKILLS (Auto-Activated)
Detailed guidance for specific tasks is available via Claude Code skills. These activate automatically when relevant - no action needed.
| Skill | Activates When |
|-------|---------------|
| `forms` | Building forms, validation, vals() pattern |
| `modals` | Creating dialogs, Modal.form(), Modal_Abstract |
| `model-enums` | Implementing status_id, type_id, enum properties |
| `file-attachments` | Upload flow, attach_to(), thumbnails |
| `date-time` | Rsx_Time, Rsx_Date, timezone handling |
| `blade-views` | Server-rendered pages, @rsx_id, @rsx_page_data |
| `scss` | Component styling, BEM, responsive breakpoints |
| `ajax-error-handling` | response_error(), Form_Utils, error codes |
| `model-fetch` | Model.fetch(), lazy relationships, #[Ajax_Endpoint_Model_Fetch] |
| `jquery-extensions` | .click() override, .exists(), .shallowFind() |
| `background-tasks` | #[Task], #[Schedule], Task::dispatch() |
| `crud-patterns` | List/view/edit structure, DataGrid, dual-route actions |
| `js-decorators` | @route, @spa, @layout, @mutex, custom decorators |
| `event-hooks` | #[OnEvent], filters, gates, actions |
| `migrations` | Raw SQL, make:migration:safe, forward-only |
| `polymorphic` | Type refs, morphTo, Polymorphic_Field_Helper |
Skills location: `~/.claude/skills/` (symlinked from `system/docs/skills/`)
---
## GETTING HELP
```bash