Add datetime system (Rsx_Time/Rsx_Date) and .expect file documentation system
Tighten CLAUDE.dist.md for LLM audience - 15% size reduction Add Repeater_Simple_Input component for managing lists of simple values Add Polymorphic_Field_Helper for JSON-encoded polymorphic form fields Fix incorrect data-sid selector in route-debug help example Fix Form_Utils to use component.$sid() instead of data-sid selector Add response helper functions and use _message as reserved metadata key 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1336
docs/CLAUDE.archive.12.24.25.md
Executable file
1336
docs/CLAUDE.archive.12.24.25.md
Executable file
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@ When editing /var/www/html/CLAUDE.md:
|
||||
**FORBIDDEN** (unless explicitly instructed):
|
||||
- `npm run compile/build` - Don't exist
|
||||
- `bin/publish` - For releases, not testing
|
||||
- `rsx:bundle:compile` / `rsx:manifest:build` - Automatic
|
||||
- `rsx:bundle:compile` / `rsx:manifest:build` / `rsx:clean` - Automatic
|
||||
- ANY "build", "compile", or "publish" command
|
||||
|
||||
Edit → Save → Refresh browser → Changes live (< 1 second)
|
||||
@@ -62,8 +62,6 @@ Edit → Save → Refresh browser → Changes live (< 1 second)
|
||||
php artisan rsx:framework:pull # User-initiated only
|
||||
```
|
||||
|
||||
Updates take 2-5 minutes. Includes code pull, manifest rebuild, bundle recompilation. Only run when requested by user.
|
||||
|
||||
### Fail Loud - No Silent Fallbacks
|
||||
|
||||
**ALWAYS fail visibly.** No redundant fallbacks, silent failures, or alternative code paths.
|
||||
@@ -132,14 +130,6 @@ cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php
|
||||
|
||||
Details: `php artisan rsx:man class_override`
|
||||
|
||||
### 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:
|
||||
@@ -258,12 +248,8 @@ public static function pre_dispatch(Request $request, array $params = []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Public endpoints: add @auth-exempt to class docblock
|
||||
/** @auth-exempt Public route */
|
||||
```
|
||||
|
||||
**Code quality**: PHP-AUTH-01 rule verifies auth checks exist. Use `@auth-exempt` for public routes.
|
||||
|
||||
### Type-Safe URLs
|
||||
|
||||
**MANDATORY**: All URLs must be generated using `Rsx::Route()` - hardcoded URLs are forbidden.
|
||||
@@ -272,10 +258,10 @@ public static function pre_dispatch(Request $request, array $params = []) {
|
||||
// PHP - Controller (defaults to 'index' method)
|
||||
Rsx::Route('User_Controller')
|
||||
|
||||
// PHP - Controller with explicit method
|
||||
// PHP - Controller with explicit method. Passing an integer for param two implies the param two is 'id' (id=123)
|
||||
Rsx::Route('User_Controller::show', 123);
|
||||
|
||||
// PHP - With query parameters
|
||||
// PHP - With query parameters. Extra params not defined in route itself become query string - automatically URL-encoded
|
||||
Rsx::Route('Login_Controller::logout', ['redirect' => '/dashboard']);
|
||||
// Generates: /logout?redirect=%2Fdashboard
|
||||
|
||||
@@ -284,16 +270,9 @@ 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
|
||||
- **Unimplemented routes**: Prefix method with `#` → `Rsx::Route('Feature::#index')` generates `href="#"` and bypasses validation
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -399,29 +378,24 @@ Pattern: `/rsx/app/(module)/(feature)/`
|
||||
|
||||
Details: `php artisan rsx:man spa`
|
||||
|
||||
### View Action Pattern (Loading Data)
|
||||
### View Action Pattern
|
||||
|
||||
For SPA actions that load data (view/edit CRUD pages), use the three-state pattern:
|
||||
Three-state pattern for data-loading actions:
|
||||
|
||||
```javascript
|
||||
on_create() {
|
||||
this.data.record = { name: '' }; // Stub prevents undefined errors
|
||||
this.data.record = { name: '' }; // Stub
|
||||
this.data.error_data = null;
|
||||
this.data.loading = true;
|
||||
}
|
||||
async on_load() {
|
||||
try {
|
||||
this.data.record = await Controller.get({id: this.args.id});
|
||||
} catch (e) {
|
||||
this.data.error_data = e;
|
||||
}
|
||||
try { this.data.record = await Controller.get({id: this.args.id}); }
|
||||
catch (e) { this.data.error_data = e; }
|
||||
this.data.loading = false;
|
||||
}
|
||||
```
|
||||
|
||||
Template uses three states: `<Loading_Spinner>` → `<Universal_Error_Page_Component>` → content.
|
||||
|
||||
**Details**: `php artisan rsx:man view_action_patterns`
|
||||
Template: `<Loading_Spinner>` → `<Universal_Error_Page_Component>` → content. Details: `rsx:man view_action_patterns`
|
||||
|
||||
---
|
||||
|
||||
@@ -446,42 +420,27 @@ The process involves creating Action classes with @route decorators and converti
|
||||
|
||||
### SCSS Component-First Architecture
|
||||
|
||||
**Philosophy**: Every styled element is a component. If it needs custom styles, give it a name, a jqhtml definition, and scoped SCSS. This eliminates CSS spaghetti - generic classes overriding each other unpredictably across files.
|
||||
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.
|
||||
|
||||
**Recognition**: When building a page, ask: "Is this structure unique, or a pattern?" A datagrid page with toolbar, tabs, filters, and search is a *pattern* - create `Datagrid_Card` once with slots, use it everywhere. A one-off project dashboard is *unique* - create `Project_Dashboard` for that page. If you're about to copy-paste structural markup, stop and extract a component.
|
||||
**BEM**: Child classes use component's exact name as prefix. `.Component_Name { &__element }` → HTML: `<div class="Component_Name__element">` (no kebab-case).
|
||||
|
||||
**Composition**: Use slots to separate structure from content. The component owns layout and styling; pages provide the variable parts via slots. This keeps pages declarative and components reusable.
|
||||
|
||||
**Enforcement**: SCSS in `rsx/app/` and `rsx/theme/components/` must wrap in a single component class matching the jqhtml/blade file. This works because all jqhtml components, SPA actions/layouts, and Blade views with `@rsx_id` automatically render with `class="Component_Name"` on their root element. `rsx/lib/` is for non-visual plumbing (validators, utilities). `rsx/theme/` (outside components/) holds primitives, variables, Bootstrap overrides.
|
||||
|
||||
**BEM Child Classes**: When using BEM notation, child element classes must use the component's exact class name as prefix. SCSS `.Component_Name { &__element }` compiles to `.Component_Name__element`, so HTML must match: `<div class="Component_Name__element">` not `<div class="component-name__element">`. No kebab-case conversion.
|
||||
|
||||
**Variables**: Define shared values (colors, spacing, border-radius) in `rsx/theme/variables.scss` or similar. These must be explicitly included before directory includes in bundle definitions. Component-local variables can be defined within the scoped rule.
|
||||
|
||||
**Supplemental files**: Multiple SCSS files can target the same component (e.g., breakpoint-specific styles) if a primary file with matching filename exists.
|
||||
**Variables**: `rsx/theme/variables.scss` - must be included before directory includes in bundles. Multiple SCSS files can target same component if primary file exists.
|
||||
|
||||
Details: `php artisan rsx:man scss`
|
||||
|
||||
### Responsive Breakpoints
|
||||
|
||||
RSX replaces Bootstrap's default breakpoints (xs/sm/md/lg/xl/xxl) with semantic device names.
|
||||
RSX replaces Bootstrap breakpoints with semantic names. **Bootstrap defaults (col-md-6, d-lg-none) do NOT work.**
|
||||
|
||||
**Tier 1 - Semantic**:
|
||||
- `mobile`: 0 - 1023px (phone + tablet)
|
||||
- `desktop`: 1024px+
|
||||
| Tier 1 | Range | Tier 2 | Range |
|
||||
|--------|-------|--------|-------|
|
||||
| `mobile` | 0-1023px | `phone` | 0-799px |
|
||||
| `desktop` | 1024px+ | `tablet` | 800-1023px |
|
||||
| | | `desktop-sm/md/lg` | 1024+ |
|
||||
|
||||
**Tier 2 - Granular**:
|
||||
- `phone`: 0 - 799px | `tablet`: 800 - 1023px | `desktop-sm`: 1024 - 1699px | `desktop-md`: 1700 - 2199px | `desktop-lg`: 2200px+
|
||||
|
||||
**SCSS Mixins**: `@include mobile { }`, `@include desktop { }`, `@include phone { }`, `@include tablet { }`, `@include desktop-sm { }`, etc.
|
||||
|
||||
**Bootstrap Classes**: `.col-mobile-6`, `.col-desktop-4`, `.d-mobile-none`, `.d-tablet-block`, `.col-phone-12 .col-tablet-6 .col-desktop-sm-4`
|
||||
|
||||
**Utility Classes**: `.mobile-only`, `.desktop-only`, `.phone-only`, `.hide-mobile`, `.hide-tablet`
|
||||
|
||||
**Note**: Bootstrap's default classes like `.col-md-6` or `.d-lg-none` do NOT work - use the RSX breakpoint names instead.
|
||||
|
||||
**JS Detection**: `Responsive.is_mobile()`, `Responsive.is_desktop()` (Tier 1 - broad); `Responsive.is_phone()`, `Responsive.is_tablet()`, `Responsive.is_desktop_sm()`, `Responsive.is_desktop_md()`, `Responsive.is_desktop_lg()` (Tier 2 - specific ranges)
|
||||
**SCSS**: `@include mobile { }`, `@include desktop { }`, `@include phone { }`, etc.
|
||||
**Classes**: `.col-mobile-6`, `.d-desktop-none`, `.mobile-only`, `.hide-tablet`
|
||||
**JS**: `Responsive.is_mobile()`, `Responsive.is_phone()`, `Responsive.is_desktop_sm()`, etc.
|
||||
|
||||
Details: `php artisan rsx:man responsive`
|
||||
|
||||
@@ -519,15 +478,9 @@ Use when data doesn't belong in DOM attributes. Multiple calls merge together.
|
||||
|
||||
---
|
||||
|
||||
## JAVASCRIPT
|
||||
|
||||
**CRITICAL**: JavaScript only executes when bundle rendered. See "JavaScript for Blade Pages" in BLADE & VIEWS section for the `on_app_ready()` pattern.
|
||||
|
||||
---
|
||||
|
||||
## BUNDLE SYSTEM
|
||||
|
||||
**One bundle per page required.** Compiles JS/CSS automatically on request - no manual build steps.
|
||||
**One bundle per module (rsx/app/(module)).** Compiles JS/CSS automatically on request - no manual build steps.
|
||||
|
||||
```php
|
||||
class Frontend_Bundle extends Rsx_Bundle_Abstract
|
||||
@@ -539,7 +492,8 @@ class Frontend_Bundle extends Rsx_Bundle_Abstract
|
||||
'jquery', // Required
|
||||
'lodash', // Required
|
||||
'rsx/theme/variables.scss', // Order matters
|
||||
'rsx/app/frontend', // Directory
|
||||
'rsx/theme', // Everything else from theme - but variables.scss will be first
|
||||
'rsx/app/frontend', // Directory -
|
||||
'rsx/models', // For JS stubs
|
||||
],
|
||||
];
|
||||
@@ -576,48 +530,30 @@ For mechanical thinkers who see structure, not visuals. Write `<User_Card>` not
|
||||
```
|
||||
|
||||
**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"<% } %> />`
|
||||
|
||||
**Conditional Attributes** `<input <% if (this.args.required) { %>required="required"<% } %> />`
|
||||
**Inline Logic**: `<% this.handler = () => action(); %>` then `@click=this.handler` - No JS file needed for simple components
|
||||
**Event Handlers**: `@click=this.method` (unquoted) - Methods defined inline or in companion .js
|
||||
**Validation**: `<% if (!this.args.required) throw new Error('Missing arg'); %>` - Fail loud in template
|
||||
|
||||
### Simple Components (No JS File Needed)
|
||||
|
||||
For simple components without external data or complex state, write JS directly in the template:
|
||||
### Simple Components (No JS File)
|
||||
|
||||
```jqhtml
|
||||
<Define:CSV_Renderer>
|
||||
<%
|
||||
// Validate input
|
||||
if (!this.args.csv_data) throw new Error('csv_data required');
|
||||
|
||||
// Parse CSV
|
||||
const rows = this.args.csv_data.split('\n').map(r => r.split(','));
|
||||
|
||||
// Define click handler inline
|
||||
this.toggle = () => { this.args.expanded = !this.args.expanded; this.render(); };
|
||||
%>
|
||||
|
||||
<table>
|
||||
<% for (let row of rows) { %>
|
||||
<tr>
|
||||
<% for (let cell of row) { %>
|
||||
<td><%= cell %></td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<tr><% for (let cell of row) { %><td><%= cell %></td><% } %></tr>
|
||||
<% } %>
|
||||
</table>
|
||||
|
||||
<button @click=this.toggle>Toggle View</button>
|
||||
</Define:CSV_Renderer>
|
||||
```
|
||||
|
||||
**When to use inline JS**: Simple data transformations, conditionals, loops, basic event handlers
|
||||
**When to create .js file**: External data loading, complex state management, multiple methods, or when JS overwhelms the template (should look mostly like HTML with some logic, not JS with some HTML)
|
||||
Use inline JS for simple transformations/handlers. Create .js file when JS overwhelms template or needs external data.
|
||||
|
||||
### State Management Rules (ENFORCED)
|
||||
|
||||
@@ -665,90 +601,43 @@ class Toggle_Button extends Component {
|
||||
|
||||
### Lifecycle
|
||||
|
||||
1. **on_create()** → Setup default state BEFORE template (sync)
|
||||
2. **render** → Template executes with initialized state
|
||||
1. **on_create()** → Setup defaults (sync) - `this.data.rows = []; this.data.loading = true;`
|
||||
2. **render** → Template executes
|
||||
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:
|
||||
If `on_load()` modifies `this.data`, component renders twice (defaults → populated).
|
||||
|
||||
```javascript
|
||||
on_create() {
|
||||
this.data.rows = []; // Prevents "not iterable" errors
|
||||
this.data.loading = true; // Template can check loading state
|
||||
}
|
||||
```
|
||||
### Component API
|
||||
|
||||
**Double-render**: If `on_load()` modifies `this.data`, component renders twice (defaults → populated).
|
||||
|
||||
### Component API - CRITICAL FOR LLM AGENTS
|
||||
|
||||
This section clarifies common misunderstandings. Read carefully.
|
||||
|
||||
**DOM Access Methods:**
|
||||
**DOM Access:**
|
||||
|
||||
| Method | Returns | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `this.$` | jQuery | Root element of component (stable, survives redraws) |
|
||||
| `this.$sid('name')` | jQuery | Child element with `$sid="name"` (always returns jQuery, even if empty) |
|
||||
| `this.sid('name')` | Component or null | Child component instance (null if not found or not a component) |
|
||||
| `this.$.find('.class')` | jQuery | Standard jQuery find (use when `$sid` isn't appropriate) |
|
||||
|
||||
**WRONG:** `this.$el` - This does not exist. Use `this.$`
|
||||
|
||||
**The reload() Paradigm - MANDATORY:**
|
||||
| `this.$` | jQuery | Root element (NOT `this.$el`) |
|
||||
| `this.$sid('name')` | jQuery | Child with `$sid="name"` |
|
||||
| `this.sid('name')` | Component/null | Child component instance |
|
||||
|
||||
**reload() vs render():**
|
||||
```
|
||||
reload() = on_load() → render() → on_ready()
|
||||
render() = template redraw only (NO on_ready)
|
||||
reload() = on_load() → render() → on_ready() ← ALWAYS USE THIS
|
||||
render() = template only (no on_ready) ← NEVER USE
|
||||
```
|
||||
|
||||
**LLM agents must ALWAYS use `reload()`, NEVER call `render()` directly.**
|
||||
|
||||
When you need to refresh a component after a mutation (add, edit, delete), call `this.reload()`. Yes, this makes another server call via `on_load()`. This is intentional. The extra round-trip is acceptable - our server is fast and the paradigm simplicity is worth it.
|
||||
|
||||
**WRONG approach (do NOT do this):**
|
||||
After mutations, call `this.reload()` - the server round-trip is intentional:
|
||||
```javascript
|
||||
// ❌ BAD - Trying to be "efficient" by skipping server round-trip
|
||||
async add_item() {
|
||||
const new_item = await Controller.add({name: 'Test'});
|
||||
this.data.items.push(new_item); // ERROR: Cannot modify this.data outside on_load
|
||||
this.render(); // WRONG: Event handlers will break, on_ready won't run
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT approach:**
|
||||
```javascript
|
||||
// ✅ GOOD - Clean, consistent, reliable
|
||||
async add_item() {
|
||||
await Controller.add({name: 'Test'});
|
||||
this.reload(); // Calls on_load() to refresh this.data, then on_ready() for handlers
|
||||
this.reload(); // Refreshes this.data via on_load(), reattaches handlers via on_ready()
|
||||
}
|
||||
```
|
||||
|
||||
**Event Handlers - Set Up in on_ready():**
|
||||
**Event handlers** go in `on_ready()` - they auto-reattach after reload. **WRONG:** Event delegation like `this.$.on('click', '[data-sid="btn"]', handler)` to "survive" render calls - use `reload()` instead.
|
||||
|
||||
Event handlers must be registered in `on_ready()`. Since `on_ready()` runs after every `reload()`, handlers automatically reattach when the DOM is redrawn.
|
||||
**this.data rules (enforced):** Writable only in `on_create()` (defaults) and `on_load()` (fetched data). Read-only elsewhere.
|
||||
|
||||
```javascript
|
||||
on_ready() {
|
||||
this.$sid('save_btn').click(() => this.save());
|
||||
this.$sid('delete_btn').click(() => this.delete());
|
||||
}
|
||||
```
|
||||
|
||||
**WRONG:** Event delegation to avoid `reload()`. If you find yourself writing `this.$.on('click', '[data-sid="btn"]', handler)` to "survive" render calls, you're doing it wrong. Use `reload()` and let `on_ready()` reattach handlers.
|
||||
|
||||
**this.data Modification Rules (ENFORCED):**
|
||||
|
||||
- `on_create()`: Set defaults only (e.g., `this.data.items = []`)
|
||||
- `on_load()`: Fetch and assign from server (e.g., `this.data.items = await Controller.list()`)
|
||||
- **Everywhere else**: Read-only. Attempting to modify `this.data` outside these methods throws an error.
|
||||
|
||||
**on_render() - LLM Should Not Use:**
|
||||
|
||||
`on_render()` exists for human developers doing performance optimization. LLM agents should pretend it doesn't exist. Use `on_ready()` for all post-render DOM work.
|
||||
**on_render():** Ignore - use `on_ready()` for post-render work.
|
||||
|
||||
### Loading Pattern
|
||||
|
||||
@@ -824,26 +713,15 @@ this.$sid('result_container').component('My_Component', {
|
||||
</Dashboard>
|
||||
```
|
||||
|
||||
### Key Pitfalls (ABSOLUTE RULES)
|
||||
### Key Pitfalls
|
||||
|
||||
1. `<Define>` IS the element - use `tag=""` attribute
|
||||
2. `this.data` starts empty `{}` - MUST set defaults in `on_create()`
|
||||
3. ONLY modify `this.data` in `on_create()` and `on_load()` (enforced by framework)
|
||||
4. `on_load()` can ONLY access `this.args` and `this.data` (no DOM, no `this.state`)
|
||||
5. Use `this.state = {}` in `on_create()` for UI state (not from Ajax)
|
||||
6. Use `this.args` for reload parameters, call `reload()` to re-fetch
|
||||
7. Use `Controller.method()` not `$.ajax()` - PHP methods with #[Ajax_Endpoint] auto-callable from JS
|
||||
8. `on_create/render/stop` must be sync
|
||||
9. `this.sid()` returns component instance, `$(selector).component()` converts jQuery to 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`
|
||||
- `<Define>` IS the element - use `tag=""` attribute
|
||||
- `this.data` starts `{}` - set defaults in `on_create()`
|
||||
- `this.data` writable only in `on_create()` and `on_load()`
|
||||
- `on_load()`: only `this.args` and `this.data` (no DOM, no `this.state`)
|
||||
- `this.state` for UI state, `this.args` + `reload()` for refetch
|
||||
- `Controller.method()` not `$.ajax()` - #[Ajax_Endpoint] auto-callable
|
||||
- `on_create/render/stop` sync; `this.sid()` → component, `$(el).component()` → component
|
||||
|
||||
---
|
||||
|
||||
@@ -952,55 +830,69 @@ Details: `php artisan rsx:man polymorphic`
|
||||
|
||||
## MODALS
|
||||
|
||||
**Basic dialogs**:
|
||||
### Built-in Dialog Types
|
||||
|
||||
| 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 |
|
||||
|
||||
```javascript
|
||||
await Modal.alert("File saved");
|
||||
if (await Modal.confirm("Delete?")) { /* confirmed */ }
|
||||
let name = await Modal.prompt("Enter name:");
|
||||
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");
|
||||
```
|
||||
|
||||
**Form modals**:
|
||||
### 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);
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Requirements**: Form component must implement `vals()` and include `<div $sid="error_container"></div>`.
|
||||
Form component must implement `vals()` and include `<div $sid="error_container"></div>`.
|
||||
|
||||
### Modal Classes
|
||||
|
||||
For complex/reusable modals, create dedicated classes:
|
||||
|
||||
**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;
|
||||
return await Modal.form({...}) || false;
|
||||
}
|
||||
}
|
||||
|
||||
// Use from page JS
|
||||
// Usage
|
||||
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.
|
||||
Pattern: Extend `Modal_Abstract`, implement static `show()`, return data or `false`.
|
||||
|
||||
Details: `php artisan rsx:man modals`
|
||||
|
||||
@@ -1222,16 +1114,6 @@ Details: `php artisan rsx:man model_fetch`
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
|
||||
---
|
||||
|
||||
## AUTHENTICATION
|
||||
|
||||
**Always use Session** - Static methods only. Never Laravel Auth or $_SESSION.
|
||||
@@ -1249,6 +1131,51 @@ Sessions persist 365 days. Never implement "Remember Me".
|
||||
|
||||
---
|
||||
|
||||
## DATE & TIME HANDLING
|
||||
|
||||
**Two Classes - Strict Separation**: `Rsx_Time` (datetimes with timezone) | `Rsx_Date` (calendar dates, no timezone)
|
||||
|
||||
### 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`
|
||||
|
||||
---
|
||||
|
||||
## JAVASCRIPT DECORATORS
|
||||
|
||||
```javascript
|
||||
@@ -1285,30 +1212,19 @@ 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.
|
||||
**`rsx:debug /path`** - Uses Playwright to render pages with full JS execution.
|
||||
|
||||
```bash
|
||||
rsx:debug /clients # Test route
|
||||
rsx:debug /dashboard --user=1 # Simulate authenticated user
|
||||
rsx:debug /contacts --console # Show console.log output
|
||||
rsx:debug /page --screenshot-path=/tmp/page.png --screenshot-width=mobile # Capture screenshot
|
||||
rsx:debug /page --dump-dimensions=".card" # Add position/size data attributes to elements
|
||||
rsx:debug /path --help # Show all options
|
||||
|
||||
# Simulate user interactions with --eval (executes before DOM capture)
|
||||
rsx:debug /contacts --user=1 --eval="$('.page-link[data-page=\"2\"]').click(); await sleep(2000)"
|
||||
rsx:debug /form --eval="$('#name').val('test'); $('form').submit(); await sleep(500)"
|
||||
rsx:debug /dashboard --user=1 # Authenticated user
|
||||
rsx:debug /page --screenshot-path=/tmp/page.png # Capture screenshot
|
||||
rsx:debug /contacts --eval="$('.btn').click(); await sleep(1000)" # Simulate interaction
|
||||
rsx:debug / --eval="return Rsx_Time.now_iso()" # Get eval result (use return)
|
||||
rsx:debug / --console --eval="console.log(Rsx_Date.today())" # Or console.log with --console
|
||||
```
|
||||
|
||||
Screenshot presets: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large
|
||||
Options: `--user=ID`, `--console`, `--screenshot-path`, `--screenshot-width=mobile|tablet|desktop-*`, `--dump-dimensions=".selector"`, `--eval="js"`, `--help`
|
||||
|
||||
The `--eval` option runs JavaScript after page load but before DOM capture. Use `await sleep(ms)` to wait for async operations. This is powerful for testing pagination, form submissions, and other interactive behavior.
|
||||
|
||||
Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features.
|
||||
|
||||
**CRITICAL: SPA routes ARE server routes.** The server knows all SPA routes. `rsx:debug` uses Playwright to fully render pages including all JavaScript and SPA navigation. If you get a 404, the route genuinely doesn't exist - check your URL pattern and route definitions. Never dismiss 404s as "SPA routes can't be tested server-side" - this analysis is incorrect.
|
||||
**SPA routes ARE server routes.** If you get 404, the route doesn't exist - check route definitions. Never dismiss as "SPA can't be tested server-side".
|
||||
|
||||
### Debugging
|
||||
|
||||
@@ -1340,21 +1256,6 @@ Run `rsx:check` before commits. Enforces naming, prohibits animations on non-act
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -1382,14 +1283,4 @@ php artisan list rsx # All commands
|
||||
|
||||
## PROJECT DOCUMENTATION
|
||||
|
||||
Project-specific technical documentation lives in `/rsx/resource/man/`. These are man-page-style text files documenting features specific to your application that build on or extend the framework.
|
||||
|
||||
**When to create a project man page**:
|
||||
- Feature has non-obvious implementation details
|
||||
- Multiple components interact in ways that need explanation
|
||||
- Configuration options or patterns need documentation
|
||||
- AI agents or future developers need reference material
|
||||
|
||||
**Format**: Plain text files (`.txt`) following Unix man page conventions. See `/rsx/resource/man/CLAUDE.md` for writing guidelines.
|
||||
|
||||
**Remember**: RSpade prioritizes simplicity and rapid development. When in doubt, choose the straightforward approach.
|
||||
Project-specific man pages in `/rsx/resource/man/*.txt`. Create when features have non-obvious details or component interactions. See `/rsx/resource/man/CLAUDE.md` for format.
|
||||
|
||||
Reference in New Issue
Block a user