@soxa-ui/theme-base 0.0.0-stage → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +399 -2
  3. package/dist/setup/index.d.ts +31 -0
  4. package/dist/setup/index.js +4 -0
  5. package/dist/setup/plugins/index.d.ts +3 -0
  6. package/dist/setup/plugins/table-head.plugin.d.ts +44 -0
  7. package/dist/setup/plugins/table-head.plugin.js +86 -0
  8. package/dist/setup/plugins/tabs-view.plugin.d.ts +32 -0
  9. package/dist/setup/plugins/tabs-view.plugin.js +100 -0
  10. package/dist/setup/plugins/toasts-front.plugin.d.ts +63 -0
  11. package/dist/setup/plugins/toasts-front.plugin.js +131 -0
  12. package/dist/setup/plugins/transitions.d.ts +43 -0
  13. package/dist/setup/plugins/transitions.js +75 -0
  14. package/package.json +55 -4
  15. package/src/anchored.css +75 -0
  16. package/src/caption.css +71 -0
  17. package/src/components/accordion.css +96 -0
  18. package/src/components/button.css +139 -0
  19. package/src/components/calendar.css +594 -0
  20. package/src/components/check-box.css +121 -0
  21. package/src/components/date-input.css +224 -0
  22. package/src/components/date-picker.css +208 -0
  23. package/src/components/dialog.css +95 -0
  24. package/src/components/drag-and-drop.css +18 -0
  25. package/src/components/drawer.css +171 -0
  26. package/src/components/field.css +440 -0
  27. package/src/components/icon.css +61 -0
  28. package/src/components/input.css +189 -0
  29. package/src/components/label.css +160 -0
  30. package/src/components/list-box.css +165 -0
  31. package/src/components/menu.css +224 -0
  32. package/src/components/number-input.css +183 -0
  33. package/src/components/pagination.css +164 -0
  34. package/src/components/popover.css +164 -0
  35. package/src/components/progress-linear.css +88 -0
  36. package/src/components/progress-spinner.css +127 -0
  37. package/src/components/radio-group.css +130 -0
  38. package/src/components/rating.css +141 -0
  39. package/src/components/scroller.css +136 -0
  40. package/src/components/select.css +233 -0
  41. package/src/components/skeleton.css +33 -0
  42. package/src/components/slider.css +482 -0
  43. package/src/components/switch.css +142 -0
  44. package/src/components/table.css +1513 -0
  45. package/src/components/tabs.css +166 -0
  46. package/src/components/tags.css +371 -0
  47. package/src/components/textarea.css +179 -0
  48. package/src/components/toasts.css +447 -0
  49. package/src/components/tooltip.css +59 -0
  50. package/src/direction.css +30 -0
  51. package/src/disabled.css +26 -0
  52. package/src/group.css +90 -0
  53. package/src/index.css +97 -0
  54. package/src/modal.css +183 -0
  55. package/src/place.css +30 -0
  56. package/src/required.css +40 -0
  57. package/src/reset.css +172 -0
  58. package/src/swipe.css +71 -0
  59. package/src/target.css +46 -0
  60. package/theme-contract.json +3373 -0
  61. package/theme-variables.json +336 -0
