@stacksjs/defaults 0.75.182 → 0.75.184

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-dashboard
3
- description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 413 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
3
+ description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 411 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
8
8
 
9
9
  # Stacks Dashboard
10
10
 
11
- The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 413 components, and a multi-section layout.
11
+ The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 411 components, and a multi-section layout.
12
12
 
13
13
  ## Key Paths
14
14
  - Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
@@ -452,7 +452,14 @@ option the original metres heuristic remains for unlabelled inputs.
452
452
  - `<NativeSheet v-model:open="editing" title="…">` raises a bottom sheet for a
453
453
  task that belongs to the screen: dragged down from its grabber or header to
454
454
  dismiss, the page behind inert. `detents="medium large"` makes it full
455
- height with detents, the page pressed into a card at large.
455
+ height with detents, the page pressed into a card at large. An open sheet is
456
+ a layer on stx's overlay stack (`window.stx.overlays`) with Dialog, Drawer,
457
+ stxConfirm and the HTML alerts: the stack locks the page, makes it inert,
458
+ gives Escape and Tab to the top layer and returns focus, so a confirm opened
459
+ from a sheet closes alone. `lockBackground` and `trapFocus` no longer exist;
460
+ an app's own overlay uses `openOverlay` / `closeOverlay` from
461
+ `@stacksjs/components/overlays` (in `<script client>`), passing `root` for
462
+ an element lifted into `<body>` and `zIndex` to stack by opening order.
456
463
  - `<NativeContextMenu :items="[...]" @select="…">` opens a menu on a
457
464
  half-second press: UIMenu in the shell, an HTML menu elsewhere.
458
465
  - `<NativeActionSheet v-model:open="choosing" :actions="[...]" @select="…">`
@@ -18,7 +18,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
18
18
  | `@stacksjs/api` | [`stacks-api`](../stacks-api/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/api/src/index.ts) | 7 |
19
19
  | `@stacksjs/arrays` | [`stacks-arrays`](../stacks-arrays/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/arrays/src/index.ts) | 6 |
20
20
  | `@stacksjs/audio` | [`stacks-audio`](../stacks-audio/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/audio/src/index.ts) | 2 |
21
- | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 113 |
21
+ | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 114 |
22
22
  | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
23
23
  | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
24
24
  | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 145 |
@@ -65,7 +65,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
65
65
  | `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
66
66
  | `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 15 |
67
67
  | `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
68
- | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 60 |
68
+ | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 61 |
69
69
  | `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
70
70
  | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 10 |
71
71
  | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 23 |
