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:
256
app/RSpade/Core/Js/Width_Group.js
Executable file
256
app/RSpade/Core/Js/Width_Group.js
Executable 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
130
app/RSpade/man/width_group.txt
Executable 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
|
||||
127
app/RSpade/upstream_changes/col_5ths_12_29.txt
Executable file
127
app/RSpade/upstream_changes/col_5ths_12_29.txt
Executable 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
|
||||
@@ -434,6 +434,7 @@ Pattern recognition:
|
||||
- Tier 2: `phone`, `tablet`, `desktop-sm`, `desktop-md`, `desktop-lg`, `desktop-xl`
|
||||
- SCSS: `@include mobile { }`, `@include desktop-xl { }`
|
||||
- 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()`
|
||||
|
||||
**Detailed guidance in `scss` skill** - auto-activates when styling components.
|
||||
|
||||
Reference in New Issue
Block a user