@marianmeres/stuic 3.152.0 → 3.154.0

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 (31) hide show
  1. package/AGENTS.md +3 -0
  2. package/API.md +1 -0
  3. package/README.md +72 -0
  4. package/dist/components/FieldsBuilder/FieldsBuilder.svelte +1214 -0
  5. package/dist/components/FieldsBuilder/FieldsBuilder.svelte.d.ts +102 -0
  6. package/dist/components/FieldsBuilder/README.md +248 -0
  7. package/dist/components/FieldsBuilder/_internal/LocalizedTextInput.svelte +229 -0
  8. package/dist/components/FieldsBuilder/_internal/LocalizedTextInput.svelte.d.ts +30 -0
  9. package/dist/components/FieldsBuilder/_internal/OptionsEditor.svelte +296 -0
  10. package/dist/components/FieldsBuilder/_internal/OptionsEditor.svelte.d.ts +18 -0
  11. package/dist/components/FieldsBuilder/i18n-sk.d.ts +22 -0
  12. package/dist/components/FieldsBuilder/i18n-sk.js +81 -0
  13. package/dist/components/FieldsBuilder/i18n.d.ts +84 -0
  14. package/dist/components/FieldsBuilder/i18n.js +90 -0
  15. package/dist/components/FieldsBuilder/index.css +187 -0
  16. package/dist/components/FieldsBuilder/index.d.ts +5 -0
  17. package/dist/components/FieldsBuilder/index.js +4 -0
  18. package/dist/components/FieldsBuilder/types.d.ts +76 -0
  19. package/dist/components/FieldsBuilder/types.js +1 -0
  20. package/dist/components/FieldsBuilder/utils.d.ts +66 -0
  21. package/dist/components/FieldsBuilder/utils.js +153 -0
  22. package/dist/css/frame.css +109 -0
  23. package/dist/index.css +4 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/docs/RATIO_LOCKED_FRAME.md +466 -0
  27. package/docs/architecture.md +6 -0
  28. package/docs/domains/components.md +61 -1
  29. package/docs/domains/css-presets.md +304 -0
  30. package/docs/domains/theming.md +2 -0
  31. package/package.json +1 -1
