seemore 1.6.1 → 1.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/README.md +8 -8
- package/dist/cli/index.js +25 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +24 -6
- package/package.json +1 -1
- package/src/app/layout/Header.tsx +14 -0
- package/src/app/layout/Sidebar.tsx +46 -3
- package/src/app/mdx/components.tsx +17 -0
- package/src/app/search/client.ts +2 -2
- package/src/app/search/worker.ts +2 -2
- package/src/app/styles/globals.css +30 -1
- package/src/shared/types.ts +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { z } from 'zod';
|
|
|
6
6
|
*/
|
|
7
7
|
declare const FEATURES: readonly ["navigation.instant.prefetch", "navigation.instant.preview", "navigation.footer", "navigation.top", "navigation.path", "navigation.sections", "navigation.prune", "toc.follow", "toc.integrate", "content.code.copy", "content.action.edit", "content.edit", "content.image.zoom", "search.suggest", "search.highlight", "social.cards"];
|
|
8
8
|
type Feature = (typeof FEATURES)[number];
|
|
9
|
-
/** What a user
|
|
9
|
+
/** What a user writes in `features`: the flags they are changing, each set on or off. */
|
|
10
|
+
type FeatureMap = Partial<Record<Feature, boolean>>;
|
|
11
|
+
/** @deprecated The array form, where a `!` prefix means off. Write {@link FeatureMap} instead. */
|
|
10
12
|
type FeatureFlag = Feature | `!${Feature}`;
|
|
11
13
|
type ResolvedFeatures = Record<Feature, boolean>;
|
|
12
14
|
|
|
@@ -50,9 +52,24 @@ declare const configSchema: z.ZodObject<{
|
|
|
50
52
|
shadcn: "shadcn";
|
|
51
53
|
}>>;
|
|
52
54
|
css: z.ZodOptional<z.ZodString>;
|
|
53
|
-
features: z.ZodDefault<z.
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
features: z.ZodDefault<z.ZodPreprocess<z.ZodRecord<z.ZodEnum<{
|
|
56
|
+
"navigation.instant.prefetch": "navigation.instant.prefetch";
|
|
57
|
+
"navigation.instant.preview": "navigation.instant.preview";
|
|
58
|
+
"navigation.footer": "navigation.footer";
|
|
59
|
+
"navigation.top": "navigation.top";
|
|
60
|
+
"navigation.path": "navigation.path";
|
|
61
|
+
"navigation.sections": "navigation.sections";
|
|
62
|
+
"navigation.prune": "navigation.prune";
|
|
63
|
+
"toc.follow": "toc.follow";
|
|
64
|
+
"toc.integrate": "toc.integrate";
|
|
65
|
+
"content.code.copy": "content.code.copy";
|
|
66
|
+
"content.action.edit": "content.action.edit";
|
|
67
|
+
"content.edit": "content.edit";
|
|
68
|
+
"content.image.zoom": "content.image.zoom";
|
|
69
|
+
"search.suggest": "search.suggest";
|
|
70
|
+
"search.highlight": "search.highlight";
|
|
71
|
+
"social.cards": "social.cards";
|
|
72
|
+
}> & z.core.$partial, z.ZodBoolean>, unknown>>;
|
|
56
73
|
nav: z.ZodOptional<z.ZodArray<z.ZodType<NavItem, unknown, z.core.$ZodTypeInternals<NavItem, unknown>>>>;
|
|
57
74
|
footer: z.ZodOptional<z.ZodObject<{
|
|
58
75
|
text: z.ZodOptional<z.ZodString>;
|
|
@@ -81,7 +98,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
81
98
|
}, z.core.$strip>;
|
|
82
99
|
/** What a user writes in `seemore.config.ts`. */
|
|
83
100
|
type SeemoreConfig = Omit<z.input<typeof configSchema>, 'features' | 'theme' | 'search'> & {
|
|
84
|
-
|
|
101
|
+
/** An array of {@link FeatureFlag} also works, but the map is the documented form. */
|
|
102
|
+
features?: FeatureMap | FeatureFlag[];
|
|
85
103
|
theme?: Theme;
|
|
86
104
|
search?: z.input<typeof searchSchema>;
|
|
87
105
|
};
|
|
@@ -141,4 +159,4 @@ declare const frontmatterSchema: z.ZodObject<{
|
|
|
141
159
|
}, z.core.$loose>;
|
|
142
160
|
type FrontmatterData = z.output<typeof frontmatterSchema> & Record<string, unknown>;
|
|
143
161
|
|
|
144
|
-
export type { Feature, FeatureFlag, FrontmatterData, NavItem, ResolvedSeemoreConfig, SearchConfig, SeemoreConfig, Theme };
|
|
162
|
+
export type { Feature, FeatureFlag, FeatureMap, FrontmatterData, NavItem, ResolvedSeemoreConfig, SearchConfig, SeemoreConfig, Theme };
|
package/package.json
CHANGED
|
@@ -4,18 +4,32 @@ import { useSearchContext } from 'fumadocs-ui/contexts/search';
|
|
|
4
4
|
import { SidebarTrigger } from 'fumadocs-ui/components/sidebar/base';
|
|
5
5
|
import { useTheme } from 'fumadocs-ui/provider/base';
|
|
6
6
|
import { config } from 'virtual:seemore/config';
|
|
7
|
+
import { useSidebarCollapse } from './Sidebar.js';
|
|
7
8
|
|
|
8
9
|
export function Header() {
|
|
9
10
|
const search = useSearchContext();
|
|
10
11
|
const { resolvedTheme, setTheme } = useTheme();
|
|
11
12
|
const location = useLocation();
|
|
13
|
+
const { collapsed, toggle } = useSidebarCollapse();
|
|
12
14
|
|
|
13
15
|
return (
|
|
14
16
|
<header className="seemore-header">
|
|
17
|
+
{/* Two triggers, one slot: below `md` the sidebar is a drawer this opens over the page,
|
|
18
|
+
and at `md` and up it is a rail this hides, leaving the article the room. */}
|
|
15
19
|
<SidebarTrigger className="seemore-sidebar-trigger" aria-label="Toggle navigation">
|
|
16
20
|
<PanelLeft />
|
|
17
21
|
</SidebarTrigger>
|
|
18
22
|
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
className="seemore-sidebar-collapse"
|
|
26
|
+
aria-label={collapsed ? 'Show navigation' : 'Hide navigation'}
|
|
27
|
+
aria-expanded={!collapsed}
|
|
28
|
+
onClick={toggle}
|
|
29
|
+
>
|
|
30
|
+
<PanelLeft aria-hidden="true" />
|
|
31
|
+
</button>
|
|
32
|
+
|
|
19
33
|
<Link to="/" className="seemore-brand" viewTransition>
|
|
20
34
|
{config.title}
|
|
21
35
|
</Link>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, type ComponentProps, type ReactNode } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, type ComponentProps, type ReactNode } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
SidebarFolder as BaseFolder,
|
|
4
4
|
SidebarFolderContent as BaseFolderContent,
|
|
@@ -43,12 +43,55 @@ const styled = {
|
|
|
43
43
|
|
|
44
44
|
const renderPageTree = createPageTreeRenderer(styled);
|
|
45
45
|
|
|
46
|
+
const COLLAPSE_KEY = 'seemore:sidebar-collapsed';
|
|
47
|
+
|
|
48
|
+
/* `useLayoutEffect` does nothing on the server and says so in a warning; the prerender pass
|
|
49
|
+
takes `useEffect`, which it never runs either. */
|
|
50
|
+
const useIsomorphicLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hiding the rail is a reader's preference, like the theme, so it outlives the page rather
|
|
54
|
+
* than resetting on the next load. fumadocs owns the state itself — every primitive reads
|
|
55
|
+
* `collapsed` from the same context — this only teaches it to persist, and gives the header
|
|
56
|
+
* a trigger to call.
|
|
57
|
+
*/
|
|
58
|
+
export function useSidebarCollapse(): { collapsed: boolean; toggle: () => void } {
|
|
59
|
+
const { collapsed, setCollapsed } = useSidebar();
|
|
60
|
+
|
|
61
|
+
const toggle = useCallback(() => {
|
|
62
|
+
const next = !collapsed;
|
|
63
|
+
try {
|
|
64
|
+
localStorage.setItem(COLLAPSE_KEY, String(next));
|
|
65
|
+
} catch {
|
|
66
|
+
// Storage blocked, private window: the choice just does not outlive the page.
|
|
67
|
+
}
|
|
68
|
+
setCollapsed(next);
|
|
69
|
+
}, [collapsed, setCollapsed]);
|
|
70
|
+
|
|
71
|
+
return { collapsed, toggle };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Applied before paint, so a remembered collapse does not flash the rail open first. */
|
|
75
|
+
function useRestoreCollapse(): void {
|
|
76
|
+
const { setCollapsed } = useSidebar();
|
|
77
|
+
|
|
78
|
+
useIsomorphicLayoutEffect(() => {
|
|
79
|
+
try {
|
|
80
|
+
if (localStorage.getItem(COLLAPSE_KEY) === 'true') setCollapsed(true);
|
|
81
|
+
} catch {
|
|
82
|
+
// Nothing readable is nothing to restore; the rail stays open.
|
|
83
|
+
}
|
|
84
|
+
}, [setCollapsed]);
|
|
85
|
+
}
|
|
86
|
+
|
|
46
87
|
export function Sidebar({ children }: { children?: ReactNode }) {
|
|
47
88
|
// Below `md` the sidebar is a drawer, and the header's trigger is what opens it. Without
|
|
48
89
|
// reading that state the trigger is decorative: the panel is hidden by CSS alone.
|
|
49
|
-
const { open, setOpen } = useSidebar();
|
|
90
|
+
const { open, setOpen, collapsed } = useSidebar();
|
|
50
91
|
const url = useRouteUrl();
|
|
51
92
|
|
|
93
|
+
useRestoreCollapse();
|
|
94
|
+
|
|
52
95
|
// Following a link should not leave the drawer covering the page you asked for.
|
|
53
96
|
useEffect(() => {
|
|
54
97
|
setOpen(false);
|
|
@@ -79,7 +122,7 @@ export function Sidebar({ children }: { children?: ReactNode }) {
|
|
|
79
122
|
/>
|
|
80
123
|
) : undefined}
|
|
81
124
|
|
|
82
|
-
<div className="seemore-sidebar-column" data-open={open}>
|
|
125
|
+
<div className="seemore-sidebar-column" data-open={open} data-collapsed={collapsed}>
|
|
83
126
|
<aside className="seemore-sidebar" aria-label="Documentation navigation">
|
|
84
127
|
{/* `toc.integrate` passes the table of contents as children: it belongs inside the
|
|
85
128
|
viewport, so nav and TOC scroll together rather than the TOC sitting below a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AnchorHTMLAttributes, ComponentProps } from 'react';
|
|
2
2
|
import { Link } from 'react-router';
|
|
3
3
|
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
|
4
|
+
import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock';
|
|
4
5
|
import { ImageZoom } from 'fumadocs-ui/components/image-zoom';
|
|
5
6
|
import { config } from 'virtual:seemore/config';
|
|
6
7
|
import { isExternalHref, stripBase } from '../../shared/base.js';
|
|
@@ -57,9 +58,25 @@ function MdxImage({ src, alt, ...props }: ComponentProps<'img'>) {
|
|
|
57
58
|
return <Image src={src} alt={alt} {...props} />;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* fumadocs' own `pre`, with the site-wide copy flag folded in.
|
|
63
|
+
*
|
|
64
|
+
* A fence's `noCopy` reaches us as `allowCopy="false"` — `rehype-code` rewrites the meta —
|
|
65
|
+
* so passing the prop straight through keeps per-block control working, and the flag only
|
|
66
|
+
* decides what every other fence does.
|
|
67
|
+
*/
|
|
68
|
+
function MdxPre({ allowCopy, children, ...props }: ComponentProps<typeof CodeBlock>) {
|
|
69
|
+
return (
|
|
70
|
+
<CodeBlock {...props} allowCopy={feature('content.code.copy') ? allowCopy : false}>
|
|
71
|
+
<Pre>{children}</Pre>
|
|
72
|
+
</CodeBlock>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
export const mdxComponents = {
|
|
61
77
|
...defaultMdxComponents,
|
|
62
78
|
a: MdxLink,
|
|
79
|
+
pre: MdxPre,
|
|
63
80
|
// For a hand-written `<a>` in `.mdx` content — MDX doesn't route those through `a` above,
|
|
64
81
|
// so `<Link href="…">` is the escape hatch when a link needs its own classes or layout.
|
|
65
82
|
Link: MdxLink,
|
package/src/app/search/client.ts
CHANGED
|
@@ -17,8 +17,8 @@ export function createSearchClient(config: ClientSearchConfig): SearchClientLike
|
|
|
17
17
|
* The static index is parsed and queried off the main thread, falling back to the main
|
|
18
18
|
* thread where workers are unavailable.
|
|
19
19
|
*
|
|
20
|
-
* On any real corpus, parsing the index on the main thread is a visible stall
|
|
21
|
-
*
|
|
20
|
+
* On any real corpus, parsing the index on the main thread is a visible stall, and it lands
|
|
21
|
+
* on the keystroke that opens search — exactly where a stall is most felt.
|
|
22
22
|
*/
|
|
23
23
|
function createStaticClient(from: string): SearchClientLike {
|
|
24
24
|
const onMainThread = (): SearchClientLike => {
|
package/src/app/search/worker.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { SortedResult } from 'fumadocs-core/search';
|
|
|
5
5
|
/**
|
|
6
6
|
* The static index is parsed and queried off the main thread.
|
|
7
7
|
*
|
|
8
|
-
* On any real corpus, parsing the index on the main thread is a visible stall
|
|
9
|
-
*
|
|
8
|
+
* On any real corpus, parsing the index on the main thread is a visible stall, and it lands
|
|
9
|
+
* on the keystroke that opens search — exactly where a stall is most felt.
|
|
10
10
|
*/
|
|
11
11
|
type Incoming = { type: 'init'; from: string } | { type: 'query'; id: number; query: string };
|
|
12
12
|
type Outgoing =
|
|
@@ -26,8 +26,31 @@
|
|
|
26
26
|
@apply truncate;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/* One glyph in one slot, whichever trigger is showing: at header scale the panel icon
|
|
30
|
+
reads as "navigation", and which way it will move is the label's job, not a second
|
|
31
|
+
arrow's. Sized and muted to sit with the search and theme buttons rather than shout
|
|
32
|
+
over them. */
|
|
33
|
+
.seemore-sidebar-trigger,
|
|
34
|
+
.seemore-sidebar-collapse {
|
|
35
|
+
@apply shrink-0 rounded-lg p-1.5 text-fd-muted-foreground transition-colors;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.seemore-sidebar-trigger:hover,
|
|
39
|
+
.seemore-sidebar-collapse:hover {
|
|
40
|
+
@apply bg-fd-accent text-fd-accent-foreground;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.seemore-sidebar-trigger svg,
|
|
44
|
+
.seemore-sidebar-collapse svg {
|
|
45
|
+
@apply size-5;
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
.seemore-sidebar-trigger {
|
|
30
|
-
@apply
|
|
49
|
+
@apply md:hidden;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.seemore-sidebar-collapse {
|
|
53
|
+
@apply hidden md:inline-flex;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
.seemore-brand {
|
|
@@ -152,6 +175,12 @@
|
|
|
152
175
|
@apply translate-x-0 rtl:translate-x-0;
|
|
153
176
|
}
|
|
154
177
|
|
|
178
|
+
/* Collapse is the rail's state alone. Below `md` the same column is the drawer, which has
|
|
179
|
+
its own trigger and its own reason to be open, so the collapsed choice must not reach it. */
|
|
180
|
+
.seemore-sidebar-column[data-collapsed='true'] {
|
|
181
|
+
@apply md:hidden;
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
.seemore-sidebar-backdrop {
|
|
156
185
|
@apply fixed inset-0 z-30 bg-black/40 md:hidden;
|
|
157
186
|
}
|
package/src/shared/types.ts
CHANGED
|
@@ -25,8 +25,11 @@ export const FEATURES = [
|
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
27
|
export type Feature = (typeof FEATURES)[number];
|
|
28
|
-
/** What a user
|
|
28
|
+
/** What a user writes in `features`: the flags they are changing, each set on or off. */
|
|
29
|
+
export type FeatureMap = Partial<Record<Feature, boolean>>;
|
|
30
|
+
/** @deprecated The array form, where a `!` prefix means off. Write {@link FeatureMap} instead. */
|
|
29
31
|
export type FeatureFlag = Feature | `!${Feature}`;
|
|
32
|
+
export type FeaturesInput = FeatureMap | readonly FeatureFlag[];
|
|
30
33
|
export type ResolvedFeatures = Record<Feature, boolean>;
|
|
31
34
|
|
|
32
35
|
export interface NavItem {
|