@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5
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/README.md +23 -13
- package/THIRD_PARTY_NOTICES.md +70 -23
- package/dist/Text-D5HUf3Fj.d.ts +452 -0
- package/dist/ansi.d.ts +137 -143
- package/dist/ansi.js +87 -2
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-Bmc2tRPk.d.ts +21 -0
- package/dist/index.d.ts +235 -754
- package/dist/index.js +1362 -3001
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOh__-65.js +324 -0
- package/dist/screen-BReKIheE.d.ts +40 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-BJmzX2NJ.js +664 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-C2sciZOo.js +1335 -0
- package/package.json +43 -17
- package/src/ansi/chalk.ts +12 -53
- package/src/ansi/cursor.ts +2 -4
- package/src/ansi/east-asian-width.ts +44 -0
- package/src/ansi/escapes.ts +18 -26
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +14 -14
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +2 -5
- package/src/ansi/slice.ts +1 -1
- package/src/ansi/string-width.ts +91 -206
- package/src/ansi/strip.ts +13 -33
- package/src/ansi/tokenize.ts +96 -169
- package/src/ansi/truncate.ts +5 -3
- package/src/ansi/wrap.ts +13 -14
- package/src/ansi-tokenizer.ts +1 -1
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +109 -22
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +43 -60
- package/src/components/CursorContext.ts +1 -1
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorOverview.tsx +8 -7
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Spacer.tsx +1 -1
- package/src/components/Static.tsx +1 -1
- package/src/components/StderrContext.ts +0 -2
- package/src/components/StdinContext.ts +0 -1
- package/src/components/StdoutContext.ts +1 -3
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +23 -46
- package/src/components/Transform.tsx +2 -2
- package/src/cursor-position.ts +1 -1
- package/src/devtools.ts +84 -24
- package/src/dom.ts +11 -8
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +1 -1
- package/src/global.d.ts +6 -4
- package/src/{boxes.ts → glyphs.ts} +17 -18
- package/src/hooks/use-animation.ts +1 -1
- package/src/hooks/use-app.ts +1 -1
- package/src/hooks/use-box-metrics.ts +1 -1
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +3 -3
- package/src/hooks/use-focus-manager.ts +1 -1
- package/src/hooks/use-focus.ts +5 -6
- package/src/hooks/use-input.ts +4 -4
- package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
- package/src/hooks/use-paste.ts +2 -2
- package/src/hooks/use-stderr.ts +1 -1
- package/src/hooks/use-stdin.ts +1 -1
- package/src/hooks/use-stdout.ts +1 -1
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-window-size.ts +2 -2
- package/src/index.ts +88 -44
- package/src/ink.tsx +620 -745
- package/src/input-parser.ts +1 -1
- package/src/instances.ts +1 -1
- package/src/kitty-keyboard.ts +128 -0
- package/src/measure-element.ts +1 -1
- package/src/measure-text.ts +2 -2
- package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
- package/src/parse-keypress.ts +4 -5
- package/src/parse-stack-line.ts +0 -1
- package/src/patch-console.ts +44 -0
- package/src/reconciler.ts +36 -62
- package/src/render-background.ts +37 -16
- package/src/render-border.ts +97 -64
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +24 -9
- package/src/render.ts +24 -16
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/sanitize-ansi.ts +1 -1
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +137 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +272 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +4 -5
- package/src/squash-text-nodes.ts +4 -7
- package/src/stream.ts +2 -4
- package/src/structured-text.ts +321 -0
- package/src/styles.ts +16 -17
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal-size.ts +9 -8
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +1 -6
- package/src/utils.ts +2 -2
- package/src/wrap-text.ts +4 -4
- package/src/yoga/config.ts +2 -2
- package/src/yoga/core/absoluteLayout.ts +15 -15
- package/src/yoga/core/baseline.ts +3 -3
- package/src/yoga/core/cache.ts +4 -4
- package/src/yoga/core/calculateLayout.ts +33 -27
- package/src/yoga/core/config.ts +1 -1
- package/src/yoga/core/flexLine.ts +3 -3
- package/src/yoga/core/helpers.ts +3 -3
- package/src/yoga/core/layoutResults.ts +4 -4
- package/src/yoga/core/node.ts +16 -16
- package/src/yoga/core/pixelGrid.ts +4 -4
- package/src/yoga/core/style.ts +11 -11
- package/src/yoga/core/types.ts +2 -2
- package/src/yoga/index.ts +6 -6
- package/src/yoga/node.ts +8 -8
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-CBiyyZzw.js +0 -2156
- package/src/ansi/supports-color.ts +0 -213
- package/src/ansi/widest-line.ts +0 -12
- package/src/auto-bind.ts +0 -41
- package/src/colorize.ts +0 -60
- package/src/devtools-window-polyfill.ts +0 -73
- package/src/indent-string.ts +0 -16
- package/src/is-in-ci.ts +0 -7
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
- package/src/write-synchronized.ts +0 -9
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
// MemoryRouter/Routes/Route/Navigate/Outlet are ported from react-router's
|
|
3
|
+
// declarative components (lib/components.tsx, MIT). Link is Sigil-native.
|
|
4
|
+
import {
|
|
5
|
+
Children,
|
|
6
|
+
Fragment,
|
|
7
|
+
isValidElement,
|
|
8
|
+
startTransition,
|
|
9
|
+
useCallback,
|
|
10
|
+
useEffect,
|
|
11
|
+
useLayoutEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
type ReactElement,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
} from "react";
|
|
18
|
+
|
|
19
|
+
import { Text, type Props as TextProps } from "#/components/Text.tsx";
|
|
20
|
+
import { useFocus } from "#/hooks/use-focus.ts";
|
|
21
|
+
import { useInput } from "#/hooks/use-input.ts";
|
|
22
|
+
import { LocationContext, NavigationContext, type Navigator } from "#/router/context.ts";
|
|
23
|
+
import {
|
|
24
|
+
createMemoryHistory,
|
|
25
|
+
type InitialEntry,
|
|
26
|
+
type Location,
|
|
27
|
+
type To,
|
|
28
|
+
} from "#/router/history.ts";
|
|
29
|
+
import {
|
|
30
|
+
useLocation,
|
|
31
|
+
useNavigate,
|
|
32
|
+
useOutlet,
|
|
33
|
+
useResolvedPath,
|
|
34
|
+
useRoutes,
|
|
35
|
+
} from "#/router/hooks.tsx";
|
|
36
|
+
import { normalizePathname, type RouteObject } from "#/router/matcher.ts";
|
|
37
|
+
|
|
38
|
+
export type MemoryRouterProps = {
|
|
39
|
+
/**
|
|
40
|
+
The navigation stack to start with. Defaults to `["/"]`.
|
|
41
|
+
*/
|
|
42
|
+
initialEntries?: InitialEntry[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
The index of the initial entry to render. Defaults to the last entry.
|
|
46
|
+
*/
|
|
47
|
+
initialIndex?: number;
|
|
48
|
+
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
The routing container for a Sigil app. Stores the navigation stack in memory —
|
|
54
|
+
routes aren't URLs, they're screen states.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<MemoryRouter>
|
|
58
|
+
<Routes>
|
|
59
|
+
<Route path="/" element={<Home />} />
|
|
60
|
+
<Route path="/settings" element={<Settings />} />
|
|
61
|
+
</Routes>
|
|
62
|
+
</MemoryRouter>
|
|
63
|
+
```
|
|
64
|
+
*/
|
|
65
|
+
export function MemoryRouter({ initialEntries, initialIndex, children }: MemoryRouterProps) {
|
|
66
|
+
const historyRef = useRef<ReturnType<typeof createMemoryHistory> | null>(null);
|
|
67
|
+
historyRef.current ??= createMemoryHistory({ initialEntries, initialIndex });
|
|
68
|
+
|
|
69
|
+
const history = historyRef.current;
|
|
70
|
+
const [state, setState] = useState({
|
|
71
|
+
action: history.action,
|
|
72
|
+
location: history.location,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Navigation is a transition: if the destination screen suspends, the
|
|
76
|
+
// current screen stays visible until it's ready.
|
|
77
|
+
useLayoutEffect(
|
|
78
|
+
() =>
|
|
79
|
+
history.listen(({ action, location }) => {
|
|
80
|
+
startTransition(() => {
|
|
81
|
+
setState({ action, location });
|
|
82
|
+
});
|
|
83
|
+
}),
|
|
84
|
+
[history],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const navigator = useMemo<Navigator>(
|
|
88
|
+
() => ({
|
|
89
|
+
push: (to, historyState) => history.push(to, historyState),
|
|
90
|
+
replace: (to, historyState) => history.replace(to, historyState),
|
|
91
|
+
go: (delta) => history.go(delta),
|
|
92
|
+
canGoBack: () => history.canGoBack,
|
|
93
|
+
canGoForward: () => history.canGoForward,
|
|
94
|
+
}),
|
|
95
|
+
[history],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const navigationContext = useMemo(() => ({ navigator }), [navigator]);
|
|
99
|
+
const locationContext = useMemo(
|
|
100
|
+
() => ({ location: state.location, navigationType: state.action }),
|
|
101
|
+
[state],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<NavigationContext.Provider value={navigationContext}>
|
|
106
|
+
<LocationContext.Provider value={locationContext}>{children}</LocationContext.Provider>
|
|
107
|
+
</NavigationContext.Provider>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type RouteProps = {
|
|
112
|
+
/**
|
|
113
|
+
The path pattern to match, relative to the parent route. Supports `:param`
|
|
114
|
+
dynamic segments, optional segments (`:param?`, `edit?`), and a trailing
|
|
115
|
+
`*` splat.
|
|
116
|
+
*/
|
|
117
|
+
path?: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
Render this route in the parent's `<Outlet>` at the parent's exact path.
|
|
121
|
+
Index routes cannot have children.
|
|
122
|
+
*/
|
|
123
|
+
index?: boolean;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
The element to render when this route matches.
|
|
127
|
+
*/
|
|
128
|
+
element?: ReactNode;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
Nested `<Route>` elements, rendered into this route's `<Outlet>`.
|
|
132
|
+
*/
|
|
133
|
+
children?: ReactNode;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
Declares a route. Only valid as a child of `<Routes>` or another `<Route>`.
|
|
138
|
+
*/
|
|
139
|
+
export function Route(_props: RouteProps): ReactElement | null {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"A <Route> is only ever to be used as the child of a <Routes> element, " +
|
|
142
|
+
"never rendered directly. Please wrap your <Route> in a <Routes>.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createRoutesFromChildren(children: ReactNode): RouteObject[] {
|
|
147
|
+
const routes: RouteObject[] = [];
|
|
148
|
+
|
|
149
|
+
Children.forEach(children, (element) => {
|
|
150
|
+
if (!isValidElement(element)) {
|
|
151
|
+
// Ignore non-elements. This allows people to more easily inline
|
|
152
|
+
// conditionals in their route config.
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (element.type === Fragment) {
|
|
157
|
+
// Transparently support React.Fragment and its children.
|
|
158
|
+
routes.push(
|
|
159
|
+
...createRoutesFromChildren((element.props as { children?: ReactNode }).children),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (element.type !== Route) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`[${typeof element.type === "string" ? element.type : ((element.type as { name?: string }).name ?? "unknown")}] ` +
|
|
167
|
+
`is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const props = element.props as RouteProps;
|
|
172
|
+
const route: RouteObject = {
|
|
173
|
+
path: props.path,
|
|
174
|
+
index: props.index,
|
|
175
|
+
element: props.element,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (props.children) {
|
|
179
|
+
route.children = createRoutesFromChildren(props.children);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
routes.push(route);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return routes;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type RoutesProps = {
|
|
189
|
+
children?: ReactNode;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
Match against this location instead of the current one. Useful for
|
|
193
|
+
rendering a screen other than the one navigated to (e.g. transitions).
|
|
194
|
+
*/
|
|
195
|
+
location?: Partial<Location> | string;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
Renders the branch of child `<Route>` elements that best matches the current
|
|
200
|
+
location.
|
|
201
|
+
*/
|
|
202
|
+
export function Routes({ children, location }: RoutesProps): ReactElement | null {
|
|
203
|
+
return useRoutes(createRoutesFromChildren(children), location);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type OutletProps = {
|
|
207
|
+
/**
|
|
208
|
+
A value to make available to descendant routes via `useOutletContext()`.
|
|
209
|
+
*/
|
|
210
|
+
context?: unknown;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
Renders the matching child route of a parent route, or nothing if no child
|
|
215
|
+
matches.
|
|
216
|
+
*/
|
|
217
|
+
export function Outlet(props: OutletProps): ReactElement | null {
|
|
218
|
+
return useOutlet(props.context);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type NavigateProps = {
|
|
222
|
+
to: To;
|
|
223
|
+
replace?: boolean;
|
|
224
|
+
state?: unknown;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
Navigates as soon as it renders. The component form of `useNavigate`, for
|
|
229
|
+
declarative redirects:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
<Route path="/" element={<Navigate to="/home" replace />} />
|
|
233
|
+
```
|
|
234
|
+
*/
|
|
235
|
+
export function Navigate({ to, replace, state }: NavigateProps): null {
|
|
236
|
+
const navigate = useNavigate();
|
|
237
|
+
const { pathname, search } = useResolvedPath(to);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
navigate({ pathname, search }, { replace, state });
|
|
241
|
+
}, [navigate, pathname, search, replace, state]);
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
type LinkRenderState = {
|
|
247
|
+
/**
|
|
248
|
+
Whether this link currently has focus.
|
|
249
|
+
*/
|
|
250
|
+
isFocused: boolean;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
Whether the current location is the link's destination or a descendant of
|
|
254
|
+
it.
|
|
255
|
+
*/
|
|
256
|
+
isActive: boolean;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export type LinkProps = Omit<TextProps, "children"> & {
|
|
260
|
+
/**
|
|
261
|
+
The destination to navigate to when the link is activated.
|
|
262
|
+
*/
|
|
263
|
+
to: To;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
Replace the current entry in the navigation stack instead of pushing.
|
|
267
|
+
*/
|
|
268
|
+
replace?: boolean;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
State to attach to the destination location.
|
|
272
|
+
*/
|
|
273
|
+
state?: unknown;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
Focus this link if nothing else is focused yet.
|
|
277
|
+
*/
|
|
278
|
+
autoFocus?: boolean;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
An ID for programmatic focus via `useFocusManager().focus(id)`.
|
|
282
|
+
*/
|
|
283
|
+
id?: string;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
Link content. Pass a function to take full control of rendering based on
|
|
287
|
+
focus and active state.
|
|
288
|
+
*/
|
|
289
|
+
children?: ReactNode | ((state: LinkRenderState) => ReactNode);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
A focusable navigation element — the terminal's `<a>` tag. Focus it with
|
|
294
|
+
<kbd>Tab</kbd> and activate it with <kbd>Enter</kbd>. By default the focused
|
|
295
|
+
link renders inverse; pass a function as `children` (or any `Text` props) to
|
|
296
|
+
customize.
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
<Link to="/settings">Settings</Link>
|
|
300
|
+
```
|
|
301
|
+
*/
|
|
302
|
+
export function Link({
|
|
303
|
+
to,
|
|
304
|
+
replace = false,
|
|
305
|
+
state,
|
|
306
|
+
autoFocus = false,
|
|
307
|
+
id,
|
|
308
|
+
children,
|
|
309
|
+
...textProps
|
|
310
|
+
}: LinkProps) {
|
|
311
|
+
const navigate = useNavigate();
|
|
312
|
+
const path = useResolvedPath(to);
|
|
313
|
+
const { pathname: locationPathname } = useLocation();
|
|
314
|
+
const { isFocused } = useFocus({ autoFocus, id });
|
|
315
|
+
|
|
316
|
+
const toPathname = normalizePathname(path.pathname);
|
|
317
|
+
const isActive =
|
|
318
|
+
locationPathname === toPathname ||
|
|
319
|
+
(locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === "/");
|
|
320
|
+
|
|
321
|
+
const activate = useCallback(() => {
|
|
322
|
+
navigate({ pathname: path.pathname, search: path.search }, { replace, state });
|
|
323
|
+
}, [navigate, path.pathname, path.search, replace, state]);
|
|
324
|
+
|
|
325
|
+
useInput(
|
|
326
|
+
(_input, key) => {
|
|
327
|
+
if (key.return) {
|
|
328
|
+
activate();
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{ isActive: isFocused },
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (typeof children === "function") {
|
|
335
|
+
return <>{children({ isFocused, isActive })}</>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<Text inverse={isFocused} {...textProps}>
|
|
340
|
+
{children}
|
|
341
|
+
</Text>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createContext, type ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
import { type Location, type NavigationType, type Path } from "#/router/history.ts";
|
|
4
|
+
import { type RouteMatch } from "#/router/matcher.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
The imperative interface `useNavigate` drives. Backed by the memory history
|
|
8
|
+
inside `<MemoryRouter>`.
|
|
9
|
+
*/
|
|
10
|
+
export type Navigator = {
|
|
11
|
+
push: (to: Path, state?: unknown) => void;
|
|
12
|
+
replace: (to: Path, state?: unknown) => void;
|
|
13
|
+
go: (delta: number) => void;
|
|
14
|
+
canGoBack: () => boolean;
|
|
15
|
+
canGoForward: () => boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type NavigationContextObject = {
|
|
19
|
+
navigator: Navigator;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const NavigationContext = createContext<NavigationContextObject | null>(null);
|
|
23
|
+
|
|
24
|
+
export type LocationContextObject = {
|
|
25
|
+
location: Location;
|
|
26
|
+
navigationType: NavigationType;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const LocationContext = createContext<LocationContextObject | null>(null);
|
|
30
|
+
|
|
31
|
+
export type RouteContextObject = {
|
|
32
|
+
outlet: ReactElement | null;
|
|
33
|
+
matches: RouteMatch[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const RouteContext = createContext<RouteContextObject>({
|
|
37
|
+
outlet: null,
|
|
38
|
+
matches: [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const OutletContext = createContext<unknown>(null);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Ported from react-router's createMemoryHistory (lib/router/history.ts, MIT),
|
|
2
|
+
// without URL semantics: no hash, no href/URL creation, no encoding.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
The type of navigation that produced the current location:
|
|
6
|
+
|
|
7
|
+
- `"POP"` — moving through existing history entries (`navigate(-1)`, initial load).
|
|
8
|
+
- `"PUSH"` — a new entry was added to the stack.
|
|
9
|
+
- `"REPLACE"` — the current entry was overwritten.
|
|
10
|
+
*/
|
|
11
|
+
export type NavigationType = "POP" | "PUSH" | "REPLACE";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
The two parseable pieces of a route path. Sigil routes have no hash — a
|
|
15
|
+
terminal has no scroll anchors.
|
|
16
|
+
*/
|
|
17
|
+
export type Path = {
|
|
18
|
+
/**
|
|
19
|
+
The path of the screen, beginning with `/`.
|
|
20
|
+
*/
|
|
21
|
+
pathname: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
The query string, beginning with `?`, or an empty string.
|
|
25
|
+
*/
|
|
26
|
+
search: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
An entry in the navigation stack.
|
|
31
|
+
*/
|
|
32
|
+
export type Location<State = unknown> = Path & {
|
|
33
|
+
/**
|
|
34
|
+
Arbitrary state attached to this entry via `navigate(to, {state})`. Unlike the
|
|
35
|
+
browser, this is held in memory and never serialized — any value works.
|
|
36
|
+
*/
|
|
37
|
+
state: State;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
A unique key for this entry, stable across re-renders. Useful as a React
|
|
41
|
+
`key` to remount a screen when re-navigating to the same path.
|
|
42
|
+
*/
|
|
43
|
+
key: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
A destination to navigate to: either a path string (`"/users/123?tab=posts"`)
|
|
48
|
+
or a partial `Path` object.
|
|
49
|
+
*/
|
|
50
|
+
export type To = string | Partial<Path>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
Splits a path string into its pathname and search parts.
|
|
54
|
+
*/
|
|
55
|
+
export const parsePath = (path: string): Partial<Path> => {
|
|
56
|
+
const parsedPath: Partial<Path> = {};
|
|
57
|
+
|
|
58
|
+
if (path) {
|
|
59
|
+
const searchIndex = path.indexOf("?");
|
|
60
|
+
if (searchIndex >= 0) {
|
|
61
|
+
parsedPath.search = path.slice(searchIndex);
|
|
62
|
+
path = path.slice(0, searchIndex);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (path) {
|
|
66
|
+
parsedPath.pathname = path;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return parsedPath;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
Joins a `Path` back into a single string.
|
|
75
|
+
*/
|
|
76
|
+
export const createPath = ({ pathname = "/", search = "" }: Partial<Path>): string =>
|
|
77
|
+
pathname + (search && search !== "?" ? (search.startsWith("?") ? search : `?${search}`) : "");
|
|
78
|
+
|
|
79
|
+
type Update = {
|
|
80
|
+
action: NavigationType;
|
|
81
|
+
location: Location;
|
|
82
|
+
delta: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type Listener = (update: Update) => void;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
The in-memory navigation stack backing `<MemoryRouter>`.
|
|
89
|
+
*/
|
|
90
|
+
export type MemoryHistory = {
|
|
91
|
+
readonly index: number;
|
|
92
|
+
readonly action: NavigationType;
|
|
93
|
+
readonly location: Location;
|
|
94
|
+
readonly canGoBack: boolean;
|
|
95
|
+
readonly canGoForward: boolean;
|
|
96
|
+
push: (to: To, state?: unknown) => void;
|
|
97
|
+
replace: (to: To, state?: unknown) => void;
|
|
98
|
+
go: (delta: number) => void;
|
|
99
|
+
listen: (listener: Listener) => () => void;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
An entry to seed the navigation stack with: a path string or a partial
|
|
104
|
+
`Location` (which may carry `state`).
|
|
105
|
+
*/
|
|
106
|
+
export type InitialEntry = string | Partial<Location>;
|
|
107
|
+
|
|
108
|
+
type MemoryHistoryOptions = {
|
|
109
|
+
initialEntries?: InitialEntry[];
|
|
110
|
+
initialIndex?: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const createMemoryHistory = ({
|
|
114
|
+
initialEntries = ["/"],
|
|
115
|
+
initialIndex,
|
|
116
|
+
}: MemoryHistoryOptions = {}): MemoryHistory => {
|
|
117
|
+
let keyCounter = 0;
|
|
118
|
+
const createKey = () => `k${keyCounter++}`;
|
|
119
|
+
|
|
120
|
+
const createLocation = (to: To, state: unknown = null): Location => {
|
|
121
|
+
const path = typeof to === "string" ? parsePath(to) : to;
|
|
122
|
+
const location: Location = {
|
|
123
|
+
pathname: path.pathname ?? "/",
|
|
124
|
+
search: path.search ?? "",
|
|
125
|
+
state,
|
|
126
|
+
key: createKey(),
|
|
127
|
+
};
|
|
128
|
+
if (!location.pathname.startsWith("/")) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Route pathnames must be absolute, but "${location.pathname}" was used to initialize the navigation stack.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return location;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const entries: Location[] = initialEntries.map((entry) =>
|
|
138
|
+
createLocation(entry, typeof entry === "string" ? null : (entry.state ?? null)),
|
|
139
|
+
);
|
|
140
|
+
let index = Math.min(Math.max(initialIndex ?? entries.length - 1, 0), entries.length - 1);
|
|
141
|
+
let action: NavigationType = "POP";
|
|
142
|
+
let listener: Listener | null = null;
|
|
143
|
+
|
|
144
|
+
const history: MemoryHistory = {
|
|
145
|
+
get index() {
|
|
146
|
+
return index;
|
|
147
|
+
},
|
|
148
|
+
get action() {
|
|
149
|
+
return action;
|
|
150
|
+
},
|
|
151
|
+
get location() {
|
|
152
|
+
return entries[index]!;
|
|
153
|
+
},
|
|
154
|
+
get canGoBack() {
|
|
155
|
+
return index > 0;
|
|
156
|
+
},
|
|
157
|
+
get canGoForward() {
|
|
158
|
+
return index < entries.length - 1;
|
|
159
|
+
},
|
|
160
|
+
push(to, state) {
|
|
161
|
+
action = "PUSH";
|
|
162
|
+
const nextLocation = createLocation(to, state);
|
|
163
|
+
index += 1;
|
|
164
|
+
// Pushing truncates any forward entries, exactly like browser history.
|
|
165
|
+
entries.splice(index, entries.length, nextLocation);
|
|
166
|
+
listener?.({ action, location: nextLocation, delta: 1 });
|
|
167
|
+
},
|
|
168
|
+
replace(to, state) {
|
|
169
|
+
action = "REPLACE";
|
|
170
|
+
const nextLocation = createLocation(to, state);
|
|
171
|
+
entries[index] = nextLocation;
|
|
172
|
+
listener?.({ action, location: nextLocation, delta: 0 });
|
|
173
|
+
},
|
|
174
|
+
go(delta) {
|
|
175
|
+
action = "POP";
|
|
176
|
+
const nextIndex = Math.min(Math.max(index + delta, 0), entries.length - 1);
|
|
177
|
+
const actualDelta = nextIndex - index;
|
|
178
|
+
index = nextIndex;
|
|
179
|
+
listener?.({ action, location: entries[index]!, delta: actualDelta });
|
|
180
|
+
},
|
|
181
|
+
listen(newListener) {
|
|
182
|
+
if (listener) {
|
|
183
|
+
throw new Error("A memory history only supports one listener at a time.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
listener = newListener;
|
|
187
|
+
return () => {
|
|
188
|
+
listener = null;
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return history;
|
|
194
|
+
};
|