@stacksjs/defaults 0.75.23 → 0.75.25

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 405 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 406 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, 405 components, and a multi-section layout.
11
+ The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 406 components, and a multi-section layout.
12
12
 
13
13
  ## Key Paths
14
14
  - Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
@@ -132,6 +132,16 @@ supported web APIs provide fallback behavior outside a native host.
132
132
  between views of one screen, with selection haptics.
133
133
  - `<NativeSheet v-model:open="editing" title="…">` raises a bottom sheet for a
134
134
  task that belongs to the screen, above the tab bar.
135
+ - `<NativeProgressRing :value="percent" :size="176">` fills a ring as something
136
+ completes (sets done, a countdown running out), with its content in the
137
+ slot. Colours follow `--native-ring` and `--native-ring-track`.
138
+ - A screen that is a task of its own (a workout player, a composer) marks an
139
+ element `data-native-hide-tab-bar`, and the tab bar steps away while it is
140
+ shown, its reserved space with it.
141
+ - `<Video :src="current.url">` is the framework's player (ts-video-player):
142
+ YouTube, Vimeo, HLS, DASH or a file. `src` is reactive, so one player in a
143
+ sheet can show whichever video is chosen; clearing it pauses the player.
144
+ Craft lets an https iframe load inside the app, so embeds play inline.
135
145
 
136
146
  `<NativeTabItem match="/m/workout">` keeps a tab lit on the detail screens