@@ -185,7 +185,7 @@ Declared runtime exports: `DEFAULT_ROLE_PACKS`, `createBqbRbacStore`, `generateT
185
185
 
186
186
  Local source modules: [src/authentication.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authentication.ts), [src/authenticator.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authenticator.ts), [src/authorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authorizable.ts), [src/browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/browser-session.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/client.ts), [src/cookie-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/cookie-auth.ts), [src/credential-user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/credential-user.ts), [src/email-verification.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/email-verification.ts), [src/gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/gate.ts), [src/magic-link.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/magic-link.ts), [src/middleware.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/middleware.ts), [src/oauth-authorization-codes.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-codes.ts), [src/oauth-authorization-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-endpoint.ts), [src/oauth-authorization-requests.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-requests.ts), [src/oauth-authorization-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-response.ts), [src/oauth-authorization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization.ts), [src/oauth-browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-browser-session.ts), [src/oauth-client-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-client-registration.ts), [src/oauth-consent.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-consent.ts), [src/oauth-delegated-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-delegated-access.ts), [src/oauth-grants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-grants.ts), [src/oauth-introspection.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-introspection.ts), [src/oauth-metadata.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-metadata.ts), [src/oauth-pkce.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pkce.ts), [src/oauth-provider.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-provider.ts), [src/oauth-pruning.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pruning.ts), [src/oauth-revocation.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-revocation.ts), [src/oauth-token-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-endpoint.ts), [src/oauth-token-exchange.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-exchange.ts), [src/oauth-token-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-request.ts), [src/oauth-token-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-response.ts), [src/page-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/page-gate.ts), [src/passkey.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkey.ts), [src/passkeys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkeys.ts), [src/password/reset.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/password/reset.ts), [src/permissions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/permissions.ts), [src/policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/policy.ts), [src/rate-limiter.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rate-limiter.ts), [src/rbac-seed.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-seed.ts), [src/rbac-store-bqb.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-store-bqb.ts), [src/rbac.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac.ts), [src/referrals.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/referrals.ts), [src/register.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/register.ts), [src/request-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/request-token.ts), [src/session-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-auth.ts), [src/socials.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/socials.ts), [src/team.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/team.ts), [src/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/tokens.ts), [src/two-factor.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/two-factor.ts), [src/user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/user.ts).
187
187
 
188
- Evidence directory: [113 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
188
+ Evidence directory: [114 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
189
189
 
190
190
  ### @stacksjs/browser
191
191
 
@@ -631,7 +631,7 @@ Declared runtime exports: `Activity`, `AnalyticsEvent`, `Auction`, `AuctionItem`
631
631
 
632
632
  Local source modules: [src/auto-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/auto-crud.ts), [src/batch-loader.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/batch-loader.ts), [src/db.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/db.ts), [src/define-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/define-model.ts), [src/extend-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/extend-model.ts), [src/gdpr/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/gdpr/index.ts), [src/generate-database-schema.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/generate-database-schema.ts), [src/locked-row.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/locked-row.ts), [src/messaging.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/messaging.ts), [src/model-types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/model-types.ts), [src/ownership.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/ownership.ts), [src/paginator-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/paginator-request.ts), [src/row-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/row-token.ts), [src/subquery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/subquery.ts), [src/traits/audit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/audit.ts), [src/traits/billable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/billable.ts), [src/traits/categorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/categorizable.ts), [src/traits/commentable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/commentable.ts), [src/traits/likeable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/likeable.ts), [src/traits/taggable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/taggable.ts), [src/transaction.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/transaction.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/types.ts), [src/utils/encrypted.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/encrypted.ts), [src/utils/prunable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/prunable.ts).
633
633
 
634
- Evidence directory: [60 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
634
+ Evidence directory: [61 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
635
635
 
636
636
  ### @stacksjs/pagination
637
637
 
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.182",
5
+ "version": "0.75.184",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.182",
5
+ "version": "0.75.184",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,8 +55,8 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.35",
58
- "@stacksjs/mobile": "0.75.182",
59
- "@stacksjs/sanitizer": "^0.2.483",
58
+ "@stacksjs/mobile": "0.75.184",
59
+ "@stacksjs/sanitizer": "^0.2.534",
60
60
  "@xterm/addon-fit": "^0.11.0",
61
61
  "@xterm/xterm": "^6.0.0",
62
62
  "ts-qr-codes": "^0.1.8"
@@ -118,10 +118,15 @@ describe('Native components', () => {
118
118
  expect(bar).toContain("useReactiveProp<boolean>('titleOnScroll', false)")
119
119
  })
120
120
 
121
- it('lifts an open sheet out of the router\'s stacking context', () => {
121
+ it('lifts a sheet into the body when it mounts, out of the router\'s stacking context', () => {
122
122
  const sheet = read('NativeSheet')
123
- expect(sheet).toContain('main:has(.native-sheet.is-open) { view-transition-name: none; }')
124
123
  expect(sheet).toContain("useModel('open'")
124
+ // Lifted before it can open, so it never draws inside <main>, which the
125
+ // router makes a stacking context; <main> no longer has to give up its
126
+ // view-transition name while a sheet is up.
127
+ const mounted = sheet.slice(sheet.indexOf('onMount(() => {'))
128
+ expect(mounted.indexOf('putBack ??= liftToBody(element)')).toBeLessThan(mounted.indexOf('if (open()) present()'))
129
+ expect(sheet).not.toContain('main:has(')
125
130
  })
126
131
 
127
132
  it('hides the native tab bar under a shell that has none, like a sign-in screen', () => {
@@ -272,12 +277,32 @@ describe('Native components feel native', () => {
272
277
  expect(html).toContain('data-stx-ref="nativeSheetHeader"')
273
278
  })
274
279
 
275
- it('opens a sheet without a haptic, holds the page behind it, and keeps focus inside', () => {
280
+ it('opens a sheet without a haptic, as a layer on the stx overlay stack', () => {
276
281
  const sheet = read('NativeSheet')
277
282
  expect(sheet).not.toContain('haptics')
278
- expect(sheet).toContain('releaseBackground = lockBackground(element)')
279
- expect(sheet).toContain('trapFocus(panel.current as HTMLElement, event)')
280
283
  expect(sheet).toContain('putBack ??= liftToBody(element)')
284
+ // The stack holds the page, traps Tab, answers Escape for the top layer
285
+ // and hands focus back, shared with Dialog, Drawer and stxConfirm. The
286
+ // sheet's own Escape listener, Tab trap, background lock and focus
287
+ // restore each fought the stack when one overlay opened over another.
288
+ expect(sheet).toContain("import { closeOverlay, openOverlay } from '@stacksjs/components/overlays'")
289
+ expect(sheet).toContain('openOverlay(overlayId, {')
290
+ expect(sheet).toContain('root: element,')
291
+ expect(sheet).toContain('zIndex: SHEET_LAYER,')
292
+ expect(sheet).toContain('onEscape: close,')
293
+ expect(sheet).toContain('initialFocus: sheetPanel,')
294
+ for (const gone of ['lockBackground', 'trapFocus', 'previousFocus', "useEventListener('keydown'", 'releaseBackground'])
295
+ expect(sheet).not.toContain(gone)
296
+ })
297
+
298
+ it('leaves the overlay stack when it closes, and when it is removed while up', () => {
299
+ const sheet = read('NativeSheet')
300
+ const dismissed = sheet.slice(sheet.indexOf('function dismissed(): void {'), sheet.indexOf('let wasOpen = false'))
301
+ expect(dismissed).toContain('closeOverlay(overlayId)')
302
+ const destroyed = sheet.slice(sheet.indexOf('onDestroy(() => {'), sheet.indexOf("useEventListener('stx:navigate'"))
303
+ expect(destroyed).toContain('closeOverlay(overlayId)')
304
+ // Each sheet gives back only its own hold on the card.
305
+ expect(destroyed).toContain('presentPageAsCard(element.ownerDocument, false)')
281
306
  })
282
307
 
283
308
  it('slides a sheet up from where it was lifted to, rather than appearing up', () => {
@@ -285,8 +310,8 @@ describe('Native components feel native', () => {
285
310
  // is-open follows `shown`, which present() sets only after the lifted
286
311
  // sheet's closed style is worked out in the body.
287
312
  expect(sheet).toContain("const sheetClass = derived(() => `${shown() ? 'is-open' : ''}")