@@ -0,0 +1,100 @@
1
+ import { afterPseudoTransitions } from "./transitions.js";
2
+ import { TBasePlugin, TCollectionBundlesPlugin, TElementPlugin, TTabsActiveTabPlugin } from "@soxa-ui/plugins";
3
+ //#region setup/plugins/tabs-view.plugin.ts
4
+ /** Признак переезда на ряду: пока он стоит, тема ведёт геометрию переходом. */
5
+ var MOVING = "--so-tabs-active-moving";
6
+ var TTabsViewPlugin = class extends TBasePlugin {
7
+ _tabs = null;
8
+ _engine = null;
9
+ /**
10
+ * Таб, чьё место плагин записал последним; `null` — ещё не записывал или
11
+ * записал нули: активного таба не было.
12
+ */
13
+ _drawn = null;
14
+ /** Ряд, на котором стоит признак переезда; `null` — признака нет. */
15
+ _moving = null;
16
+ /** Отмена ожидания конца переезда; `null` — плагин не ждёт. */
17
+ _stopWaiting = null;
18
+ install(ctx) {
19
+ super.install(ctx);
20
+ this._tabs = ctx.getInstance();
21
+ const element = ctx.get(TElementPlugin);
22
+ element?.events.on("ready", () => {
23
+ this._tabs?.dataset.add("ready-animation", true);
24
+ });
25
+ element?.events.on("removed", () => this._forget());
26
+ ctx.get(TCollectionBundlesPlugin)?.events.on("engine:bound", (engine) => {
27
+ this._engine = engine;
28
+ });
29
+ ctx.get(TTabsActiveTabPlugin)?.events.on("change:active-tab", ({ newValue: offset }) => {
30
+ this._updateActiveTab(offset);
31
+ this._updateGap(offset);
32
+ });
33
+ }
34
+ destroy() {
35
+ this._forget();
36
+ this._tabs = null;
37
+ this._engine = null;
38
+ super.destroy();
39
+ }
40
+ /** Место и длина активного таба по главной оси. */
41
+ _updateActiveTab(offset) {
42
+ const tabs = this._tabs;
43
+ const engine = this._engine;
44
+ if (!offset || !engine || !tabs) return;
45
+ const active = engine.extensions.activation.activeItem ?? null;
46
+ if (!active && engine.extensions.tabs.hasEnabledTabs()) return;
47
+ const { barEl, offsetLeft, offsetWidth, offsetTop, offsetHeight } = offset;
48
+ if (this._drawn && active && active !== this._drawn) this._startMoving(barEl);
49
+ this._drawn = active;
50
+ if (tabs.orientation === "vertical") {
51
+ barEl.style.setProperty("--so-tabs-active-pos", `${offsetTop}px`);
52
+ barEl.style.setProperty("--so-tabs-active-size", `${offsetHeight}px`);
53
+ } else {
54
+ barEl.style.setProperty("--so-tabs-active-pos", `${offsetLeft}px`);
55
+ barEl.style.setProperty("--so-tabs-active-size", `${offsetWidth}px`);
56
+ }
57
+ if (this._moving === barEl) {
58
+ this._stopWaiting?.();
59
+ this._stopWaiting = afterPseudoTransitions(barEl, () => this._stopMoving());
60
+ }
61
+ }
62
+ /** Разрыв линии ряда под активным табом. */
63
+ _updateGap(offset) {
64
+ const tabs = this._tabs;
65
+ if (!offset || !this._engine || !tabs) return;
66
+ const { barEl, offsetLeft, offsetWidth, offsetTop, offsetHeight } = offset;
67
+ if (tabs.orientation === "vertical") {
68
+ barEl.style.setProperty("--so-tabs-gap-pos", `${offsetTop + 1}px`);
69
+ barEl.style.setProperty("--so-tabs-gap-size", `${offsetHeight - 1}px`);
70
+ } else {
71
+ barEl.style.setProperty("--so-tabs-gap-pos", `${offsetLeft + 1}px`);
72
+ barEl.style.setProperty("--so-tabs-gap-size", `${offsetWidth - 1}px`);
73
+ }
74
+ }
75
+ /** Признак переезда на ряд — до записи нового места. */
76
+ _startMoving(bar) {
77
+ if (this._moving !== bar) this._stopMoving();
78
+ bar.style.setProperty(MOVING, "1");
79
+ this._moving = bar;
80
+ }
81
+ /** Снять признак переезда и ожидание его конца. */
82
+ _stopMoving() {
83
+ this._stopWaiting?.();
84
+ this._stopWaiting = null;
85
+ this._moving?.style.removeProperty(MOVING);
86
+ this._moving = null;
87
+ }
88
+ /**
89
+ * Забыть всё, что плагин нарисовал: узел снят или плагин уничтожен. Новый
90
+ * узел до первого замера получит прежнюю разметку без переменных — тема
91
+ * рисует её статично, — а его первая запись идёт без признака.
92
+ */
93
+ _forget() {
94
+ this._stopMoving();
95
+ this._drawn = null;
96
+ this._tabs?.dataset.remove("ready-animation");
97
+ }
98
+ };
99
+ //#endregion
100
+ export { TTabsViewPlugin };
@@ -0,0 +1,63 @@
1
+ import type { IToasts } from '@soxa-ui/core';
2
+ import { TBasePlugin } from '@soxa-ui/plugins';
3
+ import type { IPluginContext } from '@soxa-ui/plugins';
4
+ /**
5
+ * TToastsFrontPlugin — высота переднего тоста стопки для CSS общего слоя тем.
6
+ *
7
+ * В стопке (`layout: 'cascade'`) задние тосты стоят за передним его высоты и
8
+ * выглядывают из-под него на ступень (`components/toasts.css` общего слоя тем).
9
+ * Свою высоту задний меняет переходом темы, когда передний сменился, а для
10
+ * перехода нужно число: высоту переднего одной переменной CSS не знает — у
11
+ * тостов описание, своё содержимое, перенос строк. Её и пишет плагин —
12
+ * переменной `--so-toasts-front-height` на регионе, в px; без переменной
13
+ * задний берёт высоту ячейки стопки, то есть ту же, но без перехода.
14
+ *
15
+ * Передний — тот же, что у CSS: тост под фокусом (`:focus-within`), а без
16
+ * фокуса в стопке — первый, а за закрытым первым — следующий: закрытый
17
+ * гаснет, и на его место уже встал он. Пока у региона есть узел и тосты
18
+ * стоят стопкой, `ResizeObserver` следит за передним и пишет его высоту по
19
+ * оси блока на каждую смену. Кто передний, плагин пересматривает, когда
20
+ * меняется состав списка или закрывается тост (новый тост, перестановка,
21
+ * уход, `data-open` — `MutationObserver` на списке) и когда фокус входит в
22
+ * стопку или уходит из неё. Стопку сменили на столбец, узел сняли или плагин
23
+ * уничтожен — наблюдение снято вместе с переменной.
24
+ *
25
+ * Плагин общего слоя тем, а не библиотеки: переменную читает CSS общего слоя
26
+ * — по ней он ставит высоту задних, а тема ведёт её переходом, — и библиотека
27
+ * о ней не знает. Ставит его тема своим `defineTheme` на все Toasts (у oren —
28
+ * `setup/plugins/install.ts`).
29
+ */
30
+ export declare class TToastsFrontPlugin extends TBasePlugin<IToasts> {
31
+ private _owner;
32
+ /** Корень региона, объявленный `TElementPlugin`; `null` — узла нет. */
33
+ private _root;
34
+ /** Регион, на котором записана высота; `null` — плагин за стопкой не следит. */
35
+ private _region;
36
+ /** Список тостов региона. */
37
+ private _list;
38
+ /** Передний тост, за которым следит наблюдатель. */
39
+ private _front;
40
+ private _resize;
41
+ private _mutation;
42
+ install(ctx: IPluginContext): void;
43
+ destroy(): void;
44
+ private _bind;
45
+ /** Следить за передним — пока у региона есть узел и тосты стоят стопкой. */
46
+ private readonly _sync;
47
+ private _start;
48
+ private _stop;
49
+ /**
50
+ * Передний — тост под фокусом; без фокуса в стопке — первый, а за закрытым
51
+ * первым — следующий. Выбор тот же, что у общего слоя тем
52
+ * (`components/toasts.css`) и у движения темы (у oren — `$cascade-front`).
53
+ */
54
+ private readonly _pick;
55
+ /**
56
+ * Фокус уходит: где он окажется, известно, когда смена фокуса закончилась.
57
+ * Пришёл он в другой тост стопки — переднего пересмотрит `focusin`, ушёл из
58
+ * стопки — передним снова станет первый.
59
+ */
60
+ private readonly _onFocusOut;
61
+ /** Высота переднего по оси блока — та же ось, что у высоты задних в теме. */
62
+ private readonly _onResize;
63
+ }
@@ -0,0 +1,131 @@
1
+ import { TBasePlugin, TElementPlugin } from "@soxa-ui/plugins";
2
+ //#region setup/plugins/toasts-front.plugin.ts
3
+ /** Высота переднего тоста стопки на регионе: по ней CSS держит высоту задних. */
4
+ var FRONT_HEIGHT = "--so-toasts-front-height";
5
+ /**
6
+ * TToastsFrontPlugin — высота переднего тоста стопки для CSS общего слоя тем.
7
+ *
8
+ * В стопке (`layout: 'cascade'`) задние тосты стоят за передним его высоты и
9
+ * выглядывают из-под него на ступень (`components/toasts.css` общего слоя тем).
10
+ * Свою высоту задний меняет переходом темы, когда передний сменился, а для
11
+ * перехода нужно число: высоту переднего одной переменной CSS не знает — у
12
+ * тостов описание, своё содержимое, перенос строк. Её и пишет плагин —
13
+ * переменной `--so-toasts-front-height` на регионе, в px; без переменной
14
+ * задний берёт высоту ячейки стопки, то есть ту же, но без перехода.
15
+ *
16
+ * Передний — тот же, что у CSS: тост под фокусом (`:focus-within`), а без
17
+ * фокуса в стопке — первый, а за закрытым первым — следующий: закрытый
18
+ * гаснет, и на его место уже встал он. Пока у региона есть узел и тосты
19
+ * стоят стопкой, `ResizeObserver` следит за передним и пишет его высоту по
20
+ * оси блока на каждую смену. Кто передний, плагин пересматривает, когда
21
+ * меняется состав списка или закрывается тост (новый тост, перестановка,
22
+ * уход, `data-open` — `MutationObserver` на списке) и когда фокус входит в
23
+ * стопку или уходит из неё. Стопку сменили на столбец, узел сняли или плагин
24
+ * уничтожен — наблюдение снято вместе с переменной.
25
+ *
26
+ * Плагин общего слоя тем, а не библиотеки: переменную читает CSS общего слоя
27
+ * — по ней он ставит высоту задних, а тема ведёт её переходом, — и библиотека
28
+ * о ней не знает. Ставит его тема своим `defineTheme` на все Toasts (у oren —
29
+ * `setup/plugins/install.ts`).
30
+ */
31
+ var TToastsFrontPlugin = class extends TBasePlugin {
32
+ _owner = null;
33
+ /** Корень региона, объявленный `TElementPlugin`; `null` — узла нет. */
34
+ _root = null;
35
+ /** Регион, на котором записана высота; `null` — плагин за стопкой не следит. */
36
+ _region = null;
37
+ /** Список тостов региона. */
38
+ _list = null;
39
+ /** Передний тост, за которым следит наблюдатель. */
40
+ _front = null;
41
+ _resize = null;
42
+ _mutation = null;
43
+ install(ctx) {
44
+ super.install(ctx);
45
+ this._owner = ctx.getInstance();
46
+ const element = ctx.get(TElementPlugin);
47
+ element?.events.on("ready", (node) => this._bind(node));
48
+ element?.events.on("removed", () => this._bind(null));
49
+ this._listenTo(this._owner?.events, "change:layout", this._sync);
50
+ }
51
+ destroy() {
52
+ this._bind(null);
53
+ this._resize = null;
54
+ this._mutation = null;
55
+ this._owner = null;
56
+ super.destroy();
57
+ }
58
+ _bind(root) {
59
+ this._stop();
60
+ this._root = root;
61
+ this._sync();
62
+ }
63
+ /** Следить за передним — пока у региона есть узел и тосты стоят стопкой. */
64
+ _sync = () => {
65
+ const on = Boolean(this._root && this._owner?.layout === "cascade");
66
+ if (on && !this._region) this._start();
67
+ else if (!on && this._region) this._stop();
68
+ };
69
+ _start() {
70
+ const root = this._root;
71
+ const owner = this._owner;
72
+ if (!(root instanceof HTMLElement) || !owner) return;
73
+ const list = root.querySelector(`:scope > .${owner.classes.resolve("__list")}`);
74
+ if (!list) return;
75
+ this._region = root;
76
+ this._list = list;
77
+ this._resize ??= new ResizeObserver(this._onResize);
78
+ this._mutation ??= new MutationObserver(this._pick);
79
+ this._mutation.observe(list, {
80
+ childList: true,
81
+ subtree: true,
82
+ attributeFilter: ["data-open"]
83
+ });
84
+ root.addEventListener("focusin", this._pick);
85
+ root.addEventListener("focusout", this._onFocusOut);
86
+ this._pick();
87
+ }
88
+ _stop() {
89
+ this._resize?.disconnect();
90
+ this._mutation?.disconnect();
91
+ this._region?.removeEventListener("focusin", this._pick);
92
+ this._region?.removeEventListener("focusout", this._onFocusOut);
93
+ this._region?.style.removeProperty(FRONT_HEIGHT);
94
+ this._region = null;
95
+ this._list = null;
96
+ this._front = null;
97
+ }
98
+ /**
99
+ * Передний — тост под фокусом; без фокуса в стопке — первый, а за закрытым
100
+ * первым — следующий. Выбор тот же, что у общего слоя тем
101
+ * (`components/toasts.css`) и у движения темы (у oren — `$cascade-front`).
102
+ */
103
+ _pick = () => {
104
+ const items = [...this._list?.children ?? []];
105
+ const [first, second] = items;
106
+ const closed = first instanceof HTMLElement && first.dataset.open === "false";
107
+ const front = items.find((item) => item.matches(":focus-within")) ?? (closed ? second : void 0) ?? first ?? null;
108
+ if (front === this._front) return;
109
+ if (this._front) this._resize?.unobserve(this._front);
110
+ this._front = front;
111
+ if (front) this._resize?.observe(front);
112
+ else this._region?.style.removeProperty(FRONT_HEIGHT);
113
+ };
114
+ /**
115
+ * Фокус уходит: где он окажется, известно, когда смена фокуса закончилась.
116
+ * Пришёл он в другой тост стопки — переднего пересмотрит `focusin`, ушёл из
117
+ * стопки — передним снова станет первый.
118
+ */
119
+ _onFocusOut = () => {
120
+ queueMicrotask(this._pick);
121
+ };
122
+ /** Высота переднего по оси блока — та же ось, что у высоты задних в теме. */
123
+ _onResize = (entries) => {
124
+ for (const entry of entries) {
125
+ const [size] = entry.borderBoxSize;
126
+ if (entry.target === this._front && size) this._region?.style.setProperty(FRONT_HEIGHT, `${size.blockSize}px`);
127
+ }
128
+ };
129
+ };
130
+ //#endregion
131
+ export { TToastsFrontPlugin };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Ожидание конца переезда геометрии активного таба — когда `TTabsViewPlugin`
3
+ * может снять с ряда признак переезда.
4
+ *
5
+ * Геометрию активного таба тема рисует псевдоэлементами самого ряда — у
6
+ * oren полосу `line` и карточку `contained`, — и ведёт её переходом, пока на
7
+ * ряду стоит признак `--so-tabs-active-moving`; без него длительность
8
+ * перехода нулевая.
9
+ * Снимать признак надо, когда переход доиграл: снятый посреди переезда, он
10
+ * выключил бы переход у сдвига, пришедшего следом (сосед встал на место
11
+ * закрытого таба), и отметка доскочила бы рывком.
12
+ *
13
+ * Чисел темы здесь нет: сколько длится переход, знает сам переход.
14
+ *
15
+ * Устроено как ожидание закрытия у замка прокрутки в `@soxa-ui/plugins`
16
+ * (`utils/afterTransitions.ts`). Утилиты плагинов наружу не отдаются, а
17
+ * выборка здесь своя: переходы псевдоэлементов узла, а не самого узла.
18
+ */
19
+ /**
20
+ * Позвать `done`, когда псевдоэлементы узла доиграют CSS-переходы, которые
21
+ * идут у них кадром позже. Отдаёт отмену: после неё `done` не позовётся, даже
22
+ * если ожидание кончится потом.
23
+ *
24
+ * **Кадр.** Переменные на ряду плагин пишет сразу, а выбранный таб адаптер
25
+ * отрисует в своём цикле — от него зависит, есть ли карточка вовсе. Кадром
26
+ * позже разметка уже отрисована. Отрисовки кадра ждать не нужно:
27
+ * `getAnimations()` сам применяет отложенные стили, и переход, который начала
28
+ * запись, виден сразу.
29
+ *
30
+ * **Какие переходы.** Только псевдоэлементов самого узла: у ряда в
31
+ * `getAnimations({ subtree: true })` есть и переходы табов — цвет текста
32
+ * строки под указателем и подобное, — а переезду они не принадлежат. И только
33
+ * `CSSTransition`: у перехода конец есть всегда. Классы берутся из окна узла, а
34
+ * не глобальные: компонент живёт и в `iframe`, и объекты его анимаций — из
35
+ * чужого окна.
36
+ *
37
+ * **Конец** — `finished` у каждого перехода. Отменённый переход (`finished`
38
+ * отклонён с `AbortError`: карточку сняли, переход перебит новым значением) —
39
+ * тоже конец, и отклонение обработано здесь же: необработанное ушло бы в
40
+ * `unhandledrejection`. Переходов нет или нет Web Animations вовсе (jsdom) —
41
+ * `done` зовётся в том же кадре.
42
+ */
43
+ export declare function afterPseudoTransitions(element: Element, done: () => void): () => void;
@@ -0,0 +1,75 @@
1
+ //#region setup/plugins/transitions.ts
2
+ /**
3
+ * Ожидание конца переезда геометрии активного таба — когда `TTabsViewPlugin`
4
+ * может снять с ряда признак переезда.
5
+ *
6
+ * Геометрию активного таба тема рисует псевдоэлементами самого ряда — у
7
+ * oren полосу `line` и карточку `contained`, — и ведёт её переходом, пока на
8
+ * ряду стоит признак `--so-tabs-active-moving`; без него длительность
9
+ * перехода нулевая.
10
+ * Снимать признак надо, когда переход доиграл: снятый посреди переезда, он
11
+ * выключил бы переход у сдвига, пришедшего следом (сосед встал на место
12
+ * закрытого таба), и отметка доскочила бы рывком.
13
+ *
14
+ * Чисел темы здесь нет: сколько длится переход, знает сам переход.
15
+ *
16
+ * Устроено как ожидание закрытия у замка прокрутки в `@soxa-ui/plugins`
17
+ * (`utils/afterTransitions.ts`). Утилиты плагинов наружу не отдаются, а
18
+ * выборка здесь своя: переходы псевдоэлементов узла, а не самого узла.
19
+ */
20
+ /**
21
+ * Позвать `done`, когда псевдоэлементы узла доиграют CSS-переходы, которые
22
+ * идут у них кадром позже. Отдаёт отмену: после неё `done` не позовётся, даже
23
+ * если ожидание кончится потом.
24
+ *
25
+ * **Кадр.** Переменные на ряду плагин пишет сразу, а выбранный таб адаптер
26
+ * отрисует в своём цикле — от него зависит, есть ли карточка вовсе. Кадром
27
+ * позже разметка уже отрисована. Отрисовки кадра ждать не нужно:
28
+ * `getAnimations()` сам применяет отложенные стили, и переход, который начала
29
+ * запись, виден сразу.
30
+ *
31
+ * **Какие переходы.** Только псевдоэлементов самого узла: у ряда в
32
+ * `getAnimations({ subtree: true })` есть и переходы табов — цвет текста
33
+ * строки под указателем и подобное, — а переезду они не принадлежат. И только
34
+ * `CSSTransition`: у перехода конец есть всегда. Классы берутся из окна узла, а
35
+ * не глобальные: компонент живёт и в `iframe`, и объекты его анимаций — из
36
+ * чужого окна.
37
+ *
38
+ * **Конец** — `finished` у каждого перехода. Отменённый переход (`finished`
39
+ * отклонён с `AbortError`: карточку сняли, переход перебит новым значением) —
40
+ * тоже конец, и отклонение обработано здесь же: необработанное ушло бы в
41
+ * `unhandledrejection`. Переходов нет или нет Web Animations вовсе (jsdom) —
42
+ * `done` зовётся в том же кадре.
43
+ */
44
+ function afterPseudoTransitions(element, done) {
45
+ let cancelled = false;
46
+ const finish = () => {
47
+ if (!cancelled) done();
48
+ };
49
+ const frame = requestAnimationFrame(() => {
50
+ const transitions = pseudoTransitionsOf(element);
51
+ if (transitions.length === 0) {
52
+ finish();
53
+ return;
54
+ }
55
+ Promise.all(transitions.map(ended)).then(finish);
56
+ });
57
+ return () => {
58
+ cancelled = true;
59
+ cancelAnimationFrame(frame);
60
+ };
61
+ }
62
+ /** Идущие CSS-переходы псевдоэлементов самого узла. */
63
+ function pseudoTransitionsOf(element) {
64
+ const view = element.ownerDocument.defaultView;
65
+ const Transition = view?.CSSTransition;
66
+ const Effect = view?.KeyframeEffect;
67
+ if (typeof Transition !== "function" || typeof Effect !== "function" || typeof element.getAnimations !== "function") return [];
68
+ return element.getAnimations({ subtree: true }).filter((animation) => animation instanceof Transition && animation.effect instanceof Effect && animation.effect.target === element && animation.effect.pseudoElement !== null);
69
+ }
70
+ /** Переход доиграл или его отменили — для признака это одно и то же. */
71
+ function ended(transition) {
72
+ return transition.finished.catch(() => void 0);
73
+ }
74
+ //#endregion
75
+ export { afterPseudoTransitions };
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@soxa-ui/theme-base",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0-beta.1",
4
+ "description": "Общий слой тем Soxa UI: сброс частей библиотеки, знак направления письма и структура частей с числами в переменных — чистый CSS, без препроцессора, — и плагины раскладки для поведения темы",
5
+ "keywords": [
6
+ "soxa",
7
+ "ui",
8
+ "theme",
9
+ "css"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "Yuri Soldatov",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/youra-h/soxa-ui.git",
16
+ "directory": "packages/themes/base"
17
+ },
18
+ "homepage": "https://github.com/youra-h/soxa-ui",
19
+ "bugs": {
20
+ "url": "https://github.com/youra-h/soxa-ui/issues"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": [
27
+ "*.css"
28
+ ],
29
+ "style": "./src/index.css",
30
+ "exports": {
31
+ ".": "./src/index.css",
32
+ "./setup": {
33
+ "types": "./dist/setup/index.d.ts",
34
+ "default": "./dist/setup/index.js"
35
+ },
36
+ "./theme-variables.json": "./theme-variables.json",
37
+ "./theme-contract.json": "./theme-contract.json"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "src",
42
+ "theme-variables.json",
43
+ "theme-contract.json"
44
+ ],
45
+ "scripts": {
46
+ "build": "vite build -c vite.setup.config.ts && tsc -p tsconfig.build.json",
47
+ "test": "vitest run"
48
+ },
49
+ "peerDependencies": {
50
+ "@soxa-ui/core": "^0.3.0-beta.1",
51
+ "@soxa-ui/plugins": "^0.3.0-beta.1"
52
+ },
53
+ "devDependencies": {
54
+ "postcss": "^8.5.9",
55
+ "postcss-selector-parser": "^7.1.6"
56
+ }
57
+ }
@@ -0,0 +1,75 @@
1
+ /* ─── Панель у якоря ─────────────────────────────────────────
2
+ * Немодальный слой у своего поля или триггера: панели Popover, Select и
3
+ * DatePicker, панель Menu и панель его пункта (`Menu.PopoverItem`). Панель —
4
+ * Frame, телепортированный в `body`; её открытость пишет её слой —
5
+ * `data-open` (`true`/`false`), — а закрытую разметка прячет (`display:
6
+ * none`).
7
+ *
8
+ * Закрытая панель нажатий не ловит. Тема вправе гасить её переходом: тогда
9
+ * `display` идёт переходом с `allow-discrete`, и до конца перехода закрытая
10
+ * панель ещё в раскладке, — но она уже не открыта, и нажатие сквозь неё —
11
+ * нажатие по тому, что под ней. Конца переходов ждёт код (`afterTransitions`:
12
+ * сброс отбора Select, конец слежения за якорем); без перехода он наступает
13
+ * тем же кадром.
14
+ *
15
+ * Полоса жеста (`swipe`) у панели у якоря стоит там, откуда панель тянут, — у
16
+ * края со стороны якоря: под ним — у верхнего края панели, над ним — у
17
+ * нижнего. Сторону после flip пишет в саму панель плагин якоря
18
+ * (`data-placement`). Зона захвата — поле панели с этой стороны, поэтому
19
+ * содержимое полоса не накрывает, а его раскладка та же, что без неё. Сама
20
+ * полоса — `swipe.css`, у панели Popover в контейнере её место по краю
21
+ * (`components/popover.css`). Пока панель тянут (`data-swiping`), курсор
22
+ * «держу» — по всей панели, куда бы он ни ушёл.
23
+ *
24
+ * Во время жеста панель идёт за указателем: сдвиг плагина жеста
25
+ * (`--so-swipe-offset`, `swipe.css`) — её `translate`, вдоль оси якоря. Только
26
+ * у панели с жестом: `translate`, даже нулевой, делает панель контейнером для
27
+ * `position: fixed` внутри неё. У панели Popover в контейнере ось — её край
28
+ * (`components/popover.css`).
29
+ *
30
+ * Облик — поверхность, кромка, тень, скругление, проявление и исчезание,
31
+ * возврат отпущенной и уход смахнутой панели — рисует тема. */
32
+
33
+ .so-popover__panel[data-open='false'],
34
+ .so-select__panel[data-open='false'],
35
+ .so-date-picker__panel[data-open='false'],
36
+ .so-menu__panel[data-open='false'],
37
+ .so-menu-item__panel[data-open='false'] {
38
+ pointer-events: none;
39
+ }
40
+
41
+ .so-popover__panel:has(> .so-popover__handle),
42
+ .so-select__panel:has(> .so-select__handle),
43
+ .so-date-picker__panel:has(> .so-date-picker__handle) {
44
+ padding-block-start: var(--so-target-size);
45
+ translate: 0 var(--so-swipe-offset);
46
+ }
47
+
48
+ .so-popover__panel:has(> .so-popover__handle)[data-placement^='top'],
49
+ .so-select__panel:has(> .so-select__handle)[data-placement^='top'],
50
+ .so-date-picker__panel:has(> .so-date-picker__handle)[data-placement^='top'] {
51
+ padding-block-start: 0;
52
+ padding-block-end: var(--so-target-size);
53
+ }
54
+
55
+ .so-popover__panel:has(> .so-popover__handle)[data-swiping='true'],
56
+ .so-select__panel:has(> .so-select__handle)[data-swiping='true'],
57
+ .so-date-picker__panel:has(> .so-date-picker__handle)[data-swiping='true'] {
58
+ cursor: grabbing;
59
+ }
60
+
61
+ .so-popover__panel > .so-popover__handle,
62
+ .so-select__panel > .so-select__handle,
63
+ .so-date-picker__panel > .so-date-picker__handle {
64
+ position: absolute;
65
+ inset-inline: 0;
66
+ top: 0;
67
+ block-size: var(--so-target-size);
68
+ }
69
+
70
+ .so-popover__panel[data-placement^='top'] > .so-popover__handle,
71
+ .so-select__panel[data-placement^='top'] > .so-select__handle,
72
+ .so-date-picker__panel[data-placement^='top'] > .so-date-picker__handle {
73
+ top: auto;
74
+ bottom: 0;
75
+ }
@@ -0,0 +1,71 @@
1
+ /* ─── Шкала подписи ──────────────────────────────────────────
2
+ * Кегль текста рядом с контролом и зазор до контрола — одна шкала на всех,
3
+ * кто подписывает контрол: подпись `Label`, подпись и справка поля формы
4
+ * `Field`, текст радио. Радио со своим текстом и радио в подписи выглядят
5
+ * одинаково, а флажок в подписи и поле ввода в Field стоят от своих подписей
6
+ * на одном расстоянии. Без общей шкалы каждое место завело бы свои числа, и
7
+ * форма из разных контролов разошлась бы в зазорах.
8
+ *
9
+ * Ступени — переменные корня документа по размеру (`sm`…`2xl`): их читают
10
+ * правила размера каждого блока. Умолчания — числа темы oren; тема задаёт
11
+ * свои на корне документа, приложение — на предке своей области.
12
+ *
13
+ * - `gap` — зазор сбоку: подпись в начале или в конце строки;
14
+ * - `stack` — над контролом или под ним: подпись над полем, справка под ним.
15
+ * На глаз он равен боковому, а числом вдвое меньше: в столбце к зазору
16
+ * прибавляются полуинтерлиньяж и выносные элементы строки, сбоку — только
17
+ * поля глифа;
18
+ * - `font-size` и `line-height` — кегль подписи. У `normal` его нет: подпись
19
+ * берёт кегль места, как значение в поле рядом;
20
+ * - `note-font-size` и `note-line-height` — кегль справки при контроле
21
+ * (подсказка и ошибка Field): на ступень мельче подписи — справка поясняет
22
+ * контрол, а не называет его.
23
+ *
24
+ * Зазор меряется от коробки контрола: поле ввода и флажок стоят от подписи на
25
+ * одном расстоянии, как бы ни отличалась их высота. */
26
+
27
+ :root {
28
+ --so-caption-gap-sm: 0.375rem;
29
+ --so-caption-gap-normal: 0.5rem;
30
+ --so-caption-gap-lg: 0.625rem;
31
+ --so-caption-gap-xl: 0.75rem;
32
+ --so-caption-gap-2xl: 0.875rem;
33
+
34
+ --so-caption-stack-sm: 0.1875rem;
35
+ --so-caption-stack-normal: 0.25rem;
36
+ --so-caption-stack-lg: 0.3125rem;
37
+ --so-caption-stack-xl: 0.375rem;
38
+ --so-caption-stack-2xl: 0.4375rem;
39
+
40
+ /* The caption counts from the font of the place, which `normal` takes as is:
41
+ * `sm` is 2px smaller, `lg`, `xl` and `2xl` 2px, 4px and 8px larger — 14 /
42
+ * 16 / 18 / 20 / 24px at a 16px place, 12 / 14 / 16 / 18 / 22px at 14px.
43
+ * `em` resolves where the variable is read, against the parent, that is the
44
+ * place the part stands in. */
45
+ --so-caption-font-size-sm: calc(1em - 0.125rem);
46
+ --so-caption-line-height-sm: calc(1.25 / 0.875);
47
+ --so-caption-font-size-lg: calc(1em + 0.125rem);
48
+ --so-caption-line-height-lg: calc(1.75 / 1.125);
49
+ --so-caption-font-size-xl: calc(1em + 0.25rem);
50
+ --so-caption-line-height-xl: calc(1.75 / 1.25);
51
+ --so-caption-font-size-2xl: calc(1em + 0.5rem);
52
+ --so-caption-line-height-2xl: calc(2 / 1.5);
53
+
54
+ /* Notes are a step smaller than the text next to them, and every step counts
55
+ * from the font of the place: 4px smaller at `sm`, 2px at `normal`, the font
56
+ * of the place at `lg`, 2px and 4px larger at `xl` and `2xl` — 12 / 14 / 16
57
+ * / 18 / 20px at a 16px place, 10 / 12 / 14 / 16 / 18px at 14px. `em`
58
+ * resolves where the variable is read: on the helper and error of Field, the
59
+ * counter of Textarea, the mark labels of Slider and the group labels of
60
+ * rows, each against a parent without a font of its own, that is the place. */
61
+ --so-caption-note-font-size-sm: calc(1em - 0.25rem);
62
+ --so-caption-note-line-height-sm: calc(1 / 0.75);
63
+ --so-caption-note-font-size-normal: calc(1em - 0.125rem);
64
+ --so-caption-note-line-height-normal: calc(1.25 / 0.875);
65
+ --so-caption-note-font-size-lg: 1em;
66
+ --so-caption-note-line-height-lg: calc(1.5 / 1);
67
+ --so-caption-note-font-size-xl: calc(1em + 0.125rem);
68
+ --so-caption-note-line-height-xl: calc(1.75 / 1.125);
69
+ --so-caption-note-font-size-2xl: calc(1em + 0.25rem);
70
+ --so-caption-note-line-height-2xl: calc(1.75 / 1.25);
71
+ }