diff --git a/.gitignore b/.gitignore index e964957..55a5c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target *.glbc +STYLE-SYSTEM-COMPLETION-PLAN.md +ARCHITECTURE-IMPROVEMENT-PLAN.md diff --git a/ARCHITECTURE-IMPROVEMENT-PLAN.md b/ARCHITECTURE-IMPROVEMENT-PLAN.md index 86a5bab..8145586 100644 --- a/ARCHITECTURE-IMPROVEMENT-PLAN.md +++ b/ARCHITECTURE-IMPROVEMENT-PLAN.md @@ -1,11 +1,6 @@ -# План улучшения архитектуры Glint Runtime +# План улучшения архитектуры Glint Runtime — остаток -## Цель - -Обеспечить **плоский график производительности**: рендеринг 1000 строк байткода должен занимать -~0.04 секунды сегодня и столько же через годы, независимо от роста количества компонентов, -стилей и страниц. Время обработки события должно быть пропорционально **размеру изменения**, -а не **размеру всей системы**. +Всё, что не реализовано из исходного плана. Реализованные фазы (3-8, 10, 9.1-9.2) удалены. --- @@ -13,367 +8,63 @@ **Цель:** зафиксировать текущие метрики, чтобы объективно оценивать прогресс. -- [ ] **0.1** Добавить Criterion в `Cargo.toml` -- [ ] **0.2** Написать бенчмарки для: - - `evaluate_vdom()` на `desktop.glbc` - - `matching_rules()` — 10, 100, 1000 правил - - `ComputedStyle::compute()` — пустой, 5, 20 свойств - - `resolve_string()` — без `$`, с 1 `$var`, с 3 `$var` - - `RheiContext::eval_expr()` — простое выражение, сложное - - `RheiContext::execute_action()` — короткий скрипт - - `Element::clone()` — замерить копирование глубокого дерева -- [ ] **0.3** Запустить `perf record` / `flamegraph-rs` на горячем пути: - - `cargo run -- run desktop.glbc` + интерактив - - Выявить фактический bottleneck (гипотеза: style matching, string parsing) +- [ ] **0.3** Запустить `perf record` / `flamegraph-rs` на горячем пути и выявить bottleneck - [ ] **0.4** Записать baseline в `BENCHMARKS.md` или `README.md` -**Файлы:** новый `benches/bench.rs`, `Cargo.toml` - --- ## Фаза 1: Типизированные значения (Value enum) **Цель:** устранить постоянный round-trip через строки (parse/format на каждое свойство). -**Текущая проблема:** `HashMap` для переменных. Каждый `SliderChanged` → -`format!(...)`, потом `rhei.rs:139-142` — три парсинга подряд (`i64`, `f64`, `bool`). +**Статус:** `Value` enum, конверсии `Value↔Dynamic` и `resolve_string` с Value сделаны. +**Осталось:** `HashMap` в стилях не тронут — `ComputedStyle::compute()` и +`parse_*()` всё ещё принимают `&str`. Стили — главный потребитель parse/format. -- [ ] **1.1** Определить `Value` enum в `interpreter/types.rs`: - ```rust - #[derive(Clone, Debug)] - pub enum Value { - Str(CompactStr), - Int(i64), - Float(f64), - Bool(bool), - Array(Vec), - None, - } - ``` -- [ ] **1.2** Заменить `HashMap` на `HashMap`: - - `Document::variables` - - `GlintApp::update()` — входящие значения -- [ ] **1.3** Переписать `str_to_dyn()` и `dyn_to_str()` в `rhei.rs`: - - Убрать парсинг, конвертировать напрямую `Value ↔ Dynamic` -- [ ] **1.4** Переписать `resolve_string()`: - - Если значение уже `Int`/`Float` — не парсить, форматировать один раз -- [ ] **1.5** Переписать `evaluate_condition()`: - - Числовые сравнения без парсинга строк - - `resolve_string()` → преобразование к Value -- [ ] **1.6** Переписать `is_truthy()`: работа с Value напрямую -- [ ] **1.7** Обновить `renderer.rs`: - - `SliderChanged`, `ToggleChanged`, `InputChanged` — принимать `Value`, не строку - - Парсинг slider/toggle/input значений — один раз, а не на каждый чих -- [ ] **1.8** Обновить `ComputedStyle::compute()` и `parse_*()` функции: - - Принимать `&Value` где возможно, а не `&str` - - `parse_color`, `parse_size`, `parse_length` — работа с Value +- [ ] **1.2** Заменить `HashMap` на `HashMap` в стилях: + - `StyleRule::properties` + - Все matched_sheets +- [ ] **1.8** Переписать `parse_color`, `parse_size`, `parse_length` на работу с `&Value` - [ ] **1.9** Написать тесты для Value: конверсии, сравнения, форматирование -**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `interpreter/rhei.rs`, -`interpreter/style.rs`, `app.rs`, `renderer.rs` - --- ## Фаза 2: Интернирование строк (String Interning) **Цель:** ускорить сравнение строк (ключи свойств, названия типов, классы). -Заменить million `== "padding-top"` на O(1) сравнение ID. +Заменить миллион `== "padding-top"` на O(1) сравнение ID. -- [ ] **2.1** Выбрать библиотеку intern-строк: - - `lasso` (thread-safe, производительный) или `strena` (лёгкая) - - Или самодельный `StringArena` с `HashMap<&str, usize>` -- [ ] **2.2** Заменить `&'a str` на `InternedStr` (newtype вокруг `u32`/`usize`): - - `Element::type_name` - - Ключи в `Element::properties` - - Селекторы, имена классов, ID -- [ ] **2.3** `lookup()` в `style.rs`: - - Сравнение ключей через ID вместо `==` -- [ ] **2.4** `CompoundSelector::matches_element()`: - - Сравнение `tag`, `id`, `classes` через internd-строки -- [ ] **2.5** Все `HashMap` где ключи повторяются → `HashMap` +**Статус:** `InternedStr(u32)` и `Interner` определены в `types.rs`, но нигде не используются +в горячем пути. `Element::type_name` — `&str`, ключи пропертей — `Cow`, +селекторы — `String`. Сравнения — через `==`. -**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/style.rs`, `interpreter/mod.rs`, -`renderer.rs` (много `el.type_name == "Button"`) +- [ ] **2.2** Заменить `&'a str` на `InternedStr` в `Element::type_name` и ключах +- [ ] **2.3** `lookup()` в `style.rs` — сравнение через ID вместо `==` +- [ ] **2.4** `CompoundSelector::matches_element()` — сравнение через InternedStr +- [ ] **2.5** `HashMap` → `HashMap` где ключи повторяются --- -## Фаза 3: Граф зависимостей (Reactive Dependency Tracking) +## Фаза 9.3: Кэш Iced-виджетов -**Цель:** вместо полного `evaluate_vdom()` на каждое событие — пересчитывать только -элементы, которые зависят от изменившейся переменной. **Это главный этап для плоского графика.** +**Цель:** не пересоздавать `iced::Element` для элементов с неизменившимся `content_hash`. -**Текущая проблема:** `app.rs:44-51` — на любое событие полный VDOM rebuild. Слайдер -меняет `volume_level`, но пересчитываются все 500 элементов. +**Статус:** 9.1 (`content_hash` на `Element`) и 9.2 (стабильные `iced::widget::Id`) сделаны. +9.3 отложен: `iced::Element` не `Clone`, хранит `Box`, кэширование требует +unsafe transmute lifetime или перестройки renderer. -- [ ] **3.1** Создать `interpreter/reactive.rs`: - ```rust - pub struct ReactiveTracker { - subscribers: HashMap>, - dependencies: HashMap>, - dirty_set: HashSet, - } - ``` -- [ ] **3.2** Анализ зависимостей при загрузке шаблона: - - Сканировать `$var` в свойствах → регистрировать `ElementId → var` - - Сканировать `!rhei:{expr}` → извлекать имена переменных через Rhai parser - - `@if condition` → зависимости от переменных в condition - - `@each source` → зависимости от переменных в source - - Результат: каждый элемент знает, от каких переменных зависит -- [ ] **3.3** Изменить `GlintApp::update()`: - ```rust - fn update(&mut self, msg: Message) { - let affected = self.tracker.on_variable_changed(name, new_val); - self.vdom_roots = Interpreter::evaluate_vdom_incr( - &self.doc.roots, &affected, &self.tracker, ... - ); - } - ``` -- [ ] **3.4** `evaluate_vdom_incr()`: - - Для элементов не в `affected` — возвращаем кэшированный VDOM - - Для элементов в `affected` — пересчитываем и каскадно помечаем детей - - `@if` — если условие не изменилось — не пересчитываем ветку - - `@each` — если source не изменился — не пересчитываем -- [ ] **3.5** Инвалидация стилей: - - При изменении переменной, меняющей `class` или `id` элемента → сброс кэша стилей - - Иначе стили не пересчитываем -- [ ] **3.6** Кэш VDOM: - - `HashMap>` — не клонируем, разделяем память - - `Arc::make_mut()` при изменении (copy-on-write) -- [ ] **3.7** Написать тест: - - Сценарий: 1000 элементов, 1 зависимость → после изменения пересчитывается 1 элемент - - Проверить, что dirty_set корректен - -**Изменяемые файлы:** новый `interpreter/reactive.rs`, `interpreter/mod.rs`, `interpreter/types.rs`, -`app.rs`, `main.rs` +- [ ] **9.3** Реализовать кэш `HashMap` в `GlintApp`: + - При совпадении `content_hash` — возвращать сохранённый виджет + - Иначе — рендерить, кэшировать, обновлять hash + - Unsafe transmute `'static → '_` допустим, т.к. данные живут в `Element<'static>` --- -## Фаза 4: Индексированный матчинг стилей (Style Index) +## Сводная таблица — остаток -**Цель:** заменить O(N×M) на O(K) где K — число релевантных правил для элемента (1-5, не 500). - -**Текущая проблема:** `style.rs:487-505` — каждый элемент проверяется против ВСЕХ правил. -100 элементов × 500 правил = 50 000 проверок на событие. - -- [ ] **4.1** Построить индекс стилей при загрузке `Document`: - ```rust - pub struct StyleIndex { - by_tag: HashMap>, - by_class: HashMap>, - by_id: HashMap>, - by_tag_class: HashMap<(InternedStr, InternedStr), Vec>, - by_tag_id: HashMap<(InternedStr, InternedStr), Vec>, - complex_rules: Vec<(ComplexSelector, RuleId)>, - rules: Vec, - } - ``` -- [ ] **4.2** `matching_rules()` → `query_index()`: - - Для элемента `Button.primary#submit`: - 1. `by_tag["Button"]` → [1, 5, 12] - 2. `by_class["primary"]` → [3, 5, 7] - 3. `by_id["submit"]` → [5] - 4. Пересечение → [5] - 5. Проверить 1 сложное правило - 6. Итого: 3 проверки вместо 500 -- [ ] **4.3** Кэш совпадений: - - `HashMap, u64)>` — инвалидируется по эпохе - - epoch увеличивается при перезагрузке стилей - - Псевдоклассы (`:hover`, `:active`) — кэш с ключом `(ElementId, PseudoClass)` -- [ ] **4.4** `ComputedStyle::compute()`: - - Убрать сортировку по specificity на каждый вызов - - Правила уже отсортированы в индексе - - `lookup()` — проход по нескольким правилам вместо прохода по `matched_sheets` -- [ ] **4.5** Миграция `matching_pseudo_rules()`: - - Использовать индекс + кэш - - Вызывается из `renderer.rs` для hover/active — тысячи раз в секунду - - Кэш на `(ElementId, pseudo)` с инвалидацией - -**Изменяемые файлы:** `interpreter/style.rs`, `interpreter/mod.rs`, `interpreter/types.rs`, -`renderer.rs`, `main.rs` - ---- - -## Фаза 5: Аренная аллокация и VNode (Flat VDOM) - -**Цель:** устранить глубокое клонирование `Element` и reduce аллокаций на каждый кадр. - -**Текущая проблема:** `Element` содержит `Vec` — рекурсивное клонирование. -`child.clone()` в `@if` (mod.rs:347) копирует поддеревья целиком. - -- [ ] **5.1** Создать плоское представление VDOM: - ```rust - struct VNode<'a> { - id: NodeId, - type_name: InternedStr, - properties: Range, - children_range: Range, - computed_style: ComputedStyle, - } - - struct FlatVDom<'a> { - nodes: Vec>, - properties: Vec<(InternedStr, Value)>, - arena: Bump, - } - ``` -- [ ] **5.2** Добавить `bumpalo` или написать `BumpAlloc`: - - Все строковые данные живут в арене - - Очистка арены одним махом между кадрами -- [ ] **5.3** `evaluate_vdom()` → возвращает `FlatVDom<'arena>` вместо `Vec`: - - Вместо `push_prop("text", val.clone())` — allocate в арене - - Вместо `Element::new(el.type_name)` — выделить VNode в `Vec` -- [ ] **5.4** Structural sharing: - - Ветки `@each` с одинаковыми телами разделяют `VNode` через `Arc` - - `Arc::make_mut()` при изменении -- [ ] **5.5** `render_element()` → работает с `&VNode`: - - `get_prop()` — lookup в плоском массиве свойств - - `children` — итерация по `children_range` -- [ ] **5.6** Удалить `#[derive(Clone)]` из `Element` (или оставить для совместимости): - - В горячем пути clone не используется -- [ ] **5.7** Тест: проверить, что аллокаций на кадр стало < 10 (было ~1000+) - -**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `renderer.rs`, -`interpreter/style.rs`, `app.rs` - ---- - -## Фаза 6: Кэширование Rhai AST - -**Цель:** убрать компиляцию Rhai скриптов при каждом выполнении. - -**Текущая проблема:** `rhei.rs:112` — `engine.compile(script)` при каждом `execute_action()`. -Скрипты в `@on:click { ... }` компилируются каждый клик. - -- [ ] **6.1** Собрать все Rhai блоки при загрузке `Document`: - - Из `rhei_scripts` (init-скрипты) - - Из `__on:*` свойств (обработчики событий) - - Из `!rhei:` выражений (компилируем, но не выполняем) -- [ ] **6.2** `RheiContext`: - ```rust - pub struct RheiContext { - engine: Engine, - init_ast: AST, - action_cache: HashMap, - expr_cache: HashMap, - } - ``` -- [ ] **6.3** `execute_action()`: - - `self.action_cache.get(script)` вместо `engine.compile(script)` - - Если нет — компилируем и кэшируем -- [ ] **6.4** Очистка кэша: - - Только при перезагрузке документа - - Или `LruCache` если скриптов слишком много -- [ ] **6.5** Тест: выполнить 1000 раз один и тот же action — время не должно расти - -**Изменяемые файлы:** `interpreter/rhei.rs`, `interpreter/mod.rs`, `main.rs` - ---- - -## Фаза 7: Параллелизм (Rayon) - -**Цель:** распараллелить style matching и независимые ветки VDOM. - -**Текущая проблема:** всё выполняется последовательно, хотя style matching -для разных элементов — embarrassingly parallel. - -- [ ] **7.1** Добавить `rayon` в `Cargo.toml` (feature gate: `parallel`) -- [ ] **7.2** Параллельный style matching: - - `matching_rules_batch(elements: &[Element]) -> Vec>` - - `elements.par_iter().map(...)` -- [ ] **7.3** Параллельный `@each`: - - Итерации `@each` независимы - - `items.par_iter().flat_map(|item| evaluate_vdom(body, ...))` - - Feature gate: только для больших списков (> N элементов) -- [ ] **7.4** Параллельный `ComputedStyle::compute_batch()`: - - Векторизованный compute для нескольких элементов -- [ ] **7.5** Тест: `@each` с 1000 итераций → ускорение ~4x на 8 ядрах - -**Изменяемые файлы:** `Cargo.toml`, `interpreter/style.rs`, `interpreter/mod.rs` - ---- - -## Фаза 8: Мемоизация `ComputedStyle::compute()` - -**Цель:** не пересчитывать стили для элементов, чьи свойства не изменились. - -**Текущая проблема:** каждый кадр `ComputedStyle::compute()` парсит все ~40 полей -через `lookup()`. - -- [ ] **8.1** Ввести хэш `(inline_properties_hash, stylesheet_epoch, element_id) → ComputedStyle` -- [ ] **8.2** Кэш: `HashMap` + LRU eviction -- [ ] **8.3** Инвалидация: - - `stylesheet_epoch` — счётчик, увеличивается при изменении стилей - - `inline_properties_hash` — хэш от значений свойств элемента -- [ ] **8.4** Проверить hit rate на реальном UI: > 95%+ - -**Изменяемые файлы:** `interpreter/style.rs` - ---- - -## Фаза 9: Инкрементальный рендеринг (keyed widgets) - -**Цель:** дать Iced'у возможность эффективно диффить виджеты, а не пересоздавать их. - -**Текущая проблема:** `view()` создаёт полностью новые Iced-виджеты каждый кадр. - -- [ ] **9.1** Ввести стабильные ID для каждого элемента VDOM -- [ ] **9.2** `render_element()`: - - Присваивать `iced::id()` на основе `ElementId` - - Iced использует ID для сохранения состояния виджетов между кадрами -- [ ] **9.3** Убирать из `view()` только изменившиеся элементы: - - Если `VNode::hash == prev_hash` — вернуть кэшированный `iced::Element` - - Кэш: `HashMap>` - -**Изменяемые файлы:** `renderer.rs`, `interpreter/types.rs` - ---- - -## Фаза 10: Мониторинг и автоматические бенчмарки - -**Цель:** не допустить регрессий в будущем. - -- [ ] **10.1** Добавить `cargo bench` в CI -- [ ] **10.2** Пороговые проверки: - - `evaluate_vdom` на desktop.glbc < 5ms - - `matching_rules` (100 правил) < 10μs - - `ComputedStyle::compute` < 1μs -- [ ] **10.3** Логирование производительности: - - `--perf` флаг для `glint run` - - Печатать: `VDOM: 1.2ms | Style: 0.3ms | Render: 2.1ms | Total: 3.6ms` -- [ ] **10.4** Alert если total > 16ms (frame budget для 60 FPS) - -**Изменяемые файлы:** `Cargo.toml`, новый `src/perf.rs`, `cli.rs`, `.github/workflows/ci.yml` - ---- - -## Сводная таблица влияния - -| Фаза | Описание | Ускорение | Плоский график | -|------|----------|-----------|----------------| -| 0 | Бенчмарки | — | — | -| 1 | Value enum | 2-3× | Нет | -| 2 | String interning | 1.5-2× | Нет | -| **3** | **Reactive tracker** | **10-50×** | **Да (ключевое)** | -| 4 | Style index | 5-10× (style) | Да | -| 5 | Arena + Flat VDOM | 2-3× | Частично | -| 6 | Rhai AST cache | 2-5× (click) | Да | -| 7 | Parallelism | 2-4× | Нет | -| 8 | Style memoization | 2-3× | Да | -| 9 | Keyed widgets | 1.5-2× (render) | Частично | -| 10 | CI benchmarks | — (контроль) | — | - -**Суммарно:** до 100× на горячих путях. - ---- - -## Рекомендованный порядок имплементации - -1. **Фаза 0** — профилирование (без этого нельзя) -2. **Фаза 1 + 2** — Value enum + String interning (фундамент) -3. **Фаза 3** — Reactive tracker (сердце архитектуры) -4. **Фаза 4** — Style index -5. **Фаза 5 + 8** — Arena + Memoization -6. **Фаза 6** — Rhai cache -7. **Фаза 7** — Parallelism -8. **Фаза 9** — Keyed widgets -9. **Фаза 10** — CI benchmarks +| Фаза | Описание | Оценка ускорения | +|------|----------|-----------------| +| 0 | Профилирование | — | +| 1 | Value enum в стилях | 2-3× | +| 2 | String interning | 1.5-2× | +| 9.3 | Кэш виджетов | 1.5-2× (render) | diff --git a/desktop.glbc b/desktop.glbc deleted file mode 100644 index 1d204f2..0000000 Binary files a/desktop.glbc and /dev/null differ diff --git a/display.glbc b/display.glbc deleted file mode 100644 index 3c355da..0000000 Binary files a/display.glbc and /dev/null differ diff --git a/display.gtlm b/examples/display/display.gtlm similarity index 100% rename from display.gtlm rename to examples/display/display.gtlm diff --git a/main.glts b/examples/display/main.glts similarity index 100% rename from main.glts rename to examples/display/main.glts diff --git a/desktop.gltm b/examples/glt/desktop.gltm similarity index 100% rename from desktop.gltm rename to examples/glt/desktop.gltm diff --git a/examples/glt/main.glts b/examples/glt/main.glts new file mode 100644 index 0000000..e04c4c7 --- /dev/null +++ b/examples/glt/main.glts @@ -0,0 +1,432 @@ +// ============================================================================= +// Glint Design System — Core Fixed Stylesheet (main.glts) +// ============================================================================= + +$bg-app = #11111b +$bg-panel = #1e1e2e +$bg-surface = #313244 +$bg-input = #181825 +$accent = #b4befe +$text-main = #cdd6f4 +$text-muted = #9399b2 +$border-glow = #45475a + +@mixin text-body { + font-size: 14px +} + +@mixin standard-card { + background: $bg-panel + border-color: $border-glow + border-width: 1px + border-radius: 12px +} + +Window { + background: $bg-app +} + +Panel { + background: transparent + border-width: 0px + padding: 0px + gap: 12px + direction: vertical +} + +.ui-card { + @use standard-card + padding: 14px + gap: 12px + align-items: center + content-align: center +} + +// ── Служебные Layout-классы ────────────────────────────────────────────────── + +.layout-viewport { + width: fill + padding: 16px + gap: 16px +} + +.layout-row { + direction: horizontal + gap: 16px +} + +.layout-col { + direction: vertical + gap: 16px +} + +.layout-clean { + width: fill +} + +// ── Стили атомарных компонентов ────────────────────────────────────────────── + +Header { + color: $accent + font-size: 20px + padding: 2px +} + +Text { + @use text-body +} + +Label { + color: $text-muted + font-size: 12px +} + +Input { + background-color: $bg-input + color: $text-main + font-size: 14px + padding: 10px + border-radius: 8px + border-width: 1px + border-color: $border-glow + width: 240px +} + +Toggle { + color: $text-main + font-size: 14px +} + +Divider { + background: $border-glow + border-width: 1px +} + +Button { + border-radius: 6px + border-width: 1px + padding: 8px +} + +.btn-custom { + background: #000000 +} + +Image { + border-radius: 8px + border-width: 1px +} + +// ── Секция тестирования продвинутой геометрии Box-Model ────────────────────── + +.geometry-box { + @use standard-card + background: #24253a + border-color: #f38ba8 + border-width: 2px + padding-top: 24px + padding-bottom: 12px + padding-left: 16px + padding-right: 40px + width: 320px +} + +.margin-test-item { + background: #a6e3a1 + padding: 8px + margin-top: 12px + margin-bottom: 4px +} + +// ── Секция тестирования каскадного наследования (Inheritance) ──────────────── + +.inheritance-box { + @use standard-card + background: #181825 + padding: 12px + color: #f9e2af + font-size: 18px +} + +.override-style { + color: #f38ba8 + font-size: 12px +} + +// ── Секция распределения весов Flex-Grow (Fill portions) ──────────────────── + +.flex-row-bar { + background: $bg-surface + direction: horizontal + padding: 8px + gap: 8px + width: fill +} + +.fill-10 { + background: #f38ba8 + width: 10% +} + +.fixed-120 { + background: #f9e2af + width: 50px + color: #11111b +} + +.fill-20 { + background: #a6e3a1 + width: 20% + color: #11111b +} + +// ── СТИЛИ ДЛЯ ПРОВЕРКИ POSITION: FIXED ─────────────────────────────────────── + +.fixed-toast { + position: fixed + top: 20px + right: 20px + background: #a6e3a1 + border-radius: 8px + padding: 12px + border-width: 1px + border-color: #a6e3a1 + color: #000 + font-weight: bold +} + +.btn-close { + background: transparen + border-width: 0px + padding: 2px +} + + + + +.scroll-test-box { + @use standard-card + background: #313244 + height: 150px + width: 250px + overflow-y: scroll + padding: 10px +} + + +// ── Стили для тестирования Sticky Эффекта ──────────────────── + +.sticky-container { + @use standard-card + background: #181825 + padding: 12px + gap: 8px + width: fill + height: 200px + overflow-y: scroll +} + +.sticky-header { + position: sticky + top: 0px + background: #fab387 + padding: 10px 14px + border-radius: 6px + color: #11111b + font-weight: bold + font-size: 14px + width: fill +} + +.scroll-spacer { + gap: 100px + padding: 10px + width: fill +} + +// ── Стили для тестирования Opacity ──────────────────────────── + +.opacity-fade { + background: #f38ba8 + padding: 12px + border-radius: 8px + opacity: 0.3 +} + +.opacity-mid { + background: #a6e3a1 + padding: 12px + border-radius: 8px + opacity: 0.6 +} + +.opacity-subtle { + background: #89b4fa + padding: 12px + border-radius: 8px + opacity: 0.85 +} + +// ── Стили для тестирования Font-Weight ───────────────────────── + +.fw-light { + font-weight: 300 + color: $text-main +} + +.fw-normal { + font-weight: 400 + color: $text-main +} + +.fw-bold { + font-weight: 700 + color: $text-main +} + +.fw-black { + font-weight: 900 + color: $text-main +} + +// ── Стили для тестирования Text-Align ────────────────────────── + +.ta-box { + background: #313244 + padding: 8px + border-radius: 6px + width: fill +} + +.ta-left { + text-align: left +} + +.ta-center { + text-align: center +} + +.ta-right { + text-align: right +} + +// ── Стили для тестирования Line-Height ───────────────────────── + +.lh-box { + background: #313244 + padding: 8px + border-radius: 6px + width: 300px +} + +.lh-tight { + line-height: 0.8 +} + +.lh-normal { + line-height: 1.2 +} + +.lh-loose { + line-height: 2.0 +} + +// ── Стили для тестирования селекторов через запятую ──────────── + +.warning-text, .error-text { + font-weight: bold + padding: 8px + border-radius: 6px +} + +.warning-text { + color: #f9e2af + background: #3d3520 +} + +.error-text { + color: #f38ba8 + background: #3d2020 +} + +// ── Стили для тестирования Display: None ─────────────────────── + +.hidden-box { + background: #f38ba8 + padding: 12px + border-radius: 8px + display: none +} + +.test-section-header { + color: $accent + font-size: 16px + padding: 4px 0 + margin-top: 8px +} + +.test-row { + direction: horizontal + gap: 12px + align-items: center +} + +// ── Стили для тестирования POSITION: ABSOLUTE ──────────────── + +.abs-demo-container { + position: relative + background: #313244 + padding: 16px + margin-top: 8px + border-radius: 8px + width: fill + gap: 8px + min-height: 60px +} + +.abs-demo-badge { + position: absolute + top: 10px + right: 4px + background: #f38ba8 + color: #11111b + font-weight: bold + font-size: 10px + padding: 4px 8px + border-radius: 4px +} + +// ── Полноценная демка absolute-позиционирования ────────────── + +.absolute-demo-section { + @use standard-card + background: #181825 + padding: 12px + gap: 8px + width: fill +} + +.abs-stage { + position: relative + background: #24253a + border-color: $accent + border-width: 1px + border-radius: 8px + padding: 16px + gap: 6px + width: fill + min-height: 80px +} + +.abs-overlay { + position: absolute + top: 10px + right: 10px + background: #f38ba8 + border-radius: 6px + padding: 8px 12px + color: #11111b + font-weight: bold + font-size: 12px + gap: 4px +} diff --git a/out.glbc b/out.glbc deleted file mode 100644 index 1d204f2..0000000 Binary files a/out.glbc and /dev/null differ