From e6523bd35bd10baf361a3f910aedce25fa3f835d Mon Sep 17 00:00:00 2001 From: Glint Dev Date: Fri, 24 Jul 2026 22:04:41 +0300 Subject: [PATCH] move example template files to examples/, clean up root --- .gitignore | 2 + ARCHITECTURE-IMPROVEMENT-PLAN.md | 377 ++------------- desktop.glbc | Bin 12290 -> 0 bytes display.glbc | Bin 9258 -> 0 bytes display.gtlm => examples/display/display.gtlm | 0 main.glts => examples/display/main.glts | 0 desktop.gltm => examples/glt/desktop.gltm | 0 examples/glt/main.glts | 432 ++++++++++++++++++ out.glbc | Bin 12290 -> 0 bytes 9 files changed, 468 insertions(+), 343 deletions(-) delete mode 100644 desktop.glbc delete mode 100644 display.glbc rename display.gtlm => examples/display/display.gtlm (100%) rename main.glts => examples/display/main.glts (100%) rename desktop.gltm => examples/glt/desktop.gltm (100%) create mode 100644 examples/glt/main.glts delete mode 100644 out.glbc 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 1d204f206ba752adccd683f042d8f597701044a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12290 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 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 1d204f206ba752adccd683f042d8f597701044a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12290 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