Add col-5ths responsive columns and Width_Group JS utility

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-29 23:34:56 +00:00
parent 3afb345fbc
commit d41a534744
4 changed files with 514 additions and 0 deletions

256
app/RSpade/Core/Js/Width_Group.js Executable file
View File

@@ -0,0 +1,256 @@
/**
* Width_Group - Synchronized min-width for element groups
*
* Makes multiple elements share the same min-width, determined by the widest
* element in the group. Useful for button groups, card layouts, or any UI
* where elements should have consistent widths.
*
* =============================================================================
* API
* =============================================================================
*
* $(".selector").width_group("group-name")
* Adds matched elements to a named width group. All elements in the group
* will have their min-width set to match the widest element.
*
* - Elements are tracked individually (not by selector)
* - Calling again with same group name adds more elements to the group
* - Recalculates immediately and on window resize (debounced 100ms)
* - Returns the jQuery object for chaining
*
* $.width_group_destroy("group-name")
* Removes all tracking for the named group. Clears min-width styles from
* elements still in the DOM and removes the resize listener if no groups
* remain.
*
* $.width_group_recalculate("group-name")
* Manually triggers recalculation for a group. Useful after dynamically
* changing element content.
*
* $.width_group_recalculate()
* Recalculates all groups.
*
* =============================================================================
* AUTOMATIC CLEANUP
* =============================================================================
*
* On each resize (or manual recalculate), elements are checked for DOM presence
* using element.isConnected. Disconnected elements are automatically removed
* from tracking. When all elements in a group are gone, the group is destroyed.
* When all groups are destroyed, the resize listener is removed.
*
* This means SPA navigation that removes elements will automatically clean up
* without explicit destroy calls.
*
* =============================================================================
* EXAMPLE USAGE
* =============================================================================
*
* // Make all action buttons the same width
* $(".action-buttons .btn").width_group("action-buttons");
*
* // Add more buttons to the same group later
* $(".extra-btn").width_group("action-buttons");
*
* // Explicit cleanup (optional - auto-cleans when elements removed)
* $.width_group_destroy("action-buttons");
*
* // After changing button text, recalculate
* $(".action-buttons .btn").text("New Label");
* $.width_group_recalculate("action-buttons");
*
* =============================================================================
* HOW IT WORKS
* =============================================================================
*
* 1. On width_group() call: elements are added to the named group registry
* 2. Calculation runs immediately:
* 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
*
*/
// @JS-THIS-01-EXCEPTION
class Width_Group {
// Registry of groups: { "group-name": [element, element, ...] }
static _groups = {};
// Debounced resize handler (created on first use)
static _resize_handler = null;
// Whether resize listener is attached
static _resize_attached = false;
/**
* Initialize jQuery extensions
*/
static _on_framework_core_define() {
/**
* Add elements to a named width group
* @param {string} group_name - Name of the width group
* @returns {jQuery} The jQuery object for chaining
*/
$.fn.width_group = function (group_name) {
if (!group_name || typeof group_name !== 'string') {
console.error('width_group() requires a group name string');
return this;
}
// Initialize group if needed
if (!Width_Group._groups[group_name]) {
Width_Group._groups[group_name] = [];
}
// Add each element to the group (avoid duplicates)
this.each(function () {
const element = this;
if (!Width_Group._groups[group_name].includes(element)) {
Width_Group._groups[group_name].push(element);
}
});
// Ensure resize listener is attached
Width_Group._attach_resize_listener();
// Calculate immediately
Width_Group._calculate_group(group_name);
return this;
};
/**
* Destroy a width group, removing tracking and styles
* @param {string} group_name - Name of the width group to destroy
*/
$.width_group_destroy = function (group_name) {
Width_Group.destroy(group_name);
};
/**
* Manually recalculate a group or all groups
* @param {string} [group_name] - Optional group name. If omitted, recalculates all.
*/
$.width_group_recalculate = function (group_name) {
if (group_name) {
Width_Group._calculate_group(group_name);
} else {
Width_Group._calculate_all();
}
};
}
/**
* Destroy a named group
* @param {string} group_name
*/
static destroy(group_name) {
const elements = Width_Group._groups[group_name];
if (!elements) {
return;
}
// Clear min-width from elements still in DOM
for (const element of elements) {
if (element.isConnected) {
element.style.minWidth = '';
}
}
// Remove group from registry
delete Width_Group._groups[group_name];
// Detach resize listener if no groups remain
Width_Group._maybe_detach_resize_listener();
}
/**
* Attach the resize listener if not already attached
* @private
*/
static _attach_resize_listener() {
if (Width_Group._resize_attached) {
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);
Width_Group._resize_attached = true;
}
/**
* Detach resize listener if no groups remain
* @private
*/
static _maybe_detach_resize_listener() {
if (Object.keys(Width_Group._groups).length === 0 && Width_Group._resize_attached) {
$(window).off('resize.width_group');
Width_Group._resize_attached = false;
}
}
/**
* Calculate and apply min-width for all groups
* @private
*/
static _calculate_all() {
for (const group_name of Object.keys(Width_Group._groups)) {
Width_Group._calculate_group(group_name);
}
}
/**
* Calculate and apply min-width for a specific group
* @param {string} group_name
* @private
*/
static _calculate_group(group_name) {
let elements = Width_Group._groups[group_name];
if (!elements || elements.length === 0) {
return;
}
// Filter to only connected elements
elements = elements.filter(el => el.isConnected);
// Update registry with pruned list
Width_Group._groups[group_name] = elements;
// If all elements are gone, destroy the group
if (elements.length === 0) {
delete Width_Group._groups[group_name];
Width_Group._maybe_detach_resize_listener();
return;
}
// Step 1: Remove min-width to measure natural width
for (const element of elements) {
element.style.minWidth = '';
}
// Step 2: Find the maximum natural width
let max_width = 0;
for (const element of elements) {
// scrollWidth gives the full content width including overflow
const width = element.scrollWidth;
if (width > max_width) {
max_width = width;
}
}
// Step 3: Apply max width as min-width to all elements
if (max_width > 0) {
for (const element of elements) {
element.style.minWidth = max_width + 'px';
}
}
}
}

