@lightningtv/solid 3.1.4 → 3.1.6

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,165 +1,81 @@
1
- # AI Coding Guidelines for Lightning/Solid Framework
2
-
3
- This document outlines the specific constraints, properties, and layout systems available in the Lightning/Solid framework. AI agents should strictly adhere to these rules to generate correct and functional code.
4
-
5
- ## Core Principles
6
-
7
- 1. **Positioning System**:
8
-
9
- - All nodes are effectively `position: absolute`.
10
- - Positioning is controlled via `x`, `y`, `width`, `height`.
11
- - **Right / Bottom**: usage of `right` and `bottom` is supported for pinning elements to the parent's edges.
12
- - Setting `right` automatically implies `mountX: 1`.
13
- - Setting `bottom` automatically implies `mountY: 1`.
14
- - **Mounting**:
15
- - Default `mount` is **0, 0** (Top-Left corner).
16
- - `mount` (0-1) determines the anchor point of the element itself (0.5 = center).
17
- - **Avoid changing `mount` manually** unless specifically needed for centering (0.5) or specific anchor effects.
18
- - There is no "flow" layout by default unless `display: 'flex'` is explicitly used.
19
-
20
- 2. **Dimensions**:
21
-
22
- - **Explicit Dimensions**: It is **important** to give elements explicit `width` and `height` whenever possible.
23
- - **Default Dimensions**: If `width` and `height` are **not** specified, the element will inherit the **parent's width and height**. This can lead to unexpected full-screen overlays if not managed carefully.
24
-
25
- 3. **Flexbox Layout**:
26
-
27
- - **Must** set `display: 'flex'` on a container to enable flexbox.
28
- - **Important**: `padding` is a _single number_ only. There is NO `paddingLeft`, `paddingRight`, `paddingTop`, or `paddingBottom`.
29
- - **Margins**: Supported on flex items via `marginTop`, `marginBottom`, `marginLeft`, `marginRight`.
30
- - **Gap**: `gap`, `rowGap`, `columnGap` are supported.
31
- - **Alignment**: `justifyContent`, `alignItems`, `alignSelf` are strictly typed.
32
-
33
- 4. **Styling Restrictions**:
34
-
35
- - **No** `background`. Use `color`.
36
- - **Color Format**: **Prefer Hex Strings** (e.g., `'#ff0000ff'`). **NO** named colors.
37
- - **No** `border-radius`. Use `borderRadius` (number).
38
- - **No** `border`. Use `border` object: `{ width: number, color: string }`.
39
- - **No** `box-shadow`. Use `shadow` object.
40
- - **No** CSS class names.
41
-
42
- 5. **Props vs Styles**:
43
- - **Prefer Props**: Pass properties directly to the component (e.g., `<View x={10} y={10} color="#ff0000ff" />`).
44
- - Avoid using the `style` prop when possible.
45
-
46
- ## Available Properties
47
-
48
- ### Layout & Positioning
49
-
50
- | Property | Type | Notes |
51
- | :-------------------------- | :------- | :----------------------------------------------------------- |
52
- | `x`, `y` | `number` | Absolute position coordinates. |
53
- | `right` | `number` | Distance from parent's right edge. **Implies `mountX: 1`**. |
54
- | `bottom` | `number` | Distance from parent's bottom edge. **Implies `mountY: 1`**. |
55
- | `width` / `w` | `number` | Explicit width. **Defaults to Parent Width** if unset. |
56
- | `height` / `h` | `number` | Explicit height. **Defaults to Parent Height** if unset. |
57
- | `minWidth`, `minHeight` | `number` | Minimum dimensions. |
58
- | `maxWidth`, `maxHeight` | `number` | Maximum dimensions. |
59
- | `mount`, `mountX`, `mountY` | `number` | Anchor point. Default **0** (Top/Left). |
60
- | `pivot`, `pivotX`, `pivotY` | `number` | Pivot point. |
61
- | `rotation` | `number` | Rotation in radians. |
62
- | `scale`, `scaleX`, `scaleY` | `number` | Scaling factor. |
63
- | `alpha` | `number` | Opacity. |
64
- | `zIndex`, `zIndexLocked` | `number` | Stacking order. |
65
-
66
- ### Flexbox Container Props
67
-
68
- _Requires `display: 'flex'`_
69
-
70
- | Property | Values / Type | Notes |
71
- | :--------------------------- | :----------------------------------------------------------------------------------------- | :-------------------- |
72
- | `flexDirection` | `'row' \| 'column'` | Defaults to 'row'. |
73
- | `flexWrap` | `'nowrap' \| 'wrap'` | |
74
- | `justifyContent` | `'flexStart' \| 'flexEnd' \| 'center' \| 'spaceBetween' \| 'spaceAround' \| 'spaceEvenly'` | Main axis alignment. |
75
- | `alignItems` | `'flexStart' \| 'flexEnd' \| 'center'` | Cross axis alignment. |
76
- | `gap`, `rowGap`, `columnGap` | `number` | Space between items. |
77
- | `padding` | `number` | **Uniform only**. |
78
-
79
- ### Flexbox Item Props
80
-
81
- | Property | Type | Notes |
82
- | :----------------------------- | :------------------------------------- | :---------------------- |
83
- | `flexGrow` | `number` | |
84
- | `flexItem` | `boolean` | Set `false` to ignore. |
85
- | `alignSelf` | `'flexStart' \| 'flexEnd' \| 'center'` | Overrides `alignItems`. |
86
- | `marginTop`, `marginBottom`... | `number` | |
87
-
88
- ### Visual Styles
89
-
90
- | Property | Type | Notes |
91
- | :--------------------------- | :------------------- | :----------------------------------------------------------------------- |
92
- | `color` | `string` | **Hex String Preferred** (`'#ff0000ff'`). |
93
- | `colorTop`, `colorBottom`... | `string` | Gradient-like vertex coloring in hex string. |
94
- | `linearGradient` | `object` | `{ angle?: number, colors: string[], stops?: number[] }` |
95
- | `radialGradient` | `object` | `{ radius?: number, colors: string[], stops?: number[], ... }` |
96
- | `borderRadius` | `number \| number[]` | Single radius or [tl, tr, br, bl]. |
97
- | `border` | `object` | `{ width: number, color: string }`. |
98
- | `shadow` | `object` | `{ color: string, x: number, y: number, blur: number, spread: number }`. |
99
-
100
- ### Text Properties
101
-
102
- | Property | Type | Notes |
103
- | :------------- | :------------------------------- | :-------------- |
104
- | `text` | `string` | Content string. |
105
- | `fontSize` | `number` | |
106
- | `fontFamily` | `string` | |
107
- | `fontWeight` | `number \| string` | |
108
- | `lineHeight` | `number` | |
109
- | `textAlign` | `'left' \| 'center' \| 'right'` | |
110
- | `wordWrap` | `boolean` | |
111
- | `maxLines` | `number` | truncate text. |
112
- | `textOverflow` | `'clip' \| 'ellipsis' \| string` | |
113
-
114
- ### Interaction & Focus
115
-
116
- | Property | Type | Notes |
117
- | :--------------- | :--------- | :----------------------------------------------------------------------- |
118
- | `onFocus` | `function` | Called when element gains focus. |
119
- | `onBlur` | `function` | Called when element loses focus. |
120
- | `onFocusChanged` | `function` | **Preferred**. `(hasFocus: boolean) => void`. Combines focus/blur logic. |
121
- | `onEnter` | `function` | Called on Enter key press. |
122
-
123
- #### Focus Handling Best Practice
124
-
125
- When tracking focus state (e.g., for styling), prefer `onFocusChanged` over separate `onFocus`/`onBlur` handlers.
126
-
127
- **Preferred Pattern:**
1
+ # Custom TV-UI Framework: Lightning + SolidJS
128
2
 
