@osiris-smarttv/tv-viewport-frame 0.1.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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/core.d.ts +67 -0
- package/dist/core.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/react.d.ts +70 -0
- package/dist/react.js +1 -0
- package/dist/tv-viewport-frame.css +52 -0
- package/dist/viewportStore-yf19qOVB.d.ts +87 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OsirisTech SmartTV
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @osiris-smarttv/tv-viewport-frame
|
|
2
|
+
|
|
3
|
+
Fixed 1920x1080 viewport frame for Smart TV apps.
|
|
4
|
+
|
|
5
|
+
This package keeps your app rendered inside a deterministic Full HD artboard while the host browser window can be any size. It handles center letterboxing, root `rem` scaling, and design-unit conversion so layout remains stable across TVs, emulator windows, and desktop testing.
|
|
6
|
+
|
|
7
|
+
## Why this package
|
|
8
|
+
|
|
9
|
+
- **Stable TV canvas:** one 1920x1080 design stage, centered in the host
|
|
10
|
+
- **Responsive scale:** automatic scale-down for smaller host windows
|
|
11
|
+
- **Design math helpers:** convert design px/rem into real rendered pixels
|
|
12
|
+
- **Framework split:** pure `core` API + `react` integration layer
|
|
13
|
+
- **DOM/CSS contract:** predictable stage/app attributes for integration and testing
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i @osiris-smarttv/tv-viewport-frame
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Exports
|
|
22
|
+
|
|
23
|
+
- `@osiris-smarttv/tv-viewport-frame` (package root)
|
|
24
|
+
- `@osiris-smarttv/tv-viewport-frame/core`
|
|
25
|
+
- `@osiris-smarttv/tv-viewport-frame/react`
|
|
26
|
+
- `@osiris-smarttv/tv-viewport-frame/styles.css`
|
|
27
|
+
|
|
28
|
+
## Quick Start (React)
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { TvViewportFrame } from '@osiris-smarttv/tv-viewport-frame/react'
|
|
32
|
+
import '@osiris-smarttv/tv-viewport-frame/styles.css'
|
|
33
|
+
|
|
34
|
+
export function AppRoot() {
|
|
35
|
+
return (
|
|
36
|
+
<TvViewportFrame>
|
|
37
|
+
<App />
|
|
38
|
+
</TvViewportFrame>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### With config override
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
<TvViewportFrame
|
|
47
|
+
config={{
|
|
48
|
+
fitToWindow: true,
|
|
49
|
+
showViewportBadge: false,
|
|
50
|
+
}}
|
|
51
|
+
onViewportChange={(snapshot) => {
|
|
52
|
+
// snapshot.scale, snapshot.host, snapshot.realSize
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<App />
|
|
56
|
+
</TvViewportFrame>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start (Core only, no React)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createTvViewportSdk } from '@osiris-smarttv/tv-viewport-frame/core'
|
|
63
|
+
|
|
64
|
+
const sdk = createTvViewportSdk()
|
|
65
|
+
const snapshot = sdk.snapshot({ width: 1280, height: 720 })
|
|
66
|
+
|
|
67
|
+
const scale = snapshot.scale
|
|
68
|
+
const buttonPx = snapshot.realSize.px(96) // design 96px -> rendered px
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## DOM and CSS contract
|
|
72
|
+
|
|
73
|
+
When enabled, frame chrome renders:
|
|
74
|
+
|
|
75
|
+
- root frame: `.tv-viewport-frame` (`data-tv-viewport-frame-root`)
|
|
76
|
+
- stage: `.tv-viewport-frame__stage` (`data-tv-viewport-stage`)
|
|
77
|
+
- app shell: `.tv-viewport-frame__app` (`data-tv-viewport-app`)
|
|
78
|
+
|
|
79
|
+
The style export sets:
|
|
80
|
+
|
|
81
|
+
- `html[data-tv-viewport-frame] { font-size: calc(var(--tv-viewport-scale, 1) * 100%); }`
|
|
82
|
+
- stage dimensions via CSS vars (`--tv-viewport-stage-width-rem`, `--tv-viewport-stage-height-rem`)
|
|
83
|
+
|
|
84
|
+
Import once in app bootstrap:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import '@osiris-smarttv/tv-viewport-frame/styles.css'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## React hooks
|
|
91
|
+
|
|
92
|
+
From `@osiris-smarttv/tv-viewport-frame/react`:
|
|
93
|
+
|
|
94
|
+
- `useTvViewport()` -> full viewport state (`enabled`, `snapshot`, `config`, `sdk`)
|
|
95
|
+
- `useTvViewportScale()` -> current scale only
|
|
96
|
+
- `useTvViewportRealSize()` -> px/rem conversion helpers bound to current scale
|
|
97
|
+
- `useTvViewportSelector(selector, isEqual?)` -> optimized state selection
|
|
98
|
+
|
|
99
|
+
## Core API highlights
|
|
100
|
+
|
|
101
|
+
From `@osiris-smarttv/tv-viewport-frame/core`:
|
|
102
|
+
|
|
103
|
+
- `createTvViewportSdk()`
|
|
104
|
+
- `createTvViewportStore()`
|
|
105
|
+
- `computeTvViewportScale()`
|
|
106
|
+
- `designPxToRealPx()`, `designPxToRem()`, `designRemToRealPx()`
|
|
107
|
+
- `applyTvViewportDocumentState()`, `clearTvViewportDocumentState()`
|
|
108
|
+
- presets/constants: `TV_VIEWPORT_PRESETS`, `TV_VIEWPORT_FULL_HD`, `DEFAULT_TV_VIEWPORT_FRAME_CONFIG`
|
|
109
|
+
|
|
110
|
+
## Runtime policy
|
|
111
|
+
|
|
112
|
+
`TvViewportFrame` defaults to a runtime policy:
|
|
113
|
+
|
|
114
|
+
- disabled in `test` mode
|
|
115
|
+
- otherwise enabled unless `VITE_TV_VIEWPORT_FRAME=false`
|
|
116
|
+
|
|
117
|
+
You can always override with explicit `enabled` prop.
|
|
118
|
+
|
|
119
|
+
## Local development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
yarn install
|
|
123
|
+
yarn build
|
|
124
|
+
yarn typecheck
|
|
125
|
+
yarn test
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Manual publish (no GitHub Actions)
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
yarn publish:check
|
|
132
|
+
yarn publish:dry-run
|
|
133
|
+
yarn publish:manual
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If publish returns `404` or permission errors for `@osiris-smarttv/*`, verify:
|
|
137
|
+
|
|
138
|
+
- your npm account has publish access to scope `@osiris-smarttv`
|
|
139
|
+
- package name is allowed by org policy
|
|
140
|
+
- you are authenticated against `https://registry.npmjs.org/`
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { d as TvViewportFrameConfig, g as TvViewportSdk, C as CreateTvViewportRealSizeOptions, f as TvViewportRealSizeContext, T as TvDesignPx, a as TvDesignSize, c as TvRealSize, R as ResolveTvViewportRealSizeOptions, h as TvViewportSize, e as TvViewportPreset, b as TvHostSize } from './viewportStore-yf19qOVB.js';
|
|
2
|
+
export { i as TvViewportSnapshot, j as TvViewportStore, k as TvViewportStoreOptions, l as TvViewportStoreState, m as createTvViewportStore } from './viewportStore-yf19qOVB.js';
|
|
3
|
+
|
|
4
|
+
declare function tvViewportConfigPartialEqual(a?: Partial<TvViewportFrameConfig>, b?: Partial<TvViewportFrameConfig>): boolean;
|
|
5
|
+
|
|
6
|
+
declare function createTvViewportSdk(configInput?: Partial<TvViewportFrameConfig>): TvViewportSdk;
|
|
7
|
+
|
|
8
|
+
declare function designPxToRem(designPx: TvDesignPx, remBasePx?: number): number;
|
|
9
|
+
declare function designPxToRealPx(designPx: TvDesignPx, scale: number): number;
|
|
10
|
+
declare function designRemToRealPx(designRem: number, rootFontSizePx: number): number;
|
|
11
|
+
declare function designSizeToRealSize(design: TvDesignSize, scale: number): TvRealSize;
|
|
12
|
+
declare function createTvViewportRealSize({ scale, uaRootPx, }: CreateTvViewportRealSizeOptions): TvViewportRealSizeContext;
|
|
13
|
+
declare function resolveDesignPxToRealPx(designPx: TvDesignPx, hostWidth: number, hostHeight: number, options?: ResolveTvViewportRealSizeOptions): number;
|
|
14
|
+
declare function resolveTvViewportRealSizeFromHost(hostWidth: number, hostHeight: number, options?: ResolveTvViewportRealSizeOptions): TvViewportRealSizeContext;
|
|
15
|
+
declare function readTvViewportRealSizeFromDocument(uaRootPx?: number, root?: HTMLElement): TvViewportRealSizeContext;
|
|
16
|
+
|
|
17
|
+
declare const TvViewportDomAttr: {
|
|
18
|
+
readonly Frame: "data-tv-viewport-frame";
|
|
19
|
+
readonly Root: "data-tv-viewport-frame-root";
|
|
20
|
+
readonly Stage: "data-tv-viewport-stage";
|
|
21
|
+
readonly App: "data-tv-viewport-app";
|
|
22
|
+
};
|
|
23
|
+
declare const TvViewportCssVar: {
|
|
24
|
+
readonly Width: "--tv-viewport-width";
|
|
25
|
+
readonly Height: "--tv-viewport-height";
|
|
26
|
+
readonly Scale: "--tv-viewport-scale";
|
|
27
|
+
readonly StageWidthRem: "--tv-viewport-stage-width-rem";
|
|
28
|
+
readonly StageHeightRem: "--tv-viewport-stage-height-rem";
|
|
29
|
+
};
|
|
30
|
+
type ApplyTvViewportDocumentStateInput = {
|
|
31
|
+
readonly config: TvViewportFrameConfig;
|
|
32
|
+
readonly scale: number;
|
|
33
|
+
};
|
|
34
|
+
declare function applyTvViewportDocumentState({ config, scale }: ApplyTvViewportDocumentStateInput, root?: HTMLElement): void;
|
|
35
|
+
declare function clearTvViewportDocumentState(root?: HTMLElement): void;
|
|
36
|
+
declare function readTvViewportScaleFromDocument(fallback?: number, root?: HTMLElement): number;
|
|
37
|
+
|
|
38
|
+
declare const TV_VIEWPORT_REM_BASE_PX = 16;
|
|
39
|
+
declare const TV_VIEWPORT_ROOT_FONT_PERCENT = 100;
|
|
40
|
+
declare const TV_VIEWPORT_FULL_HD: TvViewportSize;
|
|
41
|
+
/** Closed registry of supported TV design presets. */
|
|
42
|
+
declare const TV_VIEWPORT_PRESETS: {
|
|
43
|
+
readonly "1920x1080": TvViewportSize;
|
|
44
|
+
};
|
|
45
|
+
declare function resolveTvViewportPreset(preset: TvViewportPreset): TvViewportSize;
|
|
46
|
+
declare const TV_VIEWPORT_WIDTH_REM: number;
|
|
47
|
+
declare const TV_VIEWPORT_HEIGHT_REM: number;
|
|
48
|
+
declare const DEFAULT_TV_VIEWPORT_FRAME_CONFIG: TvViewportFrameConfig;
|
|
49
|
+
declare function mergeTvViewportFrameConfig(partial?: Partial<TvViewportFrameConfig>): TvViewportFrameConfig;
|
|
50
|
+
|
|
51
|
+
/** Sub-pixel tolerance when comparing viewport scale values. */
|
|
52
|
+
declare const TV_VIEWPORT_SCALE_EPSILON = 0.000001;
|
|
53
|
+
declare function tvViewportScalesEqual(a: number, b: number, epsilon?: number): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Host scale relative to the TV design frame.
|
|
56
|
+
*
|
|
57
|
+
* `scale = min(hostW / frameW, hostH / frameH)` when `fitToWindow` (capped at 1).
|
|
58
|
+
*/
|
|
59
|
+
declare function computeTvViewportScale(host: TvHostSize, viewport?: TvViewportSize, fitToWindow?: boolean): number;
|
|
60
|
+
declare function computeTvViewportScaleFromNumbers(hostWidth: number, hostHeight: number, viewport?: TvViewportSize, fitToWindow?: boolean): number;
|
|
61
|
+
/** @deprecated Use {@link computeTvViewportScaleFromNumbers}. */
|
|
62
|
+
declare const computeTvViewportRemScale: typeof computeTvViewportScaleFromNumbers;
|
|
63
|
+
|
|
64
|
+
declare function resolveTvViewportRootFontSizePx(hostWidth: number, hostHeight: number, uaRootPx?: number, viewport?: TvViewportSize, fitToWindow?: boolean): number;
|
|
65
|
+
declare function resolveTvViewportRootFontSizePercent(hostWidth: number, hostHeight: number, viewport?: TvViewportSize, fitToWindow?: boolean): number;
|
|
66
|
+
|
|
67
|
+
export { CreateTvViewportRealSizeOptions, DEFAULT_TV_VIEWPORT_FRAME_CONFIG, ResolveTvViewportRealSizeOptions, TV_VIEWPORT_FULL_HD, TV_VIEWPORT_HEIGHT_REM, TV_VIEWPORT_PRESETS, TV_VIEWPORT_REM_BASE_PX, TV_VIEWPORT_ROOT_FONT_PERCENT, TV_VIEWPORT_SCALE_EPSILON, TV_VIEWPORT_WIDTH_REM, TvDesignPx, TvDesignSize, TvHostSize, TvRealSize, TvViewportCssVar, TvViewportDomAttr, TvViewportFrameConfig, TvViewportPreset, TvViewportRealSizeContext, TvViewportSdk, TvViewportSize, applyTvViewportDocumentState, clearTvViewportDocumentState, computeTvViewportRemScale, computeTvViewportScale, computeTvViewportScaleFromNumbers, createTvViewportRealSize, createTvViewportSdk, designPxToRealPx, designPxToRem, designRemToRealPx, designSizeToRealSize, mergeTvViewportFrameConfig, readTvViewportRealSizeFromDocument, readTvViewportScaleFromDocument, resolveDesignPxToRealPx, resolveTvViewportPreset, resolveTvViewportRealSizeFromHost, resolveTvViewportRootFontSizePercent, resolveTvViewportRootFontSizePx, tvViewportConfigPartialEqual, tvViewportScalesEqual };
|
package/dist/core.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function I(e,t){if(e===t)return true;if(!e||!t)return !e&&!t;let r=e.viewport,o=t.viewport;return e.remBasePx===t.remBasePx&&e.fitToWindow===t.fitToWindow&&e.showViewportBadge===t.showViewportBadge&&r?.width===o?.width&&r?.height===o?.height&&r?.preset===o?.preset}var P={Frame:"data-tv-viewport-frame",Root:"data-tv-viewport-frame-root",Stage:"data-tv-viewport-stage",App:"data-tv-viewport-app"},p={Width:"--tv-viewport-width",Height:"--tv-viewport-height",Scale:"--tv-viewport-scale",StageWidthRem:"--tv-viewport-stage-width-rem",StageHeightRem:"--tv-viewport-stage-height-rem"},L=[p.Width,p.Height,p.Scale,p.StageWidthRem,p.StageHeightRem];function y({config:e,scale:t},r=document.documentElement){let{width:o,height:i,preset:s}=e.viewport;r.setAttribute(P.Frame,s),r.style.setProperty(p.Width,`${o}px`),r.style.setProperty(p.Height,`${i}px`),r.style.setProperty(p.Scale,String(t)),r.style.setProperty(p.StageWidthRem,`${e.viewport.width/e.remBasePx}rem`),r.style.setProperty(p.StageHeightRem,`${e.viewport.height/e.remBasePx}rem`);}function f(e=document.documentElement){e.removeAttribute(P.Frame);for(let t of L)e.style.removeProperty(t);}function w(e=1,t=document.documentElement){let r=t.style.getPropertyValue(p.Scale).trim();if(!r)return e;let o=Number.parseFloat(r);return Number.isFinite(o)?o:e}var d={FullHd:"1920x1080"};var T=16,S=100,a={width:1920,height:1080,preset:d.FullHd},_={[d.FullHd]:a};function M(e){return _[e]}var N=a.width/T,A=a.height/T,h={viewport:a,remBasePx:T,fitToWindow:true,showViewportBadge:false};function g(e){return {...h,...e,viewport:{...h.viewport,...e?.viewport}}}var F=1e-6;function z(e,t,r=F){return Math.abs(e-t)<r}function R(e,t=a,r=true){return m(e.width,e.height,t,r)}function m(e,t,r=a,o=true){return !o||e<=0||t<=0||e>=r.width&&t>=r.height?1:Math.min(e/r.width,t/r.height)}var B=m;function D(e,t=T){return e/t}function V(e,t){return e*t}function O(e,t){return e*t}function C(e,t){return {width:V(e.width,t),height:V(e.height,t)}}function v({scale:e,uaRootPx:t=T}){let r=t*e,o=S*e;return {scale:e,rootFontSizePercent:o,rootFontSizePx:r,px:i=>V(i,e),rem:i=>D(i,t),remPx:i=>O(i,r),size:i=>C(i,e)}}function U(e,t,r,o={}){let i=m(t,r,o.viewport,o.fitToWindow??true);return V(e,i)}function q(e,t,r={}){let o=m(e,t,r.viewport,r.fitToWindow??true);return v({scale:o,uaRootPx:r.uaRootPx})}function G(e=T,t=document.documentElement){return v({scale:w(1,t),uaRootPx:e})}function X(e){let t=g(e);return {config:t,computeScale:o=>R(o,t.viewport,t.fitToWindow),snapshot:o=>{let i=R(o,t.viewport,t.fitToWindow);return {scale:i,host:o,realSize:v({scale:i,uaRootPx:t.remBasePx})}},createRealSize:o=>v({scale:o,uaRootPx:t.remBasePx}),applyDocumentState:(o,i)=>y({config:t,scale:o},i),clearDocumentState:o=>f(o),readScaleFromDocument:(o,i)=>w(o,i),readRealSizeFromDocument:o=>v({scale:w(1,o),uaRootPx:t.remBasePx})}}var $={width:1920,height:1080};function H(e,t,r){return r?e.snapshot(t):e.snapshot($)}function k(e,t,r={}){let o=r.enabled??true,i=t,s=H(e,i,o),x={enabled:o,snapshot:s},c=new Set;o&&e.applyDocumentState(s.scale);let b=(n,l)=>{s=n,o=l,x={enabled:o,snapshot:s},c.forEach(u=>u()),r.onViewportChange?.(n);},W=n=>{o?e.applyDocumentState(n):e.clearDocumentState();};return {sdk:e,getState:()=>x,getServerState:()=>x,subscribe:n=>(c.add(n),()=>c.delete(n)),setEnabled:n=>{if(o===n)return;let l=H(e,i,n);n?e.applyDocumentState(l.scale):e.clearDocumentState(),b(l,n);},setHost:n=>{if(!o)return;let l=i.width===n.width&&i.height===n.height,u=e.snapshot(n),E=z(s.scale,u.scale);i=n,!(l&&E)&&(E||(W(u.scale),b(u,o)));},dispose:()=>{c.clear(),e.clearDocumentState();}}}function K(e,t,r=T,o=a,i=true){return r*m(e,t,o,i)}function Y(e,t,r=a,o=true){return S*m(e,t,r,o)}export{h as DEFAULT_TV_VIEWPORT_FRAME_CONFIG,a as TV_VIEWPORT_FULL_HD,A as TV_VIEWPORT_HEIGHT_REM,_ as TV_VIEWPORT_PRESETS,T as TV_VIEWPORT_REM_BASE_PX,S as TV_VIEWPORT_ROOT_FONT_PERCENT,F as TV_VIEWPORT_SCALE_EPSILON,N as TV_VIEWPORT_WIDTH_REM,p as TvViewportCssVar,P as TvViewportDomAttr,d as TvViewportPreset,y as applyTvViewportDocumentState,f as clearTvViewportDocumentState,B as computeTvViewportRemScale,R as computeTvViewportScale,m as computeTvViewportScaleFromNumbers,v as createTvViewportRealSize,X as createTvViewportSdk,k as createTvViewportStore,V as designPxToRealPx,D as designPxToRem,O as designRemToRealPx,C as designSizeToRealSize,g as mergeTvViewportFrameConfig,G as readTvViewportRealSizeFromDocument,w as readTvViewportScaleFromDocument,U as resolveDesignPxToRealPx,M as resolveTvViewportPreset,q as resolveTvViewportRealSizeFromHost,Y as resolveTvViewportRootFontSizePercent,K as resolveTvViewportRootFontSizePx,I as tvViewportConfigPartialEqual,z as tvViewportScalesEqual};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { DEFAULT_TV_VIEWPORT_FRAME_CONFIG, TV_VIEWPORT_FULL_HD, TV_VIEWPORT_HEIGHT_REM, TV_VIEWPORT_PRESETS, TV_VIEWPORT_REM_BASE_PX, TV_VIEWPORT_ROOT_FONT_PERCENT, TV_VIEWPORT_SCALE_EPSILON, TV_VIEWPORT_WIDTH_REM, TvViewportCssVar, TvViewportDomAttr, applyTvViewportDocumentState, clearTvViewportDocumentState, computeTvViewportRemScale, computeTvViewportScale, computeTvViewportScaleFromNumbers, createTvViewportRealSize, createTvViewportSdk, designPxToRealPx, designPxToRem, designRemToRealPx, designSizeToRealSize, mergeTvViewportFrameConfig, readTvViewportRealSizeFromDocument, readTvViewportScaleFromDocument, resolveDesignPxToRealPx, resolveTvViewportPreset, resolveTvViewportRealSizeFromHost, resolveTvViewportRootFontSizePercent, resolveTvViewportRootFontSizePx, tvViewportConfigPartialEqual, tvViewportScalesEqual } from './core.js';
|
|
2
|
+
export { C as CreateTvViewportRealSizeOptions, R as ResolveTvViewportRealSizeOptions, T as TvDesignPx, a as TvDesignSize, b as TvHostSize, c as TvRealSize, d as TvViewportFrameConfig, e as TvViewportPreset, f as TvViewportRealSizeContext, g as TvViewportSdk, h as TvViewportSize, i as TvViewportSnapshot, j as TvViewportStore, k as TvViewportStoreOptions, l as TvViewportStoreState, m as createTvViewportStore } from './viewportStore-yf19qOVB.js';
|
|
3
|
+
export { TvViewportFrame, TvViewportHookValue, TvViewportProvider, TvViewportRuntimePolicy, TvViewportRuntimePolicyInput, computeViewportFrameScale, isTvViewportFrameEnabled, resolveTvViewportRuntimePolicy, useTvViewport, useTvViewportContext, useTvViewportContextOptional, useTvViewportRealSize, useTvViewportScale, useTvViewportSelector, useViewportFrameScale } from './react.js';
|
|
4
|
+
import 'react';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {createContext,memo,useRef,useSyncExternalStore,useContext,useMemo,useLayoutEffect,useState}from'react';import {jsx,jsxs}from'react/jsx-runtime';function W(e,t){if(e===t)return true;if(!e||!t)return !e&&!t;let o=e.viewport,r=t.viewport;return e.remBasePx===t.remBasePx&&e.fitToWindow===t.fitToWindow&&e.showViewportBadge===t.showViewportBadge&&o?.width===r?.width&&o?.height===r?.height&&o?.preset===r?.preset}var I={Frame:"data-tv-viewport-frame",Root:"data-tv-viewport-frame-root",Stage:"data-tv-viewport-stage",App:"data-tv-viewport-app"},v={Width:"--tv-viewport-width",Height:"--tv-viewport-height",Scale:"--tv-viewport-scale",StageWidthRem:"--tv-viewport-stage-width-rem",StageHeightRem:"--tv-viewport-stage-height-rem"},ie=[v.Width,v.Height,v.Scale,v.StageWidthRem,v.StageHeightRem];function N({config:e,scale:t},o=document.documentElement){let{width:r,height:i,preset:n}=e.viewport;o.setAttribute(I.Frame,n),o.style.setProperty(v.Width,`${r}px`),o.style.setProperty(v.Height,`${i}px`),o.style.setProperty(v.Scale,String(t)),o.style.setProperty(v.StageWidthRem,`${e.viewport.width/e.remBasePx}rem`),o.style.setProperty(v.StageHeightRem,`${e.viewport.height/e.remBasePx}rem`);}function L(e=document.documentElement){e.removeAttribute(I.Frame);for(let t of ie)e.style.removeProperty(t);}function f(e=1,t=document.documentElement){let o=t.style.getPropertyValue(v.Scale).trim();if(!o)return e;let r=Number.parseFloat(o);return Number.isFinite(r)?r:e}var P={FullHd:"1920x1080"};var c=16,y=100,T={width:1920,height:1080,preset:P.FullHd},X={[P.FullHd]:T};function ne(e){return X[e]}var pe=T.width/c,ae=T.height/c,V={viewport:T,remBasePx:c,fitToWindow:true,showViewportBadge:false};function g(e){return {...V,...e,viewport:{...V.viewport,...e?.viewport}}}var K=1e-6;function h(e,t,o=K){return Math.abs(e-t)<o}function b(e,t=T,o=true){return u(e.width,e.height,t,o)}function u(e,t,o=T,r=true){return !r||e<=0||t<=0||e>=o.width&&t>=o.height?1:Math.min(e/o.width,t/o.height)}var se=u;function j(e,t=c){return e/t}function R(e,t){return e*t}function Y(e,t){return e*t}function J(e,t){return {width:R(e.width,t),height:R(e.height,t)}}function w({scale:e,uaRootPx:t=c}){let o=t*e,r=y*e;return {scale:e,rootFontSizePercent:r,rootFontSizePx:o,px:i=>R(i,e),rem:i=>j(i,t),remPx:i=>Y(i,o),size:i=>J(i,e)}}function me(e,t,o,r={}){let i=u(t,o,r.viewport,r.fitToWindow??true);return R(e,i)}function ve(e,t,o={}){let r=u(e,t,o.viewport,o.fitToWindow??true);return w({scale:r,uaRootPx:o.uaRootPx})}function ue(e=c,t=document.documentElement){return w({scale:f(1,t),uaRootPx:e})}function M(e){let t=g(e);return {config:t,computeScale:r=>b(r,t.viewport,t.fitToWindow),snapshot:r=>{let i=b(r,t.viewport,t.fitToWindow);return {scale:i,host:r,realSize:w({scale:i,uaRootPx:t.remBasePx})}},createRealSize:r=>w({scale:r,uaRootPx:t.remBasePx}),applyDocumentState:(r,i)=>N({config:t,scale:r},i),clearDocumentState:r=>L(r),readScaleFromDocument:(r,i)=>f(r,i),readRealSizeFromDocument:r=>w({scale:f(1,r),uaRootPx:t.remBasePx})}}var le={width:1920,height:1080};function Q(e,t,o){return o?e.snapshot(t):e.snapshot(le)}function k(e,t,o={}){let r=o.enabled??true,i=t,n=Q(e,i,r),m={enabled:r,snapshot:n},s=new Set;r&&e.applyDocumentState(n.scale);let a=(p,d)=>{n=p,r=d,m={enabled:r,snapshot:n},s.forEach(x=>x()),o.onViewportChange?.(p);},l=p=>{r?e.applyDocumentState(p):e.clearDocumentState();};return {sdk:e,getState:()=>m,getServerState:()=>m,subscribe:p=>(s.add(p),()=>s.delete(p)),setEnabled:p=>{if(r===p)return;let d=Q(e,i,p);p?e.applyDocumentState(d.scale):e.clearDocumentState(),a(d,p);},setHost:p=>{if(!r)return;let d=i.width===p.width&&i.height===p.height,x=e.snapshot(p),G=h(n.scale,x.scale);i=p,!(d&&G)&&(G||(l(x.scale),a(x,r)));},dispose:()=>{s.clear(),e.clearDocumentState();}}}function Te(e,t,o=c,r=T,i=true){return o*u(e,t,r,i)}function ce(e,t,o=T,r=true){return y*u(e,t,o,r)}function E(e,t){return t?`${e} ${t}`:e}function S(e={}){let t=e.mode??(typeof import.meta<"u"?import.meta.env?.MODE:void 0),o=e.frameFlag??(typeof import.meta<"u"?import.meta.env?.VITE_TV_VIEWPORT_FRAME:void 0);return t==="test"?{frameEnabled:false}:{frameEnabled:o!=="false"&&o!==false}}function we(){return S().frameEnabled}function Ve(){return typeof window>"u"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function z(e){if(typeof window>"u")return ()=>{};let t=0,o=false,r=()=>{o=false,e(Ve());},i=()=>{o||(o=true,t=window.requestAnimationFrame(r));};i(),window.addEventListener("resize",i);let n=typeof ResizeObserver<"u"?new ResizeObserver(i):null;return n&&document.documentElement&&n.observe(document.documentElement),()=>{window.removeEventListener("resize",i),t&&window.cancelAnimationFrame(t),n?.disconnect();}}var F=createContext(null);function _(){let e=useContext(F);if(!e)throw new Error("useTvViewport must be used within TvViewportProvider / TvViewportFrame");return e}function C(){return useContext(F)}function ee(e){let t=useRef(e);return W(t.current,e)||(t.current=e),t.current}function B({children:e,config:t,enabled:o=true,onViewportChange:r}){let i=ee(t),n=useMemo(()=>g(i),[i]),m=useMemo(()=>M(n),[n]),s=useRef(r);s.current=r;let a=useMemo(()=>k(m,typeof window>"u"?{width:1920,height:1080}:{width:window.innerWidth,height:window.innerHeight},{enabled:o,onViewportChange:p=>s.current?.(p)}),[m]);useLayoutEffect(()=>{a.setEnabled(o);},[o,a]),useLayoutEffect(()=>{if(o)return z(p=>{a.setHost(p);})},[o,a]),useLayoutEffect(()=>()=>a.dispose(),[a]);let l=useMemo(()=>({store:a,sdk:m}),[a,m]);return jsx(F.Provider,{value:l,children:e})}function U({viewport:e,fitToWindow:t,enabled:o}){let[r,i]=useState(1),n=useRef(r);return useLayoutEffect(()=>{if(!o){n.current=1,i(1);return}let m=(s,a)=>{let l=u(s,a,e,t);h(n.current,l)||(n.current=l,i(l));};return z(({width:s,height:a})=>m(s,a))},[o,t,e.height,e.width,e.preset]),r}function Re(e,t,o,r){return u(e,t,o,r)}var be=()=>()=>{};function D(e){let t=e.getState();return {sdk:e.sdk,config:e.sdk.config,enabled:t.enabled,snapshot:t.snapshot}}function Ee(e,t=Object.is){let{store:o}=_(),r=useRef(void 0);return useSyncExternalStore(o.subscribe,()=>{let i=e(D(o));return r.current!==void 0&&t(r.current,i)?r.current:(r.current=i,i)},()=>e(D(o)))}function $(){let{store:e}=_(),t=useRef(void 0);return useSyncExternalStore(e.subscribe,()=>{let o=D(e),r=t.current;return r&&r.enabled===o.enabled&&r.snapshot===o.snapshot&&r.sdk===o.sdk?r:(t.current=o,o)},()=>D(e))}function oe(){let e=C(),t=useSyncExternalStore(e?e.store.subscribe:be,()=>e?e.store.getState().snapshot.scale:1,()=>1),o=S().frameEnabled,r=U({enabled:!e&&o,fitToWindow:V.fitToWindow,viewport:V.viewport});return e?t:r}function ze(){let e=C(),t=oe(),o=e?.sdk.config.remBasePx??V.remBasePx;return useMemo(()=>w({scale:t,uaRootPx:o}),[o,t])}var Ce=memo(function({children:t,className:o,stageClassName:r,appClassName:i}){let{config:n,enabled:m,snapshot:s}=$();if(!m)return t;let{preset:a}=n.viewport,l=s.scale<1?` \xB7 ${Math.round(s.scale*100)}%`:"";return jsx("div",{className:E("tv-viewport-frame",o),"data-tv-viewport-frame-root":true,children:jsxs("div",{className:E("tv-viewport-frame__stage",r),"data-tv-viewport-stage":true,children:[n.showViewportBadge?jsxs("div",{className:"tv-viewport-frame__badge","aria-hidden":true,children:[a,l]}):null,jsx("div",{className:E("tv-viewport-frame__app",i),"data-tv-viewport-app":true,children:t})]})})});function Oe({children:e,config:t,enabled:o,onViewportChange:r,className:i,stageClassName:n,appClassName:m}){let s=o??S().frameEnabled,a=useRef(r);return a.current=r,jsx(B,{config:t,enabled:s,onViewportChange:l=>a.current?.(l),children:jsx(Ce,{className:i,stageClassName:n,appClassName:m,children:e})})}export{V as DEFAULT_TV_VIEWPORT_FRAME_CONFIG,T as TV_VIEWPORT_FULL_HD,ae as TV_VIEWPORT_HEIGHT_REM,X as TV_VIEWPORT_PRESETS,c as TV_VIEWPORT_REM_BASE_PX,y as TV_VIEWPORT_ROOT_FONT_PERCENT,K as TV_VIEWPORT_SCALE_EPSILON,pe as TV_VIEWPORT_WIDTH_REM,v as TvViewportCssVar,I as TvViewportDomAttr,Oe as TvViewportFrame,P as TvViewportPreset,B as TvViewportProvider,N as applyTvViewportDocumentState,L as clearTvViewportDocumentState,se as computeTvViewportRemScale,b as computeTvViewportScale,u as computeTvViewportScaleFromNumbers,Re as computeViewportFrameScale,w as createTvViewportRealSize,M as createTvViewportSdk,k as createTvViewportStore,R as designPxToRealPx,j as designPxToRem,Y as designRemToRealPx,J as designSizeToRealSize,we as isTvViewportFrameEnabled,g as mergeTvViewportFrameConfig,ue as readTvViewportRealSizeFromDocument,f as readTvViewportScaleFromDocument,me as resolveDesignPxToRealPx,ne as resolveTvViewportPreset,ve as resolveTvViewportRealSizeFromHost,ce as resolveTvViewportRootFontSizePercent,Te as resolveTvViewportRootFontSizePx,S as resolveTvViewportRuntimePolicy,W as tvViewportConfigPartialEqual,h as tvViewportScalesEqual,$ as useTvViewport,_ as useTvViewportContext,C as useTvViewportContextOptional,ze as useTvViewportRealSize,oe as useTvViewportScale,Ee as useTvViewportSelector,U as useViewportFrameScale};
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { d as TvViewportFrameConfig, i as TvViewportSnapshot, j as TvViewportStore, g as TvViewportSdk, f as TvViewportRealSizeContext, h as TvViewportSize } from './viewportStore-yf19qOVB.js';
|
|
4
|
+
|
|
5
|
+
type Props$1 = {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
config?: Partial<TvViewportFrameConfig>;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
onViewportChange?: (snapshot: TvViewportSnapshot) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
stageClassName?: string;
|
|
12
|
+
appClassName?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Runtime TV shell: 1920×1080 stage, centered letterbox, root `rem` scaling.
|
|
16
|
+
* Composes {@link TvViewportProvider} + chrome view.
|
|
17
|
+
*/
|
|
18
|
+
declare function TvViewportFrame({ children, config, enabled, onViewportChange, className, stageClassName, appClassName, }: Props$1): react.JSX.Element;
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
config?: Partial<TvViewportFrameConfig>;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
onViewportChange?: (snapshot: TvViewportSnapshot) => void;
|
|
25
|
+
};
|
|
26
|
+
declare function TvViewportProvider({ children, config: configOverride, enabled, onViewportChange, }: Props): react.JSX.Element;
|
|
27
|
+
|
|
28
|
+
type TvViewportContextValue = {
|
|
29
|
+
readonly store: TvViewportStore;
|
|
30
|
+
readonly sdk: TvViewportSdk;
|
|
31
|
+
};
|
|
32
|
+
declare function useTvViewportContext(): TvViewportContextValue;
|
|
33
|
+
declare function useTvViewportContextOptional(): TvViewportContextValue | null;
|
|
34
|
+
|
|
35
|
+
type TvViewportHookValue = {
|
|
36
|
+
readonly sdk: ReturnType<typeof useTvViewportContext>['sdk'];
|
|
37
|
+
readonly config: ReturnType<typeof useTvViewportContext>['sdk']['config'];
|
|
38
|
+
readonly enabled: boolean;
|
|
39
|
+
readonly snapshot: TvViewportSnapshot;
|
|
40
|
+
};
|
|
41
|
+
declare function useTvViewportSelector<T>(selector: (state: TvViewportHookValue) => T, isEqual?: (a: T, b: T) => boolean): T;
|
|
42
|
+
declare function useTvViewport(): TvViewportHookValue;
|
|
43
|
+
declare function useTvViewportScale(): number;
|
|
44
|
+
declare function useTvViewportRealSize(): TvViewportRealSizeContext;
|
|
45
|
+
|
|
46
|
+
type Options = {
|
|
47
|
+
viewport: TvViewportSize;
|
|
48
|
+
fitToWindow: boolean;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
};
|
|
51
|
+
declare function useViewportFrameScale({ viewport, fitToWindow, enabled }: Options): number;
|
|
52
|
+
/** @deprecated Use {@link computeTvViewportScaleFromNumbers} via core. */
|
|
53
|
+
declare function computeViewportFrameScale(windowWidth: number, windowHeight: number, viewport: TvViewportSize, fitToWindow: boolean): number;
|
|
54
|
+
|
|
55
|
+
type TvViewportRuntimePolicy = {
|
|
56
|
+
readonly frameEnabled: boolean;
|
|
57
|
+
};
|
|
58
|
+
type TvViewportRuntimePolicyInput = {
|
|
59
|
+
readonly mode?: string;
|
|
60
|
+
readonly frameFlag?: string | boolean;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Default Vite policy — apps may override `enabled` on `TvViewportFrame` directly.
|
|
64
|
+
* Core SDK has zero `import.meta` coupling.
|
|
65
|
+
*/
|
|
66
|
+
declare function resolveTvViewportRuntimePolicy(input?: TvViewportRuntimePolicyInput): TvViewportRuntimePolicy;
|
|
67
|
+
/** @deprecated Use {@link resolveTvViewportRuntimePolicy}. */
|
|
68
|
+
declare function isTvViewportFrameEnabled(): boolean;
|
|
69
|
+
|
|
70
|
+
export { TvViewportFrame, type TvViewportHookValue, TvViewportProvider, type TvViewportRuntimePolicy, type TvViewportRuntimePolicyInput, computeViewportFrameScale, isTvViewportFrameEnabled, resolveTvViewportRuntimePolicy, useTvViewport, useTvViewportContext, useTvViewportContextOptional, useTvViewportRealSize, useTvViewportScale, useTvViewportSelector, useViewportFrameScale };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {createContext,memo,useRef,useSyncExternalStore,useContext,useMemo,useLayoutEffect,useState}from'react';import {jsx,jsxs}from'react/jsx-runtime';function S(e,t){return t?`${e} ${t}`:e}function w(e={}){let t=e.mode??(typeof import.meta<"u"?import.meta.env?.MODE:void 0),o=e.frameFlag??(typeof import.meta<"u"?import.meta.env?.VITE_TV_VIEWPORT_FRAME:void 0);return t==="test"?{frameEnabled:false}:{frameEnabled:o!=="false"&&o!==false}}function Z(){return w().frameEnabled}var M={Frame:"data-tv-viewport-frame"},l={Width:"--tv-viewport-width",Height:"--tv-viewport-height",Scale:"--tv-viewport-scale",StageWidthRem:"--tv-viewport-stage-width-rem",StageHeightRem:"--tv-viewport-stage-height-rem"},ee=[l.Width,l.Height,l.Scale,l.StageWidthRem,l.StageHeightRem];function B({config:e,scale:t},o=document.documentElement){let{width:r,height:i,preset:n}=e.viewport;o.setAttribute(M.Frame,n),o.style.setProperty(l.Width,`${r}px`),o.style.setProperty(l.Height,`${i}px`),o.style.setProperty(l.Scale,String(t)),o.style.setProperty(l.StageWidthRem,`${e.viewport.width/e.remBasePx}rem`),o.style.setProperty(l.StageHeightRem,`${e.viewport.height/e.remBasePx}rem`);}function A(e=document.documentElement){e.removeAttribute(M.Frame);for(let t of ee)e.style.removeProperty(t);}function E(e=1,t=document.documentElement){let o=t.style.getPropertyValue(l.Scale).trim();if(!o)return e;let r=Number.parseFloat(o);return Number.isFinite(r)?r:e}var _={FullHd:"1920x1080"};var d=16,U=100,v={width:1920,height:1080,preset:_.FullHd};v.width/d;v.height/d;var T={viewport:v,remBasePx:d,fitToWindow:true,showViewportBadge:false};function h(e){return {...T,...e,viewport:{...T.viewport,...e?.viewport}}}var te=1e-6;function y(e,t,o=te){return Math.abs(e-t)<o}function H(e,t=v,o=true){return x(e.width,e.height,t,o)}function x(e,t,o=v,r=true){return !r||e<=0||t<=0||e>=o.width&&t>=o.height?1:Math.min(e/o.width,t/o.height)}function oe(e,t=d){return e/t}function D(e,t){return e*t}function re(e,t){return e*t}function ie(e,t){return {width:D(e.width,t),height:D(e.height,t)}}function V({scale:e,uaRootPx:t=d}){let o=t*e,r=U*e;return {scale:e,rootFontSizePercent:r,rootFontSizePx:o,px:i=>D(i,e),rem:i=>oe(i,t),remPx:i=>re(i,o),size:i=>ie(i,e)}}function $(e){let t=h(e);return {config:t,computeScale:r=>H(r,t.viewport,t.fitToWindow),snapshot:r=>{let i=H(r,t.viewport,t.fitToWindow);return {scale:i,host:r,realSize:V({scale:i,uaRootPx:t.remBasePx})}},createRealSize:r=>V({scale:r,uaRootPx:t.remBasePx}),applyDocumentState:(r,i)=>B({config:t,scale:r},i),clearDocumentState:r=>A(r),readScaleFromDocument:(r,i)=>E(r,i),readRealSizeFromDocument:r=>V({scale:E(1,r),uaRootPx:t.remBasePx})}}var ne={width:1920,height:1080};function q(e,t,o){return o?e.snapshot(t):e.snapshot(ne)}function G(e,t,o={}){let r=o.enabled??true,i=t,n=q(e,i,r),m={enabled:r,snapshot:n},s=new Set;r&&e.applyDocumentState(n.scale);let a=(p,c)=>{n=p,r=c,m={enabled:r,snapshot:n},s.forEach(f=>f()),o.onViewportChange?.(p);},u=p=>{r?e.applyDocumentState(p):e.clearDocumentState();};return {sdk:e,getState:()=>m,getServerState:()=>m,subscribe:p=>(s.add(p),()=>s.delete(p)),setEnabled:p=>{if(r===p)return;let c=q(e,i,p);p?e.applyDocumentState(c.scale):e.clearDocumentState(),a(c,p);},setHost:p=>{if(!r)return;let c=i.width===p.width&&i.height===p.height,f=e.snapshot(p),L=y(n.scale,f.scale);i=p,!(c&&L)&&(L||(u(f.scale),a(f,r)));},dispose:()=>{s.clear(),e.clearDocumentState();}}}function pe(){return typeof window>"u"?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function g(e){if(typeof window>"u")return ()=>{};let t=0,o=false,r=()=>{o=false,e(pe());},i=()=>{o||(o=true,t=window.requestAnimationFrame(r));};i(),window.addEventListener("resize",i);let n=typeof ResizeObserver<"u"?new ResizeObserver(i):null;return n&&document.documentElement&&n.observe(document.documentElement),()=>{window.removeEventListener("resize",i),t&&window.cancelAnimationFrame(t),n?.disconnect();}}var b=createContext(null);function R(){let e=useContext(b);if(!e)throw new Error("useTvViewport must be used within TvViewportProvider / TvViewportFrame");return e}function P(){return useContext(b)}function X(e,t){if(e===t)return true;if(!e||!t)return !e&&!t;let o=e.viewport,r=t.viewport;return e.remBasePx===t.remBasePx&&e.fitToWindow===t.fitToWindow&&e.showViewportBadge===t.showViewportBadge&&o?.width===r?.width&&o?.height===r?.height&&o?.preset===r?.preset}function j(e){let t=useRef(e);return X(t.current,e)||(t.current=e),t.current}function N({children:e,config:t,enabled:o=true,onViewportChange:r}){let i=j(t),n=useMemo(()=>h(i),[i]),m=useMemo(()=>$(n),[n]),s=useRef(r);s.current=r;let a=useMemo(()=>G(m,typeof window>"u"?{width:1920,height:1080}:{width:window.innerWidth,height:window.innerHeight},{enabled:o,onViewportChange:p=>s.current?.(p)}),[m]);useLayoutEffect(()=>{a.setEnabled(o);},[o,a]),useLayoutEffect(()=>{if(o)return g(p=>{a.setHost(p);})},[o,a]),useLayoutEffect(()=>()=>a.dispose(),[a]);let u=useMemo(()=>({store:a,sdk:m}),[a,m]);return jsx(b.Provider,{value:u,children:e})}function W({viewport:e,fitToWindow:t,enabled:o}){let[r,i]=useState(1),n=useRef(r);return useLayoutEffect(()=>{if(!o){n.current=1,i(1);return}let m=(s,a)=>{let u=x(s,a,e,t);y(n.current,u)||(n.current=u,i(u));};return g(({width:s,height:a})=>m(s,a))},[o,t,e.height,e.width,e.preset]),r}function we(e,t,o,r){return x(e,t,o,r)}var de=()=>()=>{};function C(e){let t=e.getState();return {sdk:e.sdk,config:e.sdk.config,enabled:t.enabled,snapshot:t.snapshot}}function Ve(e,t=Object.is){let{store:o}=R(),r=useRef(void 0);return useSyncExternalStore(o.subscribe,()=>{let i=e(C(o));return r.current!==void 0&&t(r.current,i)?r.current:(r.current=i,i)},()=>e(C(o)))}function k(){let{store:e}=R(),t=useRef(void 0);return useSyncExternalStore(e.subscribe,()=>{let o=C(e),r=t.current;return r&&r.enabled===o.enabled&&r.snapshot===o.snapshot&&r.sdk===o.sdk?r:(t.current=o,o)},()=>C(e))}function J(){let e=P(),t=useSyncExternalStore(e?e.store.subscribe:de,()=>e?e.store.getState().snapshot.scale:1,()=>1),o=w().frameEnabled,r=W({enabled:!e&&o,fitToWindow:T.fitToWindow,viewport:T.viewport});return e?t:r}function fe(){let e=P(),t=J(),o=e?.sdk.config.remBasePx??T.remBasePx;return useMemo(()=>V({scale:t,uaRootPx:o}),[o,t])}var ye=memo(function({children:t,className:o,stageClassName:r,appClassName:i}){let{config:n,enabled:m,snapshot:s}=k();if(!m)return t;let{preset:a}=n.viewport,u=s.scale<1?` \xB7 ${Math.round(s.scale*100)}%`:"";return jsx("div",{className:S("tv-viewport-frame",o),"data-tv-viewport-frame-root":true,children:jsxs("div",{className:S("tv-viewport-frame__stage",r),"data-tv-viewport-stage":true,children:[n.showViewportBadge?jsxs("div",{className:"tv-viewport-frame__badge","aria-hidden":true,children:[a,u]}):null,jsx("div",{className:S("tv-viewport-frame__app",i),"data-tv-viewport-app":true,children:t})]})})});function xe({children:e,config:t,enabled:o,onViewportChange:r,className:i,stageClassName:n,appClassName:m}){let s=o??w().frameEnabled,a=useRef(r);return a.current=r,jsx(N,{config:t,enabled:s,onViewportChange:u=>a.current?.(u),children:jsx(ye,{className:i,stageClassName:n,appClassName:m,children:e})})}export{xe as TvViewportFrame,N as TvViewportProvider,we as computeViewportFrameScale,Z as isTvViewportFrameEnabled,w as resolveTvViewportRuntimePolicy,k as useTvViewport,R as useTvViewportContext,P as useTvViewportContextOptional,fe as useTvViewportRealSize,J as useTvViewportScale,Ve as useTvViewportSelector,W as useViewportFrameScale};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root rem + stage sizing for the TV frame.
|
|
3
|
+
* `html` font-size drives scale; stage uses rem so the canvas tracks root.
|
|
4
|
+
*/
|
|
5
|
+
html[data-tv-viewport-frame] {
|
|
6
|
+
font-size: calc(var(--tv-viewport-scale, 1) * 100%);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.tv-viewport-frame {
|
|
10
|
+
position: fixed;
|
|
11
|
+
inset: 0;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
background: #000;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.tv-viewport-frame__stage {
|
|
20
|
+
position: relative;
|
|
21
|
+
flex-shrink: 0;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
width: var(--tv-viewport-stage-width-rem, 120rem);
|
|
24
|
+
height: var(--tv-viewport-stage-height-rem, 67.5rem);
|
|
25
|
+
background: var(--tv-viewport-canvas, #0a0a0a);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.tv-viewport-frame__app {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
min-height: 0;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.tv-viewport-frame__badge {
|
|
39
|
+
position: absolute;
|
|
40
|
+
top: 0.5rem;
|
|
41
|
+
right: 0.5rem;
|
|
42
|
+
z-index: 2147483646;
|
|
43
|
+
padding: 0.2rem 0.45rem;
|
|
44
|
+
border-radius: 0.25rem;
|
|
45
|
+
background: rgba(0, 0, 0, 0.72);
|
|
46
|
+
color: rgba(255, 255, 255, 0.72);
|
|
47
|
+
font-size: 0.6875rem;
|
|
48
|
+
line-height: 1.35;
|
|
49
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
user-select: none;
|
|
52
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
declare const TvViewportPreset: {
|
|
2
|
+
readonly FullHd: "1920x1080";
|
|
3
|
+
};
|
|
4
|
+
type TvViewportPreset = (typeof TvViewportPreset)[keyof typeof TvViewportPreset];
|
|
5
|
+
type TvViewportSize = {
|
|
6
|
+
readonly width: number;
|
|
7
|
+
readonly height: number;
|
|
8
|
+
readonly preset: TvViewportPreset;
|
|
9
|
+
};
|
|
10
|
+
type TvViewportFrameConfig = {
|
|
11
|
+
readonly viewport: TvViewportSize;
|
|
12
|
+
/** Maps design px → rem for the stage (`1920 / remBasePx` → 120rem). */
|
|
13
|
+
readonly remBasePx: number;
|
|
14
|
+
/** Scale stage down when the host display is smaller than the frame. */
|
|
15
|
+
readonly fitToWindow: boolean;
|
|
16
|
+
/** Optional corner label (resolution / scale). */
|
|
17
|
+
readonly showViewportBadge: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Length in the 1920×1080 design coordinate system (1 unit = 1 CSS px at reference). */
|
|
20
|
+
type TvDesignPx = number;
|
|
21
|
+
type TvDesignSize = {
|
|
22
|
+
readonly width: TvDesignPx;
|
|
23
|
+
readonly height: TvDesignPx;
|
|
24
|
+
};
|
|
25
|
+
/** Rendered size in screen pixels after viewport-frame scale is applied. */
|
|
26
|
+
type TvRealSize = {
|
|
27
|
+
readonly width: number;
|
|
28
|
+
readonly height: number;
|
|
29
|
+
};
|
|
30
|
+
type TvViewportRealSizeContext = {
|
|
31
|
+
readonly scale: number;
|
|
32
|
+
readonly rootFontSizePercent: number;
|
|
33
|
+
readonly rootFontSizePx: number;
|
|
34
|
+
px: (designPx: TvDesignPx) => number;
|
|
35
|
+
rem: (designPx: TvDesignPx) => number;
|
|
36
|
+
remPx: (designRem: number) => number;
|
|
37
|
+
size: (design: TvDesignSize) => TvRealSize;
|
|
38
|
+
};
|
|
39
|
+
type TvHostSize = {
|
|
40
|
+
readonly width: number;
|
|
41
|
+
readonly height: number;
|
|
42
|
+
};
|
|
43
|
+
type TvViewportSnapshot = {
|
|
44
|
+
readonly scale: number;
|
|
45
|
+
readonly host: TvHostSize;
|
|
46
|
+
readonly realSize: TvViewportRealSizeContext;
|
|
47
|
+
};
|
|
48
|
+
type CreateTvViewportRealSizeOptions = {
|
|
49
|
+
readonly scale: number;
|
|
50
|
+
readonly uaRootPx?: number;
|
|
51
|
+
};
|
|
52
|
+
type ResolveTvViewportRealSizeOptions = {
|
|
53
|
+
readonly viewport?: TvViewportSize;
|
|
54
|
+
readonly fitToWindow?: boolean;
|
|
55
|
+
readonly uaRootPx?: number;
|
|
56
|
+
};
|
|
57
|
+
type TvViewportSdk = {
|
|
58
|
+
readonly config: TvViewportFrameConfig;
|
|
59
|
+
computeScale: (host: TvHostSize) => number;
|
|
60
|
+
snapshot: (host: TvHostSize) => TvViewportSnapshot;
|
|
61
|
+
createRealSize: (scale: number) => TvViewportRealSizeContext;
|
|
62
|
+
applyDocumentState: (scale: number, root?: HTMLElement) => void;
|
|
63
|
+
clearDocumentState: (root?: HTMLElement) => void;
|
|
64
|
+
readScaleFromDocument: (fallback?: number, root?: HTMLElement) => number;
|
|
65
|
+
readRealSizeFromDocument: (root?: HTMLElement) => TvViewportRealSizeContext;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type TvViewportStoreState = {
|
|
69
|
+
readonly enabled: boolean;
|
|
70
|
+
readonly snapshot: TvViewportSnapshot;
|
|
71
|
+
};
|
|
72
|
+
type TvViewportStoreOptions = {
|
|
73
|
+
readonly enabled?: boolean;
|
|
74
|
+
readonly onViewportChange?: (snapshot: TvViewportSnapshot) => void;
|
|
75
|
+
};
|
|
76
|
+
type TvViewportStore = {
|
|
77
|
+
readonly sdk: TvViewportSdk;
|
|
78
|
+
getState: () => TvViewportStoreState;
|
|
79
|
+
getServerState: () => TvViewportStoreState;
|
|
80
|
+
subscribe: (listener: () => void) => () => void;
|
|
81
|
+
setEnabled: (enabled: boolean) => void;
|
|
82
|
+
setHost: (host: TvHostSize) => void;
|
|
83
|
+
dispose: () => void;
|
|
84
|
+
};
|
|
85
|
+
declare function createTvViewportStore(sdk: TvViewportSdk, initialHost: TvHostSize, options?: TvViewportStoreOptions): TvViewportStore;
|
|
86
|
+
|
|
87
|
+
export { type CreateTvViewportRealSizeOptions as C, type ResolveTvViewportRealSizeOptions as R, type TvDesignPx as T, type TvDesignSize as a, type TvHostSize as b, type TvRealSize as c, type TvViewportFrameConfig as d, TvViewportPreset as e, type TvViewportRealSizeContext as f, type TvViewportSdk as g, type TvViewportSize as h, type TvViewportSnapshot as i, type TvViewportStore as j, type TvViewportStoreOptions as k, type TvViewportStoreState as l, createTvViewportStore as m };
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@osiris-smarttv/tv-viewport-frame",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fixed 1920×1080 TV viewport shell — letterbox, root rem scaling, design-unit math",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/OsirisTech-SmartTV/tv-viewport-frame.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/OsirisTech-SmartTV/tv-viewport-frame/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/OsirisTech-SmartTV/tv-viewport-frame#readme",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"registry": "https://registry.npmjs.org/"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": [
|
|
20
|
+
"**/*.css"
|
|
21
|
+
],
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./core": {
|
|
31
|
+
"types": "./dist/core.d.ts",
|
|
32
|
+
"import": "./dist/core.js"
|
|
33
|
+
},
|
|
34
|
+
"./react": {
|
|
35
|
+
"types": "./dist/react.d.ts",
|
|
36
|
+
"import": "./dist/react.js"
|
|
37
|
+
},
|
|
38
|
+
"./styles.css": "./dist/tv-viewport-frame.css",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18",
|
|
47
|
+
"react-dom": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"react": { "optional": false },
|
|
51
|
+
"react-dom": { "optional": false }
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@testing-library/dom": "^10.4.1",
|
|
55
|
+
"@testing-library/react": "^16.3.2",
|
|
56
|
+
"@types/react": "^19.2.14",
|
|
57
|
+
"@types/react-dom": "^19.2.3",
|
|
58
|
+
"jsdom": "^29.0.2",
|
|
59
|
+
"react": "^19.2.4",
|
|
60
|
+
"react-dom": "^19.2.4",
|
|
61
|
+
"tsup": "^8.5.0",
|
|
62
|
+
"typescript": "~6.0.2",
|
|
63
|
+
"vitest": "^4.1.4"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"clean": "node scripts/clean.mjs",
|
|
67
|
+
"build": "yarn build:js && yarn build:styles",
|
|
68
|
+
"build:js": "tsup",
|
|
69
|
+
"build:styles": "node scripts/copy-styles.mjs",
|
|
70
|
+
"dev": "tsup --watch",
|
|
71
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:watch": "vitest",
|
|
74
|
+
"verify": "yarn typecheck && yarn test",
|
|
75
|
+
"publish:check": "yarn clean && yarn build && yarn verify && npm pack --dry-run",
|
|
76
|
+
"publish:dry-run": "npm publish --dry-run --access public",
|
|
77
|
+
"publish:manual": "npm publish --access public",
|
|
78
|
+
"prepublishOnly": "yarn clean && yarn build && yarn verify"
|
|
79
|
+
}
|
|
80
|
+
}
|