@marktiderman/genesis-ui 2.7.0 → 2.8.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/EXTENDING.md +40 -1
- package/SKILL.md +144 -9
- package/dist/{AppShell-CX0M-dfc.d.ts → AppShell-LXQykEqe.d.ts} +47 -5
- package/dist/{chunk-M62CHO3N.js → chunk-4C4ESW6O.js} +9 -4
- package/dist/chunk-4C4ESW6O.js.map +1 -0
- package/dist/{chunk-IXGAEECT.js → chunk-DWCAC2E7.js} +6 -3
- package/dist/chunk-DWCAC2E7.js.map +1 -0
- package/dist/chunk-FUVWKTPL.js +19 -0
- package/dist/chunk-FUVWKTPL.js.map +1 -0
- package/dist/{chunk-PLIYF6KI.js → chunk-MPI33I4K.js} +2 -2
- package/dist/{chunk-PLIYF6KI.js.map → chunk-MPI33I4K.js.map} +1 -1
- package/dist/{chunk-JA3ZKULS.js → chunk-OITWCIG2.js} +3 -3
- package/dist/chunk-OITWCIG2.js.map +1 -0
- package/dist/chunk-PKFDGGZV.js +65 -0
- package/dist/chunk-PKFDGGZV.js.map +1 -0
- package/dist/{chunk-TXOZMKNW.js → chunk-S7NXD37O.js} +27 -26
- package/dist/chunk-S7NXD37O.js.map +1 -0
- package/dist/{chunk-ZPPLUGLA.js → chunk-UYI3GLFJ.js} +162 -19
- package/dist/chunk-UYI3GLFJ.js.map +1 -0
- package/dist/{chunk-CDYZRZ6U.js → chunk-WGMEVX3Y.js} +2 -2
- package/dist/chunk-WGMEVX3Y.js.map +1 -0
- package/dist/chunk-ZDHWNQTR.js +552 -0
- package/dist/chunk-ZDHWNQTR.js.map +1 -0
- package/dist/components/data/index.d.ts +239 -15
- package/dist/components/data/index.js +522 -107
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/form-page.d.ts +1 -1
- package/dist/components/layout/form-page.js +1 -1
- package/dist/components/layout/index.d.ts +2 -2
- package/dist/components/layout/index.js +8 -8
- package/dist/components/patterns/index.d.ts +1 -1
- package/dist/components/patterns/index.js +5 -5
- package/dist/components/patterns/sortable-list.js +1 -1
- package/dist/components/rich-text/rich-text-editor.d.ts +194 -0
- package/dist/components/rich-text/rich-text-editor.js +833 -0
- package/dist/components/rich-text/rich-text-editor.js.map +1 -0
- package/dist/components/rich-text/rich-text.d.ts +51 -0
- package/dist/components/rich-text/rich-text.js +4 -0
- package/dist/components/rich-text/rich-text.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +12 -1
- package/dist/components/ui/alert-dialog.js +1 -1
- package/dist/hooks/index.d.ts +3 -3
- package/dist/hooks/index.js +4 -4
- package/dist/hooks/use-data-filters.d.ts +5 -3
- package/dist/hooks/use-data-filters.js +2 -2
- package/dist/hooks/use-resource-page.d.ts +2 -1
- package/dist/hooks/use-resource-page.js +3 -2
- package/dist/hooks/use-view-preference.d.ts +17 -2
- package/dist/hooks/use-view-preference.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +30 -30
- package/dist/{navigation-DBwcZ6vb.d.ts → navigation-Bha0R3Fu.d.ts} +6 -3
- package/dist/rich-text-doc-C54fME2C.d.ts +180 -0
- package/dist/router/react-router.d.ts +47 -0
- package/dist/router/react-router.js +53 -0
- package/dist/router/react-router.js.map +1 -0
- package/dist/router/tanstack-router.d.ts +47 -0
- package/dist/router/tanstack-router.js +105 -0
- package/dist/router/tanstack-router.js.map +1 -0
- package/dist/{use-resource-page-Dd2VUv30.d.ts → use-resource-page-D4byuwAs.d.ts} +20 -3
- package/package.json +61 -10
- package/dist/chunk-CDYZRZ6U.js.map +0 -1
- package/dist/chunk-FTTNGGLA.js +0 -20
- package/dist/chunk-FTTNGGLA.js.map +0 -1
- package/dist/chunk-IXGAEECT.js.map +0 -1
- package/dist/chunk-JA3ZKULS.js.map +0 -1
- package/dist/chunk-M62CHO3N.js.map +0 -1
- package/dist/chunk-TXOZMKNW.js.map +0 -1
- package/dist/chunk-ZPPLUGLA.js.map +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich text documents: the JSON shape, the allow-list that sanitizes it, and
|
|
3
|
+
* the serializers (HTML, plain text) built on that allow-list.
|
|
4
|
+
*
|
|
5
|
+
* This file has no React and no editor dependency, so a server can import it
|
|
6
|
+
* (through `@marktiderman/genesis-ui/rich-text`) to validate what a client
|
|
7
|
+
* posted and to produce the HTML it stores or sends. Everything here works in
|
|
8
|
+
* Node, edge runtimes and browsers alike: no DOM, no `DOMParser`.
|
|
9
|
+
*
|
|
10
|
+
* WHY JSON IS CANONICAL. The editor's document is a tree (ProseMirror). JSON
|
|
11
|
+
* is that tree written down; HTML is one rendering of it. Storing JSON keeps
|
|
12
|
+
* the structure the editor understands, so a later schema change is a data
|
|
13
|
+
* migration instead of an HTML scraping job. HTML is derived: produce it from
|
|
14
|
+
* the JSON with `richTextToHtml`, on the server, after `sanitizeRichTextDoc`.
|
|
15
|
+
*
|
|
16
|
+
* WHY AN ALLOW-LIST. Anything not named below is dropped: unknown node and
|
|
17
|
+
* mark types, unknown attributes, links whose URL is not http(s), mailto, tel
|
|
18
|
+
* or relative, and embeds whose URL is not a recognised YouTube, Vimeo or Loom
|
|
19
|
+
* video. The serializers only ever emit tags this file writes itself, and
|
|
20
|
+
* every text and attribute value goes through `escapeHtml`, so the output
|
|
21
|
+
* cannot carry a `<script>`, an event handler or a `javascript:` URL no matter
|
|
22
|
+
* what JSON came in.
|
|
23
|
+
*/
|
|
24
|
+
/** A mark on a text node. `link` carries its URL in `attrs.href`. */
|
|
25
|
+
interface RichTextMark {
|
|
26
|
+
type: string;
|
|
27
|
+
attrs?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One node of a rich text document. Structurally the same as Tiptap's
|
|
31
|
+
* `JSONContent`, so editor output can be passed straight in.
|
|
32
|
+
*/
|
|
33
|
+
interface RichTextNode {
|
|
34
|
+
type: string;
|
|
35
|
+
attrs?: Record<string, unknown>;
|
|
36
|
+
content?: RichTextNode[];
|
|
37
|
+
marks?: RichTextMark[];
|
|
38
|
+
text?: string;
|
|
39
|
+
}
|
|
40
|
+
/** The root node of a rich text document. */
|
|
41
|
+
interface RichTextDoc extends RichTextNode {
|
|
42
|
+
type: "doc";
|
|
43
|
+
content: RichTextNode[];
|
|
44
|
+
}
|
|
45
|
+
/** Heading levels a document may contain. `h1` is left to the page title. */
|
|
46
|
+
type RichTextHeadingLevel = 2 | 3 | 4;
|
|
47
|
+
/** Video hosts the `videoEmbed` node accepts. */
|
|
48
|
+
type VideoProvider = "youtube" | "vimeo" | "loom";
|
|
49
|
+
/** A video reference, reduced to what is needed to rebuild its player URL. */
|
|
50
|
+
interface VideoEmbedAttrs {
|
|
51
|
+
provider: VideoProvider;
|
|
52
|
+
/** The host's video id (YouTube 11 chars, Vimeo digits, Loom alphanumeric). */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Vimeo's privacy hash for unlisted videos (`vimeo.com/123/abcdef`). */
|
|
55
|
+
hash?: string;
|
|
56
|
+
/** Accessible name for the player frame. */
|
|
57
|
+
title?: string;
|
|
58
|
+
}
|
|
59
|
+
interface SanitizeRichTextOptions {
|
|
60
|
+
/** Heading levels to keep. Others move to the nearest kept level. Default `[2, 3, 4]`. */
|
|
61
|
+
headingLevels?: readonly RichTextHeadingLevel[];
|
|
62
|
+
/** Keep `videoEmbed` nodes. Default `true`. When false they are dropped. */
|
|
63
|
+
embeds?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Reject documents whose text is longer than this (characters, counted
|
|
66
|
+
* after sanitizing). Throws `RichTextLimitError`. Off by default; set it on
|
|
67
|
+
* the server for every write that accepts rich text from a client.
|
|
68
|
+
*/
|
|
69
|
+
maxCharacters?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Reject documents with more nodes than this (every node except the root,
|
|
72
|
+
* text included, counted after sanitizing). Throws `RichTextLimitError`.
|
|
73
|
+
* Off by default; set it on the server alongside `maxCharacters`.
|
|
74
|
+
*/
|
|
75
|
+
maxNodes?: number;
|
|
76
|
+
/**
|
|
77
|
+
* What to do with content nested deeper than 32 levels. `"drop"` (default)
|
|
78
|
+
* leaves it out, which suits rendering stored documents; `"throw"` rejects
|
|
79
|
+
* the whole document with `RichTextLimitError`, which suits server writes,
|
|
80
|
+
* so nothing an author wrote disappears without them being told.
|
|
81
|
+
*/
|
|
82
|
+
onTooDeep?: "drop" | "throw";
|
|
83
|
+
}
|
|
84
|
+
/** Thrown by `sanitizeRichTextDoc` when a document breaks a limit it was given. */
|
|
85
|
+
declare class RichTextLimitError extends Error {
|
|
86
|
+
readonly reason: "characters" | "nodes" | "depth";
|
|
87
|
+
readonly limit: number;
|
|
88
|
+
/** The measured size; for `"depth"`, the limit plus one. */
|
|
89
|
+
readonly actual: number;
|
|
90
|
+
constructor(reason: "characters" | "nodes" | "depth", limit: number, actual: number);
|
|
91
|
+
}
|
|
92
|
+
interface RichTextToHtmlOptions extends SanitizeRichTextOptions {
|
|
93
|
+
/**
|
|
94
|
+
* How video embeds are written. `"iframe"` (default) emits a player frame
|
|
95
|
+
* pointing at the host's embed URL; `"link"` emits a plain link to the
|
|
96
|
+
* video, for email and other places iframes do not run.
|
|
97
|
+
*/
|
|
98
|
+
embedAs?: "iframe" | "link";
|
|
99
|
+
}
|
|
100
|
+
/** An empty document: one empty paragraph, which is what the editor produces. */
|
|
101
|
+
declare function emptyRichTextDoc(): RichTextDoc;
|
|
102
|
+
/**
|
|
103
|
+
* True when `url` is safe to put in an `href`: an absolute http(s), mailto or
|
|
104
|
+
* tel URL, a root-relative path (`/lessons/1`) or a fragment (`#notes`).
|
|
105
|
+
*
|
|
106
|
+
* Rejects `javascript:`, `data:`, `vbscript:` and every other scheme, as well
|
|
107
|
+
* as protocol-relative `//host` URLs and anything containing whitespace or
|
|
108
|
+
* control characters (browsers strip those, which is how `java\tscript:`
|
|
109
|
+
* slips past naive checks).
|
|
110
|
+
*/
|
|
111
|
+
declare function isSafeUrl(url: unknown): url is string;
|
|
112
|
+
/**
|
|
113
|
+
* Turn what a person typed into the link field into a safe href, or `null`.
|
|
114
|
+
* `example.com/page` becomes `https://example.com/page`; an email address
|
|
115
|
+
* becomes a `mailto:` link.
|
|
116
|
+
*/
|
|
117
|
+
declare function normalizeLinkUrl(input: string): string | null;
|
|
118
|
+
/**
|
|
119
|
+
* Recognise a YouTube, Vimeo or Loom video URL (watch, share, short and embed
|
|
120
|
+
* forms) and reduce it to its provider and id. Returns `null` for anything
|
|
121
|
+
* else, including other hosts that merely look similar.
|
|
122
|
+
*/
|
|
123
|
+
declare function parseVideoUrl(input: string): VideoEmbedAttrs | null;
|
|
124
|
+
/** Validate untrusted embed attributes, returning a clean copy or `null`. */
|
|
125
|
+
declare function sanitizeVideoEmbedAttrs(attrs: unknown): VideoEmbedAttrs | null;
|
|
126
|
+
/** The player URL for an embed. Built from validated parts only. */
|
|
127
|
+
declare function videoEmbedSrc(attrs: VideoEmbedAttrs): string;
|
|
128
|
+
/** The page URL for an embed (what a viewer would open in a new tab). */
|
|
129
|
+
declare function videoWatchUrl(attrs: VideoEmbedAttrs): string;
|
|
130
|
+
/** Accessible name for an embed's frame: its title, or "YouTube video" etc. */
|
|
131
|
+
declare function videoEmbedTitle(attrs: VideoEmbedAttrs): string;
|
|
132
|
+
/**
|
|
133
|
+
* Attributes for an embed's `<iframe>`. Shared by the HTML serializer, the
|
|
134
|
+
* React renderer and the editor so all three emit the same frame.
|
|
135
|
+
*
|
|
136
|
+
* `sandbox` keeps the player away from top-level navigation and forms.
|
|
137
|
+
* `allow-same-origin` is safe here because the frame is always cross-origin
|
|
138
|
+
* (the player host), and the players need it for their own storage.
|
|
139
|
+
*/
|
|
140
|
+
declare const VIDEO_IFRAME_ATTRS: {
|
|
141
|
+
readonly allow: "accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture";
|
|
142
|
+
readonly loading: "lazy";
|
|
143
|
+
readonly referrerPolicy: "strict-origin-when-cross-origin";
|
|
144
|
+
readonly sandbox: "allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox";
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Validate an untrusted value as a rich text document and return a clean
|
|
148
|
+
* copy that contains only allow-listed nodes, marks and attributes.
|
|
149
|
+
*
|
|
150
|
+
* `null`, a string or a malformed object becomes an empty document. Run
|
|
151
|
+
* this on the server on every write, before storing the JSON or deriving
|
|
152
|
+
* HTML from it, with limits so a client cannot store an oversized or
|
|
153
|
+
* pathologically nested document:
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* sanitizeRichTextDoc(body, { maxCharacters: 50_000, maxNodes: 5_000, onTooDeep: "throw" });
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* Without limits it never throws; with them it throws `RichTextLimitError`.
|
|
160
|
+
*/
|
|
161
|
+
declare function sanitizeRichTextDoc(input: unknown, options?: SanitizeRichTextOptions): RichTextDoc;
|
|
162
|
+
/** True when `value` is shaped like a rich text document (`{ type: "doc" }`). */
|
|
163
|
+
declare function isRichTextDoc(value: unknown): value is RichTextDoc;
|
|
164
|
+
/** Escape text for use in HTML element content and quoted attributes. */
|
|
165
|
+
declare function escapeHtml(value: string): string;
|
|
166
|
+
/**
|
|
167
|
+
* Serialize a document to HTML. The input is sanitized first, so this is
|
|
168
|
+
* safe to call on untrusted JSON; the output contains only the tags listed
|
|
169
|
+
* in this file's header, with every value escaped.
|
|
170
|
+
*/
|
|
171
|
+
declare function richTextToHtml(doc: unknown, options?: RichTextToHtmlOptions): string;
|
|
172
|
+
/**
|
|
173
|
+
* Plain text for search indexes, previews and notifications. Blocks are
|
|
174
|
+
* separated by blank lines; embeds become "Title (url)".
|
|
175
|
+
*/
|
|
176
|
+
declare function richTextToPlainText(doc: unknown): string;
|
|
177
|
+
/** True when the document has no visible text and no embeds. */
|
|
178
|
+
declare function isRichTextEmpty(doc: unknown): boolean;
|
|
179
|
+
|
|
180
|
+
export { type RichTextDoc as R, type SanitizeRichTextOptions as S, VIDEO_IFRAME_ATTRS as V, type RichTextHeadingLevel as a, RichTextLimitError as b, type RichTextMark as c, type RichTextNode as d, type RichTextToHtmlOptions as e, type VideoEmbedAttrs as f, type VideoProvider as g, emptyRichTextDoc as h, escapeHtml as i, isRichTextDoc as j, isRichTextEmpty as k, isSafeUrl as l, richTextToPlainText as m, normalizeLinkUrl as n, sanitizeVideoEmbedAttrs as o, parseVideoUrl as p, videoEmbedTitle as q, richTextToHtml as r, sanitizeRichTextDoc as s, videoWatchUrl as t, videoEmbedSrc as v };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { L as LinkComponent, a as NavigateFn, S as SearchParamsAdapter } from '../navigation-Bha0R3Fu.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {@link LinkComponent} for React Router. Pass it as `linkComponent` to
|
|
6
|
+
* `AppShell`, `StatCard` and anything else that takes one. `href` maps to
|
|
7
|
+
* React Router's `to` (query and hash included), and `testID` is forwarded as
|
|
8
|
+
* `data-testid`, as `DefaultLink` does.
|
|
9
|
+
*
|
|
10
|
+
* Genesis decides the active state and passes `aria-current` itself. This is
|
|
11
|
+
* a plain `Link`, not a `NavLink`, so the router never adds a second opinion.
|
|
12
|
+
* It forwards `ref` and unknown props, so it works as the child of a Radix
|
|
13
|
+
* `asChild` (`<Button asChild>`).
|
|
14
|
+
*
|
|
15
|
+
* @stability Beta
|
|
16
|
+
*/
|
|
17
|
+
declare const ReactRouterLink: LinkComponent;
|
|
18
|
+
/**
|
|
19
|
+
* {@link NavigateFn} backed by React Router's `useNavigate()`. Pass it as
|
|
20
|
+
* `ResourcePage`'s `onNavigate` (or anywhere a `NavigateFn` is accepted) for
|
|
21
|
+
* client-side navigation instead of a full page load. Absolute URLs
|
|
22
|
+
* (`https://…`, `mailto:…`, `//host/…`) leave the app with a document
|
|
23
|
+
* navigation, as `defaultNavigate` does.
|
|
24
|
+
*
|
|
25
|
+
* @stability Beta
|
|
26
|
+
*/
|
|
27
|
+
declare function useReactRouterNavigate(): NavigateFn;
|
|
28
|
+
/**
|
|
29
|
+
* {@link SearchParamsAdapter} backed by React Router's location. Pass it as
|
|
30
|
+
* `useDataFilters(key, defs, { searchParams })` so URL-synced filters go
|
|
31
|
+
* through the router (loaders re-run, back/forward stays in sync) instead of
|
|
32
|
+
* the raw History API. Like the default browser adapter, an update keeps the
|
|
33
|
+
* current `#hash`.
|
|
34
|
+
*
|
|
35
|
+
* @stability Beta
|
|
36
|
+
*/
|
|
37
|
+
declare function useReactRouterSearchParams(): SearchParamsAdapter;
|
|
38
|
+
/**
|
|
39
|
+
* The current location as `pathname + search + hash`, the shape `AppShell`'s
|
|
40
|
+
* `activePath` expects, so nav items whose `to` names a query or hash
|
|
41
|
+
* (`/admin?tab=users`, `/settings#profile`) highlight correctly.
|
|
42
|
+
*
|
|
43
|
+
* @stability Beta
|
|
44
|
+
*/
|
|
45
|
+
declare function useReactRouterActivePath(): string;
|
|
46
|
+
|
|
47
|
+
export { ReactRouterLink, useReactRouterActivePath, useReactRouterNavigate, useReactRouterSearchParams };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { isExternalHref } from '../chunk-FUVWKTPL.js';
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
import { Link, useNavigate, useSearchParams, useLocation } from 'react-router';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
var ReactRouterLink = ({ href, testID, ...props }) => (
|
|
8
|
+
// `props` carries `ref` and anything a Radix `asChild` parent merges in;
|
|
9
|
+
// React Router's `Link` forwards both to the anchor. `testID` wins over a
|
|
10
|
+
// `data-testid` in `props` (a parent's may be `undefined`), but only when
|
|
11
|
+
// given, so a parent's real one survives.
|
|
12
|
+
/* @__PURE__ */ jsx(Link, { to: href, ...props, ...testID !== void 0 ? { "data-testid": testID } : null })
|
|
13
|
+
);
|
|
14
|
+
function useReactRouterNavigate() {
|
|
15
|
+
const navigate = useNavigate();
|
|
16
|
+
return useCallback(
|
|
17
|
+
(path) => {
|
|
18
|
+
if (isExternalHref(path)) {
|
|
19
|
+
if (typeof window !== "undefined") window.location.assign(path);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
void navigate(path);
|
|
23
|
+
},
|
|
24
|
+
[navigate]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function useReactRouterSearchParams() {
|
|
28
|
+
const [searchParams] = useSearchParams();
|
|
29
|
+
const { hash } = useLocation();
|
|
30
|
+
const navigate = useNavigate();
|
|
31
|
+
const setSearchParams = useCallback(
|
|
32
|
+
(next, options) => {
|
|
33
|
+
const qs = next.toString();
|
|
34
|
+
void navigate(
|
|
35
|
+
{ search: qs ? `?${qs}` : "", hash },
|
|
36
|
+
{ replace: options?.replace }
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
[navigate, hash]
|
|
40
|
+
);
|
|
41
|
+
return useMemo(
|
|
42
|
+
() => ({ searchParams, setSearchParams }),
|
|
43
|
+
[searchParams, setSearchParams]
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
function useReactRouterActivePath() {
|
|
47
|
+
const { pathname, search, hash } = useLocation();
|
|
48
|
+
return `${pathname}${search}${hash}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { ReactRouterLink, useReactRouterActivePath, useReactRouterNavigate, useReactRouterSearchParams };
|
|
52
|
+
//# sourceMappingURL=react-router.js.map
|
|
53
|
+
//# sourceMappingURL=react-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/router/react-router.tsx"],"names":[],"mappings":";;;;;AA2CO,IAAM,kBAAiC,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,GAAG,KAAA,EAAM;AAAA;AAAA;AAAA;AAAA;AAAA,kBAKtE,GAAA,CAAC,IAAA,EAAA,EAAK,EAAA,EAAI,IAAA,EAAO,GAAG,KAAA,EAAQ,GAAI,MAAA,KAAW,MAAA,GAAY,EAAE,aAAA,EAAe,MAAA,KAAW,IAAA,EAAO;AAAA;AAYrF,SAAS,sBAAA,GAAqC;AACnD,EAAA,MAAM,WAAW,WAAA,EAAY;AAC7B,EAAA,OAAO,WAAA;AAAA,IACL,CAAC,IAAA,KAAS;AACR,MAAA,IAAI,cAAA,CAAe,IAAI,CAAA,EAAG;AACxB,QAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,QAAA,CAAS,OAAO,IAAI,CAAA;AAC9D,QAAA;AAAA,MACF;AACA,MAAA,KAAK,SAAS,IAAI,CAAA;AAAA,IACpB,CAAA;AAAA,IACA,CAAC,QAAQ;AAAA,GACX;AACF;AAWO,SAAS,0BAAA,GAAkD;AAChE,EAAA,MAAM,CAAC,YAAY,CAAA,GAAI,eAAA,EAAgB;AACvC,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,WAAA,EAAY;AAC7B,EAAA,MAAM,WAAW,WAAA,EAAY;AAE7B,EAAA,MAAM,eAAA,GAAkB,WAAA;AAAA,IACtB,CAAC,MAAM,OAAA,KAAY;AACjB,MAAA,MAAM,EAAA,GAAK,KAAK,QAAA,EAAS;AACzB,MAAA,KAAK,QAAA;AAAA,QACH,EAAE,MAAA,EAAQ,EAAA,GAAK,IAAI,EAAE,CAAA,CAAA,GAAK,IAAI,IAAA,EAAK;AAAA,QACnC,EAAE,OAAA,EAAS,OAAA,EAAS,OAAA;AAAQ,OAC9B;AAAA,IACF,CAAA;AAAA,IACA,CAAC,UAAU,IAAI;AAAA,GACjB;AAEA,EAAA,OAAO,OAAA;AAAA,IACL,OAAO,EAAE,YAAA,EAAc,eAAA,EAAgB,CAAA;AAAA,IACvC,CAAC,cAAc,eAAe;AAAA,GAChC;AACF;AASO,SAAS,wBAAA,GAAmC;AACjD,EAAA,MAAM,EAAE,QAAA,EAAU,MAAA,EAAQ,IAAA,KAAS,WAAA,EAAY;AAC/C,EAAA,OAAO,CAAA,EAAG,QAAQ,CAAA,EAAG,MAAM,GAAG,IAAI,CAAA,CAAA;AACpC","file":"react-router.js","sourcesContent":["\"use client\";\n\n/**\n * React Router (v7, library or framework mode) adapters for genesis-ui's\n * router-agnostic navigation contract.\n *\n * Import from `@marktiderman/genesis-ui/router/react-router`. This is the only\n * genesis-ui entry that imports `react-router`, and `react-router` is an\n * OPTIONAL peer: apps on another router never install it, and apps that do\n * not import this subpath never load it.\n *\n * ```tsx\n * import { AppShell } from \"@marktiderman/genesis-ui/layout\";\n * import {\n * ReactRouterLink,\n * useReactRouterActivePath,\n * } from \"@marktiderman/genesis-ui/router/react-router\";\n *\n * <AppShell linkComponent={ReactRouterLink} activePath={useReactRouterActivePath()} … />\n * ```\n */\nimport { useCallback, useMemo } from \"react\";\nimport { Link, useLocation, useNavigate, useSearchParams } from \"react-router\";\nimport type {\n LinkComponent,\n NavigateFn,\n SearchParamsAdapter,\n} from \"../navigation\";\nimport { isExternalHref } from \"./utils\";\n\n/**\n * {@link LinkComponent} for React Router. Pass it as `linkComponent` to\n * `AppShell`, `StatCard` and anything else that takes one. `href` maps to\n * React Router's `to` (query and hash included), and `testID` is forwarded as\n * `data-testid`, as `DefaultLink` does.\n *\n * Genesis decides the active state and passes `aria-current` itself. This is\n * a plain `Link`, not a `NavLink`, so the router never adds a second opinion.\n * It forwards `ref` and unknown props, so it works as the child of a Radix\n * `asChild` (`<Button asChild>`).\n *\n * @stability Beta\n */\nexport const ReactRouterLink: LinkComponent = ({ href, testID, ...props }) => (\n // `props` carries `ref` and anything a Radix `asChild` parent merges in;\n // React Router's `Link` forwards both to the anchor. `testID` wins over a\n // `data-testid` in `props` (a parent's may be `undefined`), but only when\n // given, so a parent's real one survives.\n <Link to={href} {...props} {...(testID !== undefined ? { \"data-testid\": testID } : null)} />\n);\n\n/**\n * {@link NavigateFn} backed by React Router's `useNavigate()`. Pass it as\n * `ResourcePage`'s `onNavigate` (or anywhere a `NavigateFn` is accepted) for\n * client-side navigation instead of a full page load. Absolute URLs\n * (`https://…`, `mailto:…`, `//host/…`) leave the app with a document\n * navigation, as `defaultNavigate` does.\n *\n * @stability Beta\n */\nexport function useReactRouterNavigate(): NavigateFn {\n const navigate = useNavigate();\n return useCallback<NavigateFn>(\n (path) => {\n if (isExternalHref(path)) {\n if (typeof window !== \"undefined\") window.location.assign(path);\n return;\n }\n void navigate(path);\n },\n [navigate],\n );\n}\n\n/**\n * {@link SearchParamsAdapter} backed by React Router's location. Pass it as\n * `useDataFilters(key, defs, { searchParams })` so URL-synced filters go\n * through the router (loaders re-run, back/forward stays in sync) instead of\n * the raw History API. Like the default browser adapter, an update keeps the\n * current `#hash`.\n *\n * @stability Beta\n */\nexport function useReactRouterSearchParams(): SearchParamsAdapter {\n const [searchParams] = useSearchParams();\n const { hash } = useLocation();\n const navigate = useNavigate();\n\n const setSearchParams = useCallback<SearchParamsAdapter[\"setSearchParams\"]>(\n (next, options) => {\n const qs = next.toString();\n void navigate(\n { search: qs ? `?${qs}` : \"\", hash },\n { replace: options?.replace },\n );\n },\n [navigate, hash],\n );\n\n return useMemo(\n () => ({ searchParams, setSearchParams }),\n [searchParams, setSearchParams],\n );\n}\n\n/**\n * The current location as `pathname + search + hash`, the shape `AppShell`'s\n * `activePath` expects, so nav items whose `to` names a query or hash\n * (`/admin?tab=users`, `/settings#profile`) highlight correctly.\n *\n * @stability Beta\n */\nexport function useReactRouterActivePath(): string {\n const { pathname, search, hash } = useLocation();\n return `${pathname}${search}${hash}`;\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { L as LinkComponent, a as NavigateFn, S as SearchParamsAdapter } from '../navigation-Bha0R3Fu.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {@link LinkComponent} for TanStack Router. Pass it as `linkComponent` to
|
|
6
|
+
* `AppShell`, `StatCard` and anything else that takes one. In-app hrefs
|
|
7
|
+
* navigate client-side (query and hash included, parsed by the router's
|
|
8
|
+
* `parseSearch`); absolute URLs render a plain anchor. `testID` is forwarded
|
|
9
|
+
* as `data-testid`, as `DefaultLink` does, and the active state stays the one
|
|
10
|
+
* Genesis passes: TanStack's own `aria-current` / `active` class are not
|
|
11
|
+
* applied. It forwards `ref` and unknown props, so it works as the child of a
|
|
12
|
+
* Radix `asChild` (`<Button asChild>`).
|
|
13
|
+
*
|
|
14
|
+
* @stability Beta
|
|
15
|
+
*/
|
|
16
|
+
declare const TanStackRouterLink: LinkComponent;
|
|
17
|
+
/**
|
|
18
|
+
* {@link NavigateFn} backed by the TanStack router. Pass it as
|
|
19
|
+
* `ResourcePage`'s `onNavigate` (or anywhere a `NavigateFn` is accepted) for
|
|
20
|
+
* client-side navigation instead of a full page load. Absolute URLs
|
|
21
|
+
* (`https://…`, `mailto:…`, `//host/…`) leave the app with a document
|
|
22
|
+
* navigation, as `defaultNavigate` does.
|
|
23
|
+
*
|
|
24
|
+
* @stability Beta
|
|
25
|
+
*/
|
|
26
|
+
declare function useTanStackRouterNavigate(): NavigateFn;
|
|
27
|
+
/**
|
|
28
|
+
* {@link SearchParamsAdapter} backed by the TanStack router's location. Pass
|
|
29
|
+
* it as `useDataFilters(key, defs, { searchParams })` so URL-synced filters go
|
|
30
|
+
* through the router (loaders and `validateSearch` run, back/forward stays in
|
|
31
|
+
* sync) instead of the raw History API. Like the default browser adapter, an
|
|
32
|
+
* update keeps the current path and `#hash`.
|
|
33
|
+
*
|
|
34
|
+
* @stability Beta
|
|
35
|
+
*/
|
|
36
|
+
declare function useTanStackRouterSearchParams(): SearchParamsAdapter;
|
|
37
|
+
/**
|
|
38
|
+
* The current location as `pathname + search + hash`, the shape `AppShell`'s
|
|
39
|
+
* `activePath` expects, so nav items whose `to` names a query or hash
|
|
40
|
+
* (`/admin?tab=users`, `/settings#profile`) highlight correctly. Paths are the
|
|
41
|
+
* router's (basepath removed), matching the `to` your nav items use.
|
|
42
|
+
*
|
|
43
|
+
* @stability Beta
|
|
44
|
+
*/
|
|
45
|
+
declare function useTanStackRouterActivePath(): string;
|
|
46
|
+
|
|
47
|
+
export { TanStackRouterLink, useTanStackRouterActivePath, useTanStackRouterNavigate, useTanStackRouterSearchParams };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { isExternalHref, splitNavigationHref } from '../chunk-FUVWKTPL.js';
|
|
3
|
+
import { DefaultLink } from '../chunk-WGMEVX3Y.js';
|
|
4
|
+
import { useCallback, useMemo } from 'react';
|
|
5
|
+
import { useRouter, useRouterState, useLinkProps } from '@tanstack/react-router';
|
|
6
|
+
import { jsx } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var NO_ACTIVE_PROPS = {};
|
|
9
|
+
function toLocation(router, href) {
|
|
10
|
+
const { pathname, search, hash } = splitNavigationHref(href);
|
|
11
|
+
return {
|
|
12
|
+
// `""` (a bare `?q=1` or `#x`) means "this page" — TanStack's `.`.
|
|
13
|
+
to: pathname || ".",
|
|
14
|
+
search: search ? router.options.parseSearch(`?${search}`) : void 0,
|
|
15
|
+
hash: hash || void 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function InAppLink({
|
|
19
|
+
href,
|
|
20
|
+
testID,
|
|
21
|
+
children,
|
|
22
|
+
"aria-current": ariaCurrent,
|
|
23
|
+
ref,
|
|
24
|
+
...rest
|
|
25
|
+
}) {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
const target = useMemo(() => toLocation(router, href), [router, href]);
|
|
28
|
+
const linkProps = useLinkProps(
|
|
29
|
+
{
|
|
30
|
+
...target,
|
|
31
|
+
...rest,
|
|
32
|
+
activeProps: NO_ACTIVE_PROPS
|
|
33
|
+
// The adapter is route-tree agnostic, so it cannot satisfy the generated
|
|
34
|
+
// route types; the target is checked at runtime by the router instead.
|
|
35
|
+
},
|
|
36
|
+
ref
|
|
37
|
+
);
|
|
38
|
+
const { "data-status": _status, ...anchorProps } = linkProps;
|
|
39
|
+
return /* @__PURE__ */ jsx(
|
|
40
|
+
"a",
|
|
41
|
+
{
|
|
42
|
+
...anchorProps,
|
|
43
|
+
"aria-current": ariaCurrent,
|
|
44
|
+
...testID !== void 0 ? { "data-testid": testID } : null,
|
|
45
|
+
children
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
var TanStackRouterLink = (props) => isExternalHref(props.href) ? /* @__PURE__ */ jsx(DefaultLink, { ...props }) : /* @__PURE__ */ jsx(InAppLink, { ...props });
|
|
50
|
+
function useTanStackRouterNavigate() {
|
|
51
|
+
const router = useRouter();
|
|
52
|
+
return useCallback(
|
|
53
|
+
(path) => {
|
|
54
|
+
if (isExternalHref(path)) {
|
|
55
|
+
if (typeof window !== "undefined") window.location.assign(path);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
void router.navigate(
|
|
59
|
+
toLocation(router, path)
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
[router]
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function useTanStackRouterSearchParams() {
|
|
66
|
+
const router = useRouter();
|
|
67
|
+
const searchStr = useRouterState({
|
|
68
|
+
select: (state) => state.location.searchStr
|
|
69
|
+
});
|
|
70
|
+
const searchParams = useMemo(
|
|
71
|
+
() => new URLSearchParams(searchStr),
|
|
72
|
+
[searchStr]
|
|
73
|
+
);
|
|
74
|
+
const setSearchParams = useCallback(
|
|
75
|
+
(next, options) => {
|
|
76
|
+
const qs = next.toString();
|
|
77
|
+
void router.navigate({
|
|
78
|
+
// The current URL path, not `.`: `.` resolves against the matched
|
|
79
|
+
// route's template, which is not this URL under a splat or
|
|
80
|
+
// not-found route.
|
|
81
|
+
to: router.state.location.pathname,
|
|
82
|
+
search: router.options.parseSearch(qs ? `?${qs}` : ""),
|
|
83
|
+
hash: true,
|
|
84
|
+
replace: options?.replace
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
[router]
|
|
88
|
+
);
|
|
89
|
+
return useMemo(
|
|
90
|
+
() => ({ searchParams, setSearchParams }),
|
|
91
|
+
[searchParams, setSearchParams]
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function useTanStackRouterActivePath() {
|
|
95
|
+
return useRouterState({
|
|
96
|
+
select: ({ location }) => {
|
|
97
|
+
const search = location.searchStr && !location.searchStr.startsWith("?") ? `?${location.searchStr}` : location.searchStr;
|
|
98
|
+
return `${location.pathname}${search}${location.hash ? `#${location.hash}` : ""}`;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { TanStackRouterLink, useTanStackRouterActivePath, useTanStackRouterNavigate, useTanStackRouterSearchParams };
|
|
104
|
+
//# sourceMappingURL=tanstack-router.js.map
|
|
105
|
+
//# sourceMappingURL=tanstack-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/router/tanstack-router.tsx"],"names":[],"mappings":";;;;;;AAkDA,IAAM,kBAAkB,EAAC;AAMzB,SAAS,UAAA,CAAW,QAAmB,IAAA,EAAc;AACnD,EAAA,MAAM,EAAE,QAAA,EAAU,MAAA,EAAQ,IAAA,EAAK,GAAI,oBAAoB,IAAI,CAAA;AAC3D,EAAA,OAAO;AAAA;AAAA,IAEL,IAAI,QAAA,IAAY,GAAA;AAAA,IAChB,MAAA,EAAQ,SAAS,MAAA,CAAO,OAAA,CAAQ,YAAY,CAAA,CAAA,EAAI,MAAM,EAAE,CAAA,GAAI,MAAA;AAAA,IAC5D,MAAM,IAAA,IAAQ;AAAA,GAChB;AACF;AAEA,SAAS,SAAA,CAAU;AAAA,EACjB,IAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,cAAA,EAAgB,WAAA;AAAA,EAChB,GAAA;AAAA,EACA,GAAG;AACL,CAAA,EAAwD;AACtD,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAM,UAAA,CAAW,MAAA,EAAQ,IAAI,CAAA,EAAG,CAAC,MAAA,EAAQ,IAAI,CAAC,CAAA;AAMrE,EAAA,MAAM,SAAA,GAAY,YAAA;AAAA,IAChB;AAAA,MACE,GAAG,MAAA;AAAA,MACH,GAAG,IAAA;AAAA,MACH,WAAA,EAAa;AAAA;AAAA;AAAA,KAGf;AAAA,IACA;AAAA,GACF;AAIA,EAAA,MAAM,EAAE,aAAA,EAAe,OAAA,EAAS,GAAG,aAAY,GAAI,SAAA;AACnD,EAAA,uBACE,GAAA;AAAA,IAAC,GAAA;AAAA,IAAA;AAAA,MACE,GAAG,WAAA;AAAA,MACJ,cAAA,EAAc,WAAA;AAAA,MAEb,GAAI,MAAA,KAAW,MAAA,GAAY,EAAE,aAAA,EAAe,QAAO,GAAI,IAAA;AAAA,MAEvD;AAAA;AAAA,GACH;AAEJ;AAcO,IAAM,kBAAA,GAAoC,CAAC,KAAA,KAChD,cAAA,CAAe,MAAM,IAAI,CAAA,mBAAI,GAAA,CAAC,WAAA,EAAA,EAAa,GAAG,KAAA,EAAO,CAAA,mBAAK,GAAA,CAAC,SAAA,EAAA,EAAW,GAAG,KAAA,EAAO;AAW3E,SAAS,yBAAA,GAAwC;AACtD,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,OAAO,WAAA;AAAA,IACL,CAAC,IAAA,KAAS;AACR,MAAA,IAAI,cAAA,CAAe,IAAI,CAAA,EAAG;AACxB,QAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,QAAA,CAAS,OAAO,IAAI,CAAA;AAC9D,QAAA;AAAA,MACF;AACA,MAAA,KAAK,MAAA,CAAO,QAAA;AAAA,QACV,UAAA,CAAW,QAAQ,IAAI;AAAA,OACzB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AACF;AAWO,SAAS,6BAAA,GAAqD;AACnE,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,MAAM,YAAY,cAAA,CAAe;AAAA,IAC/B,MAAA,EAAQ,CAAC,KAAA,KAAU,KAAA,CAAM,QAAA,CAAS;AAAA,GACnC,CAAA;AACD,EAAA,MAAM,YAAA,GAAe,OAAA;AAAA,IACnB,MAAM,IAAI,eAAA,CAAgB,SAAS,CAAA;AAAA,IACnC,CAAC,SAAS;AAAA,GACZ;AAEA,EAAA,MAAM,eAAA,GAAkB,WAAA;AAAA,IACtB,CAAC,MAAM,OAAA,KAAY;AACjB,MAAA,MAAM,EAAA,GAAK,KAAK,QAAA,EAAS;AACzB,MAAA,KAAK,OAAO,QAAA,CAAS;AAAA;AAAA;AAAA;AAAA,QAInB,EAAA,EAAI,MAAA,CAAO,KAAA,CAAM,QAAA,CAAS,QAAA;AAAA,QAC1B,MAAA,EAAQ,OAAO,OAAA,CAAQ,WAAA,CAAY,KAAK,CAAA,CAAA,EAAI,EAAE,KAAK,EAAE,CAAA;AAAA,QACrD,IAAA,EAAM,IAAA;AAAA,QACN,SAAS,OAAA,EAAS;AAAA,OACqB,CAAA;AAAA,IAC3C,CAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AAEA,EAAA,OAAO,OAAA;AAAA,IACL,OAAO,EAAE,YAAA,EAAc,eAAA,EAAgB,CAAA;AAAA,IACvC,CAAC,cAAc,eAAe;AAAA,GAChC;AACF;AAUO,SAAS,2BAAA,GAAsC;AACpD,EAAA,OAAO,cAAA,CAAe;AAAA,IACpB,MAAA,EAAQ,CAAC,EAAE,QAAA,EAAS,KAAM;AACxB,MAAA,MAAM,MAAA,GACJ,QAAA,CAAS,SAAA,IAAa,CAAC,QAAA,CAAS,SAAA,CAAU,UAAA,CAAW,GAAG,CAAA,GACpD,CAAA,CAAA,EAAI,QAAA,CAAS,SAAS,KACtB,QAAA,CAAS,SAAA;AACf,MAAA,OAAO,CAAA,EAAG,QAAA,CAAS,QAAQ,CAAA,EAAG,MAAM,CAAA,EAAG,QAAA,CAAS,IAAA,GAAO,CAAA,CAAA,EAAI,QAAA,CAAS,IAAI,CAAA,CAAA,GAAK,EAAE,CAAA,CAAA;AAAA,IACjF;AAAA,GACD,CAAA;AACH","file":"tanstack-router.js","sourcesContent":["\"use client\";\n\n/**\n * TanStack Router adapters for genesis-ui's router-agnostic navigation\n * contract.\n *\n * Import from `@marktiderman/genesis-ui/router/tanstack-router`. This is the\n * only genesis-ui entry that imports `@tanstack/react-router`, and that\n * package is an OPTIONAL peer: apps on another router never install it, and\n * apps that do not import this subpath never load it.\n *\n * Genesis hands links a plain string `href` (`/admin?tab=users#top`), while\n * TanStack Router wants `to`, `search` and `hash` separately. These adapters\n * do that split, and they parse the query with the router's OWN\n * `parseSearch`, so a custom search serializer and `validateSearch` see the\n * same values a typed `<Link search={…}>` would produce.\n *\n * ```tsx\n * import { AppShell } from \"@marktiderman/genesis-ui/layout\";\n * import {\n * TanStackRouterLink,\n * useTanStackRouterActivePath,\n * } from \"@marktiderman/genesis-ui/router/tanstack-router\";\n *\n * <AppShell linkComponent={TanStackRouterLink} activePath={useTanStackRouterActivePath()} … />\n * ```\n */\nimport { useCallback, useMemo, type AnchorHTMLAttributes, type Ref } from \"react\";\nimport {\n useLinkProps,\n useRouter,\n useRouterState,\n type AnyRouter,\n} from \"@tanstack/react-router\";\nimport {\n DefaultLink,\n type GenesisLinkProps,\n type LinkComponent,\n type NavigateFn,\n type SearchParamsAdapter,\n} from \"../navigation\";\nimport { isExternalHref, splitNavigationHref } from \"./utils\";\n\n// Genesis owns the active state (it passes `aria-current` itself, computed by\n// `isPathActive` or the app's `isActive`). TanStack's Link adds its own —\n// `aria-current=\"page\"`, `data-status=\"active\"` and an `active` class —\n// whenever ITS prefix match passes, which can disagree with Genesis (an\n// exact-match `/app` item, or a `?tab=` item) and would leave two items\n// announced as the current page. An empty `activeProps` stops the class;\n// the attributes are overridden below.\nconst NO_ACTIVE_PROPS = {};\n\n/**\n * The navigation target for a Genesis href, with the query parsed by the\n * router's own `parseSearch` (what `<Link search>` expects).\n */\nfunction toLocation(router: AnyRouter, href: string) {\n const { pathname, search, hash } = splitNavigationHref(href);\n return {\n // `\"\"` (a bare `?q=1` or `#x`) means \"this page\" — TanStack's `.`.\n to: pathname || \".\",\n search: search ? router.options.parseSearch(`?${search}`) : undefined,\n hash: hash || undefined,\n };\n}\n\nfunction InAppLink({\n href,\n testID,\n children,\n \"aria-current\": ariaCurrent,\n ref,\n ...rest\n}: GenesisLinkProps & { ref?: Ref<HTMLAnchorElement> }) {\n const router = useRouter();\n const target = useMemo(() => toLocation(router, href), [router, href]);\n // `useLinkProps` is the hook behind TanStack's `<Link>`: client-side\n // navigation on click, `href` built through the router (basepath, rewrites,\n // masks), preloading per the router's `defaultPreload`. Props it does not\n // know (anything a Radix `asChild` parent merges in) pass through to the\n // anchor, and `ref` is merged with the one it needs for preloading.\n const linkProps = useLinkProps(\n {\n ...target,\n ...rest,\n activeProps: NO_ACTIVE_PROPS,\n // The adapter is route-tree agnostic, so it cannot satisfy the generated\n // route types; the target is checked at runtime by the router instead.\n } as Parameters<typeof useLinkProps>[0],\n ref as Parameters<typeof useLinkProps>[1],\n ) as AnchorHTMLAttributes<HTMLAnchorElement> & {\n \"data-status\"?: string;\n ref?: Ref<HTMLAnchorElement>;\n };\n const { \"data-status\": _status, ...anchorProps } = linkProps;\n return (\n <a\n {...anchorProps}\n aria-current={ariaCurrent}\n // Only when given, so a `data-testid` merged in by an `asChild` parent survives.\n {...(testID !== undefined ? { \"data-testid\": testID } : null)}\n >\n {children}\n </a>\n );\n}\n\n/**\n * {@link LinkComponent} for TanStack Router. Pass it as `linkComponent` to\n * `AppShell`, `StatCard` and anything else that takes one. In-app hrefs\n * navigate client-side (query and hash included, parsed by the router's\n * `parseSearch`); absolute URLs render a plain anchor. `testID` is forwarded\n * as `data-testid`, as `DefaultLink` does, and the active state stays the one\n * Genesis passes: TanStack's own `aria-current` / `active` class are not\n * applied. It forwards `ref` and unknown props, so it works as the child of a\n * Radix `asChild` (`<Button asChild>`).\n *\n * @stability Beta\n */\nexport const TanStackRouterLink: LinkComponent = (props) =>\n isExternalHref(props.href) ? <DefaultLink {...props} /> : <InAppLink {...props} />;\n\n/**\n * {@link NavigateFn} backed by the TanStack router. Pass it as\n * `ResourcePage`'s `onNavigate` (or anywhere a `NavigateFn` is accepted) for\n * client-side navigation instead of a full page load. Absolute URLs\n * (`https://…`, `mailto:…`, `//host/…`) leave the app with a document\n * navigation, as `defaultNavigate` does.\n *\n * @stability Beta\n */\nexport function useTanStackRouterNavigate(): NavigateFn {\n const router = useRouter();\n return useCallback<NavigateFn>(\n (path) => {\n if (isExternalHref(path)) {\n if (typeof window !== \"undefined\") window.location.assign(path);\n return;\n }\n void router.navigate(\n toLocation(router, path) as Parameters<AnyRouter[\"navigate\"]>[0],\n );\n },\n [router],\n );\n}\n\n/**\n * {@link SearchParamsAdapter} backed by the TanStack router's location. Pass\n * it as `useDataFilters(key, defs, { searchParams })` so URL-synced filters go\n * through the router (loaders and `validateSearch` run, back/forward stays in\n * sync) instead of the raw History API. Like the default browser adapter, an\n * update keeps the current path and `#hash`.\n *\n * @stability Beta\n */\nexport function useTanStackRouterSearchParams(): SearchParamsAdapter {\n const router = useRouter();\n const searchStr = useRouterState({\n select: (state) => state.location.searchStr,\n });\n const searchParams = useMemo(\n () => new URLSearchParams(searchStr),\n [searchStr],\n );\n\n const setSearchParams = useCallback<SearchParamsAdapter[\"setSearchParams\"]>(\n (next, options) => {\n const qs = next.toString();\n void router.navigate({\n // The current URL path, not `.`: `.` resolves against the matched\n // route's template, which is not this URL under a splat or\n // not-found route.\n to: router.state.location.pathname,\n search: router.options.parseSearch(qs ? `?${qs}` : \"\"),\n hash: true,\n replace: options?.replace,\n } as Parameters<AnyRouter[\"navigate\"]>[0]);\n },\n [router],\n );\n\n return useMemo(\n () => ({ searchParams, setSearchParams }),\n [searchParams, setSearchParams],\n );\n}\n\n/**\n * The current location as `pathname + search + hash`, the shape `AppShell`'s\n * `activePath` expects, so nav items whose `to` names a query or hash\n * (`/admin?tab=users`, `/settings#profile`) highlight correctly. Paths are the\n * router's (basepath removed), matching the `to` your nav items use.\n *\n * @stability Beta\n */\nexport function useTanStackRouterActivePath(): string {\n return useRouterState({\n select: ({ location }) => {\n const search =\n location.searchStr && !location.searchStr.startsWith(\"?\")\n ? `?${location.searchStr}`\n : location.searchStr;\n return `${location.pathname}${search}${location.hash ? `#${location.hash}` : \"\"}`;\n },\n });\n}\n"]}
|
|
@@ -3,11 +3,18 @@ import { ReactNode } from 'react';
|
|
|
3
3
|
import { Density } from './hooks/use-view-settings.js';
|
|
4
4
|
import { V as ViewMode } from './view-toggle-CahQ87PW.js';
|
|
5
5
|
import { Identifier, FilterParam, SortParam } from '@marktiderman/genesis-core/provider';
|
|
6
|
+
import { ViewModeDefault } from './hooks/use-view-preference.js';
|
|
6
7
|
|
|
7
8
|
interface DataTableColumn<T> {
|
|
8
9
|
key: string;
|
|
9
10
|
header: string;
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Starting width. A number is pixels: the column gets exactly that width
|
|
13
|
+
* (until the user resizes it) and the rest share what is left. A string is
|
|
14
|
+
* a class name for the cell, as before (`"w-48"`); a bare px string
|
|
15
|
+
* (`"180"`, `"180px"`) also sets the width in pixels.
|
|
16
|
+
*/
|
|
17
|
+
width?: string | number;
|
|
11
18
|
hideBelow?: "sm" | "md" | "lg" | "xl";
|
|
12
19
|
sortable?: boolean;
|
|
13
20
|
render?: (item: T, value: unknown) => ReactNode;
|
|
@@ -88,6 +95,12 @@ interface ResourceColumnDef<T> {
|
|
|
88
95
|
header?: string;
|
|
89
96
|
sortable?: boolean;
|
|
90
97
|
hideBelow?: "sm" | "md" | "lg" | "xl";
|
|
98
|
+
/** Starting width, passed straight through to `DataTable` (a number is px). */
|
|
99
|
+
width?: DataTableColumn<T>["width"];
|
|
100
|
+
/** Smallest width the user can resize to, in px. Passed through to `DataTable`. */
|
|
101
|
+
minWidth?: number;
|
|
102
|
+
/** Whether the column shows before the user picks columns. Default true. */
|
|
103
|
+
defaultVisible?: boolean;
|
|
91
104
|
render?: (item: T, value: unknown, actions: ResourceActions<T>) => ReactNode;
|
|
92
105
|
/** In-place cell editing, passed straight through to `DataTable`. */
|
|
93
106
|
editable?: DataTableColumn<T>["editable"];
|
|
@@ -117,8 +130,12 @@ interface UseResourcePageProps<T> {
|
|
|
117
130
|
initialFilters?: FilterParam[];
|
|
118
131
|
/** Callback when filters change. */
|
|
119
132
|
onFiltersChange?: (filters: FilterParam[]) => void;
|
|
120
|
-
/**
|
|
121
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Default view mode, used until the user picks one. Pass
|
|
135
|
+
* `{ mobile, desktop }` for a viewport-dependent default (cards on phones,
|
|
136
|
+
* a table on desktops); only an explicit pick is persisted.
|
|
137
|
+
*/
|
|
138
|
+
defaultView?: ViewModeDefault;
|
|
122
139
|
/** Detail interaction mode. */
|
|
123
140
|
detail?: "panel" | "modal" | "route" | "none";
|
|
124
141
|
/** Field used for the detail title. Defaults to first column key. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marktiderman/genesis-ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -62,6 +62,26 @@
|
|
|
62
62
|
"types": "./dist/provider/index.d.ts",
|
|
63
63
|
"import": "./dist/provider/index.js"
|
|
64
64
|
},
|
|
65
|
+
"./sheet-grid": {
|
|
66
|
+
"types": "./dist/components/sheet-grid/sheet-grid.d.ts",
|
|
67
|
+
"import": "./dist/components/sheet-grid/sheet-grid.js"
|
|
68
|
+
},
|
|
69
|
+
"./rich-text": {
|
|
70
|
+
"types": "./dist/components/rich-text/rich-text.d.ts",
|
|
71
|
+
"import": "./dist/components/rich-text/rich-text.js"
|
|
72
|
+
},
|
|
73
|
+
"./rich-text-editor": {
|
|
74
|
+
"types": "./dist/components/rich-text/rich-text-editor.d.ts",
|
|
75
|
+
"import": "./dist/components/rich-text/rich-text-editor.js"
|
|
76
|
+
},
|
|
77
|
+
"./router/react-router": {
|
|
78
|
+
"types": "./dist/router/react-router.d.ts",
|
|
79
|
+
"import": "./dist/router/react-router.js"
|
|
80
|
+
},
|
|
81
|
+
"./router/tanstack-router": {
|
|
82
|
+
"types": "./dist/router/tanstack-router.d.ts",
|
|
83
|
+
"import": "./dist/router/tanstack-router.js"
|
|
84
|
+
},
|
|
65
85
|
"./styles.css": "./dist/styles.css",
|
|
66
86
|
"./accessible-icon": {
|
|
67
87
|
"types": "./dist/components/ui/accessible-icon.d.ts",
|
|
@@ -255,18 +275,10 @@
|
|
|
255
275
|
"types": "./dist/components/patterns/settings-row.d.ts",
|
|
256
276
|
"import": "./dist/components/patterns/settings-row.js"
|
|
257
277
|
},
|
|
258
|
-
"./sortable-list": {
|
|
259
|
-
"types": "./dist/components/patterns/sortable-list.d.ts",
|
|
260
|
-
"import": "./dist/components/patterns/sortable-list.js"
|
|
261
|
-
},
|
|
262
278
|
"./sheet": {
|
|
263
279
|
"types": "./dist/components/ui/sheet.d.ts",
|
|
264
280
|
"import": "./dist/components/ui/sheet.js"
|
|
265
281
|
},
|
|
266
|
-
"./sheet-grid": {
|
|
267
|
-
"types": "./dist/components/sheet-grid/sheet-grid.d.ts",
|
|
268
|
-
"import": "./dist/components/sheet-grid/sheet-grid.js"
|
|
269
|
-
},
|
|
270
282
|
"./skeleton": {
|
|
271
283
|
"types": "./dist/components/ui/skeleton.d.ts",
|
|
272
284
|
"import": "./dist/components/ui/skeleton.js"
|
|
@@ -279,6 +291,10 @@
|
|
|
279
291
|
"types": "./dist/components/ui/sonner.d.ts",
|
|
280
292
|
"import": "./dist/components/ui/sonner.js"
|
|
281
293
|
},
|
|
294
|
+
"./sortable-list": {
|
|
295
|
+
"types": "./dist/components/patterns/sortable-list.d.ts",
|
|
296
|
+
"import": "./dist/components/patterns/sortable-list.js"
|
|
297
|
+
},
|
|
282
298
|
"./spinner": {
|
|
283
299
|
"types": "./dist/components/ui/spinner.d.ts",
|
|
284
300
|
"import": "./dist/components/ui/spinner.js"
|
|
@@ -338,7 +354,13 @@
|
|
|
338
354
|
},
|
|
339
355
|
"peerDependencies": {
|
|
340
356
|
"@marktiderman/genesis-design-system": "^1.3.1",
|
|
357
|
+
"@tanstack/react-router": "^1.0.0",
|
|
341
358
|
"@tanstack/react-virtual": "^3.14.0",
|
|
359
|
+
"@tiptap/core": "^3.31.0",
|
|
360
|
+
"@tiptap/extensions": "^3.31.0",
|
|
361
|
+
"@tiptap/pm": "^3.31.0",
|
|
362
|
+
"@tiptap/react": "^3.31.0",
|
|
363
|
+
"@tiptap/starter-kit": "^3.31.0",
|
|
342
364
|
"cmdk": "^1.0.0",
|
|
343
365
|
"date-fns": "^4.0.0",
|
|
344
366
|
"embla-carousel-react": "^8.0.0",
|
|
@@ -351,12 +373,13 @@
|
|
|
351
373
|
"react-dropzone": "^14.0.0 || ^15.0.0",
|
|
352
374
|
"react-hook-form": "^7.0.0",
|
|
353
375
|
"react-resizable-panels": "^2.0.0",
|
|
376
|
+
"react-router": "^7.0.0",
|
|
354
377
|
"recharts": "^2.12.0 || ^3.0.0",
|
|
355
378
|
"sonner": "^1.0.0 || ^2.0.0",
|
|
356
379
|
"vaul": "^1.0.0"
|
|
357
380
|
},
|
|
358
381
|
"dependencies": {
|
|
359
|
-
"@marktiderman/genesis-core": "^1.
|
|
382
|
+
"@marktiderman/genesis-core": "^1.4.0",
|
|
360
383
|
"class-variance-authority": "^0.7.1",
|
|
361
384
|
"clsx": "^2.1.1",
|
|
362
385
|
"tailwind-merge": "^3.5.0"
|
|
@@ -379,9 +402,15 @@
|
|
|
379
402
|
"@radix-ui/react-toggle": "^1.1.0",
|
|
380
403
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
|
381
404
|
"@tanstack/react-query": "^5.75.0",
|
|
405
|
+
"@tanstack/react-router": "1.170.41",
|
|
382
406
|
"@tanstack/react-table": "^8.21.3",
|
|
383
407
|
"@tanstack/react-virtual": "3.14.13",
|
|
384
408
|
"@testing-library/react": "^16.3.2",
|
|
409
|
+
"@tiptap/core": "3.31.4",
|
|
410
|
+
"@tiptap/extensions": "3.31.4",
|
|
411
|
+
"@tiptap/pm": "3.31.4",
|
|
412
|
+
"@tiptap/react": "3.31.4",
|
|
413
|
+
"@tiptap/starter-kit": "3.31.4",
|
|
385
414
|
"@types/react": "^19.2.14",
|
|
386
415
|
"@types/react-dom": "^19.2.3",
|
|
387
416
|
"date-fns": "^4.1.0",
|
|
@@ -395,6 +424,7 @@
|
|
|
395
424
|
"react-dropzone": "^15.0.0",
|
|
396
425
|
"react-hook-form": "^7.72.1",
|
|
397
426
|
"react-resizable-panels": "^2.0.0",
|
|
427
|
+
"react-router": "7.14.0",
|
|
398
428
|
"recharts": "^2.15.0",
|
|
399
429
|
"sonner": "^1.0.0",
|
|
400
430
|
"tsup": "^8.3.0",
|
|
@@ -404,8 +434,29 @@
|
|
|
404
434
|
"zod": "^4.3.6"
|
|
405
435
|
},
|
|
406
436
|
"peerDependenciesMeta": {
|
|
437
|
+
"@tanstack/react-router": {
|
|
438
|
+
"optional": true
|
|
439
|
+
},
|
|
407
440
|
"@tanstack/react-virtual": {
|
|
408
441
|
"optional": true
|
|
442
|
+
},
|
|
443
|
+
"@tiptap/core": {
|
|
444
|
+
"optional": true
|
|
445
|
+
},
|
|
446
|
+
"@tiptap/extensions": {
|
|
447
|
+
"optional": true
|
|
448
|
+
},
|
|
449
|
+
"@tiptap/pm": {
|
|
450
|
+
"optional": true
|
|
451
|
+
},
|
|
452
|
+
"@tiptap/react": {
|
|
453
|
+
"optional": true
|
|
454
|
+
},
|
|
455
|
+
"@tiptap/starter-kit": {
|
|
456
|
+
"optional": true
|
|
457
|
+
},
|
|
458
|
+
"react-router": {
|
|
459
|
+
"optional": true
|
|
409
460
|
}
|
|
410
461
|
},
|
|
411
462
|
"scripts": {
|