129
- ```tsx
130
- const [focused, setFocused] = createSignal(false);
3
+ **System Role:** You are an expert frontend engineer working with a custom TV-UI framework called **Lightning**, built on **SolidJS**.
131
4
 
132
- return (
133
- <View
134
- width={180}
135
- height={100}
136
- color={focused() ? '#ffff00ff' : '#333333ff'}
137
- onFocusChanged={setFocused}
138
- />
139
- );
140
- ```
5
+ ## 1. Core Architecture & Runtime
6
+
7
+ - **Environment:** TV app development over WebGL (not the DOM). No pointer input; interaction is directional (Up/Down/Left/Right).
8
+ - **Reactivity:** Uses SolidJS primitives (`createSignal`, `createEffect`, `createMemo`).
9
+ - **Primitives:** UI is built using custom components like `<View>`, `<Text>`, `<Row>`, `<Column>`.
10
+ - **Patterns:** Always use functional components and modern TypeScript/JSX. Avoid classes.
11
+ - **Assumption:** Always frame answers within the context of Lightning + SolidJS TV environment.
12
+
13
+ ## 2. Layout & Positioning
14
+
15
+ - **Absolute by Default:** All nodes are naturally `position: absolute`.
16
+ - **Positioning:** Controlled explicitly via `x`, `y`, `width`, `height`.
17
+ - **Pinning:** Use `right` (implies `mountX: 1`) and `bottom` (implies `mountY: 1`) to pin to parent edges.
18
+ - **Mounting:** Default `mount` is `0, 0` (Top-Left). Value `0` to `1` determines anchor point. Avoid manual changes unless centering (`0.5`).
19
+ - **Dimensions:** Explicit `width` and `height` are crucial. Unspecified dimensions will inherit parent size (causing unintended overlays).
20
+
21
+ ## 3. Flexbox Engine
22
+
23
+ - **Activation:** Set `display: "flex"` on containers to enable flex layout.
24
+ - **Padding:** Supports ONLY a single overall `padding` number. (NO `paddingLeft`, `paddingTop`, etc.).
25
+ - **Margins:** Supported on items (`marginTop`, `marginBottom`, `marginLeft`, `marginRight`).
26
+ - **Gap:** `gap`, `rowGap`, and `columnGap` are supported.
27
+ - **Alignment:** Strictly typed properties: `flexDirection` ('row'|'column'), `justifyContent`, `alignItems`, `alignSelf`.
28
+
29
+ ## 4. Styling Strict Rules
30
+
31
+ - **Colors:** MUST use hex strings (e.g., `"#ff0000ff"`). NO named colors (e.g., `'red'`) or CSS variables.
32
+ - **Backgrounds:** DO NOT use `background`. Use `color` instead.
33
+ - **Borders/Shadows:** Use object structures (`border={{ width: 1, color: "#000000ff" }}`). NO CSS `border` or `box-shadow` strings.
34
+ - **Radii:** Use numeric `borderRadius` (single number or array `[tl, tr, br, bl]`).
35
+ - **Classes/Styles:** CSS classes and inline `style={{}}` props are NOT supported. Pass props directly to the component.
36
+
37
+ ## 5. Focus & Interaction
38
+
39
+ - **Navigation:** Navigation is handled via a remote control with arrow keys. Use `onUp`, `onDown`, `onLeft`, and `onRight` to handle directional input on components.
40
+ - **Handling:** Prefer the `onFocusChanged={(hasFocus: boolean) => void}` prop to easily track and react to focus state (e.g. for hover styles).
41
+ - **Events:** `onFocus`, `onBlur`, and `onEnter` are available for direct actions.
42
+ - **Auto-Focus:** Exactly one item should include the `autofocus` prop (`autofocus={true}`) when a page loads.
43
+ - **Forwarding Focus:** Use `forwardFocus` to set focus on a child element. It can take a number (e.g., `forwardFocus={1}`) to focus a specific descendant by index.
44
+ - **Row/Column:** `Row` and `Column` components automatically manage selecting and setting focus on their children.
141
45
 