137
147
  opened from it. A tab bar is a `<nav>`, where a link is otherwise current only
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.23",
5
+ "version": "0.75.25",
6
6
  "description": "A modern Stacks development environment.",
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.23",
5
+ "version": "0.75.25",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.27",
58
- "@stacksjs/mobile": "^0.75.23",
58
+ "@stacksjs/mobile": "^0.75.25",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -132,6 +132,16 @@ const shellClass = derived(() => `${nativeMobile() ? 'native-mobile ' : ''}${cla
132
132
  z-index: 30;
133
133
  }
134
134
 
135
+ /* A screen that is a task of its own — a workout player, a composer —
136
+ marks itself with data-native-hide-tab-bar, and the tab bar steps away
137
+ while it is shown, as iOS does for a pushed screen that hides the bottom
138
+ bar. Its clearance goes with it. */
139
+ .native-app-shell:has([data-native-hide-tab-bar]) { --native-tab-bar-height: 0px; }
140
+ .native-app-shell:has([data-native-hide-tab-bar]) .native-app-tab-slot { display: none; }
141
+ .native-app-shell:has([data-native-hide-tab-bar]) .native-app-content {
142
+ padding-bottom: var(--native-safe-area-bottom);
143
+ }
144
+
135
145
  /* A phone app does not select its chrome on a long press, or flash it grey
136
146
  on a tap. Text inside the content is left selectable. */
137
147
  .native-app-shell.native-mobile .native-app-tab-slot,
@@ -29,7 +29,7 @@ describe('Native components', () => {
29
29
  it('backs out through history, and sticks within the page rather than its wrapper', () => {
30
30
  const bar = read('NativeNavBar')
31
31
  expect(bar).toContain('if (goBack()) event.preventDefault()')
32
- expect(bar).toContain('.native-nav { display: contents; }')
32
+ expect(bar).toContain('[data-stx-scope]:has(> .native-nav) { display: contents; }')
33
33
  expect(bar).toContain("useReactiveProp<string>('title', '')")
34
34
  })
35
35
 
@@ -50,4 +50,17 @@ describe('Native components', () => {
50
50
  expect(control).toContain('haptics.selection()')
51
51
  expect(control).toContain("emit('change', value)")
52
52
  })
53
+
54
+ it('steps the tab bar away for a screen that is a task of its own', () => {
55
+ const shell = read('NativeAppShell')
56
+ expect(shell).toContain('.native-app-shell:has([data-native-hide-tab-bar]) .native-app-tab-slot { display: none; }')
57
+ expect(shell).toContain('.native-app-shell:has([data-native-hide-tab-bar]) { --native-tab-bar-height: 0px; }')
58
+ })
59
+
60
+ it('fills a ring to its value, clamped, with its content in the middle', () => {
61
+ const ring = read('NativeProgressRing')
62
+ expect(ring).toContain('Math.max(0, Math.min(100, Number(value()) || 0))')
63
+ expect(ring).toContain('conic-gradient(var(--native-ring')
64
+ expect(ring).toContain('<slot />')
65
+ })
53
66
  })
@@ -38,8 +38,8 @@ function tapBack(event: MouseEvent): void {
38
38
  }
39
39
  </script>
40
40
 
41
- <!-- `display: contents` on the wrapper: a sticky element sticks within its
42
- parent, and a wrapper exactly the bar's height would give it no room. -->
41
+ <!-- `display: contents` on this box and on stx's own wrapper: a sticky element
42
+ sticks within its parent, and either box would give the bar no room. -->
43
43
  <div class="native-nav" data-native-nav-bar>
44
44
  <header :class="barClass()">
45
45
  <div class="native-nav-side">
@@ -63,7 +63,11 @@ function tapBack(event: MouseEvent): void {
63
63
  </div>
64
64
 
65
65
  <style>
66
- .native-nav { display: contents; }
66
+ /* The element stx wraps each component in is a box too, as tall as the bar
67
+ and its title; the bar stuck inside it and scrolled away with the title.
68
+ Both boxes step aside so the bar sticks to the page. */
69
+ .native-nav,
70
+ [data-stx-scope]:has(> .native-nav) { display: contents; }
67
71
 
68
72
  .native-nav-bar {
69
73
  position: sticky;
@@ -0,0 +1,48 @@
1
+ <script client>
2
+ /**
3
+ * A ring that fills as something completes: sets done, a countdown running
4
+ * out. Whatever goes inside it — a count, a time — is the slot.
5
+ *
6
+ * `value` is 0–100. Colours follow `--native-ring` and `--native-ring-track`,
7
+ * its thickness `--native-ring-width`.
8
+ */
9
+ const value = useReactiveProp<number>('value', 0)
10
+ const size = useReactiveProp<number>('size', 56)
11
+ const label = useReactiveProp<string>('label', '')
12
+
13
+ const ringStyle = derived(() => {
14
+ const percent = Math.max(0, Math.min(100, Number(value()) || 0))
15
+ const px = Math.max(16, Number(size()) || 56)
16
+ return `width:${px}px;height:${px}px;background:conic-gradient(var(--native-ring, rgb(16 185 129)) ${percent}%, var(--native-ring-track, rgb(148 163 184 / 0.22)) 0)`
17
+ })
18
+ </script>
19
+
20
+ <div class="native-ring" role="img" :aria-label="label()" :style="ringStyle()" data-native-progress-ring>
21
+ <div class="native-ring-center">
22
+ <slot />
23
+ </div>
24
+ </div>
25
+
26
+ <style>
27
+ .native-ring {
28
+ position: relative;
29
+ flex-shrink: 0;
30
+ border-radius: 9999px;
31
+ transition: background 200ms linear;
32
+ }
33
+
34
+ .native-ring-center {
35
+ position: absolute;
36
+ inset: var(--native-ring-width, 5px);
37
+ display: flex;
38
+ flex-direction: column;
39
+ align-items: center;
40
+ justify-content: center;
41
+ border-radius: 9999px;
42
+ background: var(--native-ring-center, var(--native-safe-area-background, Canvas));
43
+ }
44
+
45
+ @media (prefers-reduced-motion: reduce) {
46
+ .native-ring { transition: none; }
47
+ }
48
+ </style>
@@ -44,17 +44,35 @@ const controlslist = props.controlslist || 'nodownload'
44
44
  const className = props.class || ''
45
45
  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)
46
46
  const playerTracks = tracks.map(track => ({ ...track, language: track.srclang }))
47
+ // A YouTube or Vimeo link is a web page, not a media file: the native <video>
48
+ // fallback cannot play it, so without the player it offers the link instead.
49
+ const isEmbed = /^https?:\/\/(?:[\w-]+\.)*(?:youtube\.com|youtube-nocookie\.com|youtu\.be|vimeo\.com)\//i.test(src)
47
50
  const playerConfig = JSON.stringify({ sources: playerSources, tracks: playerTracks, ...(props.drm ? { drm: props.drm } : {}) }).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026')
48
51
  </script>
49
52
 
50
53
  <script client>
51
54
  import { registerElements } from 'ts-video-player/elements'
52
55
  registerElements()
56
+
57
+ // `src` and `poster` follow the parent, so one player can show whichever
58
+ // video is chosen: `<Video :src="current.url" />` in a sheet. An empty `src`
59
+ // pauses it, which is what closing that sheet should do.
60
+ const liveSrc = useReactiveProp<string>('src', '')
61
+ const livePoster = useReactiveProp<string>('poster', '')
62
+ const element = useRef('player')
63
+
64
+ effect(() => {
65
+ if (!liveSrc())
66
+ (element.current as { player?: { pause: () => void } } | null)?.player?.pause()
67
+ })
53
68
  </script>
54
69
 
55
70
  <figure class="m-0 {{ className }}">
56
71
  <video-player
72
+ ref="player"
57
73
  src="{{ src }}"
74
+ :src="liveSrc() || null"
75
+ :poster="livePoster() || null"
58
76
  data-media-config="{{ playerConfig }}"
59
77
  @if(poster) poster="{{ poster }}" @endif
60
78
  @if(controls) controls @endif
@@ -72,6 +90,9 @@ registerElements()
72
90
  style="aspect-ratio: {{ ratio }};"
73
91
  >
74
92
  <media-error-display></media-error-display>
93
+ @if(isEmbed)
94
+ <a data-native-fallback href="{{ src }}" target="_blank" rel="noopener" class="flex items-center justify-center h-full w-full text-sm text-white underline">Watch {{ title }}</a>
95
+ @else
75
96
  <video data-native-fallback src="{{ src }}" @if(poster) poster="{{ poster }}" @endif @if(width) width="{{ width }}" @endif @if(height) height="{{ height }}" @endif @if(controls) controls @endif @if(props.autoplay) autoplay @endif @if(props.loop) loop @endif @if(props.muted) muted @endif @if(inline) playsinline @endif preload="{{ preload }}" @if(crossorigin) crossorigin="{{ crossorigin }}" @endif controlslist="{{ controlslist }}" @if(props.disablePictureInPicture) disablepictureinpicture @endif aria-label="{{ title }}" class="object-contain h-full w-full">
76
97
  @foreach (sources as source)
77
98
  <source src="{{ source.src }}" @if(source.type) type="{{ source.type }}" @endif>
@@ -81,6 +102,7 @@ registerElements()
81
102
  @endforeach
82
103
  Your browser does not support video playback.
83
104
  </video>
105
+ @endif
84
106
  <video-skin>
85
107
  <media-seek-button slot="left" seconds="-10" aria-label="Back 10 seconds"></media-seek-button>
86
108
  <media-play-button slot="left" aria-label="Play or pause"></media-play-button>
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ const video = readFileSync(join(import.meta.dir, 'Video.stx'), 'utf8')
6
+
7
+ describe('Video', () => {
8
+ it('follows a reactive src and poster, so one player can show whichever video is chosen', () => {
9
+ expect(video).toContain(`useReactiveProp<string>('src', '')`)
10
+ expect(video).toContain(`:src="liveSrc() || null"`)
11
+ expect(video).toContain(`:poster="livePoster() || null"`)
12
+ })
13
+
14
+ it('pauses when its src is cleared, as closing a sheet does', () => {
15
+ expect(video).toContain(`useRef('player')`)
16
+ expect(video).toContain('ref="player"')
17
+ expect(video).toMatch(/if \(!liveSrc\(\)\)[\s\S]*?\.pause\(\)/)
18
+ })
19
+
20
+ it('never hands a YouTube or Vimeo page to the native <video> fallback', () => {
21
+ const isEmbed = new RegExp(video.match(/const isEmbed = \/(.+)\/i\.test/)![1]!, 'i')
22
+ for (const url of ['https://www.youtube.com/watch?v=aclHkVaku9U', 'https://youtu.be/aclHkVaku9U', 'https://m.youtube.com/shorts/aclHkVaku9U', 'https://vimeo.com/76979871'])
23
+ expect(isEmbed.test(url)).toBe(true)
24
+ for (const url of ['https://cdn.example.com/clip.mp4', '/media/demo/manifest.mpd', 'https://example.com/youtube.com/fake.mp4'])
25
+ expect(isEmbed.test(url)).toBe(false)
26
+ expect(video).toContain('@if(isEmbed)')
27
+ })
28
+ })
@@ -17,6 +17,7 @@ export { default as NativeHealthButton } from './NativeHealthButton.stx'
17
17
  export { default as NativeNavBar } from './NativeNavBar.stx'
18
18
  export { default as NativeNetworkBanner } from './NativeNetworkBanner.stx'
19
19
  export { default as NativePermissionButton } from './NativePermissionButton.stx'
20
+ export { default as NativeProgressRing } from './NativeProgressRing.stx'
20
21
  export { default as NativePullToRefresh } from './NativePullToRefresh.stx'
21
22
  export { default as NativeSegmentedControl } from './NativeSegmentedControl.stx'
22
23
  export { default as NativeShareButton } from './NativeShareButton.stx'