@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.
- package/LICENSE +21 -0
- package/README.md +399 -2
- package/dist/setup/index.d.ts +31 -0
- package/dist/setup/index.js +4 -0
- package/dist/setup/plugins/index.d.ts +3 -0
- package/dist/setup/plugins/table-head.plugin.d.ts +44 -0
- package/dist/setup/plugins/table-head.plugin.js +86 -0
- package/dist/setup/plugins/tabs-view.plugin.d.ts +32 -0
- package/dist/setup/plugins/tabs-view.plugin.js +100 -0
- package/dist/setup/plugins/toasts-front.plugin.d.ts +63 -0
- package/dist/setup/plugins/toasts-front.plugin.js +131 -0
- package/dist/setup/plugins/transitions.d.ts +43 -0
- package/dist/setup/plugins/transitions.js +75 -0
- package/package.json +55 -4
- package/src/anchored.css +75 -0
- package/src/caption.css +71 -0
- package/src/components/accordion.css +96 -0
- package/src/components/button.css +139 -0
- package/src/components/calendar.css +594 -0
- package/src/components/check-box.css +121 -0
- package/src/components/date-input.css +224 -0
- package/src/components/date-picker.css +208 -0
- package/src/components/dialog.css +95 -0
- package/src/components/drag-and-drop.css +18 -0
- package/src/components/drawer.css +171 -0
- package/src/components/field.css +440 -0
- package/src/components/icon.css +61 -0
- package/src/components/input.css +189 -0
- package/src/components/label.css +160 -0
- package/src/components/list-box.css +165 -0
- package/src/components/menu.css +224 -0
- package/src/components/number-input.css +183 -0
- package/src/components/pagination.css +164 -0
- package/src/components/popover.css +164 -0
- package/src/components/progress-linear.css +88 -0
- package/src/components/progress-spinner.css +127 -0
- package/src/components/radio-group.css +130 -0
- package/src/components/rating.css +141 -0
- package/src/components/scroller.css +136 -0
- package/src/components/select.css +233 -0
- package/src/components/skeleton.css +33 -0
- package/src/components/slider.css +482 -0
- package/src/components/switch.css +142 -0
- package/src/components/table.css +1513 -0
- package/src/components/tabs.css +166 -0
- package/src/components/tags.css +371 -0
- package/src/components/textarea.css +179 -0
- package/src/components/toasts.css +447 -0
- package/src/components/tooltip.css +59 -0
- package/src/direction.css +30 -0
- package/src/disabled.css +26 -0
- package/src/group.css +90 -0
- package/src/index.css +97 -0
- package/src/modal.css +183 -0
- package/src/place.css +30 -0
- package/src/required.css +40 -0
- package/src/reset.css +172 -0
- package/src/swipe.css +71 -0
- package/src/target.css +46 -0
- package/theme-contract.json +3373 -0
- package/theme-variables.json +336 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuri Soldatov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,400 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @soxa-ui/theme-base
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The shared layer of Soxa UI themes: what the components need in any theme — a reset of the
|
|
4
|
+
library's own nodes, the writing-direction sign, the structure of the components with its numbers
|
|
5
|
+
in variables, and the layout plugins that measure what CSS cannot. The CSS is plain CSS, no
|
|
6
|
+
preprocessor, no Tailwind.**
|
|
7
|
+
|
|
8
|
+
Part of [Soxa UI](https://github.com/youra-h/soxa-ui) — UI components with the logic written once: a
|
|
9
|
+
framework-agnostic core and thin adapters that render it in Vue, React, Angular, Svelte, Solid and
|
|
10
|
+
Web Components.
|
|
11
|
+
|
|
12
|
+
## Who needs it
|
|
13
|
+
|
|
14
|
+
Theme authors — [Make your own theme](#make-your-own-theme) below walks through it. If you use a
|
|
15
|
+
ready theme such as [`@soxa-ui/theme-oren`](https://www.npmjs.com/package/@soxa-ui/theme-oren),
|
|
16
|
+
you do not install this package yourself: the theme depends on it, its stylesheet carries the CSS
|
|
17
|
+
of this package, and its behavior installs the plugins of this package.
|
|
18
|
+
|
|
19
|
+
## What is inside
|
|
20
|
+
|
|
21
|
+
- **A reset of the library's nodes.** Box sizing, margins, borders, and the font and color of
|
|
22
|
+
buttons and fields — only for elements with a `so-*` class and the inner nodes of the components.
|
|
23
|
+
The page itself is left alone: no rules for `html`, `body`, headings, lists or your own buttons.
|
|
24
|
+
The components inherit the font and line height of the page.
|
|
25
|
+
- **The writing-direction sign** `--so-dir-sign`: `1` left to right and `-1` right to left, taken
|
|
26
|
+
from the nearest `dir` attribute. A theme mirrors arrows, gradients and offsets by multiplying by
|
|
27
|
+
it — `:dir()` does not survive minification, and a `[dir]` selector on an ancestor does not see
|
|
28
|
+
the nearest `dir`.
|
|
29
|
+
- **The structure of the components** — layout and mechanics: where a part stands, what catches a
|
|
30
|
+
click, cursors, how a disabled component fades. For every component: Label, Field, Icon, Switch,
|
|
31
|
+
CheckBox, RadioGroup, Button, Input, Textarea, NumberInput, Tabs, Tags, Accordion, ListBox, Menu,
|
|
32
|
+
Select, Popover, Tooltip, Dialog, Drawer, Scroller, Slider, Rating, ProgressLinear,
|
|
33
|
+
ProgressSpinner, Skeleton, Pagination, Calendar, DateInput, DatePicker, Toasts, DragAndDrop and
|
|
34
|
+
Table. Without a theme they are bare but working: a switch toggles by a click on its track, a
|
|
35
|
+
label toggles its control, the slots of a button and of a field stay at its edges, a bar of tabs
|
|
36
|
+
stands on its side of the panel with its slots at its edges and the tabs aligned between them, a
|
|
37
|
+
row of tags scrolls, a collapsed accordion section takes no
|
|
38
|
+
room, a closed panel lets clicks through, the body of a dialog scrolls under a header and a footer
|
|
39
|
+
that stay in place, a drawer stands at its edge, the thumbs of a slider sit at the ends of the
|
|
40
|
+
travel and a click sets the value under the pointer, a click on a star sets its number, a scroller
|
|
41
|
+
pages to the start of an item, the cells of a pagination are as tall as a button, a calendar lays
|
|
42
|
+
its days out in seven columns, a date picker opens its panel at the field, toasts stand in their
|
|
43
|
+
corner of the window, stacked behind the front one when they cascade, and a table with known
|
|
44
|
+
column widths is as wide as their sum, scrolls sideways in its window when wider than its place,
|
|
45
|
+
and keeps a sticky head at the top of the scroll with the rows passing under it unseen.
|
|
46
|
+
- **The size scales** — heights, paddings, gaps and font sizes per size — as variables on the
|
|
47
|
+
document root with defaults, such as `--so-button-height-sm` or `--so-caption-gap-lg`. A theme
|
|
48
|
+
sets its own numbers there, an application on an ancestor of its area.
|
|
49
|
+
- **The layout plugins** (`@soxa-ui/theme-base/setup`) — data any theme needs and CSS cannot
|
|
50
|
+
compute: the place of the active tab, the height of a sticky table head and of the front toast in
|
|
51
|
+
a stack. They write it into CSS variables; a theme installs them with its behavior.
|
|
52
|
+
|
|
53
|
+
It has no look: no colors, borders, radii, shadows, no names of a theme's variants or views — those
|
|
54
|
+
are the theme's. Where the layout counts on a number of the look — the border width of a field, the
|
|
55
|
+
width of the fade at the edge of a scrolling row of tags or of a scroller, the thickness of a group
|
|
56
|
+
separator, the edge of a slider thumb — the theme sets it in a variable, and without a theme it is
|
|
57
|
+
zero. The surface of an overlay panel is the theme's too: the theme paints the panel with
|
|
58
|
+
`--so-panel-surface`, and this package announces it as the surface of the place inside the panel.
|
|
59
|
+
The surface of the page is announced by the theme and that of a card by the application, in
|
|
60
|
+
`--so-place-surface`: the sticky head of a table takes it as its opaque underlay. Every variable a
|
|
61
|
+
theme may set or read is listed with its meaning in `theme-variables.json`: those this package
|
|
62
|
+
reads — the ones a theme sets, and those the library and the layout plugins write, such as the size
|
|
63
|
+
of a dialog or the height of a sticky table head — and those a theme may only read, such as the
|
|
64
|
+
current height of a field or the place of the active tab.
|
|
65
|
+
|
|
66
|
+
The gestures work without a theme too: while a panel, a drawer or a toast is being swiped, or a
|
|
67
|
+
table column is being dragged by its header, this package keeps it under the pointer. The theme
|
|
68
|
+
only animates what comes after: the return when it is let go, the leave when it is swiped away.
|
|
69
|
+
|
|
70
|
+
## Make your own theme
|
|
71
|
+
|
|
72
|
+
A theme is a package with three entry points: its stylesheet, the type declarations of the values
|
|
73
|
+
its design offers, and its behavior. The structure comes from this package; the theme adds the look
|
|
74
|
+
on top of it and never styles the page. The steps go in the order to follow, and each says what
|
|
75
|
+
proves it is done.
|
|
76
|
+
|
|
77
|
+
### 1. The package
|
|
78
|
+
|
|
79
|
+
The library — `@soxa-ui/core`, `@soxa-ui/setup` and the rest — shares one version, and this
|
|
80
|
+
package has a version of its own. A theme depends on the library by a range, `^` and the library
|
|
81
|
+
version it is built against: the `so-*` classes, the `data-*` attributes and the `--so-*`
|
|
82
|
+
variables are what a theme is written against, and a release that breaks them falls outside that
|
|
83
|
+
range: before `1.0` a breaking change bumps the minor version (`0.3.1` → `0.4.0`). On this package a
|
|
84
|
+
theme depends by its exact version, with no `^`: its CSS goes into the theme's stylesheet and its
|
|
85
|
+
plugins run with the theme's behavior, so both have to come from one release. And it is a
|
|
86
|
+
dependency, not a dev dependency: the behavior of the theme imports its plugins at run time.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"name": "my-theme",
|
|
91
|
+
"type": "module",
|
|
92
|
+
"style": "./dist/index.css",
|
|
93
|
+
"types": "./index.d.ts",
|
|
94
|
+
"exports": {
|
|
95
|
+
".": { "types": "./index.d.ts", "default": "./dist/index.css" },
|
|
96
|
+
"./setup": { "types": "./dist/setup/index.d.ts", "default": "./dist/setup/index.js" }
|
|
97
|
+
},
|
|
98
|
+
"files": ["dist", "index.d.ts"],
|
|
99
|
+
"dependencies": { "@soxa-ui/theme-base": "<theme-base version>" },
|
|
100
|
+
"peerDependencies": {
|
|
101
|
+
"@soxa-ui/core": "^<library version>",
|
|
102
|
+
"@soxa-ui/setup": "^<library version>"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The application plugs the theme in as one stylesheet and one behavior object:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { useTheme } from '@soxa-ui/setup'
|
|
111
|
+
import theme from 'my-theme/setup'
|
|
112
|
+
|
|
113
|
+
import 'my-theme'
|
|
114
|
+
|
|
115
|
+
useTheme(theme)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Where the stylesheet goes depends on the build: an import from a module, as above, with Vite,
|
|
119
|
+
Next.js, SvelteKit and SolidStart; `@import 'my-theme';` in `src/styles.css` with Angular CLI; `css`
|
|
120
|
+
in `nuxt.config.ts` with Nuxt. The behavior is imported from a module in every build — step 6 has
|
|
121
|
+
it bring the values of the theme along.
|
|
122
|
+
|
|
123
|
+
### 2. The stylesheet: this package first
|
|
124
|
+
|
|
125
|
+
Import this package first in the theme's stylesheet and build the stylesheet into one file, with
|
|
126
|
+
the CSS of this package inside it:
|
|
127
|
+
|
|
128
|
+
```css
|
|
129
|
+
@import '@soxa-ui/theme-base';
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Its rules live in the cascade layer `so`, each part of it in a sub-layer of its own named after its
|
|
133
|
+
file — `so.reset` first, then `so.direction`, `so.button` and the rest — and the first line of the
|
|
134
|
+
package declares their order before any rule. A later sub-layer wins over an earlier one whatever
|
|
135
|
+
the specificity, so the order holds however the layer is cut into files: a theme that ships a
|
|
136
|
+
stylesheet per component cuts its built sheet by these sub-layers, and an application may load the
|
|
137
|
+
pieces in any order. Any rule of the theme in a later layer, or outside layers, overrides this
|
|
138
|
+
package without fighting specificity. A theme built with Tailwind puts the package into
|
|
139
|
+
Tailwind's `base` layer, where preflight used to be, so it stays below the application's
|
|
140
|
+
utilities — and imports Tailwind in parts, without preflight, which would reset the whole page:
|
|
141
|
+
|
|
142
|
+
```css
|
|
143
|
+
@layer theme, base, components, utilities;
|
|
144
|
+
@import 'tailwindcss/theme.css' layer(theme);
|
|
145
|
+
@import 'tailwindcss/utilities.css' layer(utilities) source(none);
|
|
146
|
+
@import '@soxa-ui/theme-base' layer(base);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Done when everything that works on a page with this package alone still works with your
|
|
150
|
+
stylesheet: your rules add the look and take no mechanics away.
|
|
151
|
+
|
|
152
|
+
### 3. The variables
|
|
153
|
+
|
|
154
|
+
[`theme-variables.json`](https://github.com/youra-h/soxa-ui/blob/main/packages/themes/base/theme-variables.json)
|
|
155
|
+
is the contract, with the meaning of every variable, in two sections.
|
|
156
|
+
|
|
157
|
+
`base-reads` — every variable this package reads. Four kinds:
|
|
158
|
+
|
|
159
|
+
- **Scales and geometry** with a default on `:root`, such as `--so-button-height-sm` or
|
|
160
|
+
`--so-switch-inset`. Set only those where your numbers differ, on `:root`.
|
|
161
|
+
- **Numbers of your look the layout counts on** — `--so-input-border` and the other field borders,
|
|
162
|
+
`--so-tags-fade`, `--so-scroller-fade`, `--so-group-separator-width`, `--so-slider-thumb-border`.
|
|
163
|
+
Zero without a theme; set each where you draw that look.
|
|
164
|
+
- **Surfaces.** Paint the overlay panels with `--so-panel-surface` and set it on them; set
|
|
165
|
+
`--so-place-surface` on `:root` to the color of your page. Without them a sticky table head has
|
|
166
|
+
no underlay, and the rows show through it.
|
|
167
|
+
- **Written by the library and by the layout plugins** — the size of a dialog, the positions of
|
|
168
|
+
slider thumbs, the swipe offset of a panel, the height of a sticky table head. Do not set them;
|
|
169
|
+
read them if your look needs them.
|
|
170
|
+
|
|
171
|
+
`theme-reads` — what a theme may read on top of that, and never sets:
|
|
172
|
+
|
|
173
|
+
- **The writing-direction sign** `--so-dir-sign`.
|
|
174
|
+
- **The current step of a block** — the scale value of the component's own size, and what is
|
|
175
|
+
derived from it: `--so-input-height`, `--so-slider-thumb-size`, `--so-slider-halo-size`,
|
|
176
|
+
`--so-radio-control-size`, the step of a toast in a stack `--so-toasts-lift`. Draw from these
|
|
177
|
+
when your look has to match the size of the part.
|
|
178
|
+
- **Written by plugins and not read by this package** — the place of the active tab
|
|
179
|
+
(`--so-tabs-active-pos` and the rest of `TTabsViewPlugin`, step 6), the ghost of a column being
|
|
180
|
+
resized and the width of a column being dragged in a table.
|
|
181
|
+
|
|
182
|
+
Read nothing else of this package: a variable that is not in the contract is part of its inner
|
|
183
|
+
mechanics and may change. The check `theme-reads` (step 7) fails on it.
|
|
184
|
+
|
|
185
|
+
Your own variables follow the same scheme, `--so-*`, as every name in the document's global
|
|
186
|
+
namespace: a variable of the application with the same name would replace yours by inheritance.
|
|
187
|
+
|
|
188
|
+
### 4. The look
|
|
189
|
+
|
|
190
|
+
The published [class and data-attribute reference](./theme-contract.json) lists roots, parts,
|
|
191
|
+
modifiers and datasets with their meanings, DOM scopes, values and emission sources. The
|
|
192
|
+
[variable reference](./theme-variables.json) keeps its separate two-section contract. Both JSON
|
|
193
|
+
files can also be imported from the corresponding package subpaths.
|
|
194
|
+
|
|
195
|
+
Literal roots and parts are exact class names. Modifier patterns append to the concrete block:
|
|
196
|
+
`so-<block>--size-<value>` uses the library sizes `sm`, `normal`, `lg`, `xl`, `2xl`;
|
|
197
|
+
`variant`, `view`, `shape` and `animation` values belong to the theme. The `dynamic` section
|
|
198
|
+
maps computed source expressions to their bounded families; these are checked alongside literal
|
|
199
|
+
emissions in all six adapters. Global scheme selection, `data-so-theme`, belongs to the
|
|
200
|
+
application and theme; `data-so-motion` controls the library's motion policy.
|
|
201
|
+
|
|
202
|
+
Boolean and state modifiers are data attributes: `data-readonly`, `data-required`,
|
|
203
|
+
`data-clearable`, `data-editable`, `data-open`, `data-closable`,
|
|
204
|
+
`data-indeterminate` and `data-ready-animation`. There are no parallel state classes.
|
|
205
|
+
Layout and mode classes include their prop names: `--orientation-horizontal`,
|
|
206
|
+
`--mode-range` and `--alignment-center`. Existing string-valued data contracts
|
|
207
|
+
such as `data-align` and `data-placement` retain their meaning.
|
|
208
|
+
|
|
209
|
+
Datasets may bind to an inner row, panel, trigger or input segment; use each entry's scope.
|
|
210
|
+
Boolean false is the string `"false"`, so use an explicit value selector. Only null/undefined
|
|
211
|
+
remove a TDataset attribute; gesture flags documented as presence-only disappear outside the gesture.
|
|
212
|
+
For example, style a selected list row through its part and state:
|
|
213
|
+
|
|
214
|
+
```css
|
|
215
|
+
.so-select-item[data-selected='true'] .so-button__text {
|
|
216
|
+
font-weight: 600;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Style the `so-*` classes and the `data-*` state attributes the library emits, with the same
|
|
221
|
+
selectors as the structure in `src/components/`. The rules every theme keeps:
|
|
222
|
+
|
|
223
|
+
- **Only the library's nodes.** Every selector addresses an element with a `so-*` class, in its
|
|
224
|
+
subject or in an ancestor. A node without a class inside a part is named through the class of its
|
|
225
|
+
component (`.so-input > input`); there are no rules for everything inside a part (`.so-x *`) —
|
|
226
|
+
slot content belongs to the application. Outside the library's nodes a theme sets only custom
|
|
227
|
+
properties, plus `color-scheme` on `:root` and in the block of a scheme, which stands on the
|
|
228
|
+
attribute `data-so-theme` (`[data-so-theme='my-theme-dark']`).
|
|
229
|
+
- **State from `data-*`, never from `aria-*`** — and not from the native `disabled` attribute: the
|
|
230
|
+
core duplicates every state into `data-*` on any tag, so an accessibility fix never changes the
|
|
231
|
+
look.
|
|
232
|
+
- **Disabled fades once.** This package fades a disabled part with its whole subtree by
|
|
233
|
+
`opacity: var(--so-disabled-opacity)`. Do not fade it yourself; set the variable to change the
|
|
234
|
+
amount.
|
|
235
|
+
- **Mirror with the sign**, multiplying by `--so-dir-sign`: no `:dir()` and no `[dir]` selectors.
|
|
236
|
+
- **Movement only behind the gate.** Sliding, travelling, turning, resizing over time go into rules
|
|
237
|
+
under the gate of the library's motion mode; without it the value changes at once. Color, opacity
|
|
238
|
+
and shadow transitions are not movement and need no gate:
|
|
239
|
+
|
|
240
|
+
```css
|
|
241
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
242
|
+
:where(:root:not([data-so-motion='reduce'])) .so-switch__track--thumb {
|
|
243
|
+
transition: inset-inline-start 200ms;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
:where(:root[data-so-motion='full']) .so-switch__track--thumb {
|
|
248
|
+
transition: inset-inline-start 200ms;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Following the pointer is not movement: during a swipe or a column drag this package puts the part
|
|
253
|
+
under the pointer by `translate`. Give that `translate` no transition while the part is held
|
|
254
|
+
(`[data-swiping='true']`, `[data-dragging='true']`), and write your own `translate` only for what
|
|
255
|
+
comes after — a closed layer leaving, `@starting-style` of an arrival — taking the offset from
|
|
256
|
+
the same variable.
|
|
257
|
+
|
|
258
|
+
- **Keyframes named `so-*`**, like the variables.
|
|
259
|
+
- **Hover through the label.** A hover rule of CheckBox, Switch and a radio item also matches when
|
|
260
|
+
the text of its `Label` is hovered: `:is(:hover, :where(.so-label:hover > .so-label__control *))`.
|
|
261
|
+
- **No per-cell rule of Table at rest.** A rule multiplied by every body cell of a Table stands
|
|
262
|
+
under an attribute the table carries only during a gesture (`[data-reorder-preview]`), with the
|
|
263
|
+
chain "root > body > row", and no rule has `:has()` on the table root.
|
|
264
|
+
|
|
265
|
+
Values of the look are modifiers with a prefix: `so-button--view-filled`,
|
|
266
|
+
`so-button--variant-accent`, `so-skeleton--shape-circle`, `so-skeleton--animation-wave`. A block
|
|
267
|
+
without a modifier is the default look.
|
|
268
|
+
|
|
269
|
+
### 5. The values of the look: `index.d.ts`
|
|
270
|
+
|
|
271
|
+
The library declares no `variant`, `view`, `shape` or `animation` values: the theme does, by
|
|
272
|
+
augmenting the registries of `@soxa-ui/core`, and the props of every component take exactly these
|
|
273
|
+
names. A registry the theme leaves empty gives its prop the type `never`.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
export {}
|
|
277
|
+
|
|
278
|
+
declare module '@soxa-ui/core' {
|
|
279
|
+
interface IComponentVariants {
|
|
280
|
+
accent: true
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface IButtonViews {
|
|
284
|
+
filled: true
|
|
285
|
+
outlined: true
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The registries: `IComponentVariants`, `IButtonViews` (also the rows of ListBox and Accordion and the
|
|
291
|
+
pills of Tags), `ITabsViews`, `ICheckBoxViews`, `IRadioGroupViews`, `ISkeletonShapes`,
|
|
292
|
+
`ISkeletonAnimations`. Declare every modifier your CSS has, and nothing it does not have.
|
|
293
|
+
|
|
294
|
+
The augmentation works once the file is in the application's program, and the package name brings
|
|
295
|
+
it there only from a module of TS. The stylesheet is not always imported from one: Angular CLI and
|
|
296
|
+
Nuxt take it from their configs, and there `variant="accent"` fails the type check. So the
|
|
297
|
+
declarations of `./setup`, which every application imports, reference this file (step 6): the
|
|
298
|
+
values reach the types wherever the stylesheet is plugged in, and the list of values stays in one
|
|
299
|
+
place.
|
|
300
|
+
|
|
301
|
+
### 6. The behavior: `./setup`
|
|
302
|
+
|
|
303
|
+
A theme hands over its plugins as one `defineTheme` object, and `useTheme` registers them for every
|
|
304
|
+
component of their type, including those drawn as parts of other components. This package has no
|
|
305
|
+
`defineTheme` of its own — the application plugs in one theme — so the theme installs the layout
|
|
306
|
+
plugins of this package in its object:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
/// <reference types="my-theme" preserve="true" />
|
|
310
|
+
|
|
311
|
+
import { TTable, TTabs, TToasts } from '@soxa-ui/core'
|
|
312
|
+
import { defineTheme } from '@soxa-ui/setup'
|
|
313
|
+
import { TTableHeadPlugin, TTabsViewPlugin, TToastsFrontPlugin } from '@soxa-ui/theme-base/setup'
|
|
314
|
+
|
|
315
|
+
export default defineTheme({
|
|
316
|
+
name: 'my-theme',
|
|
317
|
+
plugins: [
|
|
318
|
+
{ type: TTabs, plugins: [TTabsViewPlugin] },
|
|
319
|
+
{ type: TTable, plugins: [TTableHeadPlugin] },
|
|
320
|
+
{ type: TToasts, plugins: [TToastsFrontPlugin] },
|
|
321
|
+
],
|
|
322
|
+
})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The reference on top names the theme's own package: its `types` is `index.d.ts`, and the
|
|
326
|
+
declarations of `./setup` bring the values of step 5 along. Keep `preserve="true"` — tsc drops a
|
|
327
|
+
plain reference from the declarations it emits, as it drops `import type {}`. Done when the built
|
|
328
|
+
`dist/setup/index.d.ts` carries the reference.
|
|
329
|
+
|
|
330
|
+
- **`TTabsViewPlugin`**, on Tabs, writes on the bar `.so-tabs__bar` — the list of tabs with the
|
|
331
|
+
slots `leading` and `trailing` at its edges: `--so-tabs-active-pos` and `--so-tabs-active-size`
|
|
332
|
+
— the place and the length of the active tab along the main axis, from the edge of the bar;
|
|
333
|
+
`--so-tabs-gap-pos` and `--so-tabs-gap-size` — a gap in a line along the bar under it;
|
|
334
|
+
`--so-tabs-active-moving: 1` while the mark moves to a newly active tab. The place is measured
|
|
335
|
+
from the bar because this package positions it and not the list, so draw the mark on the bar:
|
|
336
|
+
a slot and the room left by `center` are already in the place. Once the node is measured, it
|
|
337
|
+
adds `data-ready-animation="true"` to the root. Only the theme reads these (the
|
|
338
|
+
`theme-reads` section of the contract): it draws the mark of the active tab — a stripe, a card —
|
|
339
|
+
from them. The plugin lifts the moving flag when the CSS transitions of the bar's own
|
|
340
|
+
pseudo-elements have ended, so a mark that moves is a `::before` or `::after` of the bar, with a
|
|
341
|
+
duration that depends on the flag — `calc(var(--so-tabs-active-moving, 0) * 200ms)` moves it to
|
|
342
|
+
a new tab and puts it in place at once on a resize. Without the plugin the variables are not
|
|
343
|
+
there: a mark drawn from them is not drawn, and only what the theme draws by `data-selected` of
|
|
344
|
+
the tab shows the active one.
|
|
345
|
+
- **`TTableHeadPlugin`**, on Table, writes `--so-table-head-height` on the table root while
|
|
346
|
+
`stickyHead` is on: this package keeps the focused cell below the sticky head by that height and
|
|
347
|
+
lays the head's opaque backing over all of it. Without it the offset and the backing are one row
|
|
348
|
+
of the head: a head wrapped into several rows covers the focus and is backed under its first row
|
|
349
|
+
only.
|
|
350
|
+
- **`TToastsFrontPlugin`**, on Toasts, writes `--so-toasts-front-height` on the region while the
|
|
351
|
+
toasts cascade: this package gives the toasts behind the height of the front one, and the theme
|
|
352
|
+
may transition it. Without it they take the height of the stack cell — the same height, with no
|
|
353
|
+
transition.
|
|
354
|
+
|
|
355
|
+
A plugin whose data only your own look needs belongs to your theme, in the same list.
|
|
356
|
+
|
|
357
|
+
### 7. Check it
|
|
358
|
+
|
|
359
|
+
The checks of this package run on the built CSS of every theme in the
|
|
360
|
+
[repository](https://github.com/youra-h/soxa-ui): a package in `packages/themes/` with a
|
|
361
|
+
`vite.config.ts` that builds its stylesheet — the file `style` of its manifest names, with every
|
|
362
|
+
rule of the theme — is picked up by `npm run test --workspace=@soxa-ui/theme-base` by itself. Outside the repository the same rules
|
|
363
|
+
hold — each check is a rule of step 3 or 4:
|
|
364
|
+
|
|
365
|
+
- `theme-reads` — every `--so-*` variable your stylesheet reads is your own or in the contract;
|
|
366
|
+
- `sealed` — every selector addresses a `so-*` node; outside them only custom properties and
|
|
367
|
+
`color-scheme`; only wrapping at-rules, `@layer`, `@keyframes` and `@property`;
|
|
368
|
+
- `motion` — movement only behind the gate, no `scroll-behavior: smooth`;
|
|
369
|
+
- `variable-prefix` and `keyframes-prefix` — the names `--so-*` and `so-*`, in the stylesheet and
|
|
370
|
+
in the behavior;
|
|
371
|
+
- `disabled` — a disabled rule takes its opacity only from `--so-disabled-opacity`;
|
|
372
|
+
- `label-hover` — the label context next to `:hover` in every hover rule of CheckBox, Switch and a
|
|
373
|
+
radio item;
|
|
374
|
+
- `table-reorder` — per-cell rules of Table under the gesture attribute, no `:has()` on its root.
|
|
375
|
+
|
|
376
|
+
[`@soxa-ui/theme-oren`](https://github.com/youra-h/soxa-ui/tree/main/packages/themes/oren) is a
|
|
377
|
+
complete theme built on these steps: Tailwind and Sass inside, two color schemes, its own values.
|
|
378
|
+
|
|
379
|
+
## Layer order next to Tailwind
|
|
380
|
+
|
|
381
|
+
A stylesheet that brings the `so` layer to the top level — this package imported as is, or a theme
|
|
382
|
+
that does so — goes into the application's cascade by the order in which layers first appear. If
|
|
383
|
+
the application uses Tailwind, name the order before any stylesheet loads, so that `so` stays below
|
|
384
|
+
`components` and `utilities` and a utility class on a component still wins:
|
|
385
|
+
|
|
386
|
+
```css
|
|
387
|
+
@layer theme, base, so, components, utilities;
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Documentation
|
|
391
|
+
|
|
392
|
+
- [Soxa UI README](https://github.com/youra-h/soxa-ui#readme) — what the library is and how it is
|
|
393
|
+
put together.
|
|
394
|
+
- [The package's own notes](https://github.com/youra-h/soxa-ui/blob/main/packages/themes/base/AGENTS.md)
|
|
395
|
+
— layers, the reset, the direction sign, the variable contract, the layout plugins and the checks
|
|
396
|
+
every theme passes (in Russian).
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
[MIT](https://github.com/youra-h/soxa-ui/blob/main/LICENSE)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Поведение общего слоя тем: плагины раскладки — данные, которые нужны частям
|
|
3
|
+
* в любой теме, а CSS посчитать их не может.
|
|
4
|
+
*
|
|
5
|
+
* - `TTabsViewPlugin` — место и длина активного таба на ряду Tabs: по ним
|
|
6
|
+
* тема рисует отметку активного таба;
|
|
7
|
+
* - `TTableHeadPlugin` — высота закреплённой шапки Table: по ней CSS общего
|
|
8
|
+
* слоя держит фокус в теле ниже шапки;
|
|
9
|
+
* - `TToastsFrontPlugin` — высота переднего тоста стопки Toasts: по ней CSS
|
|
10
|
+
* общего слоя ставит высоту задних, а тема ведёт её переходом.
|
|
11
|
+
*
|
|
12
|
+
* Своего `defineTheme` у общего слоя нет: приложение подключает одну тему, и
|
|
13
|
+
* плагины общего слоя ставит она — в своём объекте `defineTheme`:
|
|
14
|
+
*
|
|
15
|
+
* import { TTable, TTabs, TToasts } from '@soxa-ui/core'
|
|
16
|
+
* import { defineTheme } from '@soxa-ui/setup'
|
|
17
|
+
* import { TTableHeadPlugin, TTabsViewPlugin, TToastsFrontPlugin } from '@soxa-ui/theme-base/setup'
|
|
18
|
+
*
|
|
19
|
+
* export default defineTheme({
|
|
20
|
+
* name: 'my-theme',
|
|
21
|
+
* plugins: [
|
|
22
|
+
* { type: TTabs, plugins: [TTabsViewPlugin] },
|
|
23
|
+
* { type: TTable, plugins: [TTableHeadPlugin] },
|
|
24
|
+
* { type: TToasts, plugins: [TToastsFrontPlugin] },
|
|
25
|
+
* ],
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* Без плагина CSS остаётся рабочим: переменной нет, и часть стоит по запасному
|
|
29
|
+
* значению (`packages/themes/base/AGENTS.md`, «Плагины раскладки: `setup/`»).
|
|
30
|
+
*/
|
|
31
|
+
export { TTableHeadPlugin, TTabsViewPlugin, TToastsFrontPlugin } from './plugins';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TTableHeadPlugin } from "./plugins/table-head.plugin.js";
|
|
2
|
+
import { TTabsViewPlugin } from "./plugins/tabs-view.plugin.js";
|
|
3
|
+
import { TToastsFrontPlugin } from "./plugins/toasts-front.plugin.js";
|
|
4
|
+
export { TTableHeadPlugin, TTabsViewPlugin, TToastsFrontPlugin };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ITable } from '@soxa-ui/core';
|
|
2
|
+
import { TBasePlugin } from '@soxa-ui/plugins';
|
|
3
|
+
import type { IPluginContext } from '@soxa-ui/plugins';
|
|
4
|
+
/**
|
|
5
|
+
* TTableHeadPlugin — высота закреплённой шапки Table для CSS общего слоя тем.
|
|
6
|
+
*
|
|
7
|
+
* Закреплённая шапка (`stickyHead`) стоит у верхнего края прокрутки, и
|
|
8
|
+
* строки уходят под неё. Узел, на который браузер ведёт прокрутку к фокусу, —
|
|
9
|
+
* ячейку сетки после ↑ и PageUp, поле в ячейке после Shift+Tab, — общий слой
|
|
10
|
+
* тем (`components/table.css`) держит ниже шапки отступом прокрутки
|
|
11
|
+
* (`scroll-margin-block-start`) на её высоту. Высоту одной переменной CSS не
|
|
12
|
+
* знает: подписи заголовков переносятся, в слоте заголовка бывает что угодно,
|
|
13
|
+
* и шапка бывает выше строки шапки. Её и пишет плагин — переменной
|
|
14
|
+
* `--so-table-head-height` на корне таблицы, в px; без переменной отступ —
|
|
15
|
+
* высота строки шапки.
|
|
16
|
+
*
|
|
17
|
+
* Пока у таблицы есть корень и шапка закреплена, `ResizeObserver` следит за
|
|
18
|
+
* шапкой (`thead` — прямой ребёнок корня с классом части `__head`) и пишет её
|
|
19
|
+
* высоту на каждую смену. Шапку открепили, узел сняли или плагин уничтожен —
|
|
20
|
+
* наблюдение снято вместе с переменной. Переменная размера шапки не меняет —
|
|
21
|
+
* её читает только отступ прокрутки в теле, — и петли у наблюдателя нет.
|
|
22
|
+
*
|
|
23
|
+
* Плагин общего слоя тем, а не библиотеки: переменную читает CSS общего слоя
|
|
24
|
+
* — по ней он ставит отступ прокрутки к фокусу, — а библиотека о ней не
|
|
25
|
+
* знает. Ставит его тема своим `defineTheme` на все Table (у oren —
|
|
26
|
+
* `setup/plugins/install.ts`).
|
|
27
|
+
*/
|
|
28
|
+
export declare class TTableHeadPlugin extends TBasePlugin<ITable> {
|
|
29
|
+
private _owner;
|
|
30
|
+
/** Корень таблицы, объявленный `TElementPlugin`; `null` — узла нет. */
|
|
31
|
+
private _root;
|
|
32
|
+
/** Корень, на котором записана высота; `null` — плагин не следит за шапкой. */
|
|
33
|
+
private _target;
|
|
34
|
+
private _resize;
|
|
35
|
+
install(ctx: IPluginContext): void;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
private _bind;
|
|
38
|
+
/** Следить за шапкой — пока у таблицы есть корень и шапка закреплена. */
|
|
39
|
+
private readonly _sync;
|
|
40
|
+
private _start;
|
|
41
|
+
private _stop;
|
|
42
|
+
/** Высота шапки по оси блока — та же ось, что у отступа прокрутки темы. */
|
|
43
|
+
private readonly _onResize;
|
|
44
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { TBasePlugin, TElementPlugin } from "@soxa-ui/plugins";
|
|
2
|
+
//#region setup/plugins/table-head.plugin.ts
|
|
3
|
+
/** Высота шапки на корне таблицы: по ней CSS держит фокус в теле ниже шапки. */
|
|
4
|
+
var HEAD_HEIGHT = "--so-table-head-height";
|
|
5
|
+
/**
|
|
6
|
+
* TTableHeadPlugin — высота закреплённой шапки Table для CSS общего слоя тем.
|
|
7
|
+
*
|
|
8
|
+
* Закреплённая шапка (`stickyHead`) стоит у верхнего края прокрутки, и
|
|
9
|
+
* строки уходят под неё. Узел, на который браузер ведёт прокрутку к фокусу, —
|
|
10
|
+
* ячейку сетки после ↑ и PageUp, поле в ячейке после Shift+Tab, — общий слой
|
|
11
|
+
* тем (`components/table.css`) держит ниже шапки отступом прокрутки
|
|
12
|
+
* (`scroll-margin-block-start`) на её высоту. Высоту одной переменной CSS не
|
|
13
|
+
* знает: подписи заголовков переносятся, в слоте заголовка бывает что угодно,
|
|
14
|
+
* и шапка бывает выше строки шапки. Её и пишет плагин — переменной
|
|
15
|
+
* `--so-table-head-height` на корне таблицы, в px; без переменной отступ —
|
|
16
|
+
* высота строки шапки.
|
|
17
|
+
*
|
|
18
|
+
* Пока у таблицы есть корень и шапка закреплена, `ResizeObserver` следит за
|
|
19
|
+
* шапкой (`thead` — прямой ребёнок корня с классом части `__head`) и пишет её
|
|
20
|
+
* высоту на каждую смену. Шапку открепили, узел сняли или плагин уничтожен —
|
|
21
|
+
* наблюдение снято вместе с переменной. Переменная размера шапки не меняет —
|
|
22
|
+
* её читает только отступ прокрутки в теле, — и петли у наблюдателя нет.
|
|
23
|
+
*
|
|
24
|
+
* Плагин общего слоя тем, а не библиотеки: переменную читает CSS общего слоя
|
|
25
|
+
* — по ней он ставит отступ прокрутки к фокусу, — а библиотека о ней не
|
|
26
|
+
* знает. Ставит его тема своим `defineTheme` на все Table (у oren —
|
|
27
|
+
* `setup/plugins/install.ts`).
|
|
28
|
+
*/
|
|
29
|
+
var TTableHeadPlugin = class extends TBasePlugin {
|
|
30
|
+
_owner = null;
|
|
31
|
+
/** Корень таблицы, объявленный `TElementPlugin`; `null` — узла нет. */
|
|
32
|
+
_root = null;
|
|
33
|
+
/** Корень, на котором записана высота; `null` — плагин не следит за шапкой. */
|
|
34
|
+
_target = null;
|
|
35
|
+
_resize = null;
|
|
36
|
+
install(ctx) {
|
|
37
|
+
super.install(ctx);
|
|
38
|
+
this._owner = ctx.getInstance();
|
|
39
|
+
const element = ctx.get(TElementPlugin);
|
|
40
|
+
element?.events.on("ready", (node) => this._bind(node));
|
|
41
|
+
element?.events.on("removed", () => this._bind(null));
|
|
42
|
+
this._listenTo(this._owner?.events, "change:stickyHead", this._sync);
|
|
43
|
+
}
|
|
44
|
+
destroy() {
|
|
45
|
+
this._bind(null);
|
|
46
|
+
this._resize = null;
|
|
47
|
+
this._owner = null;
|
|
48
|
+
super.destroy();
|
|
49
|
+
}
|
|
50
|
+
_bind(root) {
|
|
51
|
+
this._stop();
|
|
52
|
+
this._root = root;
|
|
53
|
+
this._sync();
|
|
54
|
+
}
|
|
55
|
+
/** Следить за шапкой — пока у таблицы есть корень и шапка закреплена. */
|
|
56
|
+
_sync = () => {
|
|
57
|
+
const on = Boolean(this._root && this._owner?.stickyHead);
|
|
58
|
+
if (on && !this._target) this._start();
|
|
59
|
+
else if (!on && this._target) this._stop();
|
|
60
|
+
};
|
|
61
|
+
_start() {
|
|
62
|
+
const root = this._root;
|
|
63
|
+
const owner = this._owner;
|
|
64
|
+
if (!(root instanceof HTMLElement) || !owner) return;
|
|
65
|
+
const head = root.querySelector(`:scope > .${owner.classes.resolve("__head")}`);
|
|
66
|
+
if (!head) return;
|
|
67
|
+
this._target = root;
|
|
68
|
+
this._resize ??= new ResizeObserver(this._onResize);
|
|
69
|
+
this._resize.observe(head);
|
|
70
|
+
}
|
|
71
|
+
_stop() {
|
|
72
|
+
this._resize?.disconnect();
|
|
73
|
+
this._target?.style.removeProperty(HEAD_HEIGHT);
|
|
74
|
+
this._target = null;
|
|
75
|
+
}
|
|
76
|
+
/** Высота шапки по оси блока — та же ось, что у отступа прокрутки темы. */
|
|
77
|
+
_onResize = (entries) => {
|
|
78
|
+
const target = this._target;
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const [size] = entry.borderBoxSize;
|
|
81
|
+
if (target && size) target.style.setProperty(HEAD_HEIGHT, `${size.blockSize}px`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
export { TTableHeadPlugin };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ITabs } from '@soxa-ui/core';
|
|
2
|
+
import { TBasePlugin } from '@soxa-ui/plugins';
|
|
3
|
+
import type { IPluginContext } from '@soxa-ui/plugins';
|
|
4
|
+
export declare class TTabsViewPlugin extends TBasePlugin<ITabs> {
|
|
5
|
+
private _tabs;
|
|
6
|
+
private _engine;
|
|
7
|
+
/**
|
|
8
|
+
* Таб, чьё место плагин записал последним; `null` — ещё не записывал или
|
|
9
|
+
* записал нули: активного таба не было.
|
|
10
|
+
*/
|
|
11
|
+
private _drawn;
|
|
12
|
+
/** Ряд, на котором стоит признак переезда; `null` — признака нет. */
|
|
13
|
+
private _moving;
|
|
14
|
+
/** Отмена ожидания конца переезда; `null` — плагин не ждёт. */
|
|
15
|
+
private _stopWaiting;
|
|
16
|
+
install(ctx: IPluginContext): void;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
/** Место и длина активного таба по главной оси. */
|
|
19
|
+
private _updateActiveTab;
|
|
20
|
+
/** Разрыв линии ряда под активным табом. */
|
|
21
|
+
private _updateGap;
|
|
22
|
+
/** Признак переезда на ряд — до записи нового места. */
|
|
23
|
+
private _startMoving;
|
|
24
|
+
/** Снять признак переезда и ожидание его конца. */
|
|
25
|
+
private _stopMoving;
|
|
26
|
+
/**
|
|
27
|
+
* Забыть всё, что плагин нарисовал: узел снят или плагин уничтожен. Новый
|
|
28
|
+
* узел до первого замера получит прежнюю разметку без переменных — тема
|
|
29
|
+
* рисует её статично, — а его первая запись идёт без признака.
|
|
30
|
+
*/
|
|
31
|
+
private _forget;
|
|
32
|
+
}
|