Fix code quality violations for publish

Progressive breadcrumb resolution with caching, fix double headers

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-16 04:43:47 +00:00
parent ba3268caca
commit 14dd2fd223
1456 changed files with 136243 additions and 7631 deletions

402
app/RSpade/Breadcrumbs/Rsx_Breadcrumb_Resolver.js Normal file → Executable file
View File

@@ -1,58 +1,58 @@
/**
* Rsx_Breadcrumb_Resolver - Progressive breadcrumb resolution with caching
*
* Provides instant breadcrumb rendering using cached data while validating
* against live action data in the background.
* Provides instant breadcrumb rendering using cached data while resolving
* live labels progressively in the background.
*
* Resolution Phases:
* 1. Immediate Cached Render - If ALL chain links cached, render instantly
* 2. Active Action Resolution - Await live action's breadcrumb methods
* 3. Chain Walk - Walk parents using cache or detached action loads
* 4. Render Chain - Emit chain structure (may have null labels)
* 5. Progressive Updates - Update labels as they resolve
* Resolution Flow:
* 1. Keep previous breadcrumbs visible initially
* 2. Try to load cached chain - if complete, display immediately
* 3. Walk breadcrumb_parent() chain to get structure (URLs)
* 4. Compare structure to cache:
* - Different length or no cache shown: render skeleton with blank labels
* - Same length: render skeleton pre-populated with cached labels
* 5. Progressively resolve each label via breadcrumb_label()/breadcrumb_label_active()
* 6. Cache final result when all labels resolved
*
* Important: Actions that need loaded data in breadcrumb methods should
* await their own 'loaded' event internally, not rely on ready() being called.
*
* Usage:
* const cancel = Rsx_Breadcrumb_Resolver.stream(action, url, (data) => {
* // data.title - Page title (null if pending)
* // data.chain - Array of {url, label, is_active, resolved}
* // data.fully_resolved - True when all labels resolved
* });
*
* // Call cancel() to stop streaming (e.g., on navigation)
* const cancel = Rsx_Breadcrumb_Resolver.stream(action, url, callbacks);
*/
class Rsx_Breadcrumb_Resolver {
/**
* In-memory cache: url -> { label, label_active, parent_url }
* Cleared on page reload. Invalidated by visiting pages.
*/
static _cache = {};
/**
* Generation counter for cancellation detection.
* Incremented on each stream() call and when cancelled.
*/
static _generation = 0;
/**
* Cache key prefix for breadcrumb chains
*/
static CACHE_PREFIX = 'breadcrumb_chain:';
/**
* Stream breadcrumb data as it becomes available
*
* @param {Spa_Action} action - The active action instance
* @param {string} url - Current URL (pathname + search, no hash)
* @param {Function} callback - Called with breadcrumb data on each update
* @param {object} callbacks - Callback functions:
* - on_chain(chain): Called when chain structure is ready (may have null labels)
* - on_label_update(index, label): Called when individual label resolves
* - on_complete(chain): Called when all labels resolved
* @returns {Function} Cancel function - call to stop streaming
*/
static stream(action, url, callback) {
static stream(action, url, callbacks) {
const generation = ++Rsx_Breadcrumb_Resolver._generation;
// Normalize URL (strip hash if present)
const normalized_url = Rsx_Breadcrumb_Resolver._normalize_url(url);
// Start async resolution (fires callbacks as data becomes available)
Rsx_Breadcrumb_Resolver._resolve(action, normalized_url, generation, callback);
// Start async resolution
Rsx_Breadcrumb_Resolver._resolve(action, normalized_url, generation, callbacks);
// Return cancel function
return () => {
// Only increment if this is still the active generation
if (generation === Rsx_Breadcrumb_Resolver._generation) {
Rsx_Breadcrumb_Resolver._generation++;
}
@@ -75,217 +75,183 @@ class Rsx_Breadcrumb_Resolver {
}
/**
* Main resolution orchestrator
* Get cached chain for a URL
*/
static async _resolve(action, url, generation, callback) {
// Phase 1: Try immediate cached render
const cached_chain = Rsx_Breadcrumb_Resolver._try_build_cached_chain(url);
if (cached_chain) {
// Full cache hit - render immediately
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
callback({
title: null, // Title will be resolved in Phase 2
chain: cached_chain,
fully_resolved: false
});
}
// Phase 2: Await active action resolution
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
const active_data = await Rsx_Breadcrumb_Resolver._resolve_active_action(action, url);
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
// Check if parent changed from cached value (stale cache)
const cached_entry = Rsx_Breadcrumb_Resolver._cache[url];
const parent_changed = cached_chain && cached_entry &&
cached_entry.parent_url !== active_data.parent_url;
// Phase 3-5: Walk chain and render progressively
await Rsx_Breadcrumb_Resolver._walk_and_render_chain(
active_data,
url,
generation,
callback,
parent_changed || !cached_chain // Force re-render if parent changed or no cached render
);
static _get_cached_chain(url) {
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
return Rsx_Storage.session_get(cache_key);
}
/**
* Phase 1: Try to build a complete chain from cache
* Returns null if any link is missing from cache
* Cache a resolved chain
*/
static _try_build_cached_chain(url) {
const chain = [];
let current_url = url;
static _cache_chain(url, chain) {
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
// Store simplified chain data (url, label, is_active)
const cache_data = chain.map(item => ({
url: item.url,
label: item.label,
is_active: item.is_active
}));
Rsx_Storage.session_set(cache_key, cache_data);
}
while (current_url) {
const entry = Rsx_Breadcrumb_Resolver._cache[current_url];
if (!entry) {
// Cache miss - can't build complete chain
return null;
/**
* Main resolution orchestrator
*/
static async _resolve(action, url, generation, callbacks) {
// Step 1: Try to get cached chain
const cached_chain = Rsx_Breadcrumb_Resolver._get_cached_chain(url);
let showed_cached = false;
if (cached_chain && is_array(cached_chain) && cached_chain.length > 0) {
// Show cached chain immediately
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
callbacks.on_chain(cached_chain.map(item => ({
url: item.url,
label: item.label,
is_active: item.is_active,
resolved: true
})));
showed_cached = true;
}
// Step 2: Walk the chain structure to get URLs
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
const chain_structure = await Rsx_Breadcrumb_Resolver._walk_chain_structure(action, url, generation);
if (!chain_structure || !Rsx_Breadcrumb_Resolver._is_active(generation)) return;
// Step 3: Compare to cache and build display chain
const chain_length_matches = showed_cached && cached_chain.length === chain_structure.length;
const should_use_cached_labels = showed_cached && chain_length_matches;
// Build the chain with either cached labels or nulls
const chain = chain_structure.map((item, index) => {
let label = null;
if (should_use_cached_labels && cached_chain[index]) {
label = cached_chain[index].label;
}
return {
url: item.url,
label: label,
is_active: item.is_active,
resolved: false,
action: item.action // Keep action reference for label resolution
};
});
// Step 4: Emit chain structure (with cached labels or nulls)
// Only emit if we didn't show cached OR if structure differs
if (!showed_cached || !chain_length_matches) {
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
callbacks.on_chain(chain.map(item => ({
url: item.url,
label: item.label,
is_active: item.is_active,
resolved: item.label !== null
})));
}
// Step 5: Resolve labels progressively
const label_promises = chain.map(async (item, index) => {
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
try {
let label;
if (item.is_active) {
// Active item uses breadcrumb_label_active()
label = await item.action.breadcrumb_label_active();
} else {
// Parent items use breadcrumb_label()
label = await item.action.breadcrumb_label();
}
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
// Update chain and notify
chain[index].label = label;
chain[index].resolved = true;
callbacks.on_label_update(index, label);
} catch (e) {
console.warn('Breadcrumb label resolution failed:', e);
// Use generic label when resolution fails
chain[index].label = item.is_active ? 'Page' : 'Link';
chain[index].resolved = true;
callbacks.on_label_update(index, chain[index].label);
}
});
// Wait for all labels to resolve
await Promise.all(label_promises);
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
// Step 6: Cache the final result and notify completion
Rsx_Breadcrumb_Resolver._cache_chain(url, chain);
callbacks.on_complete(chain.map(item => ({
url: item.url,
label: item.label,
is_active: item.is_active,
resolved: true
})));
}
/**
* Walk the chain structure by following breadcrumb_parent()
* Returns array of {url, is_active, action} from root to current
*/
static async _walk_chain_structure(action, url, generation) {
const chain = [];
// Start with current action
let current_url = url;
let current_action = action;
while (current_action && current_url) {
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return null;
chain.unshift({
url: current_url,
label: current_url === url ? entry.label_active : entry.label,
is_active: current_url === url,
resolved: true
action: current_action
});
current_url = entry.parent_url;
// Get parent URL
let parent_url = null;
try {
parent_url = await current_action.breadcrumb_parent();
} catch (e) {
console.warn('breadcrumb_parent() failed:', e);
}
if (!parent_url) break;
// Load parent action (detached)
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return null;
const parent_action = await Spa.load_detached_action(parent_url, {
use_cached_data: true,
skip_render_and_ready: true
});
if (!parent_action) break;
current_url = parent_url;
current_action = parent_action;
}
return chain;
}
/**
* Phase 2: Resolve all breadcrumb data from the live active action
* Clear cached chain for a URL (if needed for invalidation)
*/
static async _resolve_active_action(action, url) {
// Await all breadcrumb methods in parallel
const [title, label, label_active, parent_url] = await Promise.all([
action.page_title(),
action.breadcrumb_label(),
action.breadcrumb_label_active(),
action.breadcrumb_parent()
]);
// Update cache with fresh values
Rsx_Breadcrumb_Resolver._cache[url] = {
label: label,
label_active: label_active || label,
parent_url: parent_url || null
};
return {
title: title,
label: label,
label_active: label_active || label,
parent_url: parent_url || null
};
}
/**
* Phase 3-5: Walk parent chain and render progressively
*/
static async _walk_and_render_chain(active_data, url, generation, callback, force_render) {
// Start chain with active action
const chain = [{
url: url,
label: active_data.label_active,
is_active: true,
resolved: true
}];
// Track pending async operations for progressive updates
let pending_count = 0;
let chain_complete = false;
// Helper to emit current state
const emit = () => {
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
callback({
title: active_data.title,
chain: [...chain], // Clone to prevent mutation issues
fully_resolved: chain_complete && pending_count === 0
});
};
// Walk up parent chain
let parent_url = active_data.parent_url;
while (parent_url) {
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
const cached = Rsx_Breadcrumb_Resolver._cache[parent_url];
if (cached) {
// Cache hit - use cached values immediately
chain.unshift({
url: parent_url,
label: cached.label,
is_active: false,
resolved: true
});
parent_url = cached.parent_url;
} else {
// Cache miss - need to load detached action
// Add placeholder entry
const entry_url = parent_url;
chain.unshift({
url: entry_url,
label: null, // Placeholder - will be resolved
is_active: false,
resolved: false
});
pending_count++;
// Load the detached action synchronously for chain structure
const detached = await Spa.load_detached_action(entry_url, { use_cached_data: true });
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) {
if (detached) detached.stop();
return;
}
if (detached) {
// Get parent_url to continue chain walk
const next_parent = await detached.breadcrumb_parent();
// Get label while we have the action
const label = await detached.breadcrumb_label();
const label_active = await detached.breadcrumb_label_active();
// Update cache
Rsx_Breadcrumb_Resolver._cache[entry_url] = {
label: label,
label_active: label_active || label,
parent_url: next_parent || null
};
// Update chain entry with resolved label
const found_entry = chain.find(e => e.url === entry_url);
if (found_entry) {
found_entry.label = label;
found_entry.resolved = true;
pending_count--;
}
detached.stop();
parent_url = next_parent;
} else {
// Failed to load - stop chain here
parent_url = null;
}
}
static clear_cache(url) {
if (url) {
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
Rsx_Storage.session_remove(cache_key);
}
// Chain structure complete
chain_complete = true;
// Emit final state (or intermediate if still pending)
if (force_render || pending_count === 0) {
emit();
}
}
/**
* Clear the cache (called on window reload automatically)
* Can be called manually if needed
*/
static clear_cache() {
Rsx_Breadcrumb_Resolver._cache = {};
}
/**
* Get cache entry for debugging/inspection
*/
static get_cached(url) {
return Rsx_Breadcrumb_Resolver._cache[url] || null;
}
}

View File

@@ -835,9 +835,22 @@ LIFECYCLE EVENT CALLBACKS
Supported Events:
'render' - Fires after render phase completes
'create' - Fires after create phase completes
'load' - Fires after load phase completes
'load' - Fires after load phase completes (data available)
'ready' - Fires after ready phase completes (fully initialized)
Awaiting Data in Async Methods:
When a method needs data that may not be loaded yet, await the 'load'
event. If the event already fired, the callback executes immediately:
async get_display_name() {
// Wait for on_load() to complete if not already
await new Promise(resolve => this.on('load', resolve));
return `${this.data.first_name} ${this.data.last_name}`;
}
This pattern is useful for breadcrumb methods or other async accessors
that need loaded data but may be called before on_load() completes.
Basic usage:
// Get component instance and register callback
const component = $('#my-component').component();

View File

@@ -0,0 +1,466 @@
PRIMARY_SECONDARY_NAV_BREADCRUMBS(3) RSX Framework Manual PRIMARY_SECONDARY_NAV_BREADCRUMBS(3)
NAME
Primary, Secondary Navigation and Breadcrumbs - SPA navigation systems
SYNOPSIS
// Primary navigation (sidebar) in layout
class Frontend_Spa_Layout extends Spa_Layout {
on_create() {
this.state = {
nav_sections: [
{ title: 'Overview', items: [
{ label: 'Dashboard', icon: 'bi-house', href: Rsx.Route('...') }
]}
]
};
}
}
// Secondary navigation (sublayout sidebar)
class Settings_Layout extends Spa_Layout {
static ACTION_CONFIG = {
'Settings_General_Action': { icon: 'bi-gear', label: 'General' },
'Settings_Profile_Action': { icon: 'bi-person', label: 'Profile' }
};
}
// Breadcrumb methods in actions
class Contacts_View_Action extends Spa_Action {
async page_title() { return this.data.contact.name; }
async breadcrumb_label() { return this.data.contact.name; }
async breadcrumb_label_active() { return 'View Contact'; }
async breadcrumb_parent() { return Rsx.Route('Contacts_Index_Action'); }
}
DESCRIPTION
RSX provides a comprehensive navigation system for SPAs consisting of:
1. Primary Navigation - Main sidebar with sections and links
2. Secondary Navigation - Sublayout sidebars for feature areas
3. Breadcrumbs - Progressive URL-based navigation trail
The system uses session storage caching for instant navigation feedback
while resolving live data in the background.
PRIMARY NAVIGATION
The primary navigation is typically a sidebar defined in the main SPA
layout (e.g., Frontend_Spa_Layout). It uses the Sidebar_Nav component.
Layout Structure:
// Frontend_Spa_Layout.js
class Frontend_Spa_Layout extends Spa_Layout {
on_create() {
this.state = {
nav_sections: [
{
title: 'Overview',
items: [
{
label: 'Dashboard',
icon: 'bi-house-door',
href: Rsx.Route('Dashboard_Index_Action'),
},
{
label: 'Calendar',
icon: 'bi-calendar',
href: Rsx.Route('Calendar_Index_Action'),
},
],
},
{
title: 'Business',
items: [
{ label: 'Clients', icon: 'bi-people',
href: Rsx.Route('Clients_Index_Action') },
{ label: 'Contacts', icon: 'bi-person-rolodex',
href: Rsx.Route('Contacts_Index_Action') },
],
},
],
};
}
on_action(url, action_name, args) {
// Update active state on navigation
this.sid('sidebar_nav').on_action(url, action_name, args);
}
}
Template Structure:
<Define:Frontend_Spa_Layout>
<nav class="app-sidebar">
<Sidebar_Nav $sid="sidebar_nav"
$sections=this.state.nav_sections />
</nav>
<main $sid="content"></main>
</Define:Frontend_Spa_Layout>
Sidebar_Nav Component:
Renders navigation sections with automatic active state based on
current URL. Supports icons (Bootstrap Icons), labels, and href.
on_action(url, action_name, args):
Called by layout to update active link highlighting.
Matches current URL to nav item hrefs.
SECONDARY NAVIGATION (SUBLAYOUTS)
Secondary navigation is used for feature areas with multiple related
pages (e.g., Settings). Implemented as sublayouts with their own sidebar.
Sublayout Pattern:
// Settings_Layout.js
class Settings_Layout extends Spa_Layout {
// Configure nav items per action
static ACTION_CONFIG = {
'Settings_General_Action': {
icon: 'bi-gear',
label: 'General Settings'
},
'Settings_Profile_Action': {
icon: 'bi-person',
label: 'Profile'
},
'Settings_User_Management_Index_Action': {
icon: 'bi-people',
label: 'User Management'
},
};
on_create() {
this._build_nav_items();
}
_build_nav_items() {
this.state.nav_items = [];
for (const [action_name, config] of
Object.entries(Settings_Layout.ACTION_CONFIG)) {
this.state.nav_items.push({
action: action_name,
icon: config.icon,
label: config.label,
href: Rsx.Route(action_name),
});
}
}
on_action(url, action_name, args) {
this._update_active_nav(action_name);
}
_update_active_nav(action_name) {
this.$sid('nav').find('.active').removeClass('active');
this.$sid('nav')
.find(`[data-action="${action_name}"]`)
.addClass('active');
}
}
Template Structure:
<Define:Settings_Layout>
<div class="settings-layout">
<aside class="settings-sidebar">
<nav $sid="nav">
<% for (let item of this.state.nav_items) { %>
<a href="<%= item.href %>"
data-action="<%= item.action %>"
class="settings-sidebar__item">
<i class="bi <%= item.icon %>"></i>
<span><%= item.label %></span>
</a>
<% } %>
</nav>
</aside>
<div class="settings-content" $sid="content"></div>
</div>
</Define:Settings_Layout>
Action Declaration:
Actions declare both outer and inner layouts:
@route('/frontend/settings/general')
@layout('Frontend_Spa_Layout') // Outer layout
@layout('Settings_Layout') // Sublayout (nested)
@spa('Frontend_Spa_Controller::index')
class Settings_General_Action extends Spa_Action { }
BREADCRUMB SYSTEM
Breadcrumbs provide hierarchical navigation context. The system uses
progressive resolution with caching for instant display.
Action Breadcrumb Methods:
All methods are async and can await the 'load' event if needed.
page_title()
Returns the page title for the header and document.title.
Example: "Edit: John Smith"
breadcrumb_label()
Returns the label when this page appears as a PARENT in
another page's breadcrumb trail.
Example: "John Smith" (when viewing edit page)
breadcrumb_label_active()
Returns the label when this page is the CURRENT page
(rightmost breadcrumb, no link).
Example: "Edit Contact"
breadcrumb_parent()
Returns the URL of the parent page, or null for root.
Example: Rsx.Route('Contacts_View_Action', { id: this.args.id })
Example Implementation:
class Contacts_View_Action extends Spa_Action {
on_create() {
this.data.contact = { first_name: '', last_name: '' };
}
async on_load() {
this.data.contact = await Contact_Model.fetch(this.args.id);
}
// Helper to await loaded data
async _await_loaded() {
if (this.data.contact && this.data.contact.id) return;
await new Promise(resolve => this.on('load', resolve));
}
async page_title() {
await this._await_loaded();
const c = this.data.contact;
return `${c.first_name} ${c.last_name}`.trim() || 'Contact';
}
async breadcrumb_label() {
await this._await_loaded();
const c = this.data.contact;
return `${c.first_name} ${c.last_name}`.trim() || 'Contact';
}
async breadcrumb_label_active() {
return 'View Contact'; // Static, no await needed
}
async breadcrumb_parent() {
return Rsx.Route('Contacts_Index_Action'); // Static route
}
}
Awaiting Loaded Data:
Breadcrumb methods are called BEFORE on_load() completes. If a method
needs loaded data (e.g., contact name), it must await the 'load' event:
async _await_loaded() {
// Check if data is already loaded
if (this.data.contact && this.data.contact.id) return;
// Otherwise wait for 'load' event
await new Promise(resolve => this.on('load', resolve));
}
The 'load' event fires immediately if already past that lifecycle
phase, so this pattern is safe to call multiple times.
RSX_BREADCRUMB_RESOLVER
Framework class that handles breadcrumb resolution with caching.
Location: /system/app/RSpade/Breadcrumbs/Rsx_Breadcrumb_Resolver.js
API:
Rsx_Breadcrumb_Resolver.stream(action, url, callbacks)
Streams breadcrumb data progressively.
Parameters:
action - Current Spa_Action instance
url - Current URL (pathname + search)
callbacks - Object with callback functions:
on_chain(chain) - Chain structure ready
on_label_update(idx, lbl) - Individual label resolved
on_complete(chain) - All labels resolved
Returns: Cancel function
Rsx_Breadcrumb_Resolver.clear_cache(url)
Clears cached breadcrumb chain for a URL.
Resolution Flow:
1. Check session cache for URL's breadcrumb chain
2. If cached, display immediately
3. Walk breadcrumb_parent() chain to get structure (URLs)
4. Compare to cache:
- Different length: render skeleton with blank labels
- Same length: render with cached labels
5. Resolve each label via breadcrumb_label()/breadcrumb_label_active()
6. Update display as each label resolves
7. Cache final result
Caching:
Uses Rsx_Storage.session_set/get() with key prefix 'breadcrumb_chain:'
Cached data: Array of { url, label, is_active }
LAYOUT INTEGRATION
The layout handles header rendering and breadcrumb streaming.
Frontend_Spa_Layout Integration:
class Frontend_Spa_Layout extends Spa_Layout {
static TITLE_CACHE_PREFIX = 'header_text:';
on_action(url, action_name, args) {
// Update page title with caching
this._update_page_title(url);
// Cancel previous breadcrumb stream
if (this._breadcrumb_cancel) {
this._breadcrumb_cancel();
}
// Start new breadcrumb stream
this._breadcrumb_cancel = Rsx_Breadcrumb_Resolver.stream(
this.action,
url,
{
on_chain: (chain) => this._on_breadcrumb_chain(chain),
on_label_update: (i, l) => this._on_breadcrumb_label_update(i, l),
on_complete: (chain) => this._on_breadcrumb_complete(chain)
}
);
}
async _update_page_title(url) {
const $title = this.$sid('page_title');
const cache_key = Frontend_Spa_Layout.TITLE_CACHE_PREFIX + url;
// Clear, show cached, then resolve live
$title.html('&nbsp;');
const cached = Rsx_Storage.session_get(cache_key);
if (cached) {
$title.html(cached);
document.title = 'RSX - ' + cached;
}
const live = await this.action.page_title();
if (live) {
$title.html(live);
document.title = 'RSX - ' + live;
Rsx_Storage.session_set(cache_key, live);
}
}
}
BREADCRUMB_NAV COMPONENT
Renders the breadcrumb trail from a chain array.
Location: /rsx/theme/components/page/breadcrumb_nav.jqhtml
Props:
$crumbs - Array of { label, url, is_active, resolved }
label: string or null (shows placeholder if null)
url: string or null (no link if null or is_active)
is_active: boolean (current page, no link)
resolved: boolean (false shows loading state)
Usage:
$breadcrumbs.component('Breadcrumb_Nav', {
crumbs: [
{ label: 'Contacts', url: '/contacts', is_active: false },
{ label: 'John Smith', url: '/contacts/view/1', is_active: false },
{ label: 'Edit Contact', url: null, is_active: true }
]
});
PAGE HEADER STRUCTURE
Standard page header with title, breadcrumbs, and action buttons.
Template Pattern:
<header class="page-title-header">
<div class="d-flex justify-content-between align-items-start">
<div>
<h1 $sid="page_title"></h1>
<nav $sid="page_breadcrumbs"></nav>
</div>
<div $sid="page_actions"></div>
</div>
</header>
Action Buttons:
Actions define page_actions() to provide header buttons:
class Contacts_View_Action extends Spa_Action {
page_actions() {
return `
<div class="d-flex gap-2">
<a href="${Rsx.Route('Contacts_Index_Action')}"
class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Back
</a>
<a href="${Rsx.Route('Contacts_Edit_Action', this.args.id)}"
class="btn btn-primary btn-sm">
<i class="bi bi-pencil"></i> Edit
</a>
</div>
`;
}
}
The layout renders these via _render_page_actions().
BREADCRUMB CHAIN EXAMPLE
For URL /contacts/edit/1, the breadcrumb chain resolves as:
Chain Structure (URLs):
1. /contacts (Contacts_Index_Action)
2. /contacts/view/1 (Contacts_View_Action)
3. /contacts/edit/1 (Contacts_Edit_Action) - ACTIVE
Resolution Process:
1. Contacts_Edit_Action.breadcrumb_parent() -> /contacts/view/1
2. Load detached Contacts_View_Action
3. Contacts_View_Action.breadcrumb_parent() -> /contacts
4. Load detached Contacts_Index_Action
5. Contacts_Index_Action.breadcrumb_parent() -> null (root)
Label Resolution (parallel):
- Index: breadcrumb_label() -> "Contacts"
- View: breadcrumb_label() -> "John Smith" (awaits load)
- Edit: breadcrumb_label_active() -> "Edit Contact"
Final Display:
Contacts > John Smith > Edit Contact
CACHING STRATEGY
Two caches are used for instant navigation feedback:
Page Title Cache:
Key: 'header_text:/contacts/edit/1'
Value: "Edit: John Smith"
Breadcrumb Chain Cache:
Key: 'breadcrumb_chain:/contacts/edit/1'
Value: [
{ url: '/contacts', label: 'Contacts', is_active: false },
{ url: '/contacts/view/1', label: 'John Smith', is_active: false },
{ url: '/contacts/edit/1', label: 'Edit Contact', is_active: true }
]
Cache Behavior:
- Cached values display immediately on navigation
- Live values resolve in background
- Cache updates when live values complete
- Previous breadcrumbs stay visible until new chain ready
FILES
/system/app/RSpade/Breadcrumbs/Rsx_Breadcrumb_Resolver.js
Breadcrumb resolution with progressive streaming and caching
/rsx/app/frontend/Frontend_Spa_Layout.js
Main layout with navigation and header integration
/rsx/theme/components/page/breadcrumb_nav.jqhtml
Breadcrumb rendering component
/rsx/theme/components/nav/sidebar_nav.jqhtml
Primary sidebar navigation component
SEE ALSO
spa(3), jqhtml(3), storage(3)
RSX Framework 2025-12-16 PRIMARY_SECONDARY_NAV_BREADCRUMBS(3)

View File

View File

View File

View File

0
app/RSpade/resource/vscode_extension/out/config.js Normal file → Executable file
View File

0
app/RSpade/resource/vscode_extension/out/config.js.map Normal file → Executable file
View File

View File

View File

View File

View File

View File

View File

View File

0
app/RSpade/resource/vscode_extension/out/extension.js Normal file → Executable file
View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File