Index
# Simple CSS (`.scs`) `SCS` stands for **S**imple **C**ascading **S**tyle. It is a small compile-time layer over css, compiled to plain css by [`MMO\client\SCSCompiler`](../src/SCSCompiler.php). Unlike [`DartSass`](../src/DartSass.php), nothing is shelled out: the lexer, the parser, the evaluator and the emitter are pure php, so a stylesheet compiles anywhere the framework runs — no binary, no node, no cache directory. ```text .scs → Lexer → Parser → ast → evaluation (variables, @fn, php bridge) → style expansion → themes → nesting → @screen → @order → css ``` The four rules the whole language rests on: ```text @xxx a language instruction $xxx a compile-time variable xxx::yyy a name another file declares -- a file is a namespace @theme the only construct that produces runtime css variables ; ends every statement that is not a block, as in css ``` --- > **Editor support:** [`ide/`](../ide/README.md) has a TextMate bundle for `.scs` — grammar, its own colors > (so the highlighting does not depend on the scheme) and a file icon. Install it with > `ide/install-phpstorm.sh --with-icon`, then restart the IDE. > > **See it running:** the demo application has a *Simple CSS* section (`demo/www/contents/pages/scs/`) where > every construct below is compiled live by this compiler, with a playground to try your own source: > `Framework → Simple CSS`. ## Statements end with `;` Every statement that is not a block ends with a semicolon, exactly as in css and in sass: a declaration, a `@let`, a `@use`, an `@import`, a `return`. A block is ended by its `}` and takes nothing after it. ```css @let $spacing = 16px; /* a statement */ .button { /* a block: no `;` after its `}` */ padding: $spacing; @use lib::control; } ``` The newline is whitespace, so a statement may be wrapped and several may share a line. A missing `;` is reported on the statement that is missing it: ```text Error: [;] is expected at the end of the statement. at src/styles/parts/buttons.scs:14 ``` ## Quick start ```php use MMO\client\SCSCompiler; $css = SCSCompiler::Shared()->compileText(<<<'SCS' @theme {colors {primary: #6366f1;}} @let $spacing = 16px; @component button { padding: $spacing; background: $colors.primary; } SCS)->css(); ``` ```css :root { --colors-primary: #6366f1; } .button { padding: 16px; background: var(--colors-primary); } ``` Compile a file, or a file into a file: ```php $result = SCSCompiler::Shared()->compileFile('{basepath}/styles/app.scs'); SCSCompiler::Shared()->compile('{basepath}/www/app.css', '{basepath}/styles/app.scs'); ``` `compile()` never throws: a broken stylesheet is logged and written as a visible css error message, exactly like `DartSass::compile()`. --- ## Compile-time variables — `@let` ```css @let $spacing = 16px; @let $large = $spacing * 2; @let $font = "Inter"; @let $rounded = true; @let $transition = 150ms ease; @let $sizes = [4, 8, 16]; @let $labels = (sm: small, md: medium); ``` A `@let` is **replaced** during compilation. It never becomes `--spacing`, and never becomes `var(--spacing)`. Inside a block, `@let` shadows the outer variable instead of overwriting it: ```css @let $spacing = 16px; @style card { @let $spacing = 24px; padding: $spacing; /* 24px */ } .b {padding: $spacing;} /* 16px */ ``` ### Arithmetic and units `+ - * / %` work on numbers, and units must stay compatible: | expression | result | why | |---|---|---| | `8px * 3` | `24px` | one side is unitless | | `8px / 2` | `4px` | the divisor is unitless | | `8px / 2px` | `4` | identical units cancel out | | `10px + 2rem` | **error** | incompatible units | | `10px * 2rem` | **error** | the result would have no css unit | ### `${...}` — evaluate inside a css value In a declaration, `/ * + -` are css, not arithmetic (`grid-area: 1 / 2 / 3 / 4` must survive). Wrap an expression in `${...}` when you do want it computed: ```css @let $spacing = 8px; .card { padding: $spacing ${$spacing * 4 / 2}; /* 8px 16px */ grid-area: 1 / 2 / 3 / 4; /* untouched */ } ``` `${...}` also works in selectors and inside strings — `#{...}` is accepted as a synonym: ```css @for $i in 1..6 { .mt-${$i} {content: "row ${$i}";} } ``` --- ## Design tokens — `@theme` `@theme` is the one construct that stays **dynamic at runtime**: it emits css custom properties. ```css @theme { colors { primary: #6366f1; danger: #ef4444; } spacing {sm: 8px, md: 16px;} } ``` ```css :root { --colors-primary: #6366f1; --colors-danger: #ef4444; --spacing-sm: 8px; --spacing-md: 16px; } ``` Groups nest to any depth and are flattened with `-`. Tokens are separated by a newline or a comma — a comma only ends a token when another `key:` follows it, so `body: "Inter", sans-serif` stays one value. Token values are ordinary compile-time expressions (`lg: spacing(3)` works). A token is referenced by its `$path`, and always compiles to a `var()`: ```css .button {background: $colors.primary;} /* background: var(--colors-primary); */ ``` A `@let` and a theme group may share a name: `$spacing` reads the variable, `$spacing.md` reads the token. ### Scoped themes ```css @theme dark {colors {primary: #818cf8;}} ``` ```css [data-theme="dark"] {--colors-primary: #818cf8;} ``` Switching `data-theme` at runtime re-themes the page with no recompilation. The selector is configurable (`themeSelector`), and every generated property can be prefixed (`varPrefix`, e.g. `--mmo-colors-primary`). --- ## Reusable styles — `@style` / `@use` ```css @style center { display: flex; align-items: center; justify-content: center; } @style size($width, $height) { width: $width; height: $height; } @style spacing($padding = 16px) {padding: $padding;} .avatar { @use center; @use size(48px, 48px); @use spacing; } ``` A `@style` generates nothing by itself. It may `@use` other styles; a cycle is reported as a `circular dependency` error with the chain that closes it. The body of a definition resolves its own unqualified names **where it was declared**: a `lib::card` that uses `center` means `lib::center`, wherever it is expanded from. --- ## Functions — `@fn` ```css @fn spacing($n) { return $n * 4px; } @fn buttonColor($type) { @if $type == primary {return #6366f1;} @if $type == danger {return #ef4444;} return #64748b; } .card {padding: spacing(4); background: buttonColor(primary);} ``` A `@fn` body may contain `@let`, `@if`, `@for` and `return`. It produces a value, never css. ### Built-ins `lighten()` `darken()` `mix()` `rgb()` `rgba()` `px()` `rem()` `em()` `percent()` `percentage()` `round()` `floor()` `ceil()` `abs()` `unit()` `unitless()` `length()` `nth()` `get()` `keys()` `quote()` `unquote()` `to-upper-case()` `to-lower-case()` `if()` ### Css functions are preserved A css function is emitted as written, arguments included — which is what makes `calc()` work: ```css @let $gap = 20px; .a { width: clamp(200px, 50vw, 800px); height: calc(100% - $gap); /* calc(100% - 20px) -- the `-` stays css */ } ``` Anything that is neither an `@fn`, a built-in nor a known css function is reported as `undefined function [spacng()]`. Set `strictFunctions` to `false` (globally, per compilation, or with `@config strictFunctions = false`) to let unknown functions through instead. --- ## Components and variants ```css @component button { padding: 8px; &:hover {background: blue;} @variant primary {background: blue; color: white;} @variant outline {background: transparent;} } @component .btn {display: block;} ``` ```css .button {padding: 8px;} .button:hover {background: blue;} .button--primary {background: blue; color: white;} .button--outline {background: transparent;} .btn {display: block;} ``` A bare name goes through `componentSelector` (`.{name}` by default), an explicit selector is used as written. Variants go through `variantSelector` (`{selector}--{variant}`). Both are configurable, so a design system can emit `.mmo-button.is-primary` instead. --- ## Nesting ```css .card { padding: 16px; .title {font-size: 20px;} &:hover {transform: scale(1.02);} [data-state="on"] & {color: blue;} } ``` Nesting is flattened for maximum browser compatibility. `&` is replaced by the parent selector; without a `&`, the child is a descendant. A selector list nests as a list. --- ## Responsive — `@screen` ```css @config @screen.tablet = 768px; .card { padding: 16px; @screen tablet {padding: 24px;} /* @media (min-width: 768px) */ @screen 1200px {padding: 32px;} /* @media (min-width: 1200px) */ } @screen tablet { .panel {padding: 24px;} } ``` A `@screen` target is a configured breakpoint name, a bare length (read as `min-width`), a media type (`print`) or a full query (`(max-width: 600px)`). An unknown name is an error rather than a guess. Responsive rules are always emitted **after** the unwrapped ones, so a breakpoint never loses to the rule it overrides, whatever the source order. Default breakpoints mirror the `devices` of [`_default_config.scss`](../src/sass/_default_config.scss): `tablet: 481px`, `desktop: 981px`, `wide: 1707px`. --- ## Conditions and loops ```css @let $rounded = true; .card { @if $rounded { border-radius: 8px; } @else if $flat { border-radius: 0; } @else { border-radius: 4px; } } @for $i in 1..6 { .mt-${$i} {margin-top: spacing($i);} } @let $sizes = [4, 8, 16]; @for $index:$size in $sizes { .p-${$index} {padding: ${$size}px;} /* $index is 0 based */ } @for $key:$label in (sm: small, md: medium) { .is-${$key} {content: "${$label}";} } ``` Operators: `== != < > <= >=`, `and`/`&&`, `or`/`||`, `not`/`!`. Only `false` and `null` are falsy. --- ## Emission order — `@order` Every rule is at order `0` by default and emitted in source order. `@order` moves a block earlier or later: ```css @order -100 { .reset {margin: 0;} } ``` Inside a rule, `@order` reorders the **declarations of that rule** rather than emitting a second one: ```css @component button { padding: 8px; @order -1 {border: none;} } ``` ```css .button { border: none; padding: 8px; } ``` --- ## Namespaces and imports A stylesheet of any size is several files, and reading one of them you need to know where a `@use`, a function or a variable comes from. **A file is a namespace**, named after itself, and a reference to another file says so: ```css // lib.scs -- the namespace `lib` @let $gutter = 12px; @fn step($n) {return $n * 4px;} @style center {display: flex; align-items: center;} ``` ```css // parts/buttons.scs -- the namespace `buttons` @import "../lib.scs"; @style filled {font-weight: 600;} // declared here .button { @use lib::center; // ... and this one comes from lib.scs @use filled; // no prefix: it is this file's own padding: lib::step(2); gap: $lib::gutter; background: $color.brand; // a theme token: global, unchanged } ``` | written | means | |---|---| | `@use center` | a `@style` of **this** file | | `@use lib::center` | the `@style` `lib.scs` declares | | `step(2)` | an `@fn` of this file, else a built-in, else a css function | | `lib::step(2)` | the `@fn` `lib.scs` declares | | `$gutter` | a variable of this file, or of an enclosing block | | `$lib::gutter` | the top-level `@let` of `lib.scs` | | `$color.brand` | a theme token — **not** namespaced, it belongs to the design system | | `php::round(…)`, `context::density` | the reserved namespaces of the php bridge | `@style`, `@fn`, `@component` and top-level `@let` belong to their file. A `@let` inside a rule, a `@style` or a `@fn` stays an ordinary local variable. ### Naming a namespace ```css @namespace ui; // first instruction of the file: it answers to `ui`, whatever the file is called ``` Without it, `lib.scs` is `lib` and `parts/buttons.scs` is `buttons` (the file name, never the folder). Two files claiming the same name is an error — rename one with `@namespace`, or at the import with `as`. A source with no file name of its own — an inline stylesheet, an element's `