Files
rspade_system/docs/CLAUDE.dist.md
root 9ebcc359ae Fix code quality violations and enhance ROUTE-EXISTS-01 rule
Implement JQHTML function cache ID system and fix bundle compilation
Implement underscore prefix for system tables
Fix JS syntax linter to support decorators and grant exception to Task system
SPA: Update planning docs and wishlists with remaining features
SPA: Document Navigation API abandonment and future enhancements
Implement SPA browser integration with History API (Phase 1)
Convert contacts view page to SPA action
Convert clients pages to SPA actions and document conversion procedure
SPA: Merge GET parameters and update documentation
Implement SPA route URL generation in JavaScript and PHP
Implement SPA bootstrap controller architecture
Add SPA routing manual page (rsx:man spa)
Add SPA routing documentation to CLAUDE.md
Phase 4 Complete: Client-side SPA routing implementation
Update get_routes() consumers for unified route structure
Complete SPA Phase 3: PHP-side route type detection and is_spa flag
Restore unified routes structure and Manifest_Query class
Refactor route indexing and add SPA infrastructure
Phase 3 Complete: SPA route registration in manifest
Implement SPA Phase 2: Extract router code and test decorators
Rename Jqhtml_Component to Component and complete SPA foundation setup

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 17:48:15 +00:00

1170 lines
33 KiB
Markdown

