Reorganize RSpade directory structure for clarity

Improve Jqhtml_Integration.js documentation with hydration system explanation
Add jqhtml-laravel integration packages for traditional Laravel projects

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-24 09:41:48 +00:00
parent 0143f6ae9f
commit bd5809fdbd
20716 changed files with 387 additions and 6444 deletions

View File

@@ -0,0 +1,29 @@
<%--
JS_Tree_Debug_Component
A universal "var_dump" style component for debugging JavaScript values.
Renders any JavaScript value as an expandable/collapsible tree, similar to browser DevTools.
Useful for debugging, displaying error metadata, and inspecting ORM model instances.
$data - The JavaScript value to display. Pass directly (unquoted) for objects/arrays:
$data=this.data.myObject (correct - passes object reference)
$data="<%= this.data.myObject %>" (wrong - stringifies the object)
$expand_depth - How many levels deep to expand by default (default: 1)
$root_label - Optional label for the root element
$show_class_names - If true, display class names for object instances in a small
bordered badge (default: false). Shown after { when expanded,
after } when collapsed. Only for named class instances, not
generic Object or Array.
--%>
<Define:JS_Tree_Debug_Component tag="div" class="js-tree-debug">
<% if (JS_Tree_Debug_Node.get_type(this.args.data) !== 'object' && JS_Tree_Debug_Node.get_type(this.args.data) !== 'array') { %>
<span class="js-tree-debug-value js-tree-debug-<%= JS_Tree_Debug_Node.get_type(this.args.data) %>"><%= JS_Tree_Debug_Node.format_value(this.args.data, JS_Tree_Debug_Node.get_type(this.args.data)) %></span>
<% } else { %>
<JS_Tree_Debug_Node
$data=this.args.data
$expand_depth=(this.args.expand_depth ?? 1)
$label=(this.args.root_label || null)
$show_class_names=(this.args.show_class_names ?? false)
/>
<% } %>
</Define:JS_Tree_Debug_Component>

View File

@@ -0,0 +1,3 @@
class JS_Tree_Debug_Component extends Component {
// No special logic needed at root level - just passes data to JS_Tree_Debug_Node
}

View File

@@ -0,0 +1,68 @@
<%--
JS_Tree_Debug_Node (Internal component for JS_Tree_Debug_Component)
Renders a single expandable node in the debug tree.
Not intended for direct use - use JS_Tree_Debug_Component instead.
$data - The object or array to render
$expand_depth - How many levels deep to expand
$label - Optional key/index label for this node
$show_class_names - If true, display class names for named object instances
--%>
<Define:JS_Tree_Debug_Node tag="div" class="js-tree-debug-node">
<%
const class_name = this.args.show_class_names ? JS_Tree_Debug_Node.get_class_name(this.args.data) : null;
const relationships = JS_Tree_Debug_Node.get_object_relationships(this.args.data);
%>
<span class="js-tree-debug-toggle<%= this.is_expanded ? '' : ' js-tree-debug-collapsed' %>" $sid="toggle" @click=this.toggle>
<i class="bi bi-caret-right-fill js-tree-debug-arrow"></i>
</span>
<% if (this.args.label !== null && this.args.label !== undefined) { %>
<span class="js-tree-debug-key"><%= this.args.label %></span><span class="js-tree-debug-colon">: </span>
<% } %>
<span class="js-tree-debug-bracket"><%= Array.isArray(this.args.data) ? '[' : '{' %></span>
<% if (class_name) { %>
<span class="js-tree-debug-class-badge"><span class="js-tree-debug-class-name"><%= class_name %></span><% if (this.args.data && this.args.data.id !== undefined) { %><span class="js-tree-debug-class-paren">(</span><span class="js-tree-debug-class-id"><%= this.args.data.id %></span><span class="js-tree-debug-class-paren">)</span><% } %></span>
<% } %>
<span class="js-tree-debug-preview-collapsed" $sid="preview_collapsed" style="<%= this.is_expanded ? 'display:none' : '' %>"><%= JS_Tree_Debug_Node.get_preview(this.args.data, JS_Tree_Debug_Node.get_type(this.args.data)) %></span>
<div class="js-tree-debug-children" $sid="children" style="<%= this.is_expanded ? '' : 'display:none' %>">
<%-- Regular data entries --%>
<% for (const [key, value] of JS_Tree_Debug_Node.get_entries(this.args.data, JS_Tree_Debug_Node.get_type(this.args.data))) {
const val_type = JS_Tree_Debug_Node.get_type(value);
const is_expandable = val_type === 'object' || val_type === 'array';
%>
<% if (is_expandable) { %>
<JS_Tree_Debug_Node
$data=value
$expand_depth=((this.args.expand_depth ?? 1) - 1)
$label=key
$show_class_names=(this.args.show_class_names ?? false)
/>
<% } else { %>
<div class="js-tree-debug-leaf">
<span class="js-tree-debug-key"><%= key %></span><span class="js-tree-debug-colon">: </span>
<span class="js-tree-debug-value js-tree-debug-<%= val_type %>"><%= JS_Tree_Debug_Node.format_value(value, val_type) %></span>
</div>
<% } %>
<% } %>
<%-- Relationship nodes (lazy-loaded) --%>
<% for (const rel_name of relationships) { %>
<div class="js-tree-debug-node js-tree-debug-relationship" $sid="rel_<%= rel_name %>">
<%
this.handler_toggle_rel = () => this.toggle_relationship(rel_name);
%>
<span class="js-tree-debug-toggle js-tree-debug-collapsed" @click=this.handler_toggle_rel>
<i class="bi bi-caret-right-fill js-tree-debug-arrow"></i>
</span>
<span class="js-tree-debug-key js-tree-debug-rel-key"><%= rel_name %>()</span><span class="js-tree-debug-colon">: </span>
<span class="js-tree-debug-preview-collapsed">...</span>
<div class="js-tree-debug-rel-children js-tree-debug-children" style="display:none">
<div class="js-tree-debug-leaf js-tree-debug-pending">(click to load)</div>
</div>
</div>
<% } %>
</div>
<span class="js-tree-debug-bracket"><%= Array.isArray(this.args.data) ? ']' : '}' %></span>
</Define:JS_Tree_Debug_Node>

