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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user