130
app/RSpade/man/width_group.txt Executable file
View File

@@ -0,0 +1,130 @@
NAME
width_group - synchronized min-width for element groups
SYNOPSIS
$(".selector").width_group("group-name")
$.width_group_destroy("group-name")
$.width_group_recalculate("group-name")
$.width_group_recalculate()
DESCRIPTION
Makes multiple elements share the same min-width, determined by the widest
element in the group. Useful for button groups, card layouts, navigation
items, or any UI where elements should have consistent widths.
Elements are tracked individually by DOM reference, not by selector. The
same group name can be used across multiple width_group() calls to add
more elements to an existing group.
Calculation occurs immediately on call and on window resize (debounced
100ms). Elements removed from the DOM are automatically pruned from
tracking on each calculation cycle.
API
$(".selector").width_group("group-name")
Adds matched elements to a named width group. All elements in the
group will have their min-width set to match the widest element.
Returns the jQuery object for chaining.
Example:
$(".toolbar .btn").width_group("toolbar-buttons");
$.width_group_destroy("group-name")
Removes all tracking for the named group. Clears min-width styles
from elements still in the DOM. Removes the resize listener if no
groups remain.
Example:
$.width_group_destroy("toolbar-buttons");
$.width_group_recalculate("group-name")
Manually triggers recalculation for a specific group. Useful after
dynamically changing element content (text, icons, etc.).
Example:
$(".btn-save").text("Saving...");
$.width_group_recalculate("toolbar-buttons");
$.width_group_recalculate()
Recalculates all width groups.
AUTOMATIC CLEANUP
On each resize or manual recalculate, elements are checked for DOM
presence using element.isConnected. Disconnected elements are removed
from tracking automatically.
When all elements in a group are gone, the group is destroyed. When all
groups are destroyed, the resize listener is removed. This prevents
memory leaks during SPA navigation.
Explicit $.width_group_destroy() calls are optional but can be used for
immediate cleanup.
HOW IT WORKS
1. On width_group() call, elements are added to a named registry
2. Calculation runs:
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
EXAMPLES
Button group with consistent widths:
<div class="action-buttons">
<button class="btn">Save</button>
<button class="btn">Cancel</button>
<button class="btn">Delete Forever</button>
</div>
// All buttons match width of "Delete Forever"
$(".action-buttons .btn").width_group("actions");
Adding elements to existing group:
// Initial buttons
$(".primary-actions .btn").width_group("nav-buttons");
// Later, add secondary buttons to same group
$(".secondary-actions .btn").width_group("nav-buttons");
Recalculate after content change:
async function save() {
const $btn = $(".btn-save");
const original = $btn.text();
$btn.text("Saving...");
$.width_group_recalculate("form-buttons");
await Controller.save(data);
$btn.text(original);
$.width_group_recalculate("form-buttons");
}
Card layout with equal-width headers:
<div class="dashboard-cards">
<div class="card">
<h3 class="card-title">Revenue</h3>
...
</div>
<div class="card">
<h3 class="card-title">Active Users</h3>
...
</div>
</div>
$(".dashboard-cards .card-title").width_group("card-titles");
FILES
/system/app/RSpade/Core/Js/Width_Group.js
SEE ALSO
jquery, responsive, jqhtml

