@marianmeres/stuic 3.128.0 → 3.130.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.
package/API.md CHANGED
@@ -134,37 +134,38 @@ Navigation wrapper component.
134
134
 
135
135
  Responsive navigation header with leading slot, logo, nav items, locale switcher, action icon buttons, avatar, and configurable responsive collapse (`"hamburger"` fold or `"hide"` for app-like shells). Renders as `<header>`.
136
136
 
137
- | Prop | Type | Default | Description |
138
- | ----------------------- | ------------------------------------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
139
- | `leading` | `Snippet<[{ isCollapsed }]>` | — | Leading (left-side) slot. Overrides `leadingHamburger`. |
140
- | `leadingHamburger` | `boolean \| "collapsed"` | `false` | Built-in hamburger in the leading slot (`"collapsed"` = only below threshold). Ignored when `leading` is provided. |
141
- | `onLeadingHamburger` | `() => void` | — | Click handler for the built-in leading hamburger (typically opens a drawer). |
142
- | `leadingHamburgerIcon` | `THC` | menu icon | Icon override for the built-in leading hamburger. |
143
- | `leadingHamburgerLabel` | `string` | `"Open menu"` | Aria-label for the leading hamburger. |
144
- | `logo` | `Snippet` | — | Logo/brand snippet. |
145
- | `projectName` | `string` | — | Simple text logo alternative. |
146
- | `navVariant` | `ButtonVariant` | `"ghost"` | Button variant for nav items and the locale switcher trigger. |
147
- | `items` | `HeaderNavItem[]` | `[]` | Navigation items — inline when expanded, dropdown when collapsed (hamburger mode). |
148
- | `actions` | `HeaderActionItem[]` | `[]` | Action icon buttons between the locale switcher and the avatar. Always visible — never fold into the dropdown. |
149
- | `onActionSelect` | `(action) => void` | — | Called after the per-item `onclick`. |
150
- | `avatar` | `Snippet` | — | Avatar snippet (far right). |
151
- | `avatarOnClick` | `() => void` | — | Makes the avatar interactive. In `"hamburger"` collapse mode it moves into the dropdown. |
152
- | `avatarLabel` | `THC` | `"Account"` | Label for the avatar entry inside the collapsed dropdown. |
153
- | `locales` | `HeaderLocaleItem[]` | `[]` | Locale items. Switcher only renders when 2+. |
154
- | `activeLocale` | `string` | — | Current locale id. |
155
- | `onLocaleChange` | `(localeId) => void` | — | Locale selection callback. |
156
- | `localeLabel` | `THC` | `"Language"` | Section header inside the collapsed dropdown. |
157
- | `contentMaxWidth` | `string \| number` | — | Max-width of the inner content row (outer header stays 100%). Accepts any CSS length. Maps to `--stuic-header-content-max-width`. |
158
- | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables. |
159
- | `collapseMode` | `"hamburger" \| "hide"` | `"hamburger"` | Collapse behavior. `"hide"` keeps avatar/actions visible and renders no trailing hamburger (app-shell pattern). |
160
- | `keepLocaleOnCollapse` | `boolean` | `false` | Keep the locale switcher visible in collapsed mode (only `collapseMode === "hide"`). |
161
- | `fixed` | `boolean` | `false` | Fixed positioning at the top. |
162
- | `isCollapsed` | `boolean` | | Bindable: collapsed state. |
163
- | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open. |
164
- | `dropdownPosition` | `DropdownMenuPosition` | `"bottom-span-right"` | Position of the collapsed dropdown. |
165
- | `iconSize` | `number` | `24` | Hamburger/X icon size in px. |
166
- | `onSelect` | `(item) => void` | | Item selection callback (both modes). |
167
- | `children` | `Snippet<[{ isCollapsed, items, offsetWidth }]>` | — | Escape hatch: override the entire inner layout. |
137
+ | Prop | Type | Default | Description |
138
+ | ----------------------- | ------------------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
139
+ | `leading` | `Snippet<[{ isCollapsed }]>` | — | Leading (left-side) slot. Overrides `leadingHamburger`. |
140
+ | `leadingHamburger` | `boolean \| "collapsed"` | `false` | Built-in hamburger in the leading slot (`"collapsed"` = only below threshold). Ignored when `leading` is provided. |
141
+ | `onLeadingHamburger` | `() => void` | — | Click handler for the built-in leading hamburger (typically opens a drawer). |
142
+ | `leadingHamburgerIcon` | `THC` | menu icon | Icon override for the built-in leading hamburger. |
143
+ | `leadingHamburgerLabel` | `string` | `"Open menu"` | Aria-label for the leading hamburger. |
144
+ | `logo` | `Snippet` | — | Logo/brand snippet. |
145
+ | `projectName` | `string` | — | Simple text logo alternative. |
146
+ | `navVariant` | `ButtonVariant` | `"ghost"` | Button variant for nav items and the locale switcher trigger. |
147
+ | `items` | `HeaderNavItem[]` | `[]` | Navigation items — inline when expanded, dropdown when collapsed (hamburger mode). |
148
+ | `actions` | `HeaderActionItem[]` | `[]` | Action icon buttons between the locale switcher and the avatar. Always visible — never fold into the dropdown. |
149
+ | `onActionSelect` | `(action) => void` | — | Called after the per-item `onclick`. |
150
+ | `avatar` | `Snippet` | — | Avatar snippet (far right). |
151
+ | `avatarOnClick` | `() => void` | — | Makes the avatar interactive. In `"hamburger"` collapse mode it moves into the dropdown. |
152
+ | `avatarLabel` | `THC` | `"Account"` | Label for the avatar entry inside the collapsed dropdown. |
153
+ | `locales` | `HeaderLocaleItem[]` | `[]` | Locale items. Switcher only renders when 2+. |
154
+ | `activeLocale` | `string` | — | Current locale id. |
155
+ | `onLocaleChange` | `(localeId) => void` | — | Locale selection callback. |
156
+ | `localeLabel` | `THC` | `"Language"` | Section header inside the collapsed dropdown. |
157
+ | `contentMaxWidth` | `string \| number` | — | Max-width of the inner content row (outer header stays 100%). Accepts any CSS length. Maps to `--stuic-header-content-max-width`. |
158
+ | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables. |
159
+ | `collapseMode` | `"hamburger" \| "hide"` | `"hamburger"` | Collapse behavior. `"hide"` keeps avatar/actions visible and renders no trailing hamburger (app-shell pattern). |
160
+ | `keepLocaleOnCollapse` | `boolean` | `false` | Keep the locale switcher visible in collapsed mode (only `collapseMode === "hide"`). |
161
+ | `fixed` | `boolean` | `false` | Fixed positioning at the top. |
162
+ | `safeArea` | `boolean` | `false` | PWA: when installed/standalone, offset the **top app bar** below the device safe-area insets (top + side notch). No-op in a browser tab. Set only on the top bar — not in-page/detail/drawer headers. See Header README. |
163
+ | `isCollapsed` | `boolean` | — | Bindable: collapsed state. |
164
+ | `isMenuOpen` | `boolean` | | Bindable: hamburger menu open. |
165
+ | `dropdownPosition` | `DropdownMenuPosition` | `"bottom-span-right"` | Position of the collapsed dropdown. |
166
+ | `iconSize` | `number` | `24` | Hamburger/X icon size in px. |
167
+ | `onSelect` | `(item) => void` | — | Item selection callback (both modes). |
168
+ | `children` | `Snippet<[{ isCollapsed, items, offsetWidth }]>` | — | Escape hatch: override the entire inner layout. |
168
169
 