View File

@@ -0,0 +1,257 @@
class JS_Tree_Debug_Node extends Component {
on_create() {
this.is_expanded = (this.args.expand_depth ?? 1) > 0;
// Track relationship loading states: { rel_name: 'pending'|'loading'|'loaded'|'error' }
this.state.rel_states = {};
// Store loaded relationship data: { rel_name: data }
this.state.rel_data = {};
// Store error messages: { rel_name: error_message }
this.state.rel_errors = {};
}
on_ready() {
// Relationships are never auto-loaded - they only load when explicitly expanded
}
toggle() {
this.is_expanded = !this.is_expanded;
this.$sid('children').toggle(this.is_expanded);
this.$sid('toggle').toggleClass('js-tree-debug-collapsed', !this.is_expanded);
this.$sid('preview_collapsed').toggle(!this.is_expanded);
// Note: Relationships are NOT auto-loaded here - they have their own toggle handler
}
/**
* Toggle a relationship node and load its data if not already loaded
*/
toggle_relationship(rel_name) {
const $container = this.$sid('rel_' + rel_name);
const $toggle = $container.find('.js-tree-debug-toggle').first();
const $children = $container.find('.js-tree-debug-rel-children').first();
const $preview = $container.find('.js-tree-debug-preview-collapsed').first();
const is_expanded = !$toggle.hasClass('js-tree-debug-collapsed');
if (is_expanded) {
// Collapse
$toggle.addClass('js-tree-debug-collapsed');
$children.hide();
$preview.show();
} else {
// Expand
$toggle.removeClass('js-tree-debug-collapsed');
$children.show();
$preview.hide();
// Load if not already loaded
if (!this.state.rel_states[rel_name] || this.state.rel_states[rel_name] === 'pending') {
this._load_relationship(rel_name);
}
}
}
/**
* Load a relationship and update the UI
*/
async _load_relationship(rel_name) {
// Validate the relationship function exists
const obj = this.args.data;
if (!obj || typeof obj[rel_name] !== 'function') {
return;
}
this.state.rel_states[rel_name] = 'loading';
this._update_relationship_ui(rel_name);
try {
const result = await obj[rel_name]();
this.state.rel_states[rel_name] = 'loaded';
this.state.rel_data[rel_name] = result;
this._update_relationship_ui(rel_name);
} catch (e) {
this.state.rel_states[rel_name] = 'error';
this.state.rel_errors[rel_name] = e.message || 'Error loading relationship';
this._update_relationship_ui(rel_name);
}
}
/**
* Update the UI for a relationship after loading
*/
_update_relationship_ui(rel_name) {
const $container = this.$sid('rel_' + rel_name);
const $children = $container.find('.js-tree-debug-rel-children').first();
const $preview = $container.find('.js-tree-debug-preview-collapsed').first();
const state = this.state.rel_states[rel_name];
const data = this.state.rel_data[rel_name];
const error = this.state.rel_errors[rel_name];
// Update preview text
if (state === 'loading') {
$preview.html('<span class="js-tree-debug-loading">loading...</span>');
} else if (state === 'error') {
$preview.html('<span class="js-tree-debug-error">error</span>');
} else if (state === 'loaded') {
const type = JS_Tree_Debug_Node.get_type(data);
$preview.text(JS_Tree_Debug_Node.get_preview(data, type) || JS_Tree_Debug_Node.format_value(data, type));
}
// Update children content
$children.empty();
if (state === 'loading') {
$children.html('<div class="js-tree-debug-leaf js-tree-debug-loading">Loading...</div>');
} else if (state === 'error') {
$children.html('<div class="js-tree-debug-leaf js-tree-debug-error">"' + this._escape_html(error) + '"</div>');
} else if (state === 'loaded') {
this._render_relationship_result($children, data, rel_name);
}
}
/**
* Render the result of a relationship fetch into the container
* Renders entries directly (not wrapped in another node) so user doesn't have to double-expand
*/
_render_relationship_result($container, data, rel_name) {
const type = JS_Tree_Debug_Node.get_type(data);
// Handle null/undefined
if (type === 'null' || type === 'undefined') {
$container.html('<div class="js-tree-debug-leaf js-tree-debug-empty">(no data)</div>');
return;
}
// Handle empty array
if (type === 'array' && data.length === 0) {
$container.html('<div class="js-tree-debug-leaf js-tree-debug-empty">(empty array)</div>');
return;
}
// Handle empty object
if (type === 'object' && Object.keys(data).length === 0) {
$container.html('<div class="js-tree-debug-leaf js-tree-debug-empty">(empty object)</div>');
return;
}
// Handle primitive values (shouldn't happen but be safe)
if (type !== 'array' && type !== 'object') {
$container.html('<div class="js-tree-debug-leaf"><span class="js-tree-debug-value js-tree-debug-' + type + '">' +
this._escape_html(JS_Tree_Debug_Node.format_value(data, type)) + '</span></div>');
return;
}
// Render entries directly into container (no wrapper node)
const entries = JS_Tree_Debug_Node.get_entries(data, type);
const expand_depth = Math.max(0, (this.args.expand_depth ?? 1) - 1);
const show_class_names = this.args.show_class_names ?? false;
for (const [key, value] of entries) {
const val_type = JS_Tree_Debug_Node.get_type(value);
const is_expandable = val_type === 'object' || val_type === 'array';
if (is_expandable) {
// Create a node for expandable values
const $node = $('<div>');
$container.append($node);
$node.component('JS_Tree_Debug_Node', {
data: value,
expand_depth: expand_depth,
label: key,
show_class_names: show_class_names
});
} else {
// Render leaf value directly
$container.append(
'<div class="js-tree-debug-leaf">' +
'<span class="js-tree-debug-key">' + this._escape_html(String(key)) + '</span>' +
'<span class="js-tree-debug-colon">: </span>' +
'<span class="js-tree-debug-value js-tree-debug-' + val_type + '">' +
this._escape_html(JS_Tree_Debug_Node.format_value(value, val_type)) +
'</span></div>'
);
}
}
}
_escape_html(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Static helpers for template use
static get_type(val) {
if (val === null) return 'null';
if (val === undefined) return 'undefined';
if (is_array(val)) return 'array';
return typeof val;
}
static get_preview(val, type) {
if (type === 'array') return 'Array(' + val.length + ')';
if (type === 'object') {
const keys = Object.keys(val);
if (keys.length === 0) return '{}';
if (keys.length <= 3) return '{' + keys.join(', ') + '}';
return '{' + keys.slice(0, 3).join(', ') + ', ...}';
}
return '';
}
static get_entries(data, type) {
if (type === 'array') return data.map((v, i) => [i, v]);
return Object.entries(data || {}).sort((a, b) => a[0].localeCompare(b[0]));
}
static format_value(val, type) {
if (type === 'string') return '"' + val + '"';
if (type === 'null') return 'null';
if (type === 'undefined') return 'undefined';
return str(val);
}
/**
* Get the class name of an object if it's a named class instance (not generic Object/Array)
* @param {*} val - Value to check
* @returns {string|null} - Class name or null if generic/primitive
*/
static get_class_name(val) {
if (val === null || val === undefined) return null;
if (Array.isArray(val)) return null;
if (typeof val !== 'object') return null;
const name = val.constructor?.name;
if (!name || name === 'Object') return null;
return name;
}
/**
* Get fetchable relationships for an object
* Returns array of relationship names if object's class has get_relationships()
* @param {*} obj - Object to check
* @returns {string[]} - Array of relationship names, or empty array
*/
static get_object_relationships(obj) {
try {
if (!obj || typeof obj !== 'object') return [];
if (Array.isArray(obj)) return [];
// Check if constructor has get_relationships static method
const ctor = obj.constructor;
if (!ctor || typeof ctor.get_relationships !== 'function') return [];
// Get relationships and validate it returns an array
const relationships = ctor.get_relationships();
if (!Array.isArray(relationships)) return [];
// Filter to only relationships that are actually functions on the object
return relationships.filter(name => {
return typeof name === 'string' && typeof obj[name] === 'function';
});
} catch (e) {
// Any error, just return empty array
return [];
}
}
}

View File

@@ -0,0 +1,150 @@
.js-tree-debug {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
font-size: 13px;
line-height: 1.4;
text-align: left;
width: 800px;
max-width: 100%;
height: 250px;
overflow: auto;
resize: both;
padding: 20px;
border: 1px solid #777;
border-radius: 4px;
background: #fafafa;
.js-tree-debug-node {
margin-left: 0;
}
.js-tree-debug-children {
margin-left: 20px;
border-left: 1px solid #e0e0e0;
padding-left: 8px;
}
.js-tree-debug-leaf {
padding: 1px 0;
}
.js-tree-debug-toggle {
display: inline-block;
width: 16px;
cursor: pointer;
user-select: none;
.js-tree-debug-arrow {
font-size: 10px;
color: #666;
transition: transform 0.15s ease; // @SCSS-ANIM-01-EXCEPTION
display: inline-block;
}
&:not(.js-tree-debug-collapsed) .js-tree-debug-arrow {
transform: rotate(90deg);
}
&:hover .js-tree-debug-arrow {
color: #333;
}
}
.js-tree-debug-key {
color: #881391;
}
.js-tree-debug-colon {
color: #666;
}
.js-tree-debug-preview {
color: #666;
}
.js-tree-debug-preview-collapsed {
color: #999;
font-style: italic;
}
.js-tree-debug-bracket-close {
color: #666;
}
// Value type colors
.js-tree-debug-string {
color: #c41a16;
}
.js-tree-debug-number {
color: #1c00cf;
}
.js-tree-debug-boolean {
color: #0d22aa;
}
.js-tree-debug-null,
.js-tree-debug-undefined {
color: #808080;
font-style: italic;
}
.js-tree-debug-value {
word-break: break-word;
}
.js-tree-debug-class-badge {
display: inline-block;
font-size: 10px;
padding: 0 4px;
margin-left: 4px;
border: 1px solid #ccc;
border-radius: 3px;
background: #f5f5f5;
vertical-align: middle;
line-height: 1.4;
.js-tree-debug-class-name {
color: #881391; // Same as keys
}
.js-tree-debug-class-paren {
color: #666; // Same as colons/symbols
}
.js-tree-debug-class-id {
color: #1c00cf; // Same as numbers
}
}
// Relationship nodes (lazy-loaded)
.js-tree-debug-relationship {
.js-tree-debug-rel-key {
color: #0066cc;
font-style: italic;
}
}
// Loading state
.js-tree-debug-loading {
color: #888;
font-style: italic;
}
// Error state
.js-tree-debug-error {
color: #cc0000;
}
// Empty/no data state
.js-tree-debug-empty {
color: #888;
font-style: italic;
}
// Pending (not yet loaded)
.js-tree-debug-pending {
color: #999;
font-style: italic;
}
}