@stacksjs/defaults 0.75.179 → 0.75.181
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/ai/skills/stacks-dashboard/SKILL.md +2 -2
- package/ai/skills/stacks-mobile/SKILL.md +7 -5
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/index.ts +0 -1
- package/resources/components/Video.stx +0 -132
- package/resources/components/Video.test.ts +0 -36
|
@@ -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
|
|
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/.
|
|
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,
|
|
11
|
+
The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 413 components, and a multi-section layout.
|
|
12
12
|
|
|
13
13
|
## Key Paths
|
|
14
14
|
- Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
|
|
@@ -346,7 +346,7 @@ simulator's Music app has nothing to play; launch a simulator build with
|
|
|
346
346
|
|
|
347
347
|
## Crash reporting (bughq)
|
|
348
348
|
|
|
349
|
-
`crashReporting` in `config/mobile.ts` turns on Craft's native reporter (craft-native >= 0.0.
|
|
349
|
+
`crashReporting` in `config/mobile.ts` turns on Craft's native reporter (craft-native >= 0.0.179; the build refuses an older one). It sends crashes, hangs (MetricKit), ANRs and native crashes (ApplicationExitInfo) and uncaught JavaScript errors in native screens and the app script to bughq from an on-disk outbox, with app version and build, OS, device model, session and breadcrumbs (screens shown, failed requests).
|
|
350
350
|
|
|
351
351
|
```ts
|
|
352
352
|
export default {
|
|
@@ -466,10 +466,12 @@ option the original metres heuristic remains for unlabelled inputs.
|
|
|
466
466
|
- A screen that is a task of its own (a workout player, a composer) marks an
|
|
467
467
|
element `data-native-hide-tab-bar`, and the tab bar steps away while it is
|
|
468
468
|
shown, its reserved space with it.
|
|
469
|
-
- `<Video :src="current.url">` is the
|
|
470
|
-
YouTube, Vimeo, HLS, DASH or a
|
|
471
|
-
|
|
472
|
-
|
|
469
|
+
- `<Video :src="current.url" autoplay>` is the player from
|
|
470
|
+
`@stacksjs/components` (ts-video-player): YouTube, Vimeo, HLS, DASH or a
|
|
471
|
+
file, with controls in the page's language. `src`, `poster`, `aspectRatio`
|
|
472
|
+
and `label` are reactive, so one player in a sheet can show whichever video
|
|
473
|
+
is chosen, `autoplay` starts each one, and clearing `src` pauses it. Craft
|
|
474
|
+
lets an https iframe load inside the app, so embeds play inline.
|
|
473
475
|
|
|
474
476
|
`<NativeTabItem match="/m/workout">` keeps a tab lit on the detail screens
|
|
475
477
|
opened from it. A tab bar is a `<nav>`, where a link is otherwise current only
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.181",
|
|
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.
|
|
5
|
+
"version": "0.75.181",
|
|
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.35",
|
|
58
|
-
"@stacksjs/mobile": "0.75.
|
|
58
|
+
"@stacksjs/mobile": "0.75.181",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.483",
|
|
60
60
|
"@xterm/addon-fit": "^0.11.0",
|
|
61
61
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -27,4 +27,3 @@ export { default as NativeSheet } from './NativeSheet.stx'
|
|
|
27
27
|
export { default as NativeSymbol } from './NativeSymbol.stx'
|
|
28
28
|
export { default as NativeTabBar } from './NativeTabBar.stx'
|
|
29
29
|
export { default as NativeTabItem } from './NativeTabItem.stx'
|
|
30
|
-
export { default as Video } from './Video.stx'
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
<script server>
|
|
2
|
-
interface VideoSource { src: string, type?: string }
|
|
3
|
-
interface VideoDrm {
|
|
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 VideoTrack { src: string, kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata', srclang?: string, label?: string, default?: boolean }
|
|
9
|
-
interface VideoProps {
|
|
10
|
-
src: string
|
|
11
|
-
sources?: VideoSource[]
|
|
12
|
-
drm?: VideoDrm
|
|
13
|
-
tracks?: VideoTrack[]
|
|
14
|
-
poster?: string
|
|
15
|
-
title?: string
|
|
16
|
-
width?: number
|
|
17
|
-
height?: number
|
|
18
|
-
aspectRatio?: string
|
|
19
|
-
controls?: boolean
|
|
20
|
-
autoplay?: boolean
|
|
21
|
-
loop?: boolean
|
|
22
|
-
muted?: boolean
|
|
23
|
-
playsinline?: boolean
|
|
24
|
-
preload?: 'none' | 'metadata' | 'auto'
|
|
25
|
-
crossorigin?: 'anonymous' | 'use-credentials'
|
|
26
|
-
controlslist?: string
|
|
27
|
-
disablePictureInPicture?: boolean
|
|
28
|
-
class?: string
|
|
29
|
-
}
|
|
30
|
-
const props = defineProps<VideoProps>()
|
|
31
|
-
const src = props.src || ''
|
|
32
|
-
const sources = props.sources || []
|
|
33
|
-
const tracks = props.tracks || []
|
|
34
|
-
const title = props.title || 'Video player'
|
|
35
|
-
const poster = props.poster || ''
|
|
36
|
-
const width = props.width || ''
|
|
37
|
-
const height = props.height || ''
|
|
38
|
-
const ratio = props.aspectRatio || (width && height ? `${width} / ${height}` : '16 / 9')
|
|
39
|
-
const controls = props.controls !== false
|
|
40
|
-
const inline = props.playsinline !== false
|
|
41
|
-
const preload = props.preload || 'metadata'
|
|
42
|
-
const crossorigin = props.crossorigin || ''
|
|
43
|
-
const controlslist = props.controlslist || 'nodownload'
|
|
44
|
-
const className = props.class || ''
|
|
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
|
-
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)
|
|
50
|
-
const playerConfig = JSON.stringify({ sources: playerSources, tracks: playerTracks, ...(props.drm ? { drm: props.drm } : {}) }).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026')
|
|
51
|
-
</script>
|
|
52
|
-
|
|
53
|
-
<script client>
|
|
54
|
-
import { registerElements } from 'ts-video-player/elements'
|
|
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
|
-
})
|
|
68
|
-
</script>
|
|
69
|
-
|
|
70
|
-
{{-- No src attribute until there is a video: an empty one resolves to the
|
|
71
|
-
page's own URL, which the browser then downloads as the video. --}}
|
|
72
|
-
<figure class="m-0 {{ className }}">
|
|
73
|
-
<video-player
|
|
74
|
-
ref="player"
|
|
75
|
-
@if(src) src="{{ src }}" @endif
|
|
76
|
-
:src="liveSrc() || null"
|
|
77
|
-
:poster="livePoster() || null"
|
|
78
|
-
data-media-config="{{ playerConfig }}"
|
|
79
|
-
@if(poster) poster="{{ poster }}" @endif
|
|
80
|
-
@if(controls) controls @endif
|
|
81
|
-
@if(props.autoplay) autoplay @endif
|
|
82
|
-
@if(props.loop) loop @endif
|
|
83
|
-
@if(props.muted) muted @endif
|
|
84
|
-
@if(inline) playsinline @endif
|
|
85
|
-
preload="{{ preload }}"
|
|
86
|
-
@if(crossorigin) crossorigin="{{ crossorigin }}" @endif
|
|
87
|
-
controlslist="{{ controlslist }}"
|
|
88
|
-
@if(props.disablePictureInPicture) disable-picture-in-picture @endif
|
|
89
|
-
keep-alive
|
|
90
|
-
aria-label="{{ title }}"
|
|
91
|
-
class="block overflow-hidden relative w-full bg-black ring-1 ring-black/10 rounded-2xl dark:ring-white/10 focus-within:ring-2 focus-within:ring-sky-500 shadow-sm group"
|
|
92
|
-
style="aspect-ratio: {{ ratio }};"
|
|
93
|
-
>
|
|
94
|
-
<media-error-display></media-error-display>
|
|
95
|
-
@if(isEmbed)
|
|
96
|
-
<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>
|
|
97
|
-
@else
|
|
98
|
-
<video data-native-fallback @if(src) src="{{ src }}" @endif @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">
|
|
99
|
-
@foreach (sources as source)
|
|
100
|
-
<source src="{{ source.src }}" @if(source.type) type="{{ source.type }}" @endif>
|
|
101
|
-
@endforeach
|
|
102
|
-
@foreach (tracks as track)
|
|
103
|
-
<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>
|
|
104
|
-
@endforeach
|
|
105
|
-
Your browser does not support video playback.
|
|
106
|
-
</video>
|
|
107
|
-
@endif
|
|
108
|
-
<video-skin>
|
|
109
|
-
<media-seek-button slot="left" seconds="-10" aria-label="Back 10 seconds"></media-seek-button>
|
|
110
|
-
<media-play-button slot="left" aria-label="Play or pause"></media-play-button>
|
|
111
|
-
<media-seek-button slot="left" seconds="10" aria-label="Forward 10 seconds"></media-seek-button>
|
|
112
|
-
<media-live-button slot="left" aria-label="Go to live edge"></media-live-button>
|
|
113
|
-
<media-time-group slot="center" class="font-mono tabular-nums text-xs"></media-time-group>
|
|
114
|
-
<media-progress-bar slot="center" previews="metadata" aria-label="Video progress"></media-progress-bar>
|
|
115
|
-
<media-mute-button slot="right" aria-label="Mute or unmute"></media-mute-button>
|
|
116
|
-
<media-volume-slider slot="right" aria-label="Volume"></media-volume-slider>
|
|
117
|
-
<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>
|
|
118
|
-
<media-settings-menu slot="right" aria-label="Video settings"></media-settings-menu>
|
|
119
|
-
<media-airplay-button slot="right" aria-label="Play on AirPlay"></media-airplay-button>
|
|
120
|
-
<media-remote-playback-button slot="right" aria-label="Play on another device"></media-remote-playback-button>
|
|
121
|
-
<media-pip-button slot="right" aria-label="Picture in Picture"></media-pip-button>
|
|
122
|
-
<media-fullscreen-button slot="right" aria-label="Fullscreen"></media-fullscreen-button>
|
|
123
|
-
</video-skin>
|
|
124
|
-
</video-player>
|
|
125
|
-
<figcaption class="sr-only">{{ title }}</figcaption>
|
|
126
|
-
</figure>
|
|
127
|
-
|
|
128
|
-
<style>
|
|
129
|
-
video-player:not(:defined) video-skin { display: none; }
|
|
130
|
-
video-player:defined > [data-native-fallback] { display: none; }
|
|
131
|
-
@media (prefers-reduced-motion: reduce) { video-player, video-player * { transition-duration: 0.01ms !important; } }
|
|
132
|
-
</style>
|
|
@@ -1,36 +0,0 @@
|
|
|
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('renders no src attribute without a video, which would fetch the page itself', () => {
|
|
15
|
-
// An empty src resolves to the document's URL: the browser downloads the
|
|
16
|
-
// whole page as the video and fails, every time a player with nothing
|
|
17
|
-
// chosen yet renders.
|
|
18
|
-
expect(video).not.toMatch(/(?<!@if\(src\) )src="\{\{ src \}\}"/)
|
|
19
|
-
expect(video.match(/@if\(src\) src="\{\{ src \}\}" @endif/g)?.length).toBe(2)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('pauses when its src is cleared, as closing a sheet does', () => {
|
|
23
|
-
expect(video).toContain(`useRef('player')`)
|
|
24
|
-
expect(video).toContain('ref="player"')
|
|
25
|
-
expect(video).toMatch(/if \(!liveSrc\(\)\)[\s\S]*?\.pause\(\)/)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('never hands a YouTube or Vimeo page to the native <video> fallback', () => {
|
|
29
|
-
const isEmbed = new RegExp(video.match(/const isEmbed = \/(.+)\/i\.test/)![1]!, 'i')
|
|
30
|
-
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'])
|
|
31
|
-
expect(isEmbed.test(url)).toBe(true)
|
|
32
|
-
for (const url of ['https://cdn.example.com/clip.mp4', '/media/demo/manifest.mpd', 'https://example.com/youtube.com/fake.mp4'])
|
|
33
|
-
expect(isEmbed.test(url)).toBe(false)
|
|
34
|
-
expect(video).toContain('@if(isEmbed)')
|
|
35
|
-
})
|
|
36
|
-
})
|