Fix JS-NATIVE-01 violations in checkbox_multiselect
Add SCSS class scoping enforcement, move modal to lib, cleanup legacy files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
358
app/RSpade/CodeQuality/Rules/Manifest/ScssClassScope_CodeQualityRule.php
Executable file
358
app/RSpade/CodeQuality/Rules/Manifest/ScssClassScope_CodeQualityRule.php
Executable file
@@ -0,0 +1,358 @@
|
||||
<?php
|
||||
|
||||
namespace App\RSpade\CodeQuality\Rules\Manifest;
|
||||
|
||||
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||
use App\RSpade\Core\Manifest\Manifest;
|
||||
|
||||
/**
|
||||
* ScssClassScope_CodeQualityRule - Enforces SCSS class scoping convention
|
||||
*
|
||||
* SCSS files in rsx/app/ and rsx/theme/components/ must be fully enclosed in a
|
||||
* single top-level class selector that matches their associated JavaScript class
|
||||
* or Blade view.
|
||||
*
|
||||
* Rules:
|
||||
* - rsx/app/**\/*.scss → must match a Component subclass (action/layout/component) or Blade @rsx_id
|
||||
* - rsx/theme/components/**\/*.scss → must match a Component subclass or Blade @rsx_id
|
||||
* - Filename must match the associated file's filename (different extension)
|
||||
* - EXCEPTION: Supplemental SCSS files may have different filenames if a primary SCSS file
|
||||
* (with matching filename) already exists for that wrapper class. This allows splitting
|
||||
* large stylesheets (e.g., by breakpoint or feature).
|
||||
* - Other SCSS files are not validated by this rule
|
||||
*
|
||||
* NO EXEMPTIONS: Files in these paths MUST follow the convention. If a file truly
|
||||
* cannot follow the pattern, it must be moved outside these directories (e.g., to
|
||||
* rsx/theme/base/) but this requires explicit developer approval.
|
||||
*/
|
||||
class ScssClassScope_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
{
|
||||
/**
|
||||
* Get the unique rule identifier
|
||||
*/
|
||||
public function get_id(): string
|
||||
{
|
||||
return 'SCSS-SCOPE-01';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable rule name
|
||||
*/
|
||||
public function get_name(): string
|
||||
{
|
||||
return 'SCSS Class Scope';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rule description
|
||||
*/
|
||||
public function get_description(): string
|
||||
{
|
||||
return 'Enforces SCSS files in rsx/app/ and rsx/theme/components/ are scoped to their associated action/layout/component class';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file patterns this rule applies to
|
||||
*/
|
||||
public function get_file_patterns(): array
|
||||
{
|
||||
return ['*.scss'];
|
||||
}
|
||||
|
||||
/**
|
||||
* This rule runs during manifest scan for immediate feedback
|
||||
*/
|
||||
public function is_called_during_manifest_scan(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default severity for this rule
|
||||
*/
|
||||
public function get_default_severity(): string
|
||||
{
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check SCSS files for proper class scoping
|
||||
*/
|
||||
public function check(string $file_path, string $contents, array $metadata = []): void
|
||||
{
|
||||
static $already_checked = false;
|
||||
|
||||
// Only check once per manifest build
|
||||
if ($already_checked) {
|
||||
return;
|
||||
}
|
||||
$already_checked = true;
|
||||
|
||||
// Get all manifest files
|
||||
$files = Manifest::get_all();
|
||||
if (empty($files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build lookup maps for quick matching
|
||||
// Components include Spa_Action and Spa_Layout since they extend Component
|
||||
$components = []; // class_name => file_path (without extension)
|
||||
$blade_ids = []; // id => file_path (without extension)
|
||||
|
||||
foreach ($files as $file => $file_metadata) {
|
||||
$extension = $file_metadata['extension'] ?? '';
|
||||
|
||||
// Collect Component subclasses (includes Spa_Action, Spa_Layout, and direct Component subclasses)
|
||||
if ($extension === 'js') {
|
||||
$class_name = $file_metadata['class'] ?? null;
|
||||
if ($class_name) {
|
||||
try {
|
||||
if (Manifest::js_is_subclass_of($class_name, 'Component')) {
|
||||
$components[$class_name] = pathinfo($file, PATHINFO_FILENAME);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Class not found in inheritance chain, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect Blade @rsx_id values
|
||||
if ($extension === 'blade.php') {
|
||||
$id = $file_metadata['id'] ?? null;
|
||||
if ($id) {
|
||||
// Remove .blade.php to get base filename
|
||||
$filename = basename($file, '.blade.php');
|
||||
$blade_ids[$id] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect jqhtml component IDs (from <Define:Component_Name>)
|
||||
// These are Components without a companion .js file
|
||||
if ($extension === 'jqhtml') {
|
||||
$id = $file_metadata['id'] ?? null;
|
||||
if ($id) {
|
||||
// Check if there's already a .js file for this component
|
||||
// If so, skip - the .js file takes precedence
|
||||
if (!isset($components[$id])) {
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
$components[$id] = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build map of wrapper classes that have a primary SCSS file (filename matches)
|
||||
// This allows supplemental SCSS files with different names for the same wrapper class
|
||||
$wrapper_classes_with_primary_scss = [];
|
||||
|
||||
foreach ($files as $file => $file_metadata) {
|
||||
$extension = $file_metadata['extension'] ?? '';
|
||||
if ($extension !== 'scss') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$wrapper_class = $file_metadata['scss_wrapper_class'] ?? null;
|
||||
if ($wrapper_class === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scss_filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
// Check if this SCSS file's name matches its wrapper class's associated file
|
||||
$matched_filename = $components[$wrapper_class] ?? $blade_ids[$wrapper_class] ?? null;
|
||||
if ($matched_filename !== null && strcasecmp($scss_filename, $matched_filename) === 0) {
|
||||
$wrapper_classes_with_primary_scss[$wrapper_class] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Now validate SCSS files
|
||||
foreach ($files as $file => $file_metadata) {
|
||||
$extension = $file_metadata['extension'] ?? '';
|
||||
|
||||
if ($extension !== 'scss') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine which validation to apply based on path
|
||||
$is_rsx_app = str_starts_with($file, 'rsx/app/');
|
||||
$is_theme_component = str_starts_with($file, 'rsx/theme/components/');
|
||||
|
||||
// Skip files outside our enforcement paths
|
||||
if (!$is_rsx_app && !$is_theme_component) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// NO EXEMPTIONS - all files in these paths must follow the convention
|
||||
|
||||
// Skip files that contain only variables and comments (no actual rules)
|
||||
if (!empty($file_metadata['scss_variables_only'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$wrapper_class = $file_metadata['scss_wrapper_class'] ?? null;
|
||||
$scss_filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
$this->validate_scss_file($file, $wrapper_class, $scss_filename, $components, $blade_ids, $wrapper_classes_with_primary_scss);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SCSS file against Component classes and Blade @rsx_id values
|
||||
*/
|
||||
private function validate_scss_file(
|
||||
string $file,
|
||||
?string $wrapper_class,
|
||||
string $scss_filename,
|
||||
array $components,
|
||||
array $blade_ids,
|
||||
array $wrapper_classes_with_primary_scss
|
||||
): void {
|
||||
// Check if file has a wrapper class
|
||||
if ($wrapper_class === null) {
|
||||
$this->add_violation(
|
||||
$file,
|
||||
1,
|
||||
"SCSS file must be fully enclosed in a single class rule (e.g., .My_Component { ... })",
|
||||
basename($file),
|
||||
$this->build_no_wrapper_suggestion($file),
|
||||
'critical'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if wrapper class matches a Component or Blade @rsx_id
|
||||
$matched_filename = null;
|
||||
$match_type = null;
|
||||
|
||||
if (isset($components[$wrapper_class])) {
|
||||
$matched_filename = $components[$wrapper_class];
|
||||
$match_type = 'Component';
|
||||
} elseif (isset($blade_ids[$wrapper_class])) {
|
||||
$matched_filename = $blade_ids[$wrapper_class];
|
||||
$match_type = 'Blade @rsx_id';
|
||||
}
|
||||
|
||||
if ($matched_filename === null) {
|
||||
$this->add_violation(
|
||||
$file,
|
||||
1,
|
||||
"SCSS wrapper class '{$wrapper_class}' does not match any Component class or Blade @rsx_id",
|
||||
".{$wrapper_class} { ... }",
|
||||
$this->build_no_match_suggestion($file, $wrapper_class),
|
||||
'critical'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if filename matches
|
||||
if (strcasecmp($scss_filename, $matched_filename) !== 0) {
|
||||
// Allow supplemental SCSS files if a primary file already exists for this wrapper class
|
||||
if (isset($wrapper_classes_with_primary_scss[$wrapper_class])) {
|
||||
// This is a supplemental file - filename mismatch is allowed
|
||||
return;
|
||||
}
|
||||
|
||||
$this->add_violation(
|
||||
$file,
|
||||
1,
|
||||
"SCSS filename '{$scss_filename}.scss' must match associated {$match_type} file '{$matched_filename}'. " .
|
||||
"Supplemental files with different names are allowed only if a primary file ('{$matched_filename}.scss') exists. " .
|
||||
"See: php artisan rsx:man scss",
|
||||
basename($file),
|
||||
$this->build_filename_mismatch_suggestion($file, $scss_filename, $matched_filename, $match_type),
|
||||
'critical'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build suggestion for files without a wrapper class
|
||||
*/
|
||||
private function build_no_wrapper_suggestion(string $file): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = "SCSS files in rsx/app/ and rsx/theme/components/ must be fully enclosed";
|
||||
$lines[] = "in a single top-level class selector that matches their associated";
|
||||
$lines[] = "action, layout, component, or Blade view.";
|
||||
$lines[] = "";
|
||||
$lines[] = "This prevents CSS conflicts and ensures styles are scoped to the";
|
||||
$lines[] = "element they are intended to style.";
|
||||
$lines[] = "";
|
||||
$lines[] = "VALID ASSOCIATIONS:";
|
||||
$lines[] = " - Spa_Action class (e.g., Frontend_Dashboard extends Spa_Action)";
|
||||
$lines[] = " - Spa_Layout class (e.g., Frontend_Layout extends Spa_Layout)";
|
||||
$lines[] = " - Component class (e.g., Sidebar_Nav extends Component)";
|
||||
$lines[] = " - Blade @rsx_id (e.g., @rsx_id('Login_Index'))";
|
||||
$lines[] = "";
|
||||
$lines[] = "TO FIX:";
|
||||
$lines[] = " Wrap ALL rules in the associated class:";
|
||||
$lines[] = "";
|
||||
$lines[] = " .My_Component {";
|
||||
$lines[] = " // ALL styles go here";
|
||||
$lines[] = " .card { ... }";
|
||||
$lines[] = " .button { ... }";
|
||||
$lines[] = " }";
|
||||
$lines[] = "";
|
||||
$lines[] = "NO EXEMPTIONS are allowed in these directories. If this file cannot";
|
||||
$lines[] = "be associated to an action/layout/component/view, it may need to be";
|
||||
$lines[] = "moved to rsx/theme/ (outside components/) - but this is rare and";
|
||||
$lines[] = "requires explicit developer approval.";
|
||||
$lines[] = "";
|
||||
$lines[] = "See: php artisan rsx:man scss";
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build suggestion for wrapper class that doesn't match anything
|
||||
*/
|
||||
private function build_no_match_suggestion(string $file, string $wrapper_class): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = "The wrapper class '{$wrapper_class}' does not match any known";
|
||||
$lines[] = "action, layout, component, or Blade view.";
|
||||
$lines[] = "";
|
||||
$lines[] = "VALID ASSOCIATIONS:";
|
||||
$lines[] = " - Spa_Action class (e.g., Frontend_Dashboard extends Spa_Action)";
|
||||
$lines[] = " - Spa_Layout class (e.g., Frontend_Layout extends Spa_Layout)";
|
||||
$lines[] = " - Component class (e.g., Sidebar_Nav extends Component)";
|
||||
$lines[] = " - Blade @rsx_id (e.g., @rsx_id('Login_Index'))";
|
||||
$lines[] = "";
|
||||
$lines[] = "TO FIX:";
|
||||
$lines[] = " 1. Rename the wrapper class to match your action/layout/component/view";
|
||||
$lines[] = " 2. Or create the missing .js or .blade.php file with this class/id";
|
||||
$lines[] = "";
|
||||
$lines[] = "NO EXEMPTIONS are allowed. If this file provides shared styles that";
|
||||
$lines[] = "cannot be scoped to a single element, it may need to be moved to";
|
||||
$lines[] = "rsx/theme/ (outside components/) - but this is rare and requires";
|
||||
$lines[] = "explicit developer approval.";
|
||||
$lines[] = "";
|
||||
$lines[] = "See: php artisan rsx:man scss";
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build suggestion for filename mismatch
|
||||
*/
|
||||
private function build_filename_mismatch_suggestion(
|
||||
string $file,
|
||||
string $scss_filename,
|
||||
string $expected_filename,
|
||||
string $match_type
|
||||
): string {
|
||||
$lines = [];
|
||||
$lines[] = "SCSS filename must match the associated {$match_type} file.";
|
||||
$lines[] = "";
|
||||
$lines[] = "Current: {$scss_filename}.scss";
|
||||
$lines[] = "Expected: {$expected_filename}.scss";
|
||||
$lines[] = "";
|
||||
$lines[] = "TO FIX:";
|
||||
$lines[] = " Rename the SCSS file:";
|
||||
$lines[] = " mv {$scss_filename}.scss {$expected_filename}.scss";
|
||||
$lines[] = "";
|
||||
$lines[] = "See: php artisan rsx:man scss";
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
@@ -209,17 +209,33 @@ class Scss_ManifestModule extends ManifestModule_Abstract
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if SCSS file has a single top-level class that qualifies as an ID
|
||||
* Detect if SCSS file has a single top-level class wrapper
|
||||
*
|
||||
* The SCSS file gets an 'id' if:
|
||||
* 1. All rules are contained within a single top-level class selector
|
||||
* 2. The class name matches a Blade view ID, JS class extending Component, or jqhtml template
|
||||
* 3. No other SCSS file already has this ID
|
||||
* Sets scss_wrapper_class if all rules are contained within a single top-level
|
||||
* class selector (e.g., .Frontend_Dashboard { ... }).
|
||||
*
|
||||
* SCSS variable declarations ($var: value;) are allowed outside the wrapper
|
||||
* and are stripped before checking. Files containing only variables/comments
|
||||
* are considered valid (scss_variables_only = true).
|
||||
*
|
||||
* Additionally sets 'id' if the wrapper class matches a Blade view ID,
|
||||
* JS class extending Component, or jqhtml template in the manifest.
|
||||
*/
|
||||
protected function detect_scss_id(string $clean_content, array &$metadata): void
|
||||
{
|
||||
// Remove SCSS variable declarations ($var: value;) - these are allowed outside wrapper
|
||||
// This regex matches: $variable-name: any value until semicolon;
|
||||
$content_without_vars = preg_replace('/\$[a-zA-Z_][\w-]*\s*:[^;]+;/', '', $clean_content);
|
||||
|
||||
// Remove all whitespace and newlines for easier parsing
|
||||
$compact = preg_replace('/\s+/', ' ', trim($clean_content));
|
||||
$compact = preg_replace('/\s+/', ' ', trim($content_without_vars));
|
||||
|
||||
// If content is empty after removing variables (only variables and comments),
|
||||
// mark as variables-only file - no wrapper needed
|
||||
if (empty($compact)) {
|
||||
$metadata['scss_variables_only'] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if content starts with a single class selector and everything is inside it
|
||||
// Pattern: .ClassName { ... everything ... }
|
||||
@@ -240,7 +256,10 @@ class Scss_ManifestModule extends ManifestModule_Abstract
|
||||
return;
|
||||
}
|
||||
|
||||
// Now check if this class name matches something in the manifest
|
||||
// Always set scss_wrapper_class when file is fully enclosed in a single class
|
||||
$metadata['scss_wrapper_class'] = $class_name;
|
||||
|
||||
// Now check if this class name matches something in the manifest for setting 'id'
|
||||
// During build, we need to access the in-memory manifest data
|
||||
// The get_all() method returns the cached data, not the in-progress build
|
||||
// We need a different approach - access the static data directly
|
||||
|
||||
352
app/RSpade/man/scss.txt
Executable file
352
app/RSpade/man/scss.txt
Executable file
@@ -0,0 +1,352 @@
|
||||
NAME
|
||||
scss - SCSS file organization and class scoping conventions
|
||||
|
||||
SYNOPSIS
|
||||
Action-scoped SCSS for SPA actions and Blade views:
|
||||
|
||||
// rsx/app/frontend/dashboard/dashboard_index_action.scss
|
||||
.Dashboard_Index_Action {
|
||||
.card { ... }
|
||||
.stats-grid { ... }
|
||||
}
|
||||
|
||||
Component-scoped SCSS for theme components:
|
||||
|
||||
// rsx/theme/components/sidebar/sidebar_nav.scss
|
||||
.Sidebar_Nav {
|
||||
.nav-item { ... }
|
||||
.nav-link { ... }
|
||||
}
|
||||
|
||||
Variables may be declared outside the wrapper for sharing:
|
||||
|
||||
// rsx/app/frontend/frontend_spa_layout.scss
|
||||
$sidebar-width: 215px;
|
||||
$header-height: 57px;
|
||||
|
||||
.Frontend_Spa_Layout {
|
||||
.sidebar { width: $sidebar-width; }
|
||||
}
|
||||
|
||||
DESCRIPTION
|
||||
RSX enforces a class-scoping convention for SCSS files to prevent CSS
|
||||
conflicts and ensure styles are self-contained. Every SCSS file in
|
||||
rsx/app/ or rsx/theme/components/ must wrap ALL rules inside a single
|
||||
top-level class selector that matches its associated JavaScript class
|
||||
or Blade view ID.
|
||||
|
||||
This is essentially manual CSS scoping - like CSS Modules but enforced
|
||||
by convention. The benefit is predictable specificity, no conflicts
|
||||
between pages/components, and self-documenting file organization.
|
||||
|
||||
Key principle: The SCSS filename must match the filename of its
|
||||
associated .js (action/component) or .blade.php file.
|
||||
|
||||
SCOPING RULES
|
||||
|
||||
Files in rsx/app/**/*.scss and rsx/theme/components/**/*.scss:
|
||||
|
||||
Must be fully enclosed in a class matching either:
|
||||
- A Component subclass (Spa_Action, Spa_Layout, or direct Component)
|
||||
- A Blade view's @rsx_id value (server-rendered page styles)
|
||||
|
||||
The SCSS filename must match the associated JS or Blade file's
|
||||
filename (with .scss extension instead of .js/.blade.php).
|
||||
|
||||
Example - SPA Action:
|
||||
rsx/app/frontend/dashboard/Dashboard_Index_Action.js
|
||||
rsx/app/frontend/dashboard/dashboard_index_action.scss
|
||||
|
||||
// dashboard_index_action.scss
|
||||
.Dashboard_Index_Action {
|
||||
// ALL styles nested here
|
||||
}
|
||||
|
||||
Example - SPA Layout:
|
||||
rsx/app/frontend/Frontend_Spa_Layout.js
|
||||
rsx/app/frontend/frontend_spa_layout.scss
|
||||
|
||||
// frontend_spa_layout.scss
|
||||
.Frontend_Spa_Layout {
|
||||
// ALL layout styles nested here
|
||||
.app-sidebar { ... }
|
||||
.app-content { ... }
|
||||
}
|
||||
|
||||
Example - Blade View:
|
||||
rsx/app/login/login_index.blade.php // has @rsx_id('Login_Index')
|
||||
rsx/app/login/login_index.scss
|
||||
|
||||
// login_index.scss
|
||||
.Login_Index {
|
||||
// ALL styles nested here
|
||||
}
|
||||
|
||||
Example - Theme Component:
|
||||
rsx/theme/components/sidebar/sidebar_nav.js
|
||||
rsx/theme/components/sidebar/sidebar_nav.scss
|
||||
|
||||
// sidebar_nav.scss
|
||||
.Sidebar_Nav {
|
||||
// ALL styles nested here
|
||||
}
|
||||
|
||||
Files elsewhere:
|
||||
SCSS files outside these paths are not validated by this rule
|
||||
and can be organized as needed (e.g., global utilities, variables
|
||||
in rsx/theme/base/).
|
||||
|
||||
SCSS VARIABLES
|
||||
|
||||
SCSS variable declarations ($var: value;) are allowed OUTSIDE the
|
||||
wrapper class. This enables variables to be shared when the file is
|
||||
imported by other SCSS files.
|
||||
|
||||
Example - Variables outside wrapper:
|
||||
|
||||
// frontend_spa_layout.scss
|
||||
$sidebar-width: 215px;
|
||||
$header-height: 57px;
|
||||
$mobile-breakpoint: 991.98px;
|
||||
|
||||
.Frontend_Spa_Layout {
|
||||
.sidebar {
|
||||
width: $sidebar-width;
|
||||
}
|
||||
.header {
|
||||
height: $header-height;
|
||||
}
|
||||
}
|
||||
|
||||
The manifest scanner strips variable declarations before checking
|
||||
for the wrapper class, so they do not cause validation failures.
|
||||
|
||||
VARIABLES-ONLY FILES
|
||||
|
||||
Files containing ONLY variable declarations and comments (no actual
|
||||
CSS rules or selectors) are considered valid without a wrapper class.
|
||||
These are typically partial files intended to be imported by others.
|
||||
|
||||
Example - Variables-only file (valid):
|
||||
|
||||
// _variables.scss
|
||||
$primary-color: #0d6efd;
|
||||
$secondary-color: #6c757d;
|
||||
$border-radius: 0.375rem;
|
||||
|
||||
Such files are marked with scss_variables_only in the manifest and
|
||||
skip wrapper validation entirely.
|
||||
|
||||
SUPPLEMENTAL SCSS FILES
|
||||
|
||||
When a single SCSS file becomes unwieldy, you can split styles into
|
||||
multiple files. Supplemental SCSS files may have different filenames
|
||||
as long as:
|
||||
|
||||
1. A primary SCSS file exists with the matching filename (e.g.,
|
||||
frontend_spa_layout.scss for Frontend_Spa_Layout)
|
||||
2. The supplemental file uses the SAME wrapper class as the primary
|
||||
|
||||
This allows organizing styles by breakpoint, feature, or logical
|
||||
grouping while maintaining the scoping convention.
|
||||
|
||||
Example - Splitting by breakpoint:
|
||||
|
||||
rsx/app/frontend/
|
||||
frontend_spa_layout.scss // Primary file (required)
|
||||
frontend_spa_layout_mobile.scss // Supplemental - mobile styles
|
||||
frontend_spa_layout_print.scss // Supplemental - print styles
|
||||
|
||||
// frontend_spa_layout.scss (primary)
|
||||
.Frontend_Spa_Layout {
|
||||
.sidebar { width: 215px; }
|
||||
.header { height: 57px; }
|
||||
}
|
||||
|
||||
// frontend_spa_layout_mobile.scss (supplemental)
|
||||
.Frontend_Spa_Layout {
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { width: 100%; }
|
||||
.header { height: 48px; }
|
||||
}
|
||||
}
|
||||
|
||||
// frontend_spa_layout_print.scss (supplemental)
|
||||
.Frontend_Spa_Layout {
|
||||
@media print {
|
||||
.sidebar { display: none; }
|
||||
.no-print { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
The primary file MUST exist first. Without it, supplemental files
|
||||
will fail validation with a filename mismatch error.
|
||||
|
||||
BENEFITS
|
||||
|
||||
No CSS Conflicts:
|
||||
.notice-item in Dashboard_Index_Action won't affect .notice-item
|
||||
in Calendar_Index_Action because they're in different scope wrappers.
|
||||
|
||||
Self-Documenting:
|
||||
File name tells you exactly which action/component it styles.
|
||||
Delete the action -> delete its SCSS -> no orphaned styles.
|
||||
|
||||
Simple Class Names:
|
||||
Use .team-grid instead of .dashboard-index-action__team-grid.
|
||||
The wrapper provides the scoping automatically.
|
||||
|
||||
Predictable Specificity:
|
||||
All page/component styles get the same specificity boost from
|
||||
being nested under their wrapper class.
|
||||
|
||||
Safe Refactoring:
|
||||
Moving or renaming an action means moving/renaming its SCSS.
|
||||
No hunting through global stylesheets for related rules.
|
||||
|
||||
HOW IT WORKS
|
||||
|
||||
jqhtml components and Spa_Action classes automatically add their
|
||||
class name to the root DOM element. For example, a component defined
|
||||
as <Define:Sidebar_Nav> will have class="Sidebar_Nav" on its root.
|
||||
|
||||
Blade views use @rsx_id('View_Name') which can be output to the DOM
|
||||
for the same scoping effect.
|
||||
|
||||
The manifest scanner detects if an SCSS file is fully enclosed in
|
||||
a single class rule by:
|
||||
1. Removing comments
|
||||
2. Stripping SCSS variable declarations ($var: value;)
|
||||
3. Checking if remaining content matches pattern: .ClassName { ... }
|
||||
4. Verifying bracket balance (all content inside the wrapper)
|
||||
|
||||
A code quality rule then validates:
|
||||
1. The wrapper class exists (or file is variables-only)
|
||||
2. It matches a valid Component subclass or Blade @rsx_id
|
||||
3. The filename matches the associated file
|
||||
|
||||
NO EXEMPTIONS
|
||||
|
||||
There are NO exemptions to this rule for files in rsx/app/ or
|
||||
rsx/theme/components/. Every SCSS file in these directories must
|
||||
be scoped to its associated action, layout, component, or view.
|
||||
|
||||
If a file cannot be associated with any of these (extremely rare),
|
||||
it likely belongs elsewhere:
|
||||
- rsx/theme/base/ for global utilities and variables
|
||||
- rsx/theme/layouts/ for shared layout styles
|
||||
- A dedicated partial imported via @use
|
||||
|
||||
Moving files outside the enforced directories requires explicit
|
||||
developer approval and should be carefully considered. In 99% of
|
||||
cases, the SCSS file should be properly scoped.
|
||||
|
||||
VALIDATION
|
||||
|
||||
The scoping rule is enforced at manifest build time. Violations
|
||||
produce errors like:
|
||||
|
||||
SCSS file 'rsx/app/frontend/dashboard/dashboard.scss' must be
|
||||
fully enclosed in a single class rule matching a Component
|
||||
or Blade @rsx_id.
|
||||
|
||||
Expected: .Dashboard_Index_Action { ... }
|
||||
Found: No wrapper class detected
|
||||
|
||||
Or for wrapper class mismatches:
|
||||
|
||||
SCSS wrapper class 'Frontend_Dashboard' does not match any
|
||||
Component class or Blade @rsx_id
|
||||
|
||||
Or for filename mismatches:
|
||||
|
||||
SCSS filename 'styles.scss' must match associated Component
|
||||
file 'dashboard_index_action'
|
||||
|
||||
EXAMPLES
|
||||
|
||||
Correct - SPA Action Styles:
|
||||
|
||||
// rsx/app/frontend/invoices/invoices_view_action.scss
|
||||
.Invoices_View_Action {
|
||||
.invoice-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.line-items {
|
||||
.item-row {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.invoice-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Correct - Layout with Variables:
|
||||
|
||||
// rsx/app/frontend/frontend_spa_layout.scss
|
||||
$sidebar-width: 215px;
|
||||
$header-height: 57px;
|
||||
|
||||
.Frontend_Spa_Layout {
|
||||
.app-sidebar {
|
||||
width: $sidebar-width;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
height: $header-height;
|
||||
}
|
||||
}
|
||||
|
||||
Correct - Component Styles:
|
||||
|
||||
// rsx/theme/components/modal/rsx_modal.scss
|
||||
.Rsx_Modal {
|
||||
.modal-header {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
&.modal-lg {
|
||||
.modal-dialog {
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Incorrect - Multiple Top-Level Rules:
|
||||
|
||||
// BAD: Multiple selectors at top level
|
||||
.Dashboard_Index_Action {
|
||||
.card { ... }
|
||||
}
|
||||
|
||||
.sidebar { // ERROR: This is outside the wrapper
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
Incorrect - No Wrapper:
|
||||
|
||||
// BAD: No wrapper class
|
||||
.card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
SEE ALSO
|
||||
spa - SPA routing and actions
|
||||
jqhtml - Component template system
|
||||
coding_standards - General naming conventions
|
||||
code_quality - Code quality rule system
|
||||
Reference in New Issue
Block a user