169
170
  Class slots: `class`, `classContent`, `classLeading`, `classLeadingHamburger`, `classLogo`, `classNav`, `classNavItem`, `classNavItemActive`, `classActions`, `classAction`, `classActionActive`, `classEnd`, `classAvatar`, `classLocale`, `classHamburger`, `classDropdown`.
170
171
 
package/README.md CHANGED
@@ -183,6 +183,48 @@ Cart, Checkout (CheckoutProgress, CheckoutOrderSummary, CheckoutCartReview, Chec
183
183
 
184
184
  `autogrow` · `validate` · `focusTrap` · `autoscroll` · `dimBehind` · `fileDropzone` · `highlightDragover` · `resizableWidth` · `spotlight` · `trim` · `typeahead` · `onSubmitValidityCheck` · `popover` · `tooltip` · `createTour` / `tourStep` (onboarding)
185
185
 
186
+ ## PWA safe-area insets
187
+
188
+ When a stuic app is installed and launched standalone (iOS Home Screen, Android/desktop PWA), the web view fills the entire screen, so edge-anchored chrome can render under the status bar / notch / home indicator. stuic ships an **opt-in** safe-area layer that is **inert in a normal browser tab** (`env()` → `0`) and only engages under `@media (display-mode: standalone), (display-mode: fullscreen)`.
189
+
190
+ > The insets are only non-zero when your app sets `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">` (your responsibility) **and** the device has an inset.
191
+
192
+ **Component props (opt-in):**
193
+
194
+ - `Header` — `safeArea` offsets the top app bar below the top + side insets. See the [Header README](src/lib/components/Header/README.md). Set it only on the **top** app bar, never on in-page / detail / drawer-internal headers.
195
+
196
+ **Automatic:**
197
+
198
+ - `Notifications` — the toast stack always keeps clear of the insets in standalone (you never want a toast under the home indicator).
199
+ - `Modal` — when full-bleed (below the `md` breakpoint, where it fills the screen), its content is padded by the insets so header / content / footer clear the system UI. Centered desktop modals are untouched.
200
+ - `AssetsPreview` — the lightbox image stays edge-to-edge, but the overlay controls (close, prev/next, dots, filename) are offset so they clear the insets.
201
+
202
+ All three are no-ops in a browser tab and need no prop.
203
+
204
+ **Utility classes** — offset any edge-anchored element without writing your own `env()` rule. They are active in standalone only:
205
+
206
+ ```html
207
+ <header class="my-top-bar stuic-safe-area-top">…</header>
208
+ <footer class="my-bottom-bar stuic-safe-area-bottom">…</footer>
209
+ <!-- also: stuic-safe-area-left / stuic-safe-area-right -->
210
+ ```
211
+
212
+ > ⚠️ These **set** the padding on their axis (they replace, not add). Apply them only to an element that doesn't already pad that side. To **add** inset on top of existing padding, compose the variables below instead.
213
+
214
+ **CSS variables** — composable insets (`0px` everywhere, real device insets in standalone):
215
+
216
+ ```css
217
+ .my-bottom-bar {
218
+ /* keep my own 1rem and add the home-indicator inset on top */
219
+ padding-bottom: calc(1rem + var(--stuic-safe-area-bottom));
220
+ }
221
+ /* also: --stuic-safe-area-top / --stuic-safe-area-left / --stuic-safe-area-right */
222
+ ```
223
+
224
+ **Pick ONE layer.** Don't pad the same edge twice in a nesting chain. E.g. a nav `Drawer` whose content is its own stuic `Header`: put `safeArea` on the inner `Header`, not also on the drawer panel/wrapper.
225
+
226
+ **Not covered:** remaining fixed/edge-anchored components (e.g. `Float`, or a bare `ModalDialog` used directly) do not auto-handle insets — apply a `stuic-safe-area-*` class or the variables to their content as needed.
227
+
186
228
  ## TypeScript
187
229
 
188
230
  All components export their Props types:
@@ -45,6 +45,7 @@ interface AssetPreview {
45
45
  - **Touch support**: Touch gestures for pan
46
46
  - **Auto-preload**: Preloads full-resolution images when modal opens
47
47
  - **File type icons**: Displays appropriate icons for non-image files
48
+ - **PWA safe area**: when installed/standalone, the overlay controls (close, prev/next, dots, filename) are automatically offset so they clear the status bar / notch / home indicator. The image itself stays edge-to-edge. Inert in a browser tab (`env()` → `0`); no prop required.
48
49
 
49
50
  ## Translation Keys
50
51
 
@@ -658,8 +658,11 @@
658
658
  <div
659
659
  class={twMerge(
660
660
  "absolute inset-0 flex justify-between pointer-events-none",
661
- prevNextBottom ? "items-end pb-4" : "items-center"
661
+ prevNextBottom ? "items-end" : "items-center"
662
662
  )}
663
+ style="padding-left: var(--stuic-safe-area-left, 0px); padding-right: var(--stuic-safe-area-right, 0px);{prevNextBottom
664
+ ? ' padding-bottom: calc(1rem + var(--stuic-safe-area-bottom, 0px));'
665
+ : ''}"
663
666
  >
664
667
  <Button
665
668
  class={twMerge(BUTTON_CLS, "ml-4", classControls)}
@@ -681,7 +684,17 @@
681
684
  </div>
682
685
  {/if}
683
686
 
684
- <div class="absolute top-4 left-4 right-4 flex items-center justify-between gap-3">
687
+ <!-- Top control bar (close, download, zoom, filename). Offset by the PWA
688
+ safe-area insets when installed/standalone so the controls clear the
689
+ status bar / notch / side notch. The vars are 0px in a browser tab. -->
690
+ <div
691
+ class="absolute flex items-center justify-between gap-3"
692
+ style="
693
+ top: calc(1rem + var(--stuic-safe-area-top, 0px));
694
+ left: calc(1rem + var(--stuic-safe-area-left, 0px));
695
+ right: calc(1rem + var(--stuic-safe-area-right, 0px));
696
+ "
697
+ >
685
698
  {#if !noName && previewAsset?.name}
686
699
  <span class="stuic-assets-preview-label truncate px-1">
687
700
  {previewAsset?.name}
@@ -767,7 +780,8 @@
767
780
  {#if !noDots && assets.length <= 10}
768
781
  {#if !noName && dotTooltip}
769
782
  <div
770
- class="absolute bottom-10 left-0 right-0 text-center"
783
+ class="absolute left-0 right-0 text-center"
784
+ style="bottom: calc(2.5rem + var(--stuic-safe-area-bottom, 0px));"
771
785
  transition:fade={{ duration: 100 }}
772
786
  >
773
787
  <span class="stuic-assets-preview-label p-1">
@@ -775,7 +789,10 @@
775
789
  </span>
776
790
  </div>
777
791
  {/if}
778
- <div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-3">
792
+ <div
793
+ class="absolute left-1/2 -translate-x-1/2 flex items-center gap-3"
794
+ style="bottom: calc(1rem + var(--stuic-safe-area-bottom, 0px));"
795
+ >
779
796
  {#each assets as _, i}
780
797
  <!-- svelte-ignore a11y_mouse_events_have_key_events -->
781
798
  <button
@@ -793,7 +810,10 @@
793
810
  {/each}
794
811
  </div>
795
812
  {:else if !noCurrentOfTotal}
796
- <div class="absolute bottom-4 left-1/2 -translate-x-1/2">
813
+ <div
814
+ class="absolute left-1/2 -translate-x-1/2"
815
+ style="bottom: calc(1rem + var(--stuic-safe-area-bottom, 0px));"
816
+ >
797
817
  <span class="stuic-assets-preview-label px-2 py-1 text-sm opacity-60">
798
818
  {previewIdx + 1} / {assets.length}
799
819
  </span>
@@ -158,6 +158,15 @@
158
158
  keepLocaleOnCollapse?: boolean;
159
159
  /** Fixed positioning (top of viewport) */
160
160
  fixed?: boolean;
161
+ /** When the app runs installed/standalone (display-mode standalone or
162
+ * fullscreen), offset the header below the device safe-area insets so its
163
+ * content (logo, hamburger) clears the status bar / notch / Dynamic Island.
164
+ * Pads the top plus the left/right insets (the latter only matter for a
165
+ * side notch in landscape — both 0 in portrait). The header's own
166
+ * background fills the inset strip. No-op in a browser tab / on devices
167
+ * without an inset. Only set this on the TOP app bar — never on in-page,
168
+ * detail, or drawer-internal headers (they'd get wrongly padded). */
169
+ safeArea?: boolean;
161
170
  /** Bindable: whether the header is currently in collapsed (hamburger) mode */
162
171
  isCollapsed?: boolean;
163
172
  /** Bindable: whether the hamburger dropdown is currently open */
@@ -256,6 +265,7 @@
256
265
  collapseMode = "hamburger",
257
266
  keepLocaleOnCollapse = false,
258
267
  fixed = false,
268
+ safeArea = false,
259
269
  isCollapsed = $bindable(false),
260
270
  isMenuOpen = $bindable(false),
261
271
  dropdownPosition = "bottom-span-right",
@@ -462,6 +472,7 @@
462
472
  class={_class}
463
473
  data-fixed={!unstyled && fixed ? "" : undefined}
464
474
  data-collapsed={!unstyled && _isCollapsed ? "" : undefined}
475
+ data-safe-area={!unstyled && safeArea ? "" : undefined}
465
476
  {...rest}
466
477
  >
467
478
  {#if children}
@@ -147,6 +147,15 @@ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
147
147
  keepLocaleOnCollapse?: boolean;
148
148
  /** Fixed positioning (top of viewport) */
149
149
  fixed?: boolean;
150
+ /** When the app runs installed/standalone (display-mode standalone or
151
+ * fullscreen), offset the header below the device safe-area insets so its
152
+ * content (logo, hamburger) clears the status bar / notch / Dynamic Island.
153
+ * Pads the top plus the left/right insets (the latter only matter for a
154
+ * side notch in landscape — both 0 in portrait). The header's own
155
+ * background fills the inset strip. No-op in a browser tab / on devices
156
+ * without an inset. Only set this on the TOP app bar — never on in-page,
157
+ * detail, or drawer-internal headers (they'd get wrongly padded). */
158
+ safeArea?: boolean;
150
159
  /** Bindable: whether the header is currently in collapsed (hamburger) mode */
151
160
  isCollapsed?: boolean;
152
161
  /** Bindable: whether the hamburger dropdown is currently open */
@@ -33,3 +33,31 @@ Minimal config:
33
33
  {#snippet avatar()}<Avatar initials="MM" autoColor />{/snippet}
34
34
  </Header>
35
35
  ```
36
+
37
+ ## PWA safe area (`safeArea`)
38
+
39
+ When your app is installed and launched standalone (iOS Home Screen, Android/desktop PWA), the web view fills the **entire** screen, so a top app bar renders _under_ the status bar / notch / Dynamic Island, where the system swallows touches — the logo and hamburger become untappable.
40
+
41
+ Set `safeArea` on the **top app bar** to offset its content below the device safe-area insets. It pads the **top** (status bar / notch in portrait) plus the **left/right** insets (a side notch in landscape — both `0` in portrait). The padding lands on the **outer** `<header>` (the background-bearing, full-width element), so the brand color fills the inset strip and the inner content row keeps its own padding.
42
+
43
+ ```svelte
44
+ <Header
45
+ projectName="App"
46
+ fixed
47
+ safeArea
48
+ leadingHamburger
49
+ onLeadingHamburger={openDrawer}
50
+ />
51
+ ```
52
+
53
+ - **Default `false`.** No change to any existing render path. It's a **no-op in a browser tab** and on devices without an inset (`env()` → `0`).
54
+ - The insets are only non-zero when the consuming app sets `<meta name="viewport" content="..., viewport-fit=cover">` (the app's responsibility) **and** the device has an inset.
55
+ - **Only set it on the TOP app bar.** `Header` is reused as in-page, detail, and drawer-internal headers — those must NOT get `safeArea`, or they'd be wrongly padded.
56
+ - **Respects `unstyled`** (skipped, like every other `data-*` toggle).
57
+ - It is **independent of `fixed`** — you typically want both on a top app bar, but neither implies the other.
58
+
59
+ ### Pick ONE layer (avoid double padding)
60
+
61
+ A nav **drawer** that contains its own stuic `Header` (logo + close button) is common. In that case set `safeArea` on the **inner `Header`** (so the header color fills the strip) and do **not** also offset the drawer panel — otherwise the top inset is applied twice. Each safe-area edge should be padded on exactly **one** element in any nesting chain.
62
+
63
+ > The underlying mechanism (and the reusable `.stuic-safe-area-*` utility classes / `--stuic-safe-area-*` CSS variables for any other edge-anchored element) is documented in the root README under "PWA safe-area insets".
@@ -250,3 +250,22 @@
250
250
  margin-inline-end: var(--stuic-header-trailing-hamburger-offset-inline-end);
251
251
  }
252
252
  }
253
+
254
+ /* ============================================================================
255
+ SAFE AREA (PWA installed / standalone) — opt-in via the `safeArea` prop.
256
+ Deliberately UNLAYERED (not in @layer components, unlike [data-fixed] above)
257
+ so it reliably beats a consumer's Tailwind `utilities` layer — the whole
258
+ point is a safe-area offset that a stray `pt-*` can't silently override.
259
+ Pads the OUTER .stuic-header (full-width, background-bearing) so the brand
260
+ color fills the inset strip. Top covers the status bar/notch in portrait;
261
+ left/right cover a side notch in landscape (both 0 without an inset). The
262
+ inner .stuic-header-content keeps its own padding, so the inset stacks above
263
+ the content row rather than replacing it. No-op in a browser tab (env → 0).
264
+ ============================================================================ */
265
+ @media (display-mode: standalone), (display-mode: fullscreen) {
266
+ .stuic-header[data-safe-area] {
267
+ padding-top: env(safe-area-inset-top);
268
+ padding-left: env(safe-area-inset-left);
269
+ padding-right: env(safe-area-inset-right);
270
+ }
271
+ }
@@ -107,6 +107,10 @@ By default, Modal is:
107
107
  - **Mobile**: Full screen with 1rem margins from viewport edges
108
108
  - **Desktop (md+)**: Centered, max-width 768px, auto height with max 80vh
109
109
 
110
+ ### PWA safe area (automatic)
111
+
112
+ When the app runs installed/standalone (display-mode standalone or fullscreen) and the Modal is **full-bleed** (below the `md` breakpoint), its content is automatically padded by the device safe-area insets so the header / content / footer clear the status bar, notch and home indicator. This is **inert in a normal browser tab** (`env()` → `0`) and on **centered desktop** modals (which don't touch the edges). No prop required. To go truly edge-to-edge, override the padding on the inner panel via `class`.
113
+
110
114
  ## CSS Variables
111
115
 
112
116
  Override to customize appearance:
@@ -27,4 +27,24 @@
27
27
  .stuic-modal .main {
28
28
  scrollbar-width: thin;
29
29
  }
30
+
31
+ /* ============================================================================
32
+ SAFE AREA (PWA installed / standalone) — automatic, full-bleed only.
33
+ The Modal is full-screen below the `md` breakpoint (size-full) and a
34
+ centered card at/above it (md:size-auto). Only the full-bleed case touches
35
+ the device edges, so we gate on `width < 48rem` (Tailwind's md) AND
36
+ standalone, and pad the bg-bearing .stuic-modal (a flex column) so its
37
+ header / content / footer clear the status bar, notch and home indicator.
38
+ Centered desktop modals and browser tabs (env → 0) are untouched. A
39
+ consumer wanting true edge-to-edge can override padding via `class`.
40
+ ============================================================================ */
41
+ @media (display-mode: standalone) and (width < 48rem),
42
+ (display-mode: fullscreen) and (width < 48rem) {
43
+ .stuic-modal {
44
+ padding-top: env(safe-area-inset-top);
45
+ padding-right: env(safe-area-inset-right);
46
+ padding-bottom: env(safe-area-inset-bottom);
47
+ padding-left: env(safe-area-inset-left);
48
+ }
49
+ }
30
50
  }
@@ -230,3 +230,21 @@
230
230
  );
231
231
  }
232
232
  }
233
+
234
+ /* ============================================================================
235
+ SAFE AREA (PWA installed / standalone) — automatic, not opt-in.
236
+ The .stuic-notifs wrap is `fixed inset-0` and carries no padding of its own
237
+ (the visual spacing lives on the inner `p-4` wrap), so padding it here insets
238
+ the whole toast region by the device safe area. This keeps toasts clear of
239
+ the status bar / notch / home indicator regardless of corner placement, and
240
+ composes with the inner p-4. Toasts should never sit under system UI, so this
241
+ needs no prop. No-op in a browser tab (env → 0).
242
+ ============================================================================ */
243
+ @media (display-mode: standalone), (display-mode: fullscreen) {
244
+ .stuic-notifs {
245
+ padding-top: env(safe-area-inset-top);
246
+ padding-right: env(safe-area-inset-right);
247
+ padding-bottom: env(safe-area-inset-bottom);
248
+ padding-left: env(safe-area-inset-left);
249
+ }
250
+ }
package/dist/index.css CHANGED
@@ -130,3 +130,48 @@ In practice:
130
130
  .scrollbar-thin {
131
131
  scrollbar-width: thin;
132
132
  }
133
+
134
+ /* ============================================================================
135
+ PWA SAFE-AREA INSETS (installed / standalone)
136
+ Unlayered (like .scrollbar-thin above) so they reliably beat a consumer's
137
+ Tailwind `utilities` layer — a deliberate `pt-*` elsewhere must not silently
138
+ win over a safe-area offset. The raw env() values self-zero in a normal
139
+ browser tab; they only become non-zero on a device with an inset when the
140
+ app declares `viewport-fit=cover` (the consuming app's responsibility).
141
+ ============================================================================ */
142
+
143
+ /* Composable inset variables: 0px everywhere, real device insets only when the
144
+ app runs installed/standalone. Use these to ADD inset without clobbering an
145
+ element's existing padding, e.g.:
146
+ padding-block-start: calc(1rem + var(--stuic-safe-area-top)); */
147
+ :root {
148
+ --stuic-safe-area-top: 0px;
149
+ --stuic-safe-area-right: 0px;
150
+ --stuic-safe-area-bottom: 0px;
151
+ --stuic-safe-area-left: 0px;
152
+ }
153
+
154
+ @media (display-mode: standalone), (display-mode: fullscreen) {
155
+ :root {
156
+ --stuic-safe-area-top: env(safe-area-inset-top);
157
+ --stuic-safe-area-right: env(safe-area-inset-right);
158
+ --stuic-safe-area-bottom: env(safe-area-inset-bottom);
159
+ --stuic-safe-area-left: env(safe-area-inset-left);
160
+ }
161
+
162
+ /* Utility classes. NB these SET the padding on their axis (they replace,
163
+ not add) — apply them only to an element that does not otherwise pad that
164
+ side. To ADD inset to existing padding, compose the var above instead. */
165
+ .stuic-safe-area-top {
166
+ padding-top: env(safe-area-inset-top);
167
+ }
168
+ .stuic-safe-area-right {
169
+ padding-right: env(safe-area-inset-right);
170
+ }
171
+ .stuic-safe-area-bottom {
172
+ padding-bottom: env(safe-area-inset-bottom);
173
+ }
174
+ .stuic-safe-area-left {
175
+ padding-left: env(safe-area-inset-left);
176
+ }
177
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.128.0",
3
+ "version": "3.130.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",