@marianmeres/stuic 3.128.0 → 3.129.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,44 @@ 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). No prop needed; no-op in a browser tab.
199
+
200
+ **Utility classes** — offset any edge-anchored element without writing your own `env()` rule. They are active in standalone only:
201
+
202
+ ```html
203
+ <header class="my-top-bar stuic-safe-area-top">…</header>
204
+ <footer class="my-bottom-bar stuic-safe-area-bottom">…</footer>
205
+ <!-- also: stuic-safe-area-left / stuic-safe-area-right -->
206
+ ```
207
+
208
+ > ⚠️ 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.
209
+
210
+ **CSS variables** — composable insets (`0px` everywhere, real device insets in standalone):
211
+
212
+ ```css
213
+ .my-bottom-bar {
214
+ /* keep my own 1rem and add the home-indicator inset on top */
215
+ padding-bottom: calc(1rem + var(--stuic-safe-area-bottom));
216
+ }
217
+ /* also: --stuic-safe-area-top / --stuic-safe-area-left / --stuic-safe-area-right */
218
+ ```
219
+
220
+ **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.
221
+
222
+ **Not covered:** other fixed/edge-anchored components (`Modal`/`ModalDialog` full-bleed on mobile, `Float`) do not auto-handle insets — apply a `stuic-safe-area-*` class or the variables to their content as needed.
223
+
186
224
  ## TypeScript
187
225
 
188
226
  All components export their Props types:
@@ -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
+ }
@@ -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.129.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",