Framework updates

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-06 06:03:05 +00:00
parent 1506573202
commit 71d042ff3c
4 changed files with 208 additions and 24 deletions

View File

@@ -165,8 +165,8 @@ class Dispatcher
// For GET requests: redirect to the proper route
$params = array_merge($extra_params, $request->query->all());
// Generate proper URL using Rsx::Route
$proper_url = Rsx::Route($controller_name, $action_name, $params);
// Generate proper URL using Rsx::Route (signature: "Controller::method", $params)
$proper_url = Rsx::Route($controller_name . '::' . $action_name, $params);
console_debug('DISPATCH', 'Redirecting GET to proper route:', $proper_url);
@@ -209,8 +209,9 @@ class Dispatcher
if ($route_pattern) {
// Generate proper URL for the SPA action
// Note: SPA actions use class name only (action_name is ignored for SPA routes)
$params = array_merge($extra_params, $request->query->all());
$proper_url = Rsx::Route($controller_name, $action_name, $params);
$proper_url = Rsx::Route($controller_name, $params);
console_debug('DISPATCH', 'Redirecting to SPA action route:', $proper_url);

View File

@@ -15,6 +15,8 @@
*
* - Elements are tracked individually (not by selector)
* - Calling again with same group name adds more elements to the group
* - If element is already in the same group, no-op (idempotent)
* - If element is in a different group, it's moved to the new group
* - Recalculates immediately and on window resize (debounced 100ms)
* - Returns the jQuery object for chaining
*
@@ -68,8 +70,10 @@
* a. Remove min-width from all elements in group (measure natural width)
* b. Find max scrollWidth across all connected elements
* c. Apply max as min-width to all connected elements
* 3. On window resize (debounced 100ms): recalculate all groups
* 4. Disconnected elements are pruned on each calculation
* 3. Child components are found via shallowFind('.Component') and their
* ready events trigger debounced recalculation
* 4. On window resize (debounced 100ms): recalculate all groups
* 5. Disconnected elements are pruned on each calculation
*
*/
@@ -78,8 +82,8 @@ class Width_Group {
// Registry of groups: { "group-name": [element, element, ...] }
static _groups = {};
// Debounced resize handler (created on first use)
static _resize_handler = null;
// Debounced recalculate handler (shared by resize and component ready)
static _debounced_recalculate = null;
// Whether resize listener is attached
static _resize_attached = false;
@@ -104,17 +108,50 @@ class Width_Group {
Width_Group._groups[group_name] = [];
}
// Add each element to the group (avoid duplicates)
// Add each element to the group
this.each(function () {
const element = this;
if (!Width_Group._groups[group_name].includes(element)) {
Width_Group._groups[group_name].push(element);
const current_group = element._width_group;
// Already in this group - no-op
if (current_group === group_name) {
return;
}
// In a different group - remove from old group first
if (current_group && Width_Group._groups[current_group]) {
const old_group = Width_Group._groups[current_group];
const index = old_group.indexOf(element);
if (index !== -1) {
old_group.splice(index, 1);
element.style.minWidth = '';
}
// Clean up empty group
if (old_group.length === 0) {
delete Width_Group._groups[current_group];
}
}
// Add to new group
Width_Group._groups[group_name].push(element);
element._width_group = group_name;
});
// Ensure resize listener is attached
Width_Group._attach_resize_listener();
// Listen for child component ready events
this.each(function () {
$(this).shallowFind('.Component').each(function () {
const component = $(this).component();
if (component) {
component.on('ready', () => {
Width_Group._schedule_recalculate();
});
}
});
});
// Calculate immediately
Width_Group._calculate_group(group_name);
@@ -152,11 +189,12 @@ class Width_Group {
return;
}
// Clear min-width from elements still in DOM
// Clear min-width and group tracking from elements
for (const element of elements) {
if (element.isConnected) {
element.style.minWidth = '';
}
delete element._width_group;
}
// Remove group from registry
@@ -166,6 +204,20 @@ class Width_Group {
Width_Group._maybe_detach_resize_listener();
}
/**
* Schedule a debounced recalculation of all groups
* Used by resize handler and component ready events
* @private
*/
static _schedule_recalculate() {
if (!Width_Group._debounced_recalculate) {
Width_Group._debounced_recalculate = debounce(() => {
Width_Group._calculate_all();
}, 100);
}
Width_Group._debounced_recalculate();
}
/**
* Attach the resize listener if not already attached
* @private
@@ -175,14 +227,9 @@ class Width_Group {
return;
}
// Create debounced handler on first use
if (!Width_Group._resize_handler) {
Width_Group._resize_handler = debounce(() => {
Width_Group._calculate_all();
}, 100);
}
$(window).on('resize.width_group', Width_Group._resize_handler);
$(window).on('resize.width_group', () => {
Width_Group._schedule_recalculate();
});
Width_Group._resize_attached = true;
}
@@ -218,8 +265,14 @@ class Width_Group {
return;
}
// Filter to only connected elements
elements = elements.filter(el => el.isConnected);
// Filter to only connected elements, clean up disconnected ones
elements = elements.filter(el => {
if (el.isConnected) {
return true;
}
delete el._width_group;
return false;
});
// Update registry with pruned list
Width_Group._groups[group_name] = elements;

View File

@@ -27,6 +27,10 @@ API
group will have their min-width set to match the widest element.
Returns the jQuery object for chaining.
Each element can only belong to one width group at a time:
- If already in the same group: no-op (idempotent)
- If in a different group: moved to the new group
Example:
$(".toolbar .btn").width_group("toolbar-buttons");
@@ -64,14 +68,29 @@ AUTOMATIC CLEANUP
Explicit $.width_group_destroy() calls are optional but can be used for
immediate cleanup.
COMPONENT INTEGRATION
When width_group() is called, it automatically finds child components
within each element using shallowFind('.Component'). For each component
found, it listens for the 'ready' event and triggers a debounced
recalculation when the component becomes ready.
This handles cases where a width group contains components that load
asynchronously - the widths will be recalculated once the component
content is rendered.
All recalculation triggers (resize, component ready) share the same
debounced function (100ms) to prevent excessive recalculations.
HOW IT WORKS
1. On width_group() call, elements are added to a named registry
2. Calculation runs:
2. Calculation runs immediately:
a. Remove min-width from all elements (measure natural width)
b. Find max scrollWidth across all connected elements
c. Apply max as min-width to all connected elements
3. On window resize (debounced 100ms), recalculate all groups
4. Disconnected elements are pruned on each calculation
3. Child components found via shallowFind('.Component') have their
ready events listened to for debounced recalculation
4. On window resize (debounced 100ms), recalculate all groups
5. Disconnected elements are pruned on each calculation
EXAMPLES
Button group with consistent widths:

View File

@@ -0,0 +1,111 @@
SCSS Loop Refactoring for Responsive Styles
============================================
Date: 2024-12-31
Affects: rsx/theme/responsive.scss (and any project-specific responsive SCSS)
SUMMARY
-------
The framework's responsive.scss has been refactored to use SCSS loops (@each)
for repetitive class generation. Projects with custom responsive SCSS should
analyze their files for similar optimization opportunities.
CHANGES IN FRAMEWORK
--------------------
1. col-5ths responsive classes now generated via @each loop
2. CSS custom properties (--bp-*) now generated via @each loop
3. $breakpoints-up map added for consistency in -up mixins
4. Reduced code from ~55 lines to ~30 lines for col-5ths section
ANALYSIS GUIDE FOR PROJECT SCSS
-------------------------------
Review your responsive SCSS files for these patterns:
PATTERN 1: Repeated classes with only breakpoint name changing
--------------------------------------------------------------
Before:
.my-thing-mobile { @include mobile { ... } }
.my-thing-tablet { @include tablet-up { ... } }
.my-thing-desktop { @include desktop { ... } }
After:
$my-thing-breakpoints: (
'mobile': 'max' $bp-desktop,
'tablet': 'min' $bp-tablet,
'desktop': 'min' $bp-desktop
);
@each $name, $config in $my-thing-breakpoints {
$type: nth($config, 1);
$value: nth($config, 2);
.my-thing-#{$name} {
@if $type == 'max' {
@media (max-width: #{$value - 0.02px}) { ... }
} @else {
@media (min-width: $value) { ... }
}
}
}
PATTERN 2: CSS custom properties for each breakpoint
----------------------------------------------------
Before:
:root {
--my-bp-tablet: #{$bp-tablet};
--my-bp-desktop: #{$bp-desktop};
--my-bp-desktop-lg: #{$bp-desktop-lg};
}
After:
$my-breakpoints: (
'tablet': $bp-tablet,
'desktop': $bp-desktop,
'desktop-lg': $bp-desktop-lg
);
:root {
@each $name, $value in $my-breakpoints {
--my-bp-#{$name}: #{$value};
}
}
PATTERN 3: Shared styles in a mixin
-----------------------------------
If multiple classes share identical styles, extract to a mixin:
@mixin my-shared-styles {
display: flex;
align-items: center;
gap: 8px;
}
.base-class { @include my-shared-styles; }
@each $name, $config in $breakpoints {
.responsive-#{$name} {
@media (...) { @include my-shared-styles; }
}
}
WHEN NOT TO USE LOOPS
---------------------
- When classes have different internal logic (not just breakpoint differences)
- When the repetition is 2-3 items (loop overhead not worth it)
- When the pattern would obscure the code's intent
LIMITATION: DYNAMIC MIXIN CALLS
-------------------------------
SCSS does not support: @include #{$variable}
This means you cannot dynamically call mixins by name. The workaround is to
define breakpoint values in a map and generate media queries directly, as
shown in Pattern 1 above.
ACTION REQUIRED
---------------
1. Review project-specific responsive SCSS files
2. Identify repeated patterns (3+ similar blocks)
3. Refactor using @each loops where appropriate
4. Verify compiled CSS output matches previous version
NO BREAKING CHANGES - this is a code organization improvement only.