@chancestv/tv-focus 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 ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chances
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.
22
+
23
+ ---
24
+
25
+ The spatial-navigation engine in src/engine/ is forked from
26
+ luke-chang/js-spatial-navigation and licensed under the Mozilla Public
27
+ License 2.0. See src/engine/LICENSE and src/engine/ATTRIBUTION.md.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @chancestv/tv-focus
2
+
3
+ 面向低版本 WebView(Chromium 53+)的 Vue 3 TV 端空间导航焦点系统。遥控器方向键在 DOM 元素间按几何位置移动焦点,支持 section 分区、模态层栈、记忆焦点。
4
+
5
+ 核心导航引擎 fork 自 [luke-chang/js-spatial-navigation](https://github.com/luke-chang/js-spatial-navigation)(MPL-2.0)。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pnpm add @chancestv/tv-focus vue
11
+ ```
12
+
13
+ ## 用法
14
+
15
+ ```ts
16
+ import { setupFocus, nativeKeyAdapter } from '@chancestv/tv-focus'
17
+
18
+ // 应用入口调用一次
19
+ setupFocus({ defaults: { rememberSource: true } })
20
+ // 把 OTT 原生遥控器按键透传为合成 keydown/keyup
21
+ nativeKeyAdapter('your-native-key-event-name')
22
+ ```
23
+
24
+ ```vue
25
+ <script setup lang="ts">
26
+ import { FocusSection, Focusable } from '@chancestv/tv-focus'
27
+ </script>
28
+
29
+ <template>
30
+ <FocusSection id="home">
31
+ <Focusable v-slot="{ focused }">
32
+ <div :class="{ active: focused }">item</div>
33
+ </Focusable>
34
+ </FocusSection>
35
+ </template>
36
+ ```
37
+
38
+ ## 导出
39
+
40
+ - 初始化:`setupFocus` / `nativeKeyAdapter` / `SpatialNavigation`
41
+ - 组件:`Focusable` / `FocusSection` / `FocusLayer`
42
+ - 组合式:`useFocusable` / `useFocusSection` / `useKeepAliveFocus`
43
+ - 工具:`hasOpenLayer`
44
+ - 类型:`Direction` / `Restrict` / `EnterTo` / `LeaveFor` / `SectionConfig` 等
45
+
46
+ ## 兼容目标
47
+
48
+ 产物语法降级到 `chrome53`。
49
+
50
+ ## License
51
+
52
+ MIT AND MPL-2.0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 模态层:挂载时 disable layer 之外的所有 section、保存焦点;
3
+ * 卸载时 enable 回来并恢复焦点。
4
+ *
5
+ * 通过 provide 暴露 FocusLayerContext,子树中的 useFocusSection 会
6
+ * 把 sectionId 注册进来——避免子 section 被一并禁用。
7
+ *
8
+ * 子 section 的 mount 早于本组件 onMounted,因此 innerSectionIds 在
9
+ * 我们 onMounted 时已经收集齐全。
10
+ */
11
+ interface Props {
12
+ id?: string;
13
+ tag?: string;
14
+ }
15
+ declare function __VLS_template(): {
16
+ attrs: Partial<{}>;
17
+ slots: {
18
+ default?(_: {}): any;
19
+ };
20
+ refs: {};
21
+ rootEl: any;
22
+ };
23
+ type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
24
+ declare const __VLS_component: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
25
+ tag: string;
26
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
27
+ declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
28
+ export default _default;
29
+ type __VLS_WithTemplateSlots<T, S> = T & {
30
+ new (): {
31
+ $slots: S;
32
+ };
33
+ };
@@ -0,0 +1,36 @@
1
+ import { Restrict, EnterTo, LeaveFor } from './engine';
2
+ interface Props {
3
+ id?: string;
4
+ restrict?: Restrict;
5
+ enterTo?: EnterTo;
6
+ leaveFor?: LeaveFor | null;
7
+ straightOnly?: boolean;
8
+ rememberSource?: boolean;
9
+ tag?: string;
10
+ }
11
+ declare function __VLS_template(): {
12
+ attrs: Partial<{}>;
13
+ slots: {
14
+ default?(_: {
15
+ sectionId: string;
16
+ }): any;
17
+ };
18
+ refs: {};
19
+ rootEl: any;
20
+ };
21
+ type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
22
+ declare const __VLS_component: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
23
+ straightOnly: boolean;
24
+ rememberSource: boolean;
25
+ enterTo: EnterTo;
26
+ leaveFor: LeaveFor | null;
27
+ restrict: Restrict;
28
+ tag: string;
29
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
30
+ declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
31
+ export default _default;
32
+ type __VLS_WithTemplateSlots<T, S> = T & {
33
+ new (): {
34
+ $slots: S;
35
+ };
36
+ };
@@ -0,0 +1,42 @@
1
+ interface Props {
2
+ /** 业务侧 key,便于调试和脚本聚焦(写到 data-focus-key) */
3
+ focusKey?: string;
4
+ /** 包装元素 tag,默认 div */
5
+ tag?: string;
6
+ }
7
+ declare function __VLS_template(): {
8
+ attrs: Partial<{}>;
9
+ slots: {
10
+ default?(_: {
11
+ focused: boolean;
12
+ }): any;
13
+ };
14
+ refs: {
15
+ elRef: unknown;
16
+ };
17
+ rootEl: any;
18
+ };
19
+ type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
20
+ declare const __VLS_component: import('vue').DefineComponent<Props, {
21
+ elRef: import('vue').Ref<HTMLElement | null, HTMLElement | null>;
22
+ focused: import('vue').Ref<boolean, boolean>;
23
+ }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
24
+ blur: () => any;
25
+ focus: () => any;
26
+ enter: () => any;
27
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
28
+ onBlur?: (() => any) | undefined;
29
+ onFocus?: (() => any) | undefined;
30
+ onEnter?: (() => any) | undefined;
31
+ }>, {
32
+ tag: string;
33
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
34
+ elRef: unknown;
35
+ }, any>;
36
+ declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
37
+ export default _default;
38
+ type __VLS_WithTemplateSlots<T, S> = T & {
39
+ new (): {
40
+ $slots: S;
41
+ };
42
+ };
package/dist/core.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { default as SpatialNavigation, SectionConfig } from './engine';
2
+ export interface SetupFocusOptions {
3
+ /** 应用到 GlobalConfig 的默认配置(每个 section 可单独覆盖)*/
4
+ defaults?: Partial<SectionConfig>;
5
+ }
6
+ /**
7
+ * 初始化全局 SpatialNavigation。多次调用幂等。
8
+ *
9
+ * 在应用启动(main.ts)调用一次即可。
10
+ */
11
+ export declare function setupFocus(options?: SetupFocusOptions): void;
12
+ export { SpatialNavigation };
@@ -0,0 +1,5 @@
1
+ import { SpatialNavigationAPI } from './types';
2
+ declare const SpatialNavigation: SpatialNavigationAPI;
3
+ export default SpatialNavigation;
4
+ export { SpatialNavigation };
5
+ export * from './types';
@@ -0,0 +1,19 @@
1
+ /*******************/
2
+ /*******************/
3
+ declare var SpatialNavigation: {
4
+ init: () => void;
5
+ uninit: () => void;
6
+ clear: () => void;
7
+ set: () => void;
8
+ add: () => any;
9
+ remove: (sectionId: any) => boolean;
10
+ disable: (sectionId: any) => boolean;
11
+ enable: (sectionId: any) => boolean;
12
+ pause: () => void;
13
+ resume: () => void;
14
+ focus: (elem: any, silent: any) => boolean;
15
+ move: (direction: any, selector: any) => boolean;
16
+ makeFocusable: (sectionId: any) => void;
17
+ setDefaultSection: (sectionId: any) => void;
18
+ };
19
+ export default SpatialNavigation;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @shell/core/focus 公共类型
3
+ */
4
+ export type Direction = 'up' | 'down' | 'left' | 'right';
5
+ export type Restrict = 'self-first' | 'self-only' | 'none';
6
+ export type EnterTo = '' | 'last-focused' | 'default-element';
7
+ /**
8
+ * <extSelector>
9
+ * - 'querySelectorAll' 字符串
10
+ * - NodeList / Element 数组
11
+ * - 单个 Element
12
+ * - '@<sectionId>' / '@'(指定 section)
13
+ */
14
+ export type ExtSelector = string | Element | Element[] | NodeListOf<Element> | ArrayLike<Element>;
15
+ export interface LeaveFor {
16
+ left?: ExtSelector;
17
+ right?: ExtSelector;
18
+ up?: ExtSelector;
19
+ down?: ExtSelector;
20
+ }
21
+ export interface SectionConfig {
22
+ /** querySelectorAll 字符串、NodeList、Element 数组、单个 Element(不接受 "@" 语法)*/
23
+ selector?: ExtSelector;
24
+ /** 是否只允许严格方向(不允许斜向重叠) */
25
+ straightOnly?: boolean;
26
+ /** 重叠判定阈值 [0, 1] */
27
+ straightOverlapThreshold?: number;
28
+ /** 离开 section 时记住焦点来源(再返回时可复焦) */
29
+ rememberSource?: boolean;
30
+ /** section 整体禁用 */
31
+ disabled?: boolean;
32
+ /** 进入 section 时默认聚焦的元素 */
33
+ defaultElement?: ExtSelector;
34
+ /** 进入 section 时的策略 */
35
+ enterTo?: EnterTo;
36
+ /** 跨方向跳转规则 */
37
+ leaveFor?: LeaveFor | null;
38
+ /** 边界策略 */
39
+ restrict?: Restrict;
40
+ /** 不自动加 tabindex 的元素选择器 */
41
+ tabIndexIgnoreList?: string;
42
+ /** 自定义元素可导航过滤器 */
43
+ navigableFilter?: ((elem: Element, sectionId?: string) => boolean) | null;
44
+ /** section id(add 时可显式指定) */
45
+ id?: string;
46
+ }
47
+ export type GlobalConfig = SectionConfig;
48
+ /**
49
+ * SpatialNavigation 公开 API(薄类型,主体实现来自 fork 源码)。
50
+ * 详见 spatial-navigation.ts。
51
+ */
52
+ export interface SpatialNavigationAPI {
53
+ init(): void;
54
+ uninit(): void;
55
+ clear(): void;
56
+ reset(): void;
57
+ set(config: SectionConfig): void;
58
+ set(sectionId: string, config: SectionConfig): void;
59
+ add(config: SectionConfig): string;
60
+ add(sectionId: string, config: SectionConfig): string;
61
+ remove(sectionId: string): boolean;
62
+ disable(sectionId: string): boolean;
63
+ enable(sectionId: string): boolean;
64
+ pause(): void;
65
+ resume(): void;
66
+ focus(): boolean;
67
+ focus(silent: boolean): boolean;
68
+ focus(selector: ExtSelector, silent?: boolean): boolean;
69
+ move(direction: Direction, selector?: ExtSelector): boolean;
70
+ makeFocusable(sectionId?: string): void;
71
+ setDefaultSection(sectionId?: string): void;
72
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * FocusLayer 通过 provide 注入的上下文。
3
+ * 内部 useFocusSection 通过 inject 拿到本对象,并把自己的 sectionId 注册进来,
4
+ * 避免被 layer onMounted 时一并禁用。
5
+ */
6
+ export interface FocusLayerContext {
7
+ layerId: string;
8
+ registerInnerSection(sectionId: string): void;
9
+ }
10
+ export declare const FOCUS_LAYER_KEY: symbol;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @shell/core/focus
3
+ *
4
+ * TV 焦点系统:空间导航引擎(./engine,fork 自 luke-chang/js-spatial-navigation, MPL 2.0)
5
+ * + Vue 3 适配层(组件 / composable / 原生按键 adapter)。
6
+ *
7
+ * 业务层禁止直连本入口,请用 @shell/tv-ui 的 EPage/ERow/EColumn/EFocusGroup 等组件。
8
+ */
9
+ export { setupFocus, SpatialNavigation } from './core';
10
+ export type { SetupFocusOptions } from './core';
11
+ export { nativeKeyAdapter } from './key-source/native-event';
12
+ export { useFocusable } from './useFocusable';
13
+ export type { UseFocusableOptions, UseFocusableResult } from './useFocusable';
14
+ export { useFocusSection, FOCUS_SECTION_KEY } from './useFocusSection';
15
+ export type { UseFocusSectionOptions, FocusSectionContext } from './useFocusSection';
16
+ export { useKeepAliveFocus } from './keep-alive-bridge';
17
+ export { hasOpenLayer } from './layer-stack';
18
+ export { default as Focusable } from './Focusable.vue';
19
+ export { default as FocusSection } from './FocusSection.vue';
20
+ export { default as FocusLayer } from './FocusLayer.vue';
21
+ export type { Direction, Restrict, EnterTo, LeaveFor, SectionConfig, ExtSelector, } from './engine';