@@ -0,0 +1,304 @@
1
+ # CSS Presets Domain
2
+
3
+ ## Overview
4
+
5
+ `src/lib/css/` holds **CSS presets**: a class plus a custom-property contract, with no JS and
6
+ no Svelte component. A preset is used by putting a class on **your own** element and (optionally)
7
+ setting a token on it or an ancestor.
8
+
9
+ Today there is exactly one: the **ratio-locked frame** (`src/lib/css/frame.css`). This document
10
+ also covers the two remaining global class surfaces that live directly in `src/lib/index.css`
11
+ — `.scrollbar-thin` and `.stuic-safe-area-*` — because they have no other home.
12
+
13
+ > **`G<n>` references** point at the measured gotcha list in
14
+ > [Ratio-Locked Frame Reference](../RATIO_LOCKED_FRAME.md#gotchas) — every entry there carries
15
+ > the number it was measured at, and in which engines.
16
+
17
+ **Two rules govern everything below.**
18
+
19
+ 1. **Presets are in `@layer components`, so utilities always win.** Tailwind emits
20
+ `@layer theme, base, components, utilities`. `class="stuic-frame h-dvh overflow-y-auto bg-white"`
21
+ overrides every declaration in the preset. That is the escape hatch, by design. (Contrast:
22
+ `.scrollbar-thin` and `.stuic-safe-area-*` are deliberately **unlayered**, so they beat a
23
+ consumer's `utilities` layer — see their sections.)
24
+ 2. **stuic declares none of the preset's tokens anywhere.** Defaults exist only as `var()`
25
+ fallbacks at usage sites, per the Fallback Pattern in [conventions.md](../conventions.md).
26
+ This is what makes a scoped override (`style="--stuic-frame-aspect-ratio: 0.25"` on the element,
27
+ or on any ancestor) actually work. See **G11** for the measured cost of getting this wrong.
28
+
29
+ ---
30
+
31
+ ## Ratio-locked frame (letterbox)
32
+
33
+ Lock a box to an aspect ratio, size it to whichever axis binds first, centre it, and let the
34
+ leftover space become letterboxing.
35
+
36
+ ### The pattern in one line
37
+
38
+ ```
39
+ parent: centre + clip
40
+ child: width = min(available-width, available-height × ratio)
41
+ ```
42
+
43
+ The child's height then comes from `aspect-ratio`. Deriving the width from the height and letting
44
+ `aspect-ratio` derive the height back is **not** circular: `width` resolves first, and
45
+ `aspect-ratio` only ever fills an `auto` axis (**G14**).
46
+
47
+ ### Classes
48
+
49
+ | Class | Does | Use when |
50
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
51
+ | `.stuic-frame` | The ratio-locked box, sized in `vw`/`dvh`, centred via `margin: auto` | The frame is anchored to the viewport |
52
+ | `.stuic-frame-cq` | Same width formula in `cqw`/`cqh`. **Combine with** `.stuic-frame` | The frame is nested (under a header, inside a flex column) |
53
+ | `.stuic-frame-col` | `width: min(100%, <frame width>)` + `margin-inline: auto`. Nothing else — no ratio. Viewport-space only — it cannot track a `.stuic-frame-cq` frame | Re-align a viewport-space element (top-layer dialog, body-portalled overlay) onto the frame's column |
54
+
55
+ `.stuic-frame` also sets `min-height: 0` and `overflow: hidden`. Both are **load-bearing for the
56
+ ratio**, not cosmetics — see **G13**. Do not "simplify" them away.
57
+
58
+ `.stuic-frame-cq` must stay **after** `.stuic-frame` in source order (equal specificity, same
59
+ layer). It is, in `frame.css`; do not reorder.
60
+
61
+ ### Token contract
62
+
63
+ All three are consumer **inputs**. stuic declares none of them.
64
+
65
+ | Token | Default (as a `var()` fallback) | Meaning |
66
+ | ---------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
67
+ | `--stuic-frame-aspect-ratio` | `1` | width ÷ height. Any value valid in `aspect-ratio:` |
68
+ | `--stuic-frame-width` | `min(100vw, calc(100dvh * (ratio)))`, or the `cq` form under `.stuic-frame-cq` | Wholesale width override. Bail-out value: `100vw` — **pair with `--stuic-frame-height: 100dvh`**; alone, `aspect-ratio` still supplies the height and you get a `100vw` square |
69
+ | `--stuic-frame-height` | `auto` ⇒ ratio-locked | Wholesale height override. `100dvh` ⇒ fill the height, derive the width |
70
+
71
+ The default ratio is **`1`**, not a plausible app ratio: a square is unmistakably "you forgot to set
72
+ the ratio", where a plausible default would ship a plausible-looking wrong layout.
73
+
74
+ **Ratio form.** Anything `aspect-ratio` accepts. Prefer `calc(16 / 9)` or a bare number. `16 / 9`
75
+ works too — `frame.css` parenthesises every `var()` inside every `calc()`, so the library is safe
76
+ with all three forms. **Your** `calc()`s are not: see **G9**.
77
+
78
+ **The four override scenarios**, each measured:
79
+
80
+ ```css
81
+ /* 1. GLOBAL — measured 450x900 at ratio 0.5 on a 1440x900 viewport;
82
+ 384px wide at 1200x800 with the 0.48 below */
83
+ :root {
84
+ --stuic-frame-aspect-ratio: 0.48;
85
+ }
86
+ ```
87
+
88
+ ```html
89
+ <!-- 2. SCOPED (per element, per ancestor; two frames, two ratios, one page) —
90
+ measured 225x900 at r=0.25 next to 1440x810 at r=1.7778 -->
91
+ <div class="stuic-frame" style="--stuic-frame-aspect-ratio: 0.25"></div>
92
+ ```
93
+
94
+ ```css
95
+ /* 3. MEDIA-QUERY WHOLESALE (the bail-out) — measured 320x568 full bleed;
96
+ without it, 284x568 with 18px bars */
97
+ @media (max-width: 40rem) and (max-aspect-ratio: 3 / 5) {
98
+ :root {
99
+ --stuic-frame-width: 100vw;
100
+ --stuic-frame-height: 100dvh;
101
+ }
102
+ }
103
+ ```
104
+
105
+ ```html
106
+ <!-- 4. TOP-LAYER READ — a dialog that is a DOM descendant of the frame measures 225px,
107
+ tracking the scoped ratio (custom properties inherit into the top layer; geometry
108
+ does not). The same dialog portalled to <body> measures 450px, the :root ratio. -->
109
+ <dialog class="m-0 h-dvh w-screen max-w-none border-0 p-0">
110
+ <div class="stuic-frame-col">…</div>
111
+ </dialog>
112
+ ```
113
+
114
+ **Aliasing the width** for call sites outside the frame (e.g. a dropdown that must not exceed the
115
+ column). Measured working, both engines:
116
+
117
+ ```css
118
+ :root {
119
+ --stuic-frame-aspect-ratio: 0.48;
120
+ --app-width: var(
121
+ --stuic-frame-width,
122
+ min(100vw, calc(100dvh * (var(--stuic-frame-aspect-ratio))))
123
+ );
124
+ }
125
+ ```
126
+
127
+ The alias freezes the ratio at its `:root` value — a scoped `--stuic-frame-aspect-ratio` override
128
+ will not move it (**G11**), so use it only where the ratio is global.
129
+
130
+ ### What this preset deliberately does NOT ship
131
+
132
+ Everything else in a letterbox layout is already a Tailwind v4 utility. All of these were compiled
133
+ against this repo's own `tailwindcss@4.3.3`:
134
+
135
+ | Job | Tailwind v4 utility | Compiles to |
136
+ | -------------------------------------- | ----------------------------------------- | ----------------------------- |
137
+ | The letterbox bars | `fixed inset-0 grid overflow-hidden bg-*` | — |
138
+ | Trap `position: fixed` descendants | `contain-layout contain-paint` | `contain: layout paint` |
139
+ | Frame is the scroll container | `overflow-y-auto` | `overflow-y: auto` |
140
+ | `cq` units resolve here | `@container-size` | `container-type: size` |
141
+ | (inline-size only — avoid, see **G5**) | `@container` | `container-type: inline-size` |
142
+ | Clear the ratio entirely | `aspect-auto` | `aspect-ratio: auto` |
143
+
144
+ Only the sizing formula is stuic's, because it is the only part Tailwind cannot express. The preset
145
+ ships **no background** either — three known call sites paint three different ways, one of them on
146
+ `body`.
147
+
148
+ ---
149
+
150
+ ## Decision tree
151
+
152
+ **1. Locked box, or derived-width column?**
153
+
154
+ - The whole composition scales proportionally, bars on one axis → `.stuic-frame` (leave
155
+ `--stuic-frame-height: auto`).
156
+ - You want to fill the height and only clamp the width (a column that scrolls) →
157
+ `--stuic-frame-height: 100dvh`. **This is no longer ratio-locked** — read **G15** before choosing it.
158
+
159
+ **2. Do fixed-positioned descendants belong to the frame, or to the screen?**
160
+
161
+ - **To the frame** → add `contain-layout contain-paint` to the frame element. Everything
162
+ `position: fixed` inside now resolves against the frame, with zero component changes.
163
+ **Then do not also make the frame the scroll container — G4b.**
164
+ - **To the screen** → add no containment. Top-layer dialogs and viewport-space overlays stay in
165
+ viewport space, and you reconcile them onto the column with `.stuic-frame-col`. This is the safer
166
+ default and the one that survives `Drawer`, `Backdrop` and scrolled routes.
167
+
168
+ **3. `vw`/`dvh`, or `cq`?**
169
+
170
+ - Frame anchored to the viewport (fixed wrapper, or `body`) → `.stuic-frame` alone.
171
+ - Frame nested inside a box of unknown size → `.stuic-frame .stuic-frame-cq` **plus**
172
+ `@container-size` on an ancestor. `@container` (inline-size) is a silent trap (**G5**).
173
+
174
+ **4. Where does the bail-out query go?**
175
+
176
+ - On `:root` (or any ancestor), as tokens: `--stuic-frame-width: 100vw` and
177
+ `--stuic-frame-height: 100dvh`. Not as a class override on `.stuic-frame` — a token override
178
+ needs no `!important` and no specificity game, and it works without touching the markup.
179
+ - Give it a **second condition** — a `min-height`, or a `max-aspect-ratio`. A width-only breakpoint
180
+ is almost always wrong (**G6**).
181
+
182
+ ---
183
+
184
+ ## Gotchas
185
+
186
+ **Engine provenance, stated once.** Every measured number in **both** this document and the
187
+ reference manual was taken with Playwright on **Chromium 151.0.7922.34** and **WebKit 26.5**, plus
188
+ **Firefox 153** for the containment rows; results are identical across engines unless a row says
189
+ otherwise, and none of it says anything about **old Safari**. The full list lives in
190
+ [Ratio-Locked Frame Reference](../RATIO_LOCKED_FRAME.md#gotchas), where the items that are _not_
191
+ browser measurements are labelled as such.
192
+
193
+ > ### G4b — THE ONE THAT WILL ACTUALLY BITE YOU
194
+ >
195
+ > **Do not make the frame BOTH the fixed containing block AND the scroll container.**
196
+ > `contain-layout contain-paint` + `overflow-y-auto` on the same element **breaks every
197
+ > `position: fixed` descendant.**
198
+ >
199
+ > Measured, byte-identical in both engines: at `scrollTop: 600`, a `position: fixed; inset: 0`
200
+ > child renders at **`y = -600`**, and a bottom-right FAB jumps from `y = 732` to `y = 132`. They
201
+ > are not fixed any more; they are effectively `absolute` against the scroll origin.
202
+ >
203
+ > **Concrete stuic consequence:** open a `Drawer` on a route scrolled to 600 and the backdrop and
204
+ > the drawer render at `y = -600`. The user taps, and nothing appears. `BodyScroll.lock()` does
205
+ > **not** rescue this: `src/lib/utils/body-scroll-locker.ts` only pins `document.body`, which in
206
+ > this layout has nothing to scroll (`window.scrollY === 0`), so the wheel keeps scrolling the
207
+ > frame and the invisible drawer travels on to `y = -900`.
208
+ >
209
+ > Modal `<dialog>`s survive (top layer, and they block wheel chaining). `Drawer`, `Backdrop`,
210
+ > `DropdownMenu` and the body-portalled actions do not.
211
+ >
212
+ > **Fix:** pick one. Either scroll an inner element and keep containment on the frame, or keep the
213
+ > frame scrollable and accept viewport-space overlays, reconciling them with `.stuic-frame-col`.
214
+ >
215
+ > This is the trap most likely to be shipped by someone following the naive advice, because the
216
+ > naive advice is internally consistent: _"`contain: paint` clips, so make the frame scroll."_
217
+
218
+ **The full list — G1 through G21, each with its measurement — is in
219
+ [Ratio-Locked Frame Reference](../RATIO_LOCKED_FRAME.md#gotchas).** The four most expensive, in
220
+ short: the popular `max-width`/`max-height`/`aspect-ratio` formulation collapses to 0×0 in a centred
221
+ grid/flex parent (G1); `container-type` does **not** create a fixed containing block (G2); `cq` units
222
+ on the element that declares `container-type` resolve against an _ancestor_ container, or silently
223
+ against the viewport (G5); and an explicit `height: 100dvh` is not ratio-locking (G15).
224
+
225
+ ---
226
+
227
+ ## Recipes
228
+
229
+ Four worked recipes — viewport letterbox, `body`-as-letterbox, nested container-units, and the
230
+ unit-free `max-*` variant — are in
231
+ [Ratio-Locked Frame Reference](../RATIO_LOCKED_FRAME.md#recipes).
232
+
233
+ ---
234
+
235
+ ## Interop with the rest of stuic
236
+
237
+ **The honest headline: adopting this preset does NOT make stuic's overlays frame-aware.** The
238
+ preset names an ancestor; it does not teach anything to measure against it. Expect per-call-site
239
+ tweaks.
240
+
241
+ Three categories — trapped, escaping, portalled — each verified against the source:
242
+
243
+ | Component / action | Rendering | Behaviour under a `contain:`-ed frame |
244
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
245
+ | `Backdrop` | `position: fixed; inset: 0; height: 100dvh`, in place | **Trapped** by containment — but `height: 100dvh` overrides the inset-derived height, so it overflows a short frame (**G18**) |
246
+ | `Drawer` | wraps `Backdrop`, `position: fixed`, in place | **Trapped** — and the primary victim of **G4b** |
247
+ | `DropdownMenu`, `Float`, `HoverExpandableWidth` | `position: fixed`, in place | **Trapped**, and already **CB-aware**: they measure via `fixedContainingBlockRect()` (3.152.0) |
248
+ | `ModalDialog`, and `Modal` through it | `<dialog>` + **always** `.showModal()`, **no portal** | **Escapes** — top layer, viewport geometry. But it is still a DOM descendant, so custom properties inherit (**G3**): size its content from the inherited `--stuic-frame-aspect-ratio`, or use `.stuic-frame-col`, which repeats the fallback (**G12**). `Modal`'s own box is `md:max-w-[calc(100vw-2rem)]` / `md:max-h-[80dvh]` — viewport units (**G18**) |
249
+ | `Notifications` | `popover="manual"` + `showPopover()`, `position: fixed; inset: 0; width/height: 100%` | **Escapes** — top layer, viewport-sized. Toasts render on the bars, not in the frame |
250
+ | `popover`, `spotlight`, `dimBehind` actions | **portalled to `document.body`** by default | **Escape by portalling** (**G19**). Pass the `container` option added in 3.152.0 |
251
+
252
+ **What to do about each** — passing `container` to the portalled actions, reconciling top-layer
253
+ escapees with `.stuic-frame-col`, and what `BodyScroll` does and does not lock — is in
254
+ [Ratio-Locked Frame Reference](../RATIO_LOCKED_FRAME.md#working-with-stuics-overlays).
255
+
256
+ ---
257
+
258
+ ## Safe-area insets (`.stuic-safe-area-*`)
259
+
260
+ Global, **unlayered** so a deliberate `pt-*` in a consumer's `utilities` layer cannot silently win
261
+ over a safe-area offset. Declared at the end of `src/lib/index.css`.
262
+
263
+ Two surfaces:
264
+
265
+ | Surface | Semantics |
266
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
267
+ | `--stuic-safe-area-{top,right,bottom,left}` | `0px` always; the real `env()` inset only under standalone/fullscreen. **Compose** them: `padding-block-start: calc(1rem + var(--stuic-safe-area-top))` |
268
+ | `.stuic-safe-area-{top,right,bottom,left}` | **Set** the padding on that axis (they replace, not add). Only for elements that do not otherwise pad that side |
269
+
270
+ Both are gated behind `@media (display-mode: standalone), (display-mode: fullscreen)` and are
271
+ therefore **inert in a normal browser tab** and **hard-zero in a Capacitor WKWebView** (**G21**).
272
+ Non-zero values also require the consuming app to declare `viewport-fit=cover`.
273
+
274
+ Full detail, including which components handle insets automatically and which do not, is in the
275
+ **PWA safe-area insets** section of [README.md](../../README.md).
276
+
277
+ ---
278
+
279
+ ## `.scrollbar-thin`
280
+
281
+ Global, unlayered, one declaration: `scrollbar-width: thin`. Declared in `src/lib/index.css`.
282
+
283
+ Standard-property only — no `::-webkit-scrollbar` rules, no colour token. Apply it to any scroll
284
+ container whose default scrollbar is too heavy. Because it is unlayered, a Tailwind utility will not
285
+ override it; use an inline style or your own more-specific rule if you need to opt an element back
286
+ out.
287
+
288
+ ---
289
+
290
+ ## Key files
291
+
292
+ | File | Purpose |
293
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------------- |
294
+ | `src/lib/css/frame.css` | The preset. Three classes, nine declarations, and the reasoning inline |
295
+ | `src/lib/index.css` | `@import "./css/frame.css"`; also home to `.scrollbar-thin` and `.stuic-safe-area-*` |
296
+ | `src/lib/css/frame.svelte.test.ts` | Browser (Chromium) regression locks: the formula, plus G11–G15 and the G2 distinction |
297
+ | `src/lib/css-wiring.test.ts` | Asserts every stylesheet under `src/lib` is reachable from `index.css` — the failure nothing else can see |
298
+ | `src/lib/utils/containing-block.ts` | `isFixedContainingBlock()` / `fixedContainingBlockRect()` — the G2 rule set |
299
+ | `src/lib/utils/overlay-container.ts` | The `container` option shared by `popover` / `spotlight` / `dimBehind` (**G19**) |
300
+ | `src/lib/utils/body-scroll-locker.ts` | `BodyScroll.lock()` — locks `document.body` only (**G4b**) |
301
+ | `src/lib/components/Backdrop/index.css` | `position: fixed; inset: 0; height: 100dvh` (**G18**) |
302
+
303
+ Related: [theming.md](./theming.md) for the token system, [components.md](./components.md) for the
304
+ overlay components, [actions.md](./actions.md) for the portalled overlay actions.
@@ -249,6 +249,8 @@ Override locally:
249
249
  <Button style="--stuic-button-radius: 0;">Square</Button>
250
250
  ```
251
251
 
252
+ Some token sets belong to a CSS-only preset rather than to a component — e.g. `--stuic-frame-*` (ratio-locked frame / letterbox). Those are consumer **inputs** that stuic never declares; see [CSS presets](./css-presets.md).
253
+
252
254
  ---
253
255
 
254
256
  ## Key Files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.152.0",
3
+ "version": "3.154.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",