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`
|
- 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user