142
- ## Strict "Do Not Use" List
46
+ ## 6. Property Reference
143
47
 
144
- | Invalid Property | Correct Alternative |
145
- | :-------------------- | :----------------------------------------------------- |
146
- | `background` | Use `color`. |
147
- | Named colors | Use hex strings (`'#ff0000ff'`). |
148
- | `textColor` | Use `color` on the Text node itself. |
149
- | `paddingLeft`... | Use `padding` (uniform) or `margin` props on children. |
150
- | `border-style` string | Use `border` object. |
151
- | `display: 'grid'` | Not supported. Use nested Flexboxes. |
152
- | `className` | Not supported. |
153
- | `style={{ ... }}` | **Avoid**. Pass props directly to the component. |
48
+ ### Positioning & Transformation
154
49
 
155
- ## Example Usage
50
+ `x`, `y`, `right`, `bottom`, `width` (w), `height` (h), `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `mount`, `mountX`, `mountY`, `pivot`, `pivotX`, `pivotY`, `rotation`, `scale`, `scaleX`, `scaleY`, `alpha`, `zIndex`, `zIndexLocked`
156
51
 
157
- ### Incorrect
52
+ ### Container Flexbox
53
+
54
+ `display: "flex"`, `flexDirection`, `flexWrap`, `justifyContent`, `alignItems`, `gap`, `rowGap`, `columnGap`, `padding`
55
+
56
+ ### Item Flexbox
57
+
58
+ `flexGrow`, `flexItem`, `alignSelf`, `marginTop`, `marginBottom`, `marginLeft`, `marginRight`
59
+
60
+ ### Visual & Text
61
+
62
+ `color`, `colorTop`, `colorBottom`, `linearGradient`, `radialGradient`, `borderRadius`, `border`, `shadow`, `text`, `fontSize`, `fontFamily`, `fontWeight`, `lineHeight`, `textAlign`, `wordWrap`, `maxLines`, `textOverflow`
63
+
64
+ ## 7. DO NOT USE 🚫
65
+
66
+ - Standard DOM Elements (`<div>`, `<span>`, etc.)
67
+ - CSS Class Names (`class`, `className`)
68
+ - The `style={{}}` Prop (Use native node props instead)
69
+ - `display: 'grid'`
70
+ - String literal colors without hex (e.g. `'red'`, `'#F00'`) - Use full hex codes like `'#ff0000ff'`
71
+ - Directional paddings (e.g., `paddingLeft`)
72
+
73
+ ## 8. Code Examples
74
+
75
+ **❌ Incorrect:**
158
76
 
159
77
  ```tsx
