@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.
Files changed (69) hide show
  1. package/EXTENDING.md +40 -1
  2. package/SKILL.md +144 -9
  3. package/dist/{AppShell-CX0M-dfc.d.ts → AppShell-LXQykEqe.d.ts} +47 -5
  4. package/dist/{chunk-M62CHO3N.js → chunk-4C4ESW6O.js} +9 -4
  5. package/dist/chunk-4C4ESW6O.js.map +1 -0
  6. package/dist/{chunk-IXGAEECT.js → chunk-DWCAC2E7.js} +6 -3
  7. package/dist/chunk-DWCAC2E7.js.map +1 -0
  8. package/dist/chunk-FUVWKTPL.js +19 -0
  9. package/dist/chunk-FUVWKTPL.js.map +1 -0
  10. package/dist/{chunk-PLIYF6KI.js → chunk-MPI33I4K.js} +2 -2
  11. package/dist/{chunk-PLIYF6KI.js.map → chunk-MPI33I4K.js.map} +1 -1
  12. package/dist/{chunk-JA3ZKULS.js → chunk-OITWCIG2.js} +3 -3
  13. package/dist/chunk-OITWCIG2.js.map +1 -0
  14. package/dist/chunk-PKFDGGZV.js +65 -0
  15. package/dist/chunk-PKFDGGZV.js.map +1 -0
  16. package/dist/{chunk-TXOZMKNW.js → chunk-S7NXD37O.js} +27 -26
  17. package/dist/chunk-S7NXD37O.js.map +1 -0
  18. package/dist/{chunk-ZPPLUGLA.js → chunk-UYI3GLFJ.js} +162 -19
  19. package/dist/chunk-UYI3GLFJ.js.map +1 -0
  20. package/dist/{chunk-CDYZRZ6U.js → chunk-WGMEVX3Y.js} +2 -2
  21. package/dist/chunk-WGMEVX3Y.js.map +1 -0
  22. package/dist/chunk-ZDHWNQTR.js +552 -0
  23. package/dist/chunk-ZDHWNQTR.js.map +1 -0
  24. package/dist/components/data/index.d.ts +239 -15
  25. package/dist/components/data/index.js +522 -107
  26. package/dist/components/data/index.js.map +1 -1
  27. package/dist/components/layout/form-page.d.ts +1 -1
  28. package/dist/components/layout/form-page.js +1 -1
  29. package/dist/components/layout/index.d.ts +2 -2
  30. package/dist/components/layout/index.js +8 -8
  31. package/dist/components/patterns/index.d.ts +1 -1
  32. package/dist/components/patterns/index.js +5 -5
  33. package/dist/components/patterns/sortable-list.js +1 -1
  34. package/dist/components/rich-text/rich-text-editor.d.ts +194 -0
  35. package/dist/components/rich-text/rich-text-editor.js +833 -0
  36. package/dist/components/rich-text/rich-text-editor.js.map +1 -0
  37. package/dist/components/rich-text/rich-text.d.ts +51 -0
  38. package/dist/components/rich-text/rich-text.js +4 -0
  39. package/dist/components/rich-text/rich-text.js.map +1 -0
  40. package/dist/components/ui/alert-dialog.d.ts +12 -1
  41. package/dist/components/ui/alert-dialog.js +1 -1
  42. package/dist/hooks/index.d.ts +3 -3
  43. package/dist/hooks/index.js +4 -4
  44. package/dist/hooks/use-data-filters.d.ts +5 -3
  45. package/dist/hooks/use-data-filters.js +2 -2
  46. package/dist/hooks/use-resource-page.d.ts +2 -1
  47. package/dist/hooks/use-resource-page.js +3 -2
  48. package/dist/hooks/use-view-preference.d.ts +17 -2
  49. package/dist/hooks/use-view-preference.js +2 -1
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.js +30 -30
  52. package/dist/{navigation-DBwcZ6vb.d.ts → navigation-Bha0R3Fu.d.ts} +6 -3
  53. package/dist/rich-text-doc-C54fME2C.d.ts +180 -0
  54. package/dist/router/react-router.d.ts +47 -0
  55. package/dist/router/react-router.js +53 -0
  56. package/dist/router/react-router.js.map +1 -0
  57. package/dist/router/tanstack-router.d.ts +47 -0
  58. package/dist/router/tanstack-router.js +105 -0
  59. package/dist/router/tanstack-router.js.map +1 -0
  60. package/dist/{use-resource-page-Dd2VUv30.d.ts → use-resource-page-D4byuwAs.d.ts} +20 -3
  61. package/package.json +61 -10
  62. package/dist/chunk-CDYZRZ6U.js.map +0 -1
  63. package/dist/chunk-FTTNGGLA.js +0 -20
  64. package/dist/chunk-FTTNGGLA.js.map +0 -1
  65. package/dist/chunk-IXGAEECT.js.map +0 -1
  66. package/dist/chunk-JA3ZKULS.js.map +0 -1
  67. package/dist/chunk-M62CHO3N.js.map +0 -1
  68. package/dist/chunk-TXOZMKNW.js.map +0 -1
  69. 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":"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","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
- width?: string;
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
- /** Default view mode. */
121
- defaultView?: ViewMode;
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.7.0",
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.3.0",
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": {