288
- expect(sheet).toContain('sheetPanel.getBoundingClientRect()')
289
- expect(sheet.indexOf('sheetPanel.getBoundingClientRect()')).toBeLessThan(sheet.indexOf('shown.set(true)'))
313
+ expect(sheet).toContain('sheetPanel()?.getBoundingClientRect()')
314
+ expect(sheet.indexOf('sheetPanel()?.getBoundingClientRect()')).toBeLessThan(sheet.indexOf('shown.set(true)'))
290
315
  expect(sheet.indexOf('putBack ??= liftToBody(element)')).toBeLessThan(sheet.indexOf('shown.set(true)'))
291
316
  })
292
317
 
@@ -1,6 +1,7 @@
1
1
  <script client>
2
2
  import type { SheetDetent } from '@stacksjs/mobile'
3
- import { coverTabBar, liftToBody, lockBackground, observeSheetDrag, presentPageAsCard, trapFocus } from '@stacksjs/mobile'
3
+ import { closeOverlay, openOverlay } from '@stacksjs/components/overlays'
4
+ import { coverTabBar, liftToBody, observeSheetDrag, presentPageAsCard } from '@stacksjs/mobile'
4
5
 
5
6
  /**
6
7
  * A bottom sheet: a task that belongs to this screen, raised over it.
@@ -19,6 +20,15 @@ import { coverTabBar, liftToBody, lockBackground, observeSheetDrag, presentPageA
19
20
  * it is up. It belongs to the screen it was opened on, so leaving that screen
20
21
  * (a link from outside, a tab switch) closes it rather than carrying it over
21
22
  * the next one.
23
+ *
24
+ * An open sheet is a layer on the page's overlay stack (stx's
25
+ * `window.stx.overlays`), shared with Dialog, Drawer, stxConfirm, the web
26
+ * stand-ins for native alerts and the app's own overlays. The stack holds the
27
+ * page still from the first layer to the last, makes the rest of the page
28
+ * inert, gives Escape and Tab to whichever layer is on top, and hands focus
29
+ * back to whatever opened each one. A confirmation opened from a sheet, or a
30
+ * sheet opened over a drawer, closes with one Escape and leaves the layer
31
+ * under it up, locked and in charge of the keyboard.
22
32
  */
