Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -165,8 +165,8 @@ class Dispatcher
|
|||||||
// For GET requests: redirect to the proper route
|
// For GET requests: redirect to the proper route
|
||||||
$params = array_merge($extra_params, $request->query->all());
|
$params = array_merge($extra_params, $request->query->all());
|
||||||
|
|
||||||
// Generate proper URL using Rsx::Route
|
// Generate proper URL using Rsx::Route (signature: "Controller::method", $params)
|
||||||
$proper_url = Rsx::Route($controller_name, $action_name, $params);
|
$proper_url = Rsx::Route($controller_name . '::' . $action_name, $params);
|
||||||
|
|
||||||
console_debug('DISPATCH', 'Redirecting GET to proper route:', $proper_url);
|
console_debug('DISPATCH', 'Redirecting GET to proper route:', $proper_url);
|
||||||
|
|
||||||
@@ -209,8 +209,9 @@ class Dispatcher
|
|||||||
|
|
||||||
if ($route_pattern) {
|
if ($route_pattern) {
|
||||||
// Generate proper URL for the SPA action
|
// 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());
|
$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);
|
console_debug('DISPATCH', 'Redirecting to SPA action route:', $proper_url);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
*
|
*
|
||||||
* - Elements are tracked individually (not by selector)
|
* - Elements are tracked individually (not by selector)
|
||||||
* - Calling again with same group name adds more elements to the group
|
* - 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)
|
* - Recalculates immediately and on window resize (debounced 100ms)
|
||||||
* - Returns the jQuery object for chaining
|
* - Returns the jQuery object for chaining
|
||||||
*
|
*
|
||||||
@@ -68,8 +70,10 @@
|
|||||||
* a. Remove min-width from all elements in group (measure natural width)
|
* a. Remove min-width from all elements in group (measure natural width)
|
||||||
* b. Find max scrollWidth across all connected elements
|
* b. Find max scrollWidth across all connected elements
|
||||||
* c. Apply max as min-width to all connected elements
|
* c. Apply max as min-width to all connected elements
|
||||||
* 3. On window resize (debounced 100ms): recalculate all groups
|
* 3. Child components are found via shallowFind('.Component') and their
|
||||||
* 4. Disconnected elements are pruned on each calculation
|
* 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, ...] }
|
// Registry of groups: { "group-name": [element, element, ...] }
|
||||||
static _groups = {};
|
static _groups = {};
|
||||||
|
|
||||||
// Debounced resize handler (created on first use)
|
// Debounced recalculate handler (shared by resize and component ready)
|
||||||
static _resize_handler = null;
|
static _debounced_recalculate = null;
|
||||||
|
|
||||||
// Whether resize listener is attached
|
// Whether resize listener is attached
|
||||||
static _resize_attached = false;
|
static _resize_attached = false;
|
||||||
@@ -104,17 +108,50 @@ class Width_Group {
|
|||||||
Width_Group._groups[group_name] = [];
|
Width_Group._groups[group_name] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add each element to the group (avoid duplicates)
|
// Add each element to the group
|
||||||
this.each(function () {
|
this.each(function () {
|
||||||
const element = this;
|
const element = this;
|
||||||
if (!Width_Group._groups[group_name].includes(element)) {
|
const current_group = element._width_group;
|
||||||
Width_Group._groups[group_name].push(element);
|
|
||||||
|
// 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
|
// Ensure resize listener is attached
|
||||||
Width_Group._attach_resize_listener();
|
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
|
// Calculate immediately
|
||||||
Width_Group._calculate_group(group_name);
|
Width_Group._calculate_group(group_name);
|
||||||
|
|
||||||
@@ -152,11 +189,12 @@ class Width_Group {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear min-width from elements still in DOM
|
// Clear min-width and group tracking from elements
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (element.isConnected) {
|
if (element.isConnected) {
|
||||||
element.style.minWidth = '';
|
element.style.minWidth = '';
|
||||||
}
|
}
|
||||||
|
delete element._width_group;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove group from registry
|
// Remove group from registry
|
||||||
@@ -166,6 +204,20 @@ class Width_Group {
|
|||||||
Width_Group._maybe_detach_resize_listener();
|
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
|
* Attach the resize listener if not already attached
|
||||||
* @private
|
* @private
|
||||||
@@ -175,14 +227,9 @@ class Width_Group {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create debounced handler on first use
|
$(window).on('resize.width_group', () => {
|
||||||
if (!Width_Group._resize_handler) {
|
Width_Group._schedule_recalculate();
|
||||||
Width_Group._resize_handler = debounce(() => {
|
});
|
||||||
Width_Group._calculate_all();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(window).on('resize.width_group', Width_Group._resize_handler);
|
|
||||||
Width_Group._resize_attached = true;
|
Width_Group._resize_attached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,8 +265,14 @@ class Width_Group {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter to only connected elements
|
// Filter to only connected elements, clean up disconnected ones
|
||||||
elements = elements.filter(el => el.isConnected);
|
elements = elements.filter(el => {
|
||||||
|
if (el.isConnected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete el._width_group;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
// Update registry with pruned list
|
// Update registry with pruned list
|
||||||
Width_Group._groups[group_name] = elements;
|
Width_Group._groups[group_name] = elements;
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ API
|
|||||||
group will have their min-width set to match the widest element.
|
group will have their min-width set to match the widest element.
|
||||||
Returns the jQuery object for chaining.
|
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:
|
Example:
|
||||||
$(".toolbar .btn").width_group("toolbar-buttons");
|
$(".toolbar .btn").width_group("toolbar-buttons");
|
||||||
|
|
||||||
@@ -64,14 +68,29 @@ AUTOMATIC CLEANUP
|
|||||||
Explicit $.width_group_destroy() calls are optional but can be used for
|
Explicit $.width_group_destroy() calls are optional but can be used for
|
||||||
immediate cleanup.
|
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
|
HOW IT WORKS
|
||||||
1. On width_group() call, elements are added to a named registry
|
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)
|
a. Remove min-width from all elements (measure natural width)
|
||||||
b. Find max scrollWidth across all connected elements
|
b. Find max scrollWidth across all connected elements
|
||||||
c. Apply max as min-width to all connected elements
|
c. Apply max as min-width to all connected elements
|
||||||
3. On window resize (debounced 100ms), recalculate all groups
|
3. Child components found via shallowFind('.Component') have their
|
||||||
4. Disconnected elements are pruned on each calculation
|
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
|
EXAMPLES
|
||||||
Button group with consistent widths:
|
Button group with consistent widths:
|
||||||
|
|||||||
111
app/RSpade/upstream_changes/scss_loop_refactor_12_31.txt
Executable file
111
app/RSpade/upstream_changes/scss_loop_refactor_12_31.txt
Executable 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.
|
||||||
Reference in New Issue
Block a user