@neoinkjs/components 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/package.json +46 -0
- package/src/background-context.ts +13 -0
- package/src/box.tsx +142 -0
- package/src/index.ts +11 -0
- package/src/newline.tsx +22 -0
- package/src/spacer.tsx +13 -0
- package/src/static.tsx +53 -0
- package/src/text.tsx +82 -0
- package/src/transform.tsx +44 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neoinkjs/components",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Box, Text, Spacer, Newline, Static, Transform — the React components for neoink.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Nicholas Zolton",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/NicholasZolton/neoink.git",
|
|
10
|
+
"directory": "packages/components"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/NicholasZolton/neoink#readme",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"neovim",
|
|
15
|
+
"nvim",
|
|
16
|
+
"react",
|
|
17
|
+
"tui",
|
|
18
|
+
"terminal",
|
|
19
|
+
"react-reconciler",
|
|
20
|
+
"ink"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./src/index.ts",
|
|
26
|
+
"default": "./src/index.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./src/index.ts",
|
|
30
|
+
"types": "./src/index.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"src"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@neoinkjs/core": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": "^19.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "^19.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ambient background color set by the nearest ancestor `<Box backgroundColor>`.
|
|
5
|
+
* `<Text>` reads this as a fallback when it has no `backgroundColor` of its
|
|
6
|
+
* own, so text nested inside a colored box inherits the box's background
|
|
7
|
+
* (matching Ink — see §7 of the ink-architecture-digest). `undefined` means
|
|
8
|
+
* "no ancestor Box set one" (or there is no ancestor Box at all).
|
|
9
|
+
*
|
|
10
|
+
* Internal to `@neoinkjs/components` — not re-exported from `index.ts`. `Box`
|
|
11
|
+
* is the only producer, `Text` the only consumer.
|
|
12
|
+
*/
|
|
13
|
+
export const backgroundContext = createContext<string | undefined>(undefined);
|
package/src/box.tsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import type { ReactNode, Ref } from "react";
|
|
3
|
+
import type { AriaRole, AriaState, NeoElement, Styles, TextStyle } from "@neoinkjs/core";
|
|
4
|
+
import { DEFAULT_BOX_STYLE as CORE_DEFAULT_BOX_STYLE, useIsScreenReaderEnabled } from "@neoinkjs/core";
|
|
5
|
+
import { backgroundContext } from "./background-context";
|
|
6
|
+
import { Text } from "./text";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The layout-affecting subset of `Styles`: everything except the cosmetic
|
|
10
|
+
* `TextStyle` keys (inapplicable to `<Box>`) and `textWrap` (a `<Text>`-only
|
|
11
|
+
* concern). Deriving this via `Omit` keeps it in sync with `Styles`
|
|
12
|
+
* automatically — no re-listing of layout keys. Note `borderColor`/
|
|
13
|
+
* `borderBackgroundColor` and their per-edge variants are NOT part of
|
|
14
|
+
* `TextStyle`, so they already flow through this `Omit` untouched.
|
|
15
|
+
*/
|
|
16
|
+
type BoxNodeStyle = Omit<Styles, keyof TextStyle | "textWrap">;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `backgroundColor` IS a `TextStyle` key, so the `Omit` above drops it —
|
|
20
|
+
* unlike border colors, it has to be re-added explicitly. It's still a
|
|
21
|
+
* cosmetic `<Box>` prop (Ink lets `<Box>` set it directly, not just via
|
|
22
|
+
* inheritance), so `<Box>` writes it into `style` and also fans it out to
|
|
23
|
+
* descendant `<Text>` via `backgroundContext`.
|
|
24
|
+
*/
|
|
25
|
+
type BoxNodeStyleWithBackground = BoxNodeStyle & Pick<Styles, "backgroundColor">;
|
|
26
|
+
|
|
27
|
+
export type BoxProps = BoxNodeStyleWithBackground & {
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* A label for the element for screen readers. In screen-reader mode
|
|
31
|
+
* (`useIsScreenReaderEnabled()`), this REPLACES `children` in the box's
|
|
32
|
+
* screen-reader output — matches Ink's `<Box>` `aria-label` exactly.
|
|
33
|
+
*/
|
|
34
|
+
"aria-label"?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Hides the element (and its subtree) from screen readers. Has NO effect
|
|
37
|
+
* outside screen-reader mode — matches Ink: `aria-hidden` never changes a
|
|
38
|
+
* normal render's visual output.
|
|
39
|
+
*/
|
|
40
|
+
"aria-hidden"?: boolean;
|
|
41
|
+
/** The element's ARIA role, surfaced in screen-reader output as a `"<role>: "` prefix. */
|
|
42
|
+
"aria-role"?: AriaRole;
|
|
43
|
+
/** The element's ARIA state, surfaced in screen-reader output as a `"(<flags>) "` prefix. */
|
|
44
|
+
"aria-state"?: AriaState;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Layout defaults merged *under* the caller's props, matching Ink's `<Box>`
|
|
49
|
+
* defaults. Re-exported (narrowed to `BoxNodeStyle`) so the fallback flex
|
|
50
|
+
* behavior can't drift from what callers expect when they omit these props —
|
|
51
|
+
* `<Transform>` and `<Static>`, which also emit a `neoink-box`, spread this
|
|
52
|
+
* as their base rather than re-listing the same triple. The actual literal
|
|
53
|
+
* values live in `@neoinkjs/core`'s `style.ts` (`DEFAULT_BOX_STYLE`), NOT
|
|
54
|
+
* here — that package's own `components/error-overview.tsx` needs the same
|
|
55
|
+
* defaults but can't import this `<Box>` COMPONENT (that would make
|
|
56
|
+
* `@neoinkjs/core` depend on `@neoinkjs/components`, which already depends on
|
|
57
|
+
* `core` — a cycle), so the raw values are defined once in `core` and both
|
|
58
|
+
* this re-export and that file build on them.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately field-by-field, NOT `{ ...CORE_DEFAULT_BOX_STYLE }`: `Styles`
|
|
61
|
+
* (core's constant's type) is a strict superset of `BoxNodeStyle` (it also
|
|
62
|
+
* allows every `TextStyle`/`textWrap` key `BoxNodeStyle`'s `Omit` exists to
|
|
63
|
+
* exclude), and TypeScript's excess-property check — the thing that would
|
|
64
|
+
* catch a future `DEFAULT_BOX_STYLE` in core accidentally growing a stray
|
|
65
|
+
* cosmetic key — only fires for a FRESH object literal, not for values
|
|
66
|
+
* copied in via a spread. Spreading here would silently defeat that check
|
|
67
|
+
* (verified: temporarily adding `color: "red"` to core's constant compiled
|
|
68
|
+
* with zero errors under a spread-based version of this line); listing each
|
|
69
|
+
* field explicitly keeps it real.
|
|
70
|
+
*/
|
|
71
|
+
export const DEFAULT_BOX_STYLE: BoxNodeStyle = {
|
|
72
|
+
flexDirection: CORE_DEFAULT_BOX_STYLE.flexDirection,
|
|
73
|
+
flexGrow: CORE_DEFAULT_BOX_STYLE.flexGrow,
|
|
74
|
+
flexShrink: CORE_DEFAULT_BOX_STYLE.flexShrink,
|
|
75
|
+
flexWrap: CORE_DEFAULT_BOX_STYLE.flexWrap,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A layout container. Flattens its layout props (Ink-style) into a single
|
|
80
|
+
* `style` object — defaults first, then the passed props — and renders a
|
|
81
|
+
* `neoink-box` host node. When `backgroundColor` is set, wraps `children` in
|
|
82
|
+
* a `backgroundContext.Provider` so descendant `<Text>` without their own
|
|
83
|
+
* `backgroundColor` inherit it (Ink behavior — see §7 of the architecture
|
|
84
|
+
* digest).
|
|
85
|
+
*
|
|
86
|
+
* Forwards a `ref` to the underlying `neoink-box` host element — the
|
|
87
|
+
* reconciler's `getPublicInstance` resolves it to that element's `NeoElement`
|
|
88
|
+
* (see `@neoinkjs/core`'s `reconciler.ts`), so `ref.current` is exactly what
|
|
89
|
+
* `measureElement()`/`useBoxMetrics()` expect. Matches Ink's `forwardRef`
|
|
90
|
+
* `<Box>`.
|
|
91
|
+
*
|
|
92
|
+
* Screen-reader accessibility (Task 6), ported from Ink's `<Box>`
|
|
93
|
+
* (`src/components/Box.tsx`) field for field:
|
|
94
|
+
* - `aria-hidden`: when `useIsScreenReaderEnabled()` is true, returns `null`
|
|
95
|
+
* — the whole subtree is omitted, exactly like Ink. Outside screen-reader
|
|
96
|
+
* mode this is a no-op: a normal render never reads it.
|
|
97
|
+
* - `aria-label`: rendered (via `<Text>`, standing in for Ink's raw
|
|
98
|
+
* `<ink-text>`) and used AS this box's content — instead of `children` —
|
|
99
|
+
* only when `useIsScreenReaderEnabled()` is true. A non-screen-reader
|
|
100
|
+
* render always uses `children`, regardless of `aria-label`.
|
|
101
|
+
* - `aria-role`/`aria-state`: always attached to the node as
|
|
102
|
+
* `internal_accessibility` (Ink does the same — its `internal_accessibility`
|
|
103
|
+
* prop is unconditional, not gated on screen-reader mode); the ordinary
|
|
104
|
+
* paint path ignores it, only `renderNodeToScreenReaderOutput` reads it.
|
|
105
|
+
*/
|
|
106
|
+
export const Box = forwardRef(function Box(
|
|
107
|
+
props: BoxProps,
|
|
108
|
+
ref: Ref<NeoElement>,
|
|
109
|
+
): React.ReactElement | null {
|
|
110
|
+
const {
|
|
111
|
+
children,
|
|
112
|
+
"aria-label": ariaLabel,
|
|
113
|
+
"aria-hidden": ariaHidden,
|
|
114
|
+
"aria-role": role,
|
|
115
|
+
"aria-state": ariaState,
|
|
116
|
+
...layoutProps
|
|
117
|
+
} = props;
|
|
118
|
+
const style: BoxNodeStyleWithBackground = { ...DEFAULT_BOX_STYLE, ...layoutProps };
|
|
119
|
+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
120
|
+
|
|
121
|
+
if (isScreenReaderEnabled && ariaHidden) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const label = ariaLabel ? React.createElement(Text, null, ariaLabel) : undefined;
|
|
126
|
+
const boxChildren = isScreenReaderEnabled && label ? label : children;
|
|
127
|
+
|
|
128
|
+
const content =
|
|
129
|
+
style.backgroundColor !== undefined
|
|
130
|
+
? React.createElement(
|
|
131
|
+
backgroundContext.Provider,
|
|
132
|
+
{ value: style.backgroundColor },
|
|
133
|
+
boxChildren,
|
|
134
|
+
)
|
|
135
|
+
: boxChildren;
|
|
136
|
+
|
|
137
|
+
return React.createElement(
|
|
138
|
+
"neoink-box",
|
|
139
|
+
{ style, ref, internal_accessibility: { role, state: ariaState } },
|
|
140
|
+
content,
|
|
141
|
+
);
|
|
142
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { Box } from "./box";
|
|
2
|
+
export type { BoxProps } from "./box";
|
|
3
|
+
export { Text } from "./text";
|
|
4
|
+
export type { TextProps } from "./text";
|
|
5
|
+
export { Spacer } from "./spacer";
|
|
6
|
+
export { Newline } from "./newline";
|
|
7
|
+
export type { NewlineProps } from "./newline";
|
|
8
|
+
export { Transform } from "./transform";
|
|
9
|
+
export type { TransformProps } from "./transform";
|
|
10
|
+
export { Static } from "./static";
|
|
11
|
+
export type { StaticProps } from "./static";
|
package/src/newline.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "./text";
|
|
3
|
+
|
|
4
|
+
export interface NewlineProps {
|
|
5
|
+
count?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders `count` newline characters as text content. Only meaningful inside
|
|
10
|
+
* a `<Text>`: it delegates to `<Text>`, which the reconciler remaps to a
|
|
11
|
+
* cosmetic `neoink-virtual-text` node when nested inside another `<Text>`
|
|
12
|
+
* (see `createInstance` in `@neoinkjs/core`'s reconciler), so its newline
|
|
13
|
+
* string composes into the ancestor `<Text>`'s squashed content exactly like
|
|
14
|
+
* `<Text>Hello <Text>World</Text></Text>` does.
|
|
15
|
+
*/
|
|
16
|
+
export function Newline({ count = 1 }: NewlineProps): React.ReactElement {
|
|
17
|
+
if (count < 0) {
|
|
18
|
+
throw new Error(`<Newline count> must be >= 0, got ${count}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return React.createElement(Text, null, "\n".repeat(count));
|
|
22
|
+
}
|
package/src/spacer.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box } from "./box";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A flexible filler: a `<Box>` that grows to consume any remaining space
|
|
6
|
+
* along its parent's main axis, pushing its siblings toward opposite ends
|
|
7
|
+
* of the parent. Inherits `Box`'s default `flexShrink: 1`, so — matching
|
|
8
|
+
* Ink's `<Spacer>` — it can still shrink below its grown size in an
|
|
9
|
+
* overflowing row.
|
|
10
|
+
*/
|
|
11
|
+
export function Spacer(): React.ReactElement {
|
|
12
|
+
return React.createElement(Box, { flexGrow: 1 });
|
|
13
|
+
}
|
package/src/static.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { Styles } from "@neoinkjs/core";
|
|
4
|
+
import { DEFAULT_BOX_STYLE } from "./box";
|
|
5
|
+
|
|
6
|
+
export interface StaticProps<T> {
|
|
7
|
+
items: readonly T[];
|
|
8
|
+
children: (item: T, index: number) => ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Column layout for a stacked list of static entries. Spreads `<Box>`'s
|
|
13
|
+
* shared `DEFAULT_BOX_STYLE` (single source of truth for the flex-default
|
|
14
|
+
* triple) and overrides only `flexDirection` to `"column"`.
|
|
15
|
+
*/
|
|
16
|
+
const STATIC_NODE_STYLE: Styles = {
|
|
17
|
+
...DEFAULT_BOX_STYLE,
|
|
18
|
+
flexDirection: "column",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Renders `items` once each, via `children(item, index)`, stacked in a
|
|
23
|
+
* column.
|
|
24
|
+
*
|
|
25
|
+
* Ink's `<Static>` renders its subtree exactly once and then removes it from
|
|
26
|
+
* the live tree, so the *persistent* render loop's per-frame diff never
|
|
27
|
+
* repaints it — the content is written once, above the diffed region, and
|
|
28
|
+
* never re-erased (see `internal_static`/`isStaticDirty` in Ink's DOM).
|
|
29
|
+
* neoink's `<Static>` instead keeps rendering the FULL `items` array every
|
|
30
|
+
* commit (simpler: no internal index/slicing state) and tags the wrapping
|
|
31
|
+
* node `internal_static` (a typed field on `NeoElement`, see `@neoinkjs/core`'s
|
|
32
|
+
* `dom.ts`); it's `@neoinkjs/core`'s persistent render loop
|
|
33
|
+
* (`instance.ts`/`render.ts`) that re-derives the equivalent of Ink's
|
|
34
|
+
* once-only semantics — finding this subtree, diffing its growing output
|
|
35
|
+
* against what it already emitted, and writing only the new suffix, once,
|
|
36
|
+
* above the diffed dynamic frame. `renderToString`'s one-shot mount has no
|
|
37
|
+
* persistent frame to dedup against, so there `<Static>` simply renders
|
|
38
|
+
* inline, in order, like any other content.
|
|
39
|
+
*
|
|
40
|
+
* Like `<Transform>`, its host element is `neoink-box` — a deliberate neoink
|
|
41
|
+
* design choice, not Ink parity. As a result `<Static>` can't be nested
|
|
42
|
+
* inside a `<Text>` subtree: the reconciler throws (`<Box>` can't be nested
|
|
43
|
+
* inside `<Text>`) by design.
|
|
44
|
+
*/
|
|
45
|
+
export function Static<T>(props: StaticProps<T>): React.ReactElement {
|
|
46
|
+
const { items, children } = props;
|
|
47
|
+
|
|
48
|
+
return React.createElement(
|
|
49
|
+
"neoink-box",
|
|
50
|
+
{ style: STATIC_NODE_STYLE, internal_static: true },
|
|
51
|
+
items.map((item, index) => children(item, index)),
|
|
52
|
+
);
|
|
53
|
+
}
|
package/src/text.tsx
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { DEFAULT_TEXT_WRAP, TEXT_STYLE_KEYS, useIsScreenReaderEnabled } from "@neoinkjs/core";
|
|
4
|
+
import type { Styles, TextStyle } from "@neoinkjs/core";
|
|
5
|
+
import { backgroundContext } from "./background-context";
|
|
6
|
+
|
|
7
|
+
export interface TextProps extends TextStyle {
|
|
8
|
+
wrap?: Styles["textWrap"];
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* A label for the element for screen readers. In screen-reader mode
|
|
12
|
+
* (`useIsScreenReaderEnabled()`), this REPLACES `children` — matches
|
|
13
|
+
* Ink's `<Text>` `aria-label` exactly. Note: unlike `<Box>`, Ink's
|
|
14
|
+
* `<Text>` has no `aria-role`/`aria-state` — neither does neoink's.
|
|
15
|
+
*/
|
|
16
|
+
"aria-label"?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Hides the element from screen readers. Has NO effect outside
|
|
19
|
+
* screen-reader mode — matches Ink.
|
|
20
|
+
*/
|
|
21
|
+
"aria-hidden"?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The subset of `Styles` a `<Text>` node ever writes: `textWrap` + the cosmetic keys. */
|
|
25
|
+
type TextNodeStyle = Pick<Styles, keyof TextStyle | "textWrap">;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Renders text. Builds a `style` object carrying `textWrap` (defaulting to
|
|
29
|
+
* `"wrap"`) plus whichever cosmetic keys the caller actually set — an unset
|
|
30
|
+
* prop is omitted rather than written as `color: undefined`, so it never
|
|
31
|
+
* shadows an inherited/default style at paint time. Reuses core's
|
|
32
|
+
* `TEXT_STYLE_KEYS` (the same list `extractTextStyle` in the reconciler
|
|
33
|
+
* consumes) so the two never drift apart. Renders `null` when the effective
|
|
34
|
+
* content — `children`, or `aria-label` in screen-reader mode (see below) —
|
|
35
|
+
* is `null`/`undefined` (no `neoink-text` node at all).
|
|
36
|
+
*
|
|
37
|
+
* `backgroundColor` falls back to the nearest ancestor `<Box backgroundColor>`
|
|
38
|
+
* (read via `backgroundContext`) when this `<Text>` doesn't set its own —
|
|
39
|
+
* matching Ink's inheritance. `useContext` is called unconditionally, before
|
|
40
|
+
* the early-return below, so it stays consistent across renders regardless
|
|
41
|
+
* of whether `children` is present.
|
|
42
|
+
*
|
|
43
|
+
* Screen-reader accessibility (Task 6), ported from Ink's `<Text>`
|
|
44
|
+
* (`src/components/Text.tsx`) field for field:
|
|
45
|
+
* - `aria-label`: when `useIsScreenReaderEnabled()` is true AND set, stands
|
|
46
|
+
* in for `children` (`childrenOrAriaLabel` below) — so `<Text aria-label="x" />`
|
|
47
|
+
* with no children still renders `"x"` in screen-reader mode. Outside
|
|
48
|
+
* screen-reader mode this is a no-op: `children` is always used.
|
|
49
|
+
* - `aria-hidden`: when `useIsScreenReaderEnabled()` is true, returns `null`
|
|
50
|
+
* — checked AFTER the null-content check below, exactly mirroring Ink's
|
|
51
|
+
* ordering (`Text.tsx` computes `childrenOrAriaLabel`/its null-check first,
|
|
52
|
+
* then the `aria-hidden` check, right before constructing `<ink-text>`).
|
|
53
|
+
*/
|
|
54
|
+
export function Text(props: TextProps): React.ReactElement | null {
|
|
55
|
+
const { wrap, children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false } = props;
|
|
56
|
+
const inheritedBackgroundColor = useContext(backgroundContext);
|
|
57
|
+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
58
|
+
|
|
59
|
+
const childrenOrAriaLabel = isScreenReaderEnabled && ariaLabel ? ariaLabel : children;
|
|
60
|
+
|
|
61
|
+
if (childrenOrAriaLabel === null || childrenOrAriaLabel === undefined) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isScreenReaderEnabled && ariaHidden) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const style: TextNodeStyle = { textWrap: wrap ?? DEFAULT_TEXT_WRAP };
|
|
70
|
+
for (const key of TEXT_STYLE_KEYS) {
|
|
71
|
+
const value = props[key];
|
|
72
|
+
if (value !== undefined) {
|
|
73
|
+
(style[key] as TextStyle[typeof key]) = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (style.backgroundColor === undefined && inheritedBackgroundColor !== undefined) {
|
|
78
|
+
style.backgroundColor = inheritedBackgroundColor;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return React.createElement("neoink-text", { style }, childrenOrAriaLabel);
|
|
82
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { LineTransform } from "@neoinkjs/core";
|
|
4
|
+
import { DEFAULT_BOX_STYLE } from "./box";
|
|
5
|
+
|
|
6
|
+
export interface TransformProps {
|
|
7
|
+
/**
|
|
8
|
+
* A pure text→text mapping applied to each line of the squashed content
|
|
9
|
+
* beneath this node (e.g. uppercasing). Unlike Ink's ANSI-emitting
|
|
10
|
+
* `<Transform>`, neoink's transform never carries styling — that stays
|
|
11
|
+
* `<Text>`'s job — it only ever rewrites line content.
|
|
12
|
+
*/
|
|
13
|
+
transform: LineTransform;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps `children` in a `neoink-box` carrying `internal_transform`. At paint
|
|
19
|
+
* time (`renderNodeToOutput` in `@neoinkjs/core`), every descendant
|
|
20
|
+
* `<Text>`'s squashed + wrapped lines are passed through this (and any
|
|
21
|
+
* enclosing `<Transform>`'s) transform before being written — nested
|
|
22
|
+
* `<Transform>`s compose inner-out. Uses `<Box>`'s shared `DEFAULT_BOX_STYLE`
|
|
23
|
+
* (same source, not a re-listed copy) so the wrapper lays out like a plain
|
|
24
|
+
* `<Box>` around the same children.
|
|
25
|
+
*
|
|
26
|
+
* Architecture note — a deliberate neoink divergence from Ink, NOT parity:
|
|
27
|
+
* Ink's `<Transform>` renders an `ink-text` node (`<ink-text
|
|
28
|
+
* internal_transform=...>`), so upstream it CAN be nested inside `<Text>`.
|
|
29
|
+
* neoink instead hosts a `neoink-box`, a P2 design choice. The consequence is
|
|
30
|
+
* a real, intentional limitation: because the reconciler throws when a
|
|
31
|
+
* `neoink-box` is instantiated inside a `<Text>` subtree (`<Box>` can't be
|
|
32
|
+
* nested inside `<Text>`), `<Transform>` can only wrap a `<Text>`/`<Box>`
|
|
33
|
+
* from the outside — nesting it as an inline child of `<Text>` throws loudly
|
|
34
|
+
* by design.
|
|
35
|
+
*/
|
|
36
|
+
export function Transform(props: TransformProps): React.ReactElement {
|
|
37
|
+
const { transform, children } = props;
|
|
38
|
+
|
|
39
|
+
return React.createElement(
|
|
40
|
+
"neoink-box",
|
|
41
|
+
{ style: DEFAULT_BOX_STYLE, internal_transform: transform },
|
|
42
|
+
children,
|
|
43
|
+
);
|
|
44
|
+
}
|