View File

@@ -0,0 +1,127 @@
FIFTH-WIDTH COLUMN CLASSES - MIGRATION GUIDE
Date: 2024-12-29
SUMMARY
New .col-5ths classes provide 20% width columns that are not available in
Bootstrap's 12-column grid system. These follow the same responsive pattern
as standard Bootstrap column classes, activating at each breakpoint.
AFFECTED FILES
Framework files (already updated):
rsx/theme/responsive.scss New col-5ths class definitions
Application files requiring migration:
rsx/theme/responsive.scss Must add col-5ths classes if customized
CHANGES REQUIRED
1. Add Fifth-Width Column Classes to responsive.scss
-------------------------------------------------------------------------
If your project has customized responsive.scss, add the following classes
before the CSS CUSTOM PROPERTIES section:
// ==========================================================================
// FIFTH-WIDTH COLUMNS (.col-5ths)
// ==========================================================================
// 20% width columns not available in Bootstrap's 12-column grid.
// Follows same responsive pattern as Bootstrap column classes.
.col-5ths {
flex: 0 0 20%;
max-width: 20%;
}
.col-mobile-5ths {
@include mobile {
flex: 0 0 20%;
max-width: 20%;
}
}
.col-tablet-5ths {
@include tablet-up {
flex: 0 0 20%;
max-width: 20%;
}
}
.col-desktop-5ths {
@include desktop {
flex: 0 0 20%;
max-width: 20%;
}
}
.col-desktop-sm-5ths {
@include desktop-sm-up {
flex: 0 0 20%;
max-width: 20%;
}
}
.col-desktop-md-5ths {
@include desktop-md-up {
flex: 0 0 20%;
max-width: 20%;
}
}
.col-desktop-lg-5ths {
@include desktop-lg-up {
flex: 0 0 20%;
max-width: 20%;
}
}
.col-desktop-xl-5ths {
@include desktop-xl-up {
flex: 0 0 20%;
max-width: 20%;
}
}
CLASS REFERENCE
Class Breakpoint Width
--------------------- -------------------- -----
.col-5ths Always 20%
.col-mobile-5ths 0-1023px 20%
.col-tablet-5ths 800px+ 20%
.col-desktop-5ths 1024px+ 20%
.col-desktop-sm-5ths 1024px+ 20%
.col-desktop-md-5ths 1200px+ 20%
.col-desktop-lg-5ths 1640px+ 20%
.col-desktop-xl-5ths 2200px+ 20%
USAGE
Five-column layout on desktop, two columns on mobile:
<div class="row">
<div class="col-mobile-6 col-desktop-5ths">Item 1</div>
<div class="col-mobile-6 col-desktop-5ths">Item 2</div>
<div class="col-mobile-6 col-desktop-5ths">Item 3</div>
<div class="col-mobile-6 col-desktop-5ths">Item 4</div>
<div class="col-mobile-6 col-desktop-5ths">Item 5</div>
</div>
Progressive five-column layout:
<div class="row">
<div class="col-12 col-tablet-6 col-desktop-md-5ths">Item</div>
...
</div>
VERIFICATION
1. Compile bundles and verify no SCSS errors:
php artisan rsx:bundle:compile Frontend_Bundle
2. Test the classes in browser:
- Add class="col-5ths" to 5 elements in a row, verify each is 20% width
- Test responsive variants at different viewport widths
REFERENCE
php artisan rsx:man responsive - Full responsive system documentation

View File

@@ -434,6 +434,7 @@ Pattern recognition:
- Tier 2: `phone`, `tablet`, `desktop-sm`, `desktop-md`, `desktop-lg`, `desktop-xl` - Tier 2: `phone`, `tablet`, `desktop-sm`, `desktop-md`, `desktop-lg`, `desktop-xl`
- SCSS: `@include mobile { }`, `@include desktop-xl { }` - SCSS: `@include mobile { }`, `@include desktop-xl { }`
- Classes: `.col-mobile-12`, `.d-tablet-none`, `.mobile-only` - Classes: `.col-mobile-12`, `.d-tablet-none`, `.mobile-only`
- Fifth-width columns: `.col-5ths`, `.col-mobile-5ths`, `.col-desktop-5ths`, etc.
- JS: `Responsive.is_mobile()`, `Responsive.is_phone()` - JS: `Responsive.is_mobile()`, `Responsive.is_phone()`
**Detailed guidance in `scss` skill** - auto-activates when styling components. **Detailed guidance in `scss` skill** - auto-activates when styling components.