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. COMPONENT-FIRST PHILOSOPHY Every styled element should be a component. If an element needs custom styles, it deserves a name, a jqhtml definition, and scoped SCSS. This eliminates CSS spaghetti - generic classes like .page-header, .filter-bar, .action-buttons scattered across files, overriding each other unpredictably. Pattern Recognition: When building a page, ask: "Is this structure unique, or a pattern?" Pattern (shared structure): A datagrid page with toolbar, tabs, filters, and search appears on 8 different pages. Create Datagrid_Card once with slots, use it everywhere. Changes propagate automatically. Unique (one-off structure): A project dashboard with custom widgets specific to that page. Create Project_Dashboard for that page alone. Decision heuristic: If you're about to copy-paste structural markup, stop and extract a component. Slot-Based Composition: Use slots to separate structure from content. The component owns layout and styling; pages provide the variable parts via slots. // Datagrid_Card owns the structure
<%= content('toolbar') %>
<%= content('body') %>
// Page provides content via slots This keeps pages declarative and components reusable. What Remains Shared: Only primitives should be shared/unscoped styles: - Buttons (.btn-primary, .btn-secondary) - Spacing utilities (.mb-3, .p-2) - Typography (.text-muted, .fw-bold) - Bootstrap overrides Everything else - page layouts, card variations, custom UI patterns - should be component-scoped SCSS. 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 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