23
33
  const emit = defineEmits()
24
34
  const open = useModel('open', { default: false })
@@ -44,9 +54,15 @@ const panel = useRef('nativeSheetPanel')
44
54
  const grabber = useRef('nativeSheetGrabber')
45
55
  const header = useRef('nativeSheetHeader')
46
56
 
47
- let releaseBackground: (() => void) | null = null
57
+ // This sheet's id on the overlay stack.
58
+ const overlayId = useId('native-sheet')
59
+ // Sheets stack above the page at 70 (the stylesheet's z-index); the stack
60
+ // adds each open sheet's place to it, so a sheet opened over another draws
61
+ // above it wherever the two were lifted into the body.
62
+ const SHEET_LAYER = 70
63
+
64
+ let presented = false
48
65
  let putBack: (() => void) | null = null
49
- let previousFocus: HTMLElement | null = null
50
66
  let carded = false
51
67
  let stopDrag: (() => void) | null = null
52
68
  let openedOn = ''
@@ -59,35 +75,47 @@ function root(): HTMLElement | null {
59
75
  return sheetRoot.current as HTMLElement | null
60
76
  }
61
77
 
78
+ function sheetPanel(): HTMLElement | null {
79
+ return panel.current as HTMLElement | null
80
+ }
81
+
62
82
  function present(): void {
63
83
  const element = root()
64
- if (!element || releaseBackground) return
84
+ if (!element || presented) return
85
+ presented = true
65
86
  detent.set(detents()[0] ?? 'large')
66
- previousFocus = element.ownerDocument.activeElement as HTMLElement | null
67
87
  // Out of the page's stacking contexts and transforms (the card below
68
- // scales the page), and everything else made inert behind it.
88
+ // scales the page). A child of the body, it is what the overlay stack keeps
89
+ // usable while everything else goes inert.
69
90
  putBack ??= liftToBody(element)
70
- releaseBackground = lockBackground(element)
71
91
  openedOn = currentPath()
72
92
  coverTabBar(element, true)
73
93
  // A node moved in the document starts over with no style to transition
74
94
  // from, so a sheet lifted and marked open in one go appears already up.
75
95
  // Work its closed style out where it now is, then open it: the panel
76
96
  // slides from there.
77
- const sheetPanel = panel.current as HTMLElement | null
78
- if (sheetPanel) void sheetPanel.getBoundingClientRect()
97
+ void sheetPanel()?.getBoundingClientRect()
79
98
  shown.set(true)
80
- void nextTick(() => (panel.current as HTMLElement | null)?.focus())
99
+ // Focus goes to the panel itself, not its first control: on a phone a
100
+ // focused field raises the keyboard over the sheet before it is up.
101
+ openOverlay(overlayId, {
102
+ panel: sheetPanel,
103
+ root: element,
104
+ zIndex: SHEET_LAYER,
105
+ initialFocus: sheetPanel,
106
+ onEscape: close,
107
+ })
81
108
  }
82
109
 
83
110
  function dismissed(): void {
111
+ if (!presented) return
112
+ presented = false
84
113
  shown.set(false)
85
114
  const element = root()
86
115
  if (element) coverTabBar(element, false)
87
- releaseBackground?.()
88
- releaseBackground = null
89
- previousFocus?.focus?.()
90
- previousFocus = null
116
+ // Off the stack: the layer underneath (or the page) gets the keyboard back,
117
+ // and focus returns to whatever opened the sheet.
118
+ closeOverlay(overlayId)
91
119
  }
92
120
 
93
121
  let wasOpen = false
@@ -98,9 +126,9 @@ effect(() => {
98
126
  wasOpen = isOpen
99
127
  })
100
128
 