<!--
===============================================================================
WARNING: READ-ONLY FRAMEWORK DOCUMENTATION
===============================================================================
This file contains RSpade framework documentation for AI/LLM assistants and is
maintained by the framework developers. It is marked read-only and will be
REPLACED during framework updates via `php artisan rsx:framework:pull`.
DO NOT modify this file directly. Any changes you make will be lost during the
next framework update.
For application-specific documentation and project context:
- Edit the CLAUDE.md file in your project root
- That file is NOT managed by the framework and persists across updates
This separation ensures:
- Framework documentation stays current with updates
- Your project-specific documentation is preserved
- AI assistants have access to both framework and application context
===============================================================================
-->
# RSpade Framework - AI/LLM Development Guide
**PURPOSE**: Essential directives for AI/LLM assistants developing RSX applications with RSpade.
## What is RSpade?
**Visual Basic-like development for PHP/Laravel.** Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.
**Philosophy**: Modern anti-modernization. While JavaScript fragments into complexity and React demands quarterly paradigm shifts, RSpade asks: "What if coding was easy again?" Business apps need quick builds and easy maintenance, not bleeding-edge architecture.
**Important**: RSpade is built on Laravel but diverges significantly. Do not assume Laravel patterns work in RSX without verification.
**Terminology**: **RSpade** = Complete framework | **RSX** = Your application code in `/rsx/`
---
## CRITICAL RULES
### 🔴 RSpade Builds Automatically - NEVER RUN BUILD COMMANDS
**RSpade is an INTERPRETED framework** - like Python or PHP, changes are automatically detected and compiled on-the-fly. There is NO manual build step.
**ABSOLUTELY FORBIDDEN** (unless explicitly instructed):
- `npm run compile` / `npm run build` - **DO NOT EXIST**
- `bin/publish` - Creates releases for OTHER developers (not for testing YOUR changes)
- `rsx:bundle:compile` - Bundles compile automatically in dev mode
- `rsx:manifest:build` - Manifest rebuilds automatically in dev mode
- ANY command with "build", "compile", or "publish"
**How it works**:
1. Edit JS/SCSS/PHP files
2. Refresh browser
3. Changes are live (< 1 second)
**If you find yourself wanting to run build commands**: STOP. You're doing something wrong. Changes are already live.
### 🔴 Framework Updates
```bash
php artisan rsx:framework:pull # 5-minute timeout required
```
Updates take 2-5 minutes. Includes code pull, manifest rebuild, bundle recompilation. **For AI: Always use 5-minute timeout.**
### 🔴 Fail Loud - No Silent Fallbacks
**ALWAYS fail visibly.** No redundant fallbacks, silent failures, or alternative code paths.
```php
// ❌ CATASTROPHIC
try { $clean = Sanitizer::sanitize($input); }
catch (Exception $e) { $clean = $input; } // DISASTER
// ✅ CORRECT
$clean = Sanitizer::sanitize($input); // Let it throw
```
**SECURITY-CRITICAL**: If sanitization/validation/auth fails, NEVER continue. Always throw immediately.
**NO BLANKET TRY/CATCH**: Use try/catch only for expected failures (file uploads, external APIs, user input parsing). NEVER wrap database operations or entire functions "just in case".
```php
// ❌ WRONG - Defensive "on error resume"
try {
$user->save();
$result = process_data($user);
return $result;
} catch (Exception $e) {
throw new Exception("Failed: " . $e->getMessage(), 0, $e);
}
// ✅ CORRECT - Let exceptions bubble
$user->save();
$result = process_data($user);
return $result;
```
Exception handlers format errors for different contexts (Ajax JSON, CLI, HTML). Don't wrap exceptions with generic messages - let them bubble to the global handler.
### 🔴 No Defensive Coding
Core classes ALWAYS exist. Never check.
```javascript
// ❌ BAD: if (typeof Rsx !== 'undefined') { Rsx.Route('Controller::method') }
// ✅ GOOD: Rsx.Route('Controller::method')
```
### 🔴 Static-First Philosophy
Classes are namespacing tools. Use static unless instances needed (models, resources). Avoid dependency injection.
### 🔴 Git Workflow - Framework is READ-ONLY
**NEVER modify `/var/www/html/system/`** - It's like node_modules or the Linux kernel.
- **App repo**: `/var/www/html/.git` (you control)
- **Framework**: `/var/www/html/system/` (submodule, don't touch)
- **Your code**: `/var/www/html/rsx/` (all changes here)
**Commit discipline**: ONLY commit when explicitly asked. Commits are milestones, not individual changes.
### 🔴 DO NOT RUN `rsx:clean`
**RSpade's cache auto-invalidates on file changes.** Running `rsx:clean` causes 30-60 second rebuilds with zero benefit.
**When to use**: Only on catastrophic corruption, after framework updates (automatic), or when explicitly instructed.
**Correct workflow**: Edit → Save → Reload browser → See changes (< 1 second)
### 🔴 Trust Code Quality Rules
Each `rsx:check` rule has remediation text that tells AI assistants exactly what to do:
- Some rules say "fix immediately"
- Some rules say "present options and wait for decision"
AI should follow the rule's guidance precisely. Rules are deliberately written and well-reasoned.
---
## NAMING CONVENTIONS
**Enforced by `rsx:check`**:
| Context | Convention | Example |
|---------|------------|---------|
| PHP Methods/Variables | `underscore_case` | `user_name` |
| PHP Classes | `Like_This` | `User_Controller` |
| JavaScript Classes | `Like_This` | `User_Card` |
| Files | `lowercase_underscore` | `user_controller.php` |
| Database Tables | `lowercase_plural` | `users` |
| Constants | `UPPERCASE` | `MAX_SIZE` |
### File Prefix Grouping
Files sharing a common prefix are a related set. When renaming, maintain the grouping across ALL files with that prefix.
**Example** - `rsx/app/frontend/calendar/`:
```
frontend_calendar_event.scss
frontend_calendar_event_controller.php
frontend_calendar_event.blade.php
frontend_calendar_event.js
```
**Critical**: Never create same-name different-case files (e.g., `user.php` and `User.php`).
---
## DIRECTORY STRUCTURE
```
/var/www/html/
├── rsx/ # YOUR CODE
│ ├── app/ # Modules
│ ├── models/ # Database models
│ ├── public/ # Static files (web-accessible)
│ ├── resource/ # Framework-ignored
│ └── theme/ # Global assets
└── system/ # FRAMEWORK (read-only)
```
### Special Directories (Path-Agnostic)
**`resource/`** - ANY directory named this is framework-ignored. Store helpers, docs, third-party code. Exception: `/rsx/resource/config/` IS processed.
**`public/`** - ANY directory named this is web-accessible, framework-ignored. 5min cache, 30d with `?v=`.
### Path-Agnostic Loading
Classes found by name, not path. No imports needed.
```php
$user = User_Model::find(1); // Framework finds it
// NOT: use Rsx\Models\User_Model; // Auto-generated
```
---
## CONFIGURATION
**Two-tier system**:
- **Framework**: `/system/config/rsx.php` (never modify)
- **User**: `/rsx/resource/config/rsx.php` (your overrides)
Merged via `array_merge_deep()`. Common overrides: `development.auto_rename_files`, `bundle_aliases`, `console_debug`.
---
## ROUTING & CONTROLLERS
```php
class Frontend_Controller extends Rsx_Controller_Abstract
{
#[Auth('Permission::anybody()')]
#[Route('/', methods: ['GET'])]
public static function index(Request $request, array $params = [])
{
return rsx_view('Frontend_Index', [
'bundle' => Frontend_Bundle::render()
]);
}
}
```
**Rules**: Only GET/POST allowed. Use `:param` syntax. All routes MUST have `#[Auth]`.
### #[Auth] Attribute
```php
#[Auth('Permission::anybody()')] // Public
#[Auth('Permission::authenticated()')] // Require login
#[Auth('Permission::has_role("admin")')] // Custom
```
**Controller-wide**: Add to `pre_dispatch()`. Multiple attributes = all must pass.
### Type-Safe URLs
**MANDATORY**: All URLs must be generated using `Rsx::Route()` - hardcoded URLs are forbidden.
```php
// PHP - Controller (defaults to 'index' method)
Rsx::Route('User_Controller')
// PHP - Controller with explicit method
Rsx::Route('User_Controller::show', 123);
// PHP - With query parameters
Rsx::Route('Login_Controller::logout', ['redirect' => '/dashboard']);
// Generates: /logout?redirect=%2Fdashboard
// JavaScript (identical syntax)
Rsx.Route('User_Controller')
Rsx.Route('User_Controller::show', 123);
Rsx.Route('Login_Controller::logout', {redirect: '/dashboard'});
```
**Signature**: `Rsx::Route($action, $params = null)` / `Rsx.Route(action, params = null)`
- `$action` - Controller class, SPA action, or "Class::method" (defaults to 'index' if no `::` present)
- `$params` - Integer sets 'id', array/object provides named params
**Query Parameters**: Extra params become query string - automatically URL-encoded
**Enforcement**: `rsx:check` will flag hardcoded URLs like `/login` or `/logout?redirect=...`
and require you to use `Rsx::Route()`. Do it right the first time to avoid rework.
---
## SPA (SINGLE PAGE APPLICATION) ROUTING
Client-side routing for authenticated application areas. One PHP bootstrap controller, multiple JavaScript actions that navigate without page reloads.
### SPA Components
**1. PHP Bootstrap Controller (ONE per feature/bundle)**
```php
class Frontend_Spa_Controller extends Rsx_Controller_Abstract {
#[SPA]
#[Auth('Permission::authenticated()')]
public static function index(Request $request, array $params = []) {
return rsx_view(SPA);
}
}
```
**CRITICAL**: One #[SPA] per feature/bundle (e.g., `/app/frontend`, `/app/root`, `/app/login`). Bundles separate features to save bandwidth, reduce processing time, and segregate confidential code (e.g., root admin from unauthorized users). The #[SPA] bootstrap performs server-side auth checks with failure/redirect before loading client-side actions. Typically one #[SPA] per feature at `rsx/app/(feature)/(feature)_spa_controller::index`.
**2. JavaScript Actions (MANY)**
```javascript
@route('/contacts')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
@title('Contacts') // Optional browser title
class Contacts_Index_Action extends Spa_Action {
async on_load() {
this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
}
}
```
**3. Layout**
```javascript
class Frontend_Layout extends Spa_Layout {
on_action(url, action_name, args) {
// Called after action created, before on_ready
// Access this.action immediately
this.update_navigation(url);
}
}
```
Layout template must have `$id="content"` element where actions render.
### URL Generation & Navigation
```php
// PHP/JavaScript - same syntax
Rsx::Route('Contacts_Index_Action') // /contacts
Rsx::Route('Contacts_View_Action', 123) // /contacts/123
```
```javascript
Spa.dispatch('/contacts/123'); // Programmatic navigation
Spa.layout // Current layout instance
Spa.action // Current action instance
```
### URL Parameters
```javascript
// URL: /contacts/123?tab=history
@route('/contacts/:id')
class Contacts_View_Action extends Spa_Action {
on_create() {
console.log(this.args.id); // "123" (route param)
console.log(this.args.tab); // "history" (query param)
}
}
```
### File Organization
```
/rsx/app/frontend/
├── Frontend_Spa_Controller.php # Single SPA bootstrap
├── Frontend_Layout.js
├── Frontend_Layout.jqhtml
└── contacts/
├── frontend_contacts_controller.php # Ajax endpoints only
├── Contacts_Index_Action.js # /contacts
├── Contacts_Index_Action.jqhtml
├── Contacts_View_Action.js # /contacts/:id
└── Contacts_View_Action.jqhtml
```
**Use SPA for:** Authenticated areas, dashboards, admin panels
**Avoid for:** Public pages (SEO needed), simple static pages
Details: `php artisan rsx:man spa`
---
## CONVERTING BLADE PAGES TO SPA ACTIONS
**7-step procedure to convert server-side pages to client-side SPA:**
**1. Create Action Files**
```bash
# In feature directory
touch Feature_Index_Action.js Feature_Index_Action.jqhtml
```
**2. Create Action Class (.js)**
```javascript
@route('/path')
@layout('Frontend_Spa_Layout')
@spa('Frontend_Spa_Controller::index')
@title('Page Title')
class Feature_Index_Action extends Spa_Action {
full_width = true; // For DataGrid pages
async on_load() {
this.data.items = await Feature_Controller.fetch_items();
}
}
```
**3. Convert Template (.jqhtml)**
**Blade → jqhtml syntax:**
- `{{ $var }}``<%= this.data.var %>`
- `{!! $html !!}``<%!= this.data.html %>`
- `@if($cond)``<% if (this.data.cond) { %>`
- `@foreach($items as $item)``<% for (let item of this.data.items) { %>`
- `@endforeach``<% } %>`
- `{{-- comment --}}``<%-- comment --%>`
- `{{ Rsx::Route('Class') }}``<%= Rsx.Route('Class') %>`
```jqhtml
<Define:Feature_Index_Action>
<Page>
<Page_Header><Page_Title>Title</Page_Title></Page_Header>
<% for (let item of this.data.items) { %>
<div><%= item.name %></div>
<% } %>
</Page>
</Define:Feature_Index_Action>
```
**4. Update Controller**
```php
// Change #[Route] to #[SPA]
#[SPA]
public static function index(Request $request, array $params = []) {
return rsx_view(SPA);
}
// Add Ajax endpoints
#[Ajax_Endpoint]
public static function fetch_items(Request $request, array $params = []): array {
return ['items' => Feature_Model::all()];
}
```
**5. Update Route References**
**Search entire codebase for old route references:**
```bash
grep -r "Feature_Controller::method" rsx/app/
```
Find/replace in all files:
- `Rsx::Route('Feature_Controller::method')``Rsx::Route('Feature_Action')`
- `Rsx.Route('Feature_Controller::method')``Rsx.Route('Feature_Action')`
- Hardcoded `/feature/method/123``Rsx.Route('Feature_Action', 123)`
**Check:** DataGrids, dashboards, save endpoints, navigation, breadcrumbs
**6. Archive Old Files**
```bash
mkdir -p rsx/resource/archive/frontend/feature/
mv feature_index.blade.php rsx/resource/archive/frontend/feature/
```
**7. Test**
```bash
php artisan rsx:debug /path
```
Verify: No JS errors, page renders, data loads.
**Common Patterns:**
**DataGrid (no data loading):**
```javascript
class Items_Index_Action extends Spa_Action {
full_width = true;
async on_load() {} // DataGrid loads own data
}
```
**Detail page (load data):**
```javascript
@route('/items/:id')
class Items_View_Action extends Spa_Action {
async on_load() {
this.data.item = await Items_Controller.get({id: this.args.id});
}
}
```
---
## BLADE & VIEWS
```blade
@rsx_id('Frontend_Index') {{-- Every view starts with this --}}
<body class="{{ rsx_body_class() }}"> {{-- Adds view class --}}
@rsx_include('Component_Name') {{-- Include by name, not path --}}
```
**NO inline styles, scripts, or event handlers** in Blade views:
- No `<style>` tags
- No `<script>` tags
- No inline event handlers: `onclick=""`, `onchange=""`, etc. are **forbidden**
- Use companion `.scss` and `.js` files instead
- Exception: jqhtml templates may use `@click` directive syntax (jqhtml-specific feature)
**Why no inline handlers**:
- Violates separation of concerns (HTML structure vs behavior)
- Makes code harder to maintain and test
- `rsx:check` will flag inline event handlers and require refactoring
**Correct pattern**:
```php
// Blade view - NO event handlers
<button id="submit-btn" class="btn btn-primary">Submit</button>
// Companion .js file
$('#submit-btn').click(() => {
// Handle click
});
```
### SCSS Pairing
```scss
/* frontend_index.scss - Same directory as view */
.Frontend_Index { /* Matches @rsx_id */
.content { padding: 20px; }
}
```
### 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.
---
## JAVASCRIPT
### Auto-Initialization
```javascript
class Frontend_Index {
static async on_app_ready() {
// DOM ready
}
static async on_jqhtml_ready() {
// Components ready
}
}
```
**CRITICAL**: JavaScript only executes when bundle rendered.
---
## BUNDLE SYSTEM
**One bundle per page required.**
```php
class Frontend_Bundle extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => [
'jquery', // Required
'lodash', // Required
'rsx/theme/variables.scss', // Order matters
'rsx/app/frontend', // Directory
'rsx/models', // For JS stubs
'/public/vendor/css/core.css', // Public directory asset (filemtime cache-busting)
],
];
}
}
```
Bundles support `/public/` prefix for including static assets from public directories with automatic cache-busting.
Auto-compiles on page reload in development.
```blade
<head>
{!! Frontend_Bundle::render() !!}
</head>
```
---
## JQHTML COMPONENTS
### Philosophy
For mechanical thinkers who see structure, not visuals. Write `<User_Card>` not `<div class="card">`. Name what things ARE.
### Template Syntax
**🔴 CRITICAL: `<Define>` IS the element, not a wrapper**
```jqhtml
<!-- ✅ CORRECT - Define becomes button -->
<Define:Save_Button tag="button" class="btn btn-primary">
Save
</Define:Save_Button>
<!-- Renders as: -->
<button class="Save_Button Component btn btn-primary">Save</button>
```
**Interpolation**: `<%= escaped %>` | `<%!= unescaped %>` | `<% javascript %>`
**Conditional Attributes** (v2.2.162+): Apply attributes conditionally using `<% if (condition) { %>attr="value"<% } %>`
directly in attribute context. Works with static values, interpolations, and multiple conditions per element.
Example: `<input <% if (this.args.required) { %>required="required"<% } %> />`
### 🔴 State Management Rules (ENFORCED)
**this.args** - Component arguments (read-only in on_load(), modifiable elsewhere)
**this.data** - Ajax-loaded data (writable ONLY in on_create() and on_load())
**this.state** - Arbitrary component state (modifiable anytime)
**Quick Guide:**
- Loading from API? Use `this.data` in `on_load()`
- Need to reload with new params? Modify `this.args`, call `reload()`
- UI state (toggles, counters, etc)? Use `this.state`
```javascript
// WITH Ajax data
class Users_List extends Component {
on_create() {
this.data.users = []; // Defaults
}
async on_load() {
this.data.users = await User_Controller.fetch({filter: this.args.filter});
}
on_ready() {
// Change filter → reload
this.args.filter = 'new';
this.reload();
}
}
// WITHOUT Ajax data
class Toggle_Button extends Component {
on_create() {
this.state = {is_open: false};
}
on_ready() {
this.$.on('click', () => {
this.state.is_open = !this.state.is_open;
});
}
}
```
**on_load() restrictions** (enforced):
- ✅ Read `this.args`, write `this.data`
- ❌ NO DOM access, NO `this.state`, NO modifying `this.args`
### Lifecycle
1. **on_create()** → Setup default state BEFORE template (sync)
2. **render** → Template executes with initialized state
3. **on_render()** → Hide uninitialized UI (sync)
4. **on_load()** → Fetch data into `this.data` (async)
5. **on_ready()** → DOM manipulation safe (async)
**on_create() now runs first** - Initialize `this.data` properties here so templates can safely reference them:
```javascript
on_create() {
this.data.rows = []; // Prevents "not iterable" errors
this.data.loading = true; // Template can check loading state
}
```
**Double-render**: If `on_load()` modifies `this.data`, component renders twice (defaults → populated).
### Loading Pattern
```javascript
async on_load() {
const result = await Product_Controller.list({page: 1});
this.data.products = result.products;
this.data.loaded = true; // Simple flag at END
}
```
```jqhtml
<% if (!this.data.loaded) { %>
Loading...
<% } else { %>
<!-- Show data -->
<% } %>
```
**NEVER call `this.render()` in `on_load()` - automatic re-render happens.**
### Attributes
- **`$quoted="string"`** → String literal
- **`$unquoted=expression`** → JavaScript expression
- **`$id="name"`** → Scoped element ID
### Component Access
**this.$id(name)** → jQuery object (for DOM):
```javascript
this.$id('button').on('click', ...);
```
**this.id(name)** → Component instance (for methods):
```javascript
const comp = this.id('child'); // ✅ Returns component
await comp.reload();
const comp = this.id('child').component(); // ❌ WRONG
```
### Incremental Scaffolding
**Undefined components work immediately** - they render as div with the component name as a class.
```blade
<Dashboard>
<Stats_Panel />
<Recent_Activity />
</Dashboard>
```
### Common Pitfalls
1. `<Define>` IS the element - use `tag=""` attribute
2. `this.data` starts empty `{}`, set defaults in on_create()
3. ONLY modify `this.data` in on_create() and on_load() (enforced)
4. on_load() can ONLY access this.args and this.data (enforced)
5. Use `this.state = {}` in on_create() for component state (not loaded from Ajax)
6. Use this.args for reload parameters, reload() to re-fetch
7. Use `Controller.method()` not `$.ajax()`
8. Blade components self-closing only
9. `on_create/render/stop` must be sync
10. Use this.id() for components, NOT this.id().component()
### Bundle Integration Required
```blade
{!! Frontend_Bundle::render() !!} {{-- Required for JS --}}
<User_Card user_id="123" /> {{-- Now JS executes --}}
```
For advanced topics: `php artisan rsx:man jqhtml`
---
## 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>
<Form_Field_Hidden $name="id" />
</Rsx_Form>
```
- **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.$id('name').val(values.name || '');
return null;
} else {
// Getter - extract values
return {name: this.$id('name').val()};
}
}
}
```
**Validation**: `Form_Utils.apply_form_errors(form.$, errors)` - Matches by `name` attribute.
---
## MODALS
**Basic dialogs**:
```javascript
await Modal.alert("File saved");
if (await Modal.confirm("Delete?")) { /* confirmed */ }
let name = await Modal.prompt("Enter name:");
```
**Form modals**:
```javascript
const result = await Modal.form({
title: "Edit User",
component: "User_Form",
component_args: {data: user},
on_submit: async (form) => {
const values = form.vals();
const response = await User_Controller.save(values);
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false; // Keep open
}
return response.data; // Close and return
}
});
```
**Requirements**: Form component must implement `vals()` and include `<div $id="error_container"></div>`.
**Modal Classes** (for complex/reusable modals):
```javascript
// Define modal class
class Add_User_Modal extends Modal_Abstract {
static async show() {
const result = await Modal.form({...});
return result || false;
}
}
// Use from page JS
const user = await Add_User_Modal.show();
if (user) {
// Orchestrate post-modal actions
grid.reload();
await Next_Modal.show(user.id);
}
```
Pattern: Extend `Modal_Abstract`, implement static `show()`, return data or `false`. Page JS orchestrates flow, modal classes encapsulate UI.
Details: `php artisan rsx:man modals`
---
## JQUERY EXTENSIONS
RSpade extends jQuery with utility methods:
**Element existence**: `$('.element').exists()` instead of `.length > 0`
**Component traversal**: `this.$.shallowFind('.Widget')` - Finds child elements matching selector that don't have another element of the same class as a parent between them and the component. Prevents selecting widgets from nested child components.
```javascript
// Use case: Finding form widgets without selecting nested widgets
this.$.shallowFind('.Form_Field').each(function() {
// Only processes fields directly in this form,
// not fields in nested sub-forms
});
```
**Sibling component lookup**: `$('.element').closest_sibling('.Widget')` - Searches for elements within progressively higher ancestors. Like `.closest()` but searches within ancestors instead of matching them. Stops at body tag. Useful for component-to-component communication.
**Form validation**: `$('form').checkValidity()` instead of `$('form')[0].checkValidity()`
**Click override**: `.click()` automatically calls `e.preventDefault()`. Use `.click_allow_default()` for native behavior.
For complete details: `php artisan rsx:man jquery`
---
## MODELS & DATABASE
### No Mass Assignment
```php
// ✅ CORRECT
$user = new User_Model();
$user->email = $email;
$user->save();
// ❌ WRONG
User_Model::create(['email' => $email]);
```
### Enums
**🔴 Read `php artisan rsx:man enum` for complete documentation before implementing.**
Integer-backed enums with model-level mapping to constants, labels, and custom properties.
```php
class Project_Model extends Rsx_Model_Abstract {
public static $enums = [
'status_id' => [
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success'],
2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false],
],
];
}
// Usage
$project->status_id = Project_Model::STATUS_ACTIVE;
echo $project->status_label; // "Active"
echo $project->status_badge; // "bg-success" (custom property)
```
**Migration:** Use BIGINT for enum columns, TINYINT(1) for booleans. Run `rsx:migrate:document_models` after adding enums.
### Migrations
**Forward-only, no rollbacks.**
```bash
php artisan make:migration:safe create_users_table
php artisan migrate:begin
php artisan migrate
php artisan migrate:commit
```
---
## FILE ATTACHMENTS
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)
}
// 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`
---
## AJAX ENDPOINTS
```php
#[Ajax_Endpoint]
public static function get_data(Request $request, array $params = [])
{
return ['success' => true, 'data' => ...];
}
```
```javascript
const result = await Demo_Controller.get_data({user_id: 123});
```
### Flash Alerts in Ajax Endpoints
**Server-side: Use success alerts ONLY when providing a redirect.**
When the Ajax response includes a redirect, there's no on-screen element from the previous page to display the success message. Flash alerts persist across navigation to show the result.
```php
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []): array {
// Validation errors - return for client to handle
if (empty($params['name'])) {
return ['success' => false, 'errors' => ['name' => 'Required']];
}
$client->save();
// Success with redirect - use flash alert
Flash_Alert::success('Client saved successfully');
return [
'client_id' => $client->id,
'redirect' => Rsx::Route('Clients_View_Action', $client->id),
];
}
```
**Server pattern:** Success alerts with redirect only. Client handles errors by updating on-screen UI.
**Client-side:** Flash alerts are a UI choice. Use `Flash_Alert.error()`, `Flash_Alert.warning()`, etc. on case-by-case basis (e.g., "Failed to add item to cart - item not available").
---
## BROWSER STORAGE
**Rsx_Storage** - Scoped sessionStorage/localStorage with automatic fallback and quota management. All keys automatically scoped by session, user, site, and build. Gracefully handles unavailable storage and quota exceeded errors. Storage is volatile - use only for non-critical data (caching, UI state, transient messages).
`Rsx_Storage.session_set(key, value)` / `Rsx_Storage.session_get(key)` / `Rsx_Storage.local_set(key, value)` / `Rsx_Storage.local_get(key)`
Details: `php artisan rsx:man storage`
---
### Model Fetch
```php
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!RsxAuth::check()) return false;
return static::find($id);
}
```
```javascript
const user = await User_Model.fetch(1);
```
---
## AUTHENTICATION
**Always use RsxAuth**, never Laravel Auth or $_SESSION.
```php
RsxAuth::check(); // Is authenticated
RsxAuth::user(); // User model
RsxAuth::id(); // User ID
```
Sessions persist 365 days. Never implement "Remember Me".
---
## JAVASCRIPT DECORATORS
```javascript
/** @decorator */
function logCalls(target, key, descriptor) { /* ... */ }
class Service {
@logCalls
@mutex
async save() { /* ... */ }
}
```
---
## COMMANDS
### Module Creation
```bash
rsx:app:module:create <name> # /name
rsx:app:module:feature:create <m> <f> # /m/f
rsx:app:component:create --name=x # Component
```
### Development
```bash
rsx:check # Code quality
rsx:debug /page # Test routes (see below)
rsx:man <topic> # Documentation
db:query "SQL" --json
```
### Testing Routes
**`rsx:debug /path`** - Preferred method for testing routes
Uses Playwright to render the page and show rendered output, JavaScript errors, and console messages.
```bash
rsx:debug /clients # Test route
rsx:debug /dashboard --user=1 # Simulate authenticated user
rsx:debug /contacts --console # Show console.log output
rsx:debug /path --help # Show all options
```
Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features.
### Debugging
- **rsx_dump_die()** - Debug output
- **console_debug("CHANNEL", ...)** - Channel logging
- **CONSOLE_DEBUG_FILTER=CHANNEL** - Filter output
---
## ERROR HANDLING
```php
if (!$expected) {
shouldnt_happen("Class {$expected} missing");
}
```
Use for "impossible" conditions that indicate broken assumptions.
---
## CODE QUALITY
**Professional UI**: Hover effects ONLY on buttons, links, form fields. Static elements remain static.
Run `rsx:check` before commits. Enforces naming, prohibits animations on non-actionable elements.
---
## MAIN_ABSTRACT MIDDLEWARE
Optional `/rsx/main.php`:
```php
class Main extends Main_Abstract
{
public function init() { } // Bootstrap once
public function pre_dispatch($request, $params) { return null; } // Before routes
public function unhandled_route($request, $params) { } // 404s
}
```
---
## KEY REMINDERS
1. **Fail loud** - No silent failures
2. **Static by default** - Unless instances needed
3. **Path-agnostic** - Reference by name
4. **Bundles required** - For JavaScript
5. **Use RsxAuth** - Never Laravel Auth
6. **No mass assignment** - Explicit only
7. **Forward migrations** - No rollbacks
8. **Don't run rsx:clean** - Cache auto-invalidates
9. **All routes need #[Auth]** - No exceptions
---
## GETTING HELP
```bash
php artisan rsx:man <topic> # Detailed docs
php artisan list rsx # All commands
```
**Topics**: bundle_api, jqhtml, routing, migrations, console_debug, model_fetch, vs_code_extension, deployment, framework_divergences
**Remember**: RSpade prioritizes simplicity and rapid development. When in doubt, choose the straightforward approach.