160
- // 1. Missing dimensions (might default to full screen)
161
- // 2. Using named colors
162
- // 3. Using style object
78
+ // Missing dimensions, invalid colors, using styles, directional padding
163
79
  <View
164
80
  style={{
165
81
  backgroundColor: 'red',
@@ -172,20 +88,21 @@ return (
172
88
  </View>
173
89
  ```
174
90
 
175
- ### Correct
91
+ **✅ Correct:**
176
92
 
177
93
  ```tsx
178
- // 1. Explicit dimensions
179
- // 2. Direct Props
180
- // 3. Hex Strings
94
+ const [focused, setFocused] = createSignal(false);
95
+
96
+ // Uses direct props, hex colors, clear dimensions, and focus tracking
181
97
  <View
182
- width={400} // Explicit Width
183
- height={200} // Explicit Height
184
- color="#ff0000ff" // Hex string
185
- padding={20} // Uniform padding
186
- borderRadius={10} // Number
187
- display="flex" // defaults to flexDirection row
98
+ width={400}
99
+ height={200}
100
+ color={focused() ? '#ffff00ff' : '#ff0000ff'}
101
+ padding={20}
102
+ borderRadius={10}
103
+ display="flex"
104
+ onFocusChanged={setFocused}
188
105
  >
189
106
  <Text color="#ffffffff">Hello</Text>
190
- </View>
107
+ </View>;
191
108
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningtv/solid",
3
- "version": "3.1.4",
3
+ "version": "3.1.6",
4
4
  "description": "Lightning Renderer for Solid Universal",
5
5
  "type": "module",
6
6
  "exports": {
@@ -61,6 +61,8 @@ import {
61
61
  } from './focusManager.js';
62
62
  import simpleAnimation, { SimpleAnimationSettings } from './animation.js';
63
63
 
64
+ let nextActiveElement: ElementNode | null = null;
65
+ let focusQueued: boolean = false;
64
66
  let layoutRunQueued = false;
65
67
  const layoutQueue = new Set<ElementNode>();
66
68
 
@@ -835,7 +837,10 @@ export class ElementNode extends Object {
835
837
  const animationSettings =
836
838
  this.transition === true || this.transition[name] === true
837
839
  ? undefined
838
- : (this.transition[name] as undefined | AnimationSettings);
840
+ : this.transition[name] ||
841
+ (this.transition[getPropertyAlias(name)] as
842
+ | undefined
843
+ | AnimationSettings);
839
844
 
840
845
  if (Config.simpleAnimationsEnabled) {
841
846
  simpleAnimation.add(
@@ -957,7 +962,15 @@ export class ElementNode extends Object {
957
962
  }
958
963
  }
959
964
  // Delay setting focus so children can render (useful for Row + Column)
960
- queueMicrotask(() => setActiveElement(this));
965
+ nextActiveElement = this;
966
+ if (focusQueued === false) {
967
+ focusQueued = true;
968
+ queueMicrotask(() => {
969
+ if (nextActiveElement) setActiveElement(nextActiveElement);
970
+ nextActiveElement = null;
971
+ focusQueued = false;
972
+ });
973
+ }
961
974
  } else {
962
975
  this._autofocus = true;
963
976
  }
@@ -173,11 +173,13 @@ const propagateKeyPress = (
173
173
  }
174
174
  lastGlobalKeyPressTime = currentTime;
175
175
  }
176
- let finalFocusElm: ElementNode | undefined;
177
- let handlerAvailable: ElementNode | undefined;
178
176
  const numItems = focusPath.length;
179
- const captureEvent =
180
- `onCapture${mappedEvent || e.key}` + isUp ? 'Release' : '';
177
+ if (numItems === 0) return false;
178
+
179
+ let handlerAvailable: ElementNode | undefined;
180
+ const finalFocusElm = focusPath[0]!;
181
+ const keyBase = mappedEvent || (e.key as string);
182
+ const captureEvent = `onCapture${keyBase}${isUp ? 'Release' : ''}`;
181
183
  const captureKey = isUp ? 'onCaptureKeyRelease' : 'onCaptureKey';
182
184
 
183
185
  for (let i = numItems - 1; i >= 0; i--) {
@@ -205,12 +207,10 @@ const propagateKeyPress = (
205
207
  }
206
208
 
207
209
  let eventHandlerKey: string | undefined;
208
- let releaseEventHandlerKey: string | undefined;
209
210
  let fallbackHandlerKey: 'onKeyHold' | 'onKeyPress' | undefined;
210
211
 
211
212
  if (mappedEvent) {
212
- eventHandlerKey = `on${mappedEvent}`;
213
- releaseEventHandlerKey = `on${mappedEvent}Release`;
213
+ eventHandlerKey = isUp ? `on${mappedEvent}Release` : `on${mappedEvent}`;
214
214
  }
215
215
 
216
216
  if (!isUp) {
@@ -219,9 +219,6 @@ const propagateKeyPress = (
219
219
 
220
220
  for (let i = 0; i < numItems; i++) {
221
221
  const elm = focusPath[i]!;
222
- if (!finalFocusElm) {
223
- finalFocusElm = elm;
224
- }
225
222
 
226
223
  // Check throttle for bubbling phase
227
224
  if (elm.throttleInput) {
@@ -236,21 +233,13 @@ const propagateKeyPress = (
236
233
 
237
234
  let handled = false;
238
235
 
239
- // Check for the release event handler if isUp is true and the key is defined
240
- if (isUp && releaseEventHandlerKey) {
241
- const eventHandler = elm[releaseEventHandlerKey];
242
- if (isFunction(eventHandler)) {
243
- handlerAvailable = elm;
244
- if (eventHandler.call(elm, e, elm, finalFocusElm) === true)
245
- handled = true;
246
- }
247
- } else if (!isUp && eventHandlerKey) {
248
- // Check for the regular event handler if isUp is false and the key is defined
236
+ if (eventHandlerKey) {
249
237
  const eventHandler = elm[eventHandlerKey];
250
238
  if (isFunction(eventHandler)) {
251
239
  handlerAvailable = elm;
252
- if (eventHandler.call(elm, e, elm, finalFocusElm) === true)
240
+ if (eventHandler.call(elm, e, elm, finalFocusElm) === true) {
253
241
  handled = true;
242
+ }
254
243
  }
255
244
  }
256
245
 
@@ -261,8 +250,9 @@ const propagateKeyPress = (
261
250
  handlerAvailable = elm;
262
251
  if (
263
252
  fallbackHandler.call(elm, e, mappedEvent, elm, finalFocusElm) === true
264
- )
253
+ ) {
265
254
  handled = true;
255
+ }
266
256
  }
267
257
  }
268
258
 
@@ -1,5 +1,5 @@
1
- import { For, createSignal, createMemo, createEffect, JSX } from "solid-js";
2
- import { type NodeProps, ElementNode, NewOmit } from "@lightningtv/solid";
1
+ import { For, createSignal, createMemo, createEffect, JSX, untrack, Index } from "solid-js";
2
+ import { type NodeProps, ElementNode, NewOmit, hasFocus } from "@lightningtv/solid";
3
3
  import { chainRefs } from "./utils/chainFunctions.js";
4
4
 
5
5
  export interface GridItemProps<T> {
@@ -20,6 +20,7 @@ export interface GridProps<T> extends NewOmit<NodeProps, 'children'> {
20
20
  columns?: number;
21
21
  looping?: boolean;
22
22
  scroll?: "auto" | "none";
23
+ selected?: number;
23
24
  onSelectedChanged?: (index: number, grid: ElementNode, elm?: ElementNode) => void;
24
25
  }
25
26
 
@@ -28,16 +29,25 @@ export function Grid<T>(props: GridProps<T>): JSX.Element {
28
29
  const [focusedIndex, setFocusedIndex] = createSignal(0);
29
30
  const baseColumns = 4;
30
31
 
32
+ createEffect(() => {
33
+ const currentIndex = untrack(focusedIndex);
34
+ if (props.selected === currentIndex) return;
35
+ if (props.selected !== undefined && props.items?.length > props.selected) {
36
+ moveFocus(props.selected! - currentIndex);
37
+ }
38
+ });
39
+
31
40
  const itemWidth = () => props.itemWidth ?? 300
32
41
  const itemHeight = () => props.itemHeight ?? 300
33
42
 
34
43
  const columns = createMemo(() => props.columns || baseColumns);
35
44
  const totalWidth = createMemo(() => itemWidth() + (props.itemOffset ?? 0));
36
45
  const totalHeight = createMemo(() => itemHeight() + (props.itemOffset ?? 0));
46
+ const rows = createMemo(() => Math.ceil(props.items.length / columns()));
37
47
 
38
48
  function focus() {
39
49
  const focusedElm = gridRef.children[focusedIndex()];
40
- if (focusedElm instanceof ElementNode && !focusedElm.states.has('$focus')) {
50
+ if (focusedElm instanceof ElementNode && !hasFocus(focusedElm)) {
41
51
  focusedElm.setFocus();
42
52
  props.onSelectedChanged?.call(gridRef, focusedIndex(), gridRef, focusedElm);
43
53
  return true;
@@ -99,27 +109,27 @@ export function Grid<T>(props: GridProps<T>): JSX.Element {
99
109
  <view
100
110
  {...props}
101
111
  ref={chainRefs(el => gridRef = el, props.ref)}
102
- transition={{ y: true }}
112
+ transition={/* @once */ { y: true }}
113
+ height={totalHeight() * rows()}
103
114
  onUp={() => moveFocus(-columns())}
104
115
  onDown={() => moveFocus(columns())}
105
116
  onLeft={() => handleHorizontalFocus(-1)}
106
117
  onRight={() => handleHorizontalFocus(1)}
107
118
  onFocus={() => handleHorizontalFocus(0)}
108
- strictBounds={false}
109
119
  y={scrollY()}
110
120
  >
111
- <For each={props.items}>
121
+ <Index each={props.items}>
112
122
  {(item, index) => (
113
123
  <props.children
114
- item={item}
115
- index={index()}
124
+ item={item()}
125
+ index={index}
116
126
  width={itemWidth()}
117
127
  height={itemHeight()}
118
- x={(index() % columns()) * totalWidth()}
119
- y={Math.floor(index() / columns()) * totalHeight()}
128
+ x={(index % columns()) * totalWidth()}
129
+ y={Math.floor(index / columns()) * totalHeight()}
120
130
  />
121
131
  )}
122
- </For>
132
+ </Index>
123
133
  </view>
124
134
  );
125
135
  };