101
- // The page behind becomes a card while a full-height sheet is at large. Only
102
- // this sheet's own change is applied, so a closed sheet never undoes an open
103
- // one's card.
129
+ // The page behind becomes a card while a full-height sheet is at large. Each
130
+ // sheet hands back only its own hold, so a sheet closing over another never
131
+ // undoes the card the other still wants.
104
132
  effect(() => {
105
133
  const element = root()
106
134
  const want = open() && hasDetents() && detent() === 'large'
@@ -117,13 +145,15 @@ function close(): void {
117
145
  onMount(() => {
118
146
  // Give the sheet its permanent presentation parent before its slot loads
119
147
  // media. Reparenting an already loading iframe or video interrupts playback.
148
+ // A closed sheet waits in the body like this, so one nested in another's
149
+ // content can open over it without leaving the document mid-transition.
120
150
  const element = root()
121
151
  if (element) putBack ??= liftToBody(element)
122
- const sheetPanel = panel.current as HTMLElement | null
123
- if (!sheetPanel) return
152
+ const dragPanel = sheetPanel()
153
+ if (!dragPanel) return
124
154
  const handles = [grabber.current, header.current].filter(Boolean) as HTMLElement[]
125
155
  stopDrag = observeSheetDrag({
126
- panel: sheetPanel,
156
+ panel: dragPanel,
127
157
  handles,
128
158
  detents: () => detents(),
129
159
  detent: () => detent(),
@@ -137,10 +167,16 @@ onMount(() => {
137
167
 
138
168
  onDestroy(() => {
139
169
  stopDrag?.()
140
- releaseBackground?.()
141
170
  const element = root()
142
- if (element && open()) coverTabBar(element, false)
143
- if (carded && element) presentPageAsCard(element.ownerDocument, false)
171
+ if (element && presented) coverTabBar(element, false)
172
+ // Removed while up: off the stack, so the page is not left locked under a
173
+ // sheet that is gone.
174
+ presented = false
175
+ closeOverlay(overlayId)
176
+ if (carded && element) {
177
+ carded = false
178
+ presentPageAsCard(element.ownerDocument, false)
179
+ }
144
180
  putBack?.()
145
181
  })
146
182
 
@@ -153,12 +189,6 @@ useEventListener('stx:navigate', (event: Event) => {
153
189
  if (to !== openedOn) close()
154
190
  })
155
191
 
156
- useEventListener('keydown', (event: KeyboardEvent) => {
157
- if (!open()) return
158
- if (event.key === 'Escape') close()
159
- else if (panel.current) trapFocus(panel.current as HTMLElement, event)
160
- })
161
-
162
192
  const sheetClass = derived(() => `${shown() ? 'is-open' : ''}${hasDetents() ? ' has-detents' : ''}${detent() === 'medium' ? ' is-medium' : ''}`.trim())
163
193
  </script>
164
194
 
@@ -308,9 +338,4 @@ const sheetClass = derived(() => `${shown() ? 'is-open' : ''}${hasDetents() ? '
308
338
  :root[data-native-reduce-motion] .native-sheet-panel { transition: none; }
309
339
  :root[data-native-reduce-motion][data-native-sheet-card] .native-app-content { transform: none; border-radius: 0; }
310
340
 
311
- /* The router names the page's <main> for view transitions, which makes it a
312
- stacking context: a sheet inside it stacked under the tab bar whatever its
313
- z-index. While a sheet is open <main> gives the name up. Nothing navigates
314
- with a sheet open, so no transition is lost. */
315
- main:has(.native-sheet.is-open) { view-transition-name: none; }
316
341
  </style>
@@ -10,8 +10,6 @@ export { default as DashboardNavbar } from './Dashboard/Navbar.stx'
10
10
 
11
11
  // Dashboard and UI components
12
12
  export * from './Dashboard'
13
- export { default as Audio } from './Audio.stx'
14
- export { default as Image } from './Image.stx'
15
13
  export { default as NativeActionSheet } from './NativeActionSheet.stx'
16
14
  export { default as NativeAppShell } from './NativeAppShell.stx'
17
15
  export { default as NativeContextMenu } from './NativeContextMenu.stx'
@@ -1,92 +0,0 @@
1
- <script server>
2
- interface AudioSource { src: string, type?: string }
3
- interface AudioDrm {
4
- widevine?: { url: string, headers?: Record<string, string> }
5
- playready?: { url: string, headers?: Record<string, string> }
6
- fairplay?: { url: string, certificateUrl: string, headers?: Record<string, string> }
7
- }
8
- interface AudioTrack { src: string, kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata', srclang?: string, label?: string, default?: boolean }
9
- interface AudioProps {
10
- src: string
11
- sources?: AudioSource[]
12
- drm?: AudioDrm
13
- tracks?: AudioTrack[]
14
- title?: string
15
- artist?: string
16
- artwork?: string
17
- waveform?: string
18
- controls?: boolean
19
- autoplay?: boolean
20
- loop?: boolean
21
- muted?: boolean
22
- preload?: 'none' | 'metadata' | 'auto'
23
- crossorigin?: 'anonymous' | 'use-credentials'
24
- class?: string
25
- }
26
- const props = defineProps<AudioProps>()
27
- const src = props.src || ''
28
- const sources = props.sources || []
29
- const tracks = props.tracks || []
30
- const title = props.title || 'Audio player'
31
- const artist = props.artist || ''
32
- const artwork = props.artwork || ''
33
- const waveform = props.waveform || ''
34
- const controls = props.controls !== false
35
- const preload = props.preload || 'metadata'
36
- const crossorigin = props.crossorigin || ''
37
- const className = props.class || ''
38
- const playerSources = (sources.length ? sources : src ? [{ src }] : []).map(source => props.drm && (source.type === 'application/dash+xml' || source.src.split(/[?#]/, 1)[0].toLowerCase().endsWith('.mpd')) ? { ...source, type: 'application/dash+xml', drm: props.drm } : source)
39
- const playerTracks = tracks.map(track => ({ ...track, language: track.srclang }))
40
- const playerConfig = JSON.stringify({ sources: playerSources, tracks: playerTracks, ...(props.drm ? { drm: props.drm } : {}) }).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026')
41
- </script>
42
-
43
- <script client>
44
- import { registerElements } from 'ts-video-player/elements'
45
- registerElements()
46
- </script>
47
-
48
- <figure class="m-0 overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-stone-200 dark:bg-stone-950 dark:ring-stone-800 {{ className }}">
49
- <div class="flex gap-4 items-center p-4 sm:p-5">
50
- @if(artwork)
51
- <img src="{{ artwork }}" alt="" width="72" height="72" loading="lazy" decoding="async" class="object-cover shrink-0 h-18 w-18 ring-1 ring-black/5 rounded-xl dark:ring-white/10">
52
- @else
53
- <div class="flex items-center justify-center shrink-0 h-18 w-18 text-stone-500 dark:text-stone-400 bg-stone-100 dark:bg-stone-900 rounded-xl" aria-hidden="true"><span class="h-7 w-7 i-hugeicons-music-note-01"></span></div>
54
- @endif
55
- <div class="flex-1 min-w-0">
56
- <figcaption class="font-semibold text-sm text-stone-950 truncate dark:text-white">{{ title }}</figcaption>
57
- @if(artist)<p class="mt-0.5 text-sm text-stone-500 truncate dark:text-stone-400">{{ artist }}</p>@endif
58
- @if(waveform)<img src="{{ waveform }}" alt="" loading="lazy" decoding="async" class="object-cover mt-3 h-10 w-full opacity-70 dark:opacity-80">@endif
59
- </div>
60
- </div>
61
- <div class="p-3 bg-stone-950 border-stone-100 border-t dark:border-stone-800">
62
- <video-player src="{{ src }}" title="{{ title }}" data-media-config="{{ playerConfig }}" @if(controls) controls @endif @if(props.autoplay) autoplay @endif @if(props.loop) loop @endif @if(props.muted) muted @endif preload="{{ preload }}" @if(crossorigin) crossorigin="{{ crossorigin }}" keep-alive aria-label="{{ title }}" class="block overflow-hidden relative h-16 w-full text-white rounded-xl focus-within:ring-2 focus-within:ring-sky-500">
63
- <media-error-display></media-error-display>
64
- <audio data-native-fallback src="{{ src }}" @if(controls) controls @endif @if(props.autoplay) autoplay @endif @if(props.loop) loop @endif @if(props.muted) muted @endif preload="{{ preload }}" @if(crossorigin) crossorigin="{{ crossorigin }}" @endif aria-label="{{ title }}" class="block h-16 w-full accent-sky-500">
65
- @foreach (sources as source)<source src="{{ source.src }}" @if(source.type) type="{{ source.type }}" @endif>@endforeach
66
- @foreach (tracks as track)<track src="{{ track.src }}" kind="{{ track.kind }}" @if(track.srclang) srclang="{{ track.srclang }}" @endif @if(track.label) label="{{ track.label }}" @endif @if(track.default) default @endif>@endforeach
67
- Your browser does not support audio playback.
68
- </audio>
69
- @if(controls)
70
- <video-skin>
71
- <media-seek-button slot="left" seconds="-10" aria-label="Back 10 seconds"></media-seek-button>
72
- <media-play-button slot="left" aria-label="Play or pause"></media-play-button>
73
- <media-seek-button slot="left" seconds="10" aria-label="Forward 10 seconds"></media-seek-button>
74
- <media-time-group slot="center" class="font-mono tabular-nums text-xs"></media-time-group>
75
- <media-progress-bar slot="center" @if(waveform) waveform="{{ waveform }}" @endif aria-label="Audio progress"></media-progress-bar>
76
- <media-mute-button slot="right" aria-label="Mute or unmute"></media-mute-button>
77
- <media-volume-slider slot="right" aria-label="Volume"></media-volume-slider>
78
- <media-playback-rate-button slot="right" rates="0.5,0.75,1,1.25,1.5,2" aria-label="Playback speed"></media-playback-rate-button>
79
- <media-settings-menu slot="right" aria-label="Audio settings"></media-settings-menu>
80
- <media-airplay-button slot="right" aria-label="Play on AirPlay"></media-airplay-button>
81
- <media-remote-playback-button slot="right" aria-label="Play on another device"></media-remote-playback-button>
82
- </video-skin>
83
- @endif
84
- </video-player>
85
- </div>
86
- </figure>
87
-
88
- <style>
89
- video-player:not(:defined) video-skin { display: none; }
90
- video-player:defined > [data-native-fallback] { display: none; }
91
- @media (prefers-reduced-motion: reduce) { video-player, video-player * { transition-duration: 0.01ms !important; } }
92
- </style>
@@ -1,57 +0,0 @@
1
- <script server>
2
- interface ImageProps {
3
- src: string
4
- alt?: string
5
- width?: number
6
- height?: number
7
- aspectRatio?: string
8
- srcset?: string
9
- sizes?: string
10
- loading?: 'lazy' | 'eager'
11
- decoding?: 'async' | 'sync' | 'auto'
12
- fetchpriority?: 'high' | 'low' | 'auto'
13
- priority?: boolean
14
- decorative?: boolean
15
- fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
16
- position?: string
17
- placeholder?: 'blur' | 'thumbhash' | 'color' | 'empty' | 'none' | string
18
- placeholderColor?: string
19
- class?: string
20
- }
21
- const props = defineProps<ImageProps>()
22
- if (!props.decorative && !props.alt?.trim()) throw new TypeError('Image alt text is required unless decorative is true')
23
- const src = props.src || ''
24
- const alt = props.decorative ? '' : props.alt || ''
25
- const width = props.width || ''
26
- const height = props.height || ''
27
- const ratio = props.aspectRatio || (width && height ? `${width} / ${height}` : '')
28
- const srcset = props.srcset || ''
29
- const sizes = props.sizes || ''
30
- const loading = props.loading || ''
31
- const decoding = props.decoding || ''
32
- const fetchpriority = props.fetchpriority || ''
33
- const fit = props.fit || 'cover'
34
- const position = props.position || 'center'
35
- const placeholder = props.placeholder || ''
36
- const placeholderColor = props.placeholderColor || ''
37
- const className = props.class || ''
38
- const imageStyle = `${ratio ? `aspect-ratio:${ratio};` : ''}object-fit:${fit};object-position:${position};`
39
- </script>
40
-
41
- <StxImage
42
- src="{{ src }}"
43
- alt="{{ alt }}"
44
- class="{{ className }}"
45
- style="{{ imageStyle }}"
46
- @if(width) width="{{ width }}" @endif
47
- @if(height) height="{{ height }}" @endif
48
- @if(srcset) srcset="{{ srcset }}" @endif
49
- @if(sizes) sizes="{{ sizes }}" @endif
50
- @if(loading) loading="{{ loading }}" @endif
51
- @if(decoding) decoding="{{ decoding }}" @endif
52
- @if(fetchpriority) fetchpriority="{{ fetchpriority }}" @endif
53
- @if(props.priority) priority @endif
54
- @if(props.decorative) aria-hidden="true" @endif
55
- @if(placeholder) placeholder="{{ placeholder }}" @endif
56
- @if(placeholderColor) placeholder-color="{{ placeholderColor }}" @endif
57
- />