@terrafirmagreg-team/ui 0.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/components/blocks/button/icon-button.tsx +1 -0
- package/src/components/blocks/canvas/chevron-edge.tsx +126 -18
- package/src/components/blocks/canvas/flow-canvas.tsx +4 -0
- package/src/components/blocks/canvas/flow-toolbar.tsx +1 -0
- package/src/components/blocks/canvas/flow-viewport.tsx +1 -0
- package/src/components/blocks/fit-box.tsx +26 -0
- package/src/components/blocks/git/github-info.tsx +1 -0
- package/src/components/blocks/lang/content.tsx +40 -10
- package/src/components/blocks/lang/flag.tsx +1 -0
- package/src/components/blocks/lang/link.tsx +1 -0
- package/src/components/blocks/lang/redirect.tsx +2 -1
- package/src/components/blocks/lang/switcher.tsx +1 -0
- package/src/components/blocks/lang/untranslated-callout.tsx +4 -3
- package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
- package/src/components/blocks/link/link-preview.tsx +2 -1
- package/src/components/blocks/list/row.tsx +1 -0
- package/src/components/blocks/media/glb/model.tsx +1 -0
- package/src/components/blocks/media/glb/scene.tsx +13 -12
- package/src/components/blocks/media/glb/viewer.tsx +1 -0
- package/src/components/blocks/media/image.tsx +3 -2
- package/src/components/blocks/media/slideshow.tsx +4 -3
- package/src/components/blocks/nav/breadcrumb.tsx +1 -0
- package/src/components/blocks/nav/copy-menu.tsx +37 -0
- package/src/components/blocks/nav/link.tsx +1 -0
- package/src/components/blocks/nav/site-menu.tsx +1 -0
- package/src/components/blocks/nav/site-title.tsx +1 -0
- package/src/components/blocks/nav/switcher.tsx +1 -0
- package/src/components/blocks/page/empty-state.tsx +1 -0
- package/src/components/blocks/page/heading.tsx +3 -2
- package/src/components/blocks/page/pager.tsx +110 -2
- package/src/components/blocks/readability/fields.tsx +250 -0
- package/src/components/blocks/readability/menu.tsx +74 -258
- package/src/components/blocks/readability/track.tsx +2 -1
- package/src/components/blocks/search/command.tsx +1 -0
- package/src/components/blocks/search/dialog.tsx +21 -9
- package/src/components/blocks/search/provider.tsx +2 -1
- package/src/components/blocks/sidebar.tsx +5 -5
- package/src/components/blocks/sprite.tsx +231 -0
- package/src/components/blocks/text/format-text.tsx +110 -0
- package/src/components/blocks/theme/toggle.tsx +4 -0
- package/src/components/blocks/tooltip/hint.tsx +1 -0
- package/src/components/blocks/tooltip/lazy.tsx +41 -0
- package/src/components/blocks/viewer/controls.tsx +11 -6
- package/src/components/blocks/viewer/float-bar.tsx +1 -0
- package/src/components/layouts/layout.tsx +2 -1
- package/src/components/layouts/left-sidebar/content.tsx +1 -0
- package/src/components/layouts/left-sidebar/context.tsx +1 -0
- package/src/components/layouts/left-sidebar/expand.tsx +2 -1
- package/src/components/layouts/left-sidebar/footer.tsx +10 -4
- package/src/components/layouts/left-sidebar/index.ts +1 -0
- package/src/components/layouts/left-sidebar/items.tsx +2 -1
- package/src/components/layouts/left-sidebar/search.tsx +4 -3
- package/src/components/layouts/left-sidebar/sidebar.tsx +3 -2
- package/src/components/layouts/left-sidebar/tree.tsx +1 -0
- package/src/components/layouts/right-sidebar.tsx +1 -0
- package/src/components/layouts/tree-context.tsx +1 -0
- package/src/components/mdx/accordion.tsx +3 -2
- package/src/components/mdx/banner.tsx +15 -12
- package/src/components/mdx/callout.tsx +1 -0
- package/src/components/mdx/card.tsx +12 -5
- package/src/components/mdx/codeblock.tsx +5 -4
- package/src/components/mdx/collapsible.tsx +1 -0
- package/src/components/mdx/files.tsx +1 -0
- package/src/components/mdx/glow-header.tsx +17 -3
- package/src/components/mdx/gradient-text.tsx +1 -0
- package/src/components/mdx/icon.tsx +35 -0
- package/src/components/mdx/image-zoom.tsx +5 -4
- package/src/components/mdx/math.tsx +30 -0
- package/src/components/mdx/mermaid.tsx +8 -1
- package/src/components/mdx/multiblock.tsx +1 -0
- package/src/components/mdx/preview.tsx +2 -1
- package/src/components/mdx/steps.tsx +1 -0
- package/src/components/mdx/table.tsx +1 -0
- package/src/components/mdx/tabs.tsx +2 -1
- package/src/components/mdx/type-table.tsx +7 -6
- package/src/{provider.tsx → components/provider.tsx} +1 -3
- package/src/hooks/canvas/use-flow-hover.ts +1 -0
- package/src/hooks/canvas/use-flow-viewport-persist.ts +1 -0
- package/src/hooks/page/use-fit-pages.ts +1 -0
- package/src/hooks/page/use-location-search.ts +8 -2
- package/src/hooks/page/use-page-preview.ts +2 -1
- package/src/hooks/page/use-paged-list.ts +1 -0
- package/src/hooks/readability/{provider.ts → use-readability.ts} +1 -0
- package/src/hooks/search/{dialog.ts → use-dialog.ts} +1 -0
- package/src/hooks/search/use-highlight-query.ts +8 -1
- package/src/hooks/search/{provider.ts → use-provider.ts} +1 -0
- package/src/hooks/use-async-value.ts +6 -3
- package/src/hooks/use-close-on-scroll.ts +1 -0
- package/src/hooks/use-copy-button.ts +1 -0
- package/src/hooks/use-cycle-index.ts +1 -0
- package/src/hooks/use-document-title.ts +1 -0
- package/src/hooks/use-element-size.ts +1 -0
- package/src/hooks/use-external-meta.ts +2 -1
- package/src/hooks/use-hot-key.ts +1 -0
- package/src/hooks/use-hover-follow.ts +1 -0
- package/src/hooks/use-in-view.ts +1 -0
- package/src/hooks/{lang.ts → use-lang.ts} +1 -0
- package/src/hooks/use-last-value.ts +1 -0
- package/src/hooks/{layout.ts → use-layout.ts} +1 -0
- package/src/hooks/use-marquee.ts +1 -0
- package/src/hooks/use-media-query.ts +1 -0
- package/src/hooks/use-mobile.ts +1 -0
- package/src/hooks/use-modifier-keys.ts +1 -0
- package/src/hooks/use-on-change.ts +1 -0
- package/src/hooks/use-pane-keys.ts +1 -0
- package/src/hooks/{sidebar.ts → use-sidebar.ts} +1 -0
- package/src/hooks/use-swipe.ts +1 -0
- package/src/hooks/use-tree.ts +1 -3
- package/src/hooks/viewer/use-in-float-bar.ts +1 -0
- package/src/hooks/viewer/use-viewer-prefs.ts +1 -0
- package/src/lib/canvas/edge.ts +78 -49
- package/src/lib/canvas/geometry.ts +1 -0
- package/src/lib/canvas/state.ts +1 -0
- package/src/lib/canvas/viewport.ts +1 -0
- package/src/lib/canvas/xyflow.ts +1 -0
- package/src/lib/config.ts +25 -0
- package/src/lib/mdx/callout.ts +1 -0
- package/src/lib/mdx/gradient.ts +1 -0
- package/src/lib/mdx/mermaid.ts +1 -0
- package/src/lib/media/crop-opaque.ts +1 -0
- package/src/lib/media/nearby-indexes.ts +1 -0
- package/src/lib/media/trim-transparent.ts +1 -0
- package/src/lib/readability/config.ts +1 -0
- package/src/lib/readability/spotlight.ts +1 -0
- package/src/lib/readability/store.ts +1 -0
- package/src/lib/tree.ts +1 -3
- package/src/lib/utils/cn.ts +1 -0
- package/src/lib/utils/format.ts +3 -3
- package/src/lib/utils/highlight-style.ts +10 -0
- package/src/lib/utils/history.ts +1 -0
- package/src/lib/utils/merge-refs.ts +1 -0
- package/src/lib/utils/nav-active.ts +1 -0
- package/src/lib/utils/stored-value.ts +1 -0
- package/src/lib/utils/urls.ts +2 -1
- package/src/locales/en_us.json +108 -117
- package/src/locales/pl_pl.json +33 -35
- package/src/locales/ru_ru.json +108 -117
- package/src/locales/uk_ua.json +33 -35
- package/src/locales/zh_cn.json +15 -17
- package/src/locales/zh_tw.json +15 -17
- package/src/locales.ts +1 -0
- package/src/types/canvas.ts +6 -1
- package/src/types/glb.ts +1 -0
- package/src/types/mdx-directives.ts +106 -0
- package/src/types/nav.ts +1 -0
- package/src/types/preview.ts +7 -1
- package/src/types/readability.ts +1 -0
- package/src/types/search.ts +1 -0
- package/src/types/tree.ts +1 -0
- package/src/types/viewer.ts +1 -0
- package/styles/content/base.css +198 -194
- package/styles/content/shiki.css +107 -108
- package/styles/content/typography.mjs +3637 -3941
- package/styles/content.css +5 -7
- package/styles.css +430 -413
- package/src/lib/utils/async-cache.ts +0 -32
|
@@ -89,13 +89,13 @@ export function MediaImage({
|
|
|
89
89
|
return (
|
|
90
90
|
<div
|
|
91
91
|
role="img"
|
|
92
|
-
aria-label={alt || t('ui.image_unavailable')}
|
|
92
|
+
aria-label={alt || t('Image unavailable', { note: 'ui.image_unavailable' })}
|
|
93
93
|
className={cn(
|
|
94
94
|
'flex min-h-24 w-full items-center justify-center rounded-md border border-border bg-secondary text-sm text-muted-foreground',
|
|
95
95
|
className,
|
|
96
96
|
)}
|
|
97
97
|
>
|
|
98
|
-
{t('ui.image_unavailable')}
|
|
98
|
+
{t('Image unavailable', { note: 'ui.image_unavailable' })}
|
|
99
99
|
</div>
|
|
100
100
|
);
|
|
101
101
|
}
|
|
@@ -133,3 +133,4 @@ export function MediaImage({
|
|
|
133
133
|
</a>
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
|
+
|
|
@@ -137,12 +137,12 @@ export function Slideshow({
|
|
|
137
137
|
}
|
|
138
138
|
expand={{ target: surfaceRef }}
|
|
139
139
|
settings={{
|
|
140
|
-
label: t('ui.viewer.settings'),
|
|
140
|
+
label: t('Viewer settings', { note: 'ui.viewer.settings' }),
|
|
141
141
|
sliders: multi
|
|
142
142
|
? [
|
|
143
143
|
{
|
|
144
144
|
icon: <Timer />,
|
|
145
|
-
label: t('ui.viewer.autoplay_speed'),
|
|
145
|
+
label: t('Autoplay speed', { note: 'ui.viewer.autoplay_speed' }),
|
|
146
146
|
min: AUTOPLAY_SPEED_MIN,
|
|
147
147
|
max: AUTOPLAY_SPEED_MAX,
|
|
148
148
|
step: 0.5,
|
|
@@ -157,7 +157,7 @@ export function Slideshow({
|
|
|
157
157
|
? [
|
|
158
158
|
{
|
|
159
159
|
icon: <Play />,
|
|
160
|
-
label: t('ui.viewer.autoplay'),
|
|
160
|
+
label: t('Autoplay', { note: 'ui.viewer.autoplay' }),
|
|
161
161
|
checked: autoplay,
|
|
162
162
|
onChange: setAutoplay,
|
|
163
163
|
},
|
|
@@ -175,3 +175,4 @@ export function Slideshow({
|
|
|
175
175
|
</figure>
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
ContextMenu,
|
|
6
|
+
ContextMenuContent,
|
|
7
|
+
ContextMenuItem,
|
|
8
|
+
ContextMenuTrigger,
|
|
9
|
+
} from '@terrafirmagreg-team/ui/components/ui/context-menu';
|
|
10
|
+
|
|
11
|
+
export function CopyMenu({
|
|
12
|
+
items,
|
|
13
|
+
extra,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
items: { label: ReactNode; value: string; icon?: ReactNode }[];
|
|
17
|
+
extra?: ReactNode;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
const copy = (value: string) => () => {
|
|
21
|
+
void navigator.clipboard.writeText(value);
|
|
22
|
+
};
|
|
23
|
+
return (
|
|
24
|
+
<ContextMenu>
|
|
25
|
+
<ContextMenuTrigger className="contents">{children}</ContextMenuTrigger>
|
|
26
|
+
<ContextMenuContent>
|
|
27
|
+
{items.map((item, index) => (
|
|
28
|
+
<ContextMenuItem key={index} onClick={copy(item.value)}>
|
|
29
|
+
{item.icon}
|
|
30
|
+
{item.label}
|
|
31
|
+
</ContextMenuItem>
|
|
32
|
+
))}
|
|
33
|
+
{extra}
|
|
34
|
+
</ContextMenuContent>
|
|
35
|
+
</ContextMenu>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -44,10 +44,11 @@ export function Heading({
|
|
|
44
44
|
{isChecked ? <CopyCheckIcon /> : <LinkIcon />}
|
|
45
45
|
<span className="sr-only">
|
|
46
46
|
{isChecked
|
|
47
|
-
? t('ui.copied_anchor'
|
|
48
|
-
: t('ui.copy_anchor'
|
|
47
|
+
? t('Copied Anchor Link', { note: 'ui.copied_anchor' })
|
|
48
|
+
: t('Copy Anchor Link', { note: 'ui.copy_anchor' })}
|
|
49
49
|
</span>
|
|
50
50
|
</button>
|
|
51
51
|
</As>
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
|
+
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
-
import type
|
|
4
|
+
import { useRef, useState, type MouseEvent, type ReactNode } from 'react';
|
|
5
5
|
import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
|
|
6
|
+
import { SPRITE_CHIP_CLASS } from '@terrafirmagreg-team/ui/components/blocks/sprite';
|
|
6
7
|
import {
|
|
7
8
|
Pagination,
|
|
8
9
|
PaginationContent,
|
|
9
10
|
PaginationItem,
|
|
10
11
|
} from '@terrafirmagreg-team/ui/components/ui/pagination';
|
|
11
12
|
import { Progress } from '@terrafirmagreg-team/ui/components/ui/progress';
|
|
13
|
+
import {
|
|
14
|
+
Tooltip,
|
|
15
|
+
TooltipContent,
|
|
16
|
+
TooltipTrigger,
|
|
17
|
+
} from '@terrafirmagreg-team/ui/components/ui/tooltip';
|
|
12
18
|
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
19
|
+
import { useElementSize } from '@terrafirmagreg-team/ui/hooks/use-element-size';
|
|
20
|
+
import { useSwipe } from '@terrafirmagreg-team/ui/hooks/use-swipe';
|
|
13
21
|
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
14
22
|
|
|
15
23
|
export function PagerArrow({
|
|
@@ -34,7 +42,12 @@ export function PagerArrow({
|
|
|
34
42
|
const t = useTranslations();
|
|
35
43
|
return (
|
|
36
44
|
<IconButton
|
|
37
|
-
label={
|
|
45
|
+
label={
|
|
46
|
+
label ??
|
|
47
|
+
(dir < 0
|
|
48
|
+
? t('Previous Page', { note: 'ui.prev_page' })
|
|
49
|
+
: t('Next Page', { note: 'ui.next_page' }))
|
|
50
|
+
}
|
|
38
51
|
tooltip={false}
|
|
39
52
|
variant={variant}
|
|
40
53
|
size={size}
|
|
@@ -110,3 +123,98 @@ export function Pager({
|
|
|
110
123
|
</Pagination>
|
|
111
124
|
);
|
|
112
125
|
}
|
|
126
|
+
|
|
127
|
+
type IconPagerOption<T extends string> = {
|
|
128
|
+
id: T;
|
|
129
|
+
icon: ReactNode;
|
|
130
|
+
help?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const PAGER_CELL = 40;
|
|
134
|
+
const PAGER_GAP = 4;
|
|
135
|
+
const PAGER_ARROW = 36;
|
|
136
|
+
|
|
137
|
+
export function IconPager<T extends string>({
|
|
138
|
+
value,
|
|
139
|
+
options,
|
|
140
|
+
onChange,
|
|
141
|
+
className,
|
|
142
|
+
}: {
|
|
143
|
+
value?: T;
|
|
144
|
+
options: IconPagerOption<T>[];
|
|
145
|
+
onChange: (id: T) => void;
|
|
146
|
+
className?: string;
|
|
147
|
+
}) {
|
|
148
|
+
const stripRef = useRef<HTMLDivElement | null>(null);
|
|
149
|
+
const { w } = useElementSize(stripRef);
|
|
150
|
+
const per = Math.max(
|
|
151
|
+
1,
|
|
152
|
+
Math.floor((w - 2 * (PAGER_ARROW + PAGER_GAP) + PAGER_GAP) / PAGER_CELL),
|
|
153
|
+
);
|
|
154
|
+
const pages = Math.max(1, Math.ceil(options.length / per));
|
|
155
|
+
const valueIndex = options.findIndex((option) => option.id === value);
|
|
156
|
+
const autoPage = Math.max(0, Math.floor(valueIndex / per));
|
|
157
|
+
const [manual, setManual] = useState<number | null>(null);
|
|
158
|
+
const [lastValue, setLastValue] = useState(value);
|
|
159
|
+
if (lastValue !== value) {
|
|
160
|
+
setLastValue(value);
|
|
161
|
+
setManual(null);
|
|
162
|
+
}
|
|
163
|
+
const page = Math.min(manual ?? autoPage, pages - 1);
|
|
164
|
+
const shown = options.slice(page * per, (page + 1) * per);
|
|
165
|
+
|
|
166
|
+
const go = (dir: number) => setManual((page + dir + pages) % pages);
|
|
167
|
+
const swipe = useSwipe((dir) => {
|
|
168
|
+
const next = valueIndex + dir;
|
|
169
|
+
if (next >= 0 && next < options.length) {
|
|
170
|
+
onChange(options[next]!.id);
|
|
171
|
+
} else {
|
|
172
|
+
go(dir);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
ref={stripRef}
|
|
179
|
+
{...swipe}
|
|
180
|
+
onWheel={(event) => {
|
|
181
|
+
if (pages > 1) go(event.deltaY > 0 ? 1 : -1);
|
|
182
|
+
}}
|
|
183
|
+
className={cn('flex min-h-9 items-center gap-1', className)}
|
|
184
|
+
>
|
|
185
|
+
<PagerArrow dir={-1} disabled={pages <= 1} onClick={() => go(-1)} />
|
|
186
|
+
<div
|
|
187
|
+
className={cn(
|
|
188
|
+
'flex min-w-0 flex-1 items-center justify-center gap-1',
|
|
189
|
+
w === 0 && 'invisible',
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{shown.map((option) => (
|
|
193
|
+
<Tooltip key={option.id}>
|
|
194
|
+
<TooltipTrigger
|
|
195
|
+
render={
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onClick={() => onChange(option.id)}
|
|
199
|
+
aria-label={option.help ?? option.id}
|
|
200
|
+
aria-pressed={option.id === value}
|
|
201
|
+
className={cn(
|
|
202
|
+
SPRITE_CHIP_CLASS,
|
|
203
|
+
'size-9 rounded-sm p-0 text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:border-primary aria-pressed:bg-accent aria-pressed:text-foreground',
|
|
204
|
+
)}
|
|
205
|
+
/>
|
|
206
|
+
}
|
|
207
|
+
>
|
|
208
|
+
{option.icon}
|
|
209
|
+
</TooltipTrigger>
|
|
210
|
+
{option.help ? (
|
|
211
|
+
<TooltipContent>{option.help}</TooltipContent>
|
|
212
|
+
) : null}
|
|
213
|
+
</Tooltip>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
<PagerArrow dir={1} disabled={pages <= 1} onClick={() => go(1)} />
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
|
+
import { CircleHelp } from 'lucide-react';
|
|
5
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
6
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
7
|
+
import {
|
|
8
|
+
Popover,
|
|
9
|
+
PopoverContent,
|
|
10
|
+
PopoverTrigger,
|
|
11
|
+
} from '@terrafirmagreg-team/ui/components/ui/popover';
|
|
12
|
+
import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
|
|
13
|
+
import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
|
|
14
|
+
import {
|
|
15
|
+
Field,
|
|
16
|
+
FieldContent,
|
|
17
|
+
FieldDescription,
|
|
18
|
+
FieldLabel,
|
|
19
|
+
} from '@terrafirmagreg-team/ui/components/ui/field';
|
|
20
|
+
import { Slider } from '@terrafirmagreg-team/ui/components/ui/slider';
|
|
21
|
+
import {
|
|
22
|
+
ToggleGroup,
|
|
23
|
+
ToggleGroupItem,
|
|
24
|
+
} from '@terrafirmagreg-team/ui/components/ui/toggle-group';
|
|
25
|
+
|
|
26
|
+
export function ReadabilityHelp({
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
items,
|
|
30
|
+
}: {
|
|
31
|
+
title: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
items?: { icon?: ReactNode; label: string; help: string }[];
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="space-y-2">
|
|
37
|
+
<div className="space-y-1">
|
|
38
|
+
<p className="font-medium">{title}</p>
|
|
39
|
+
{description && <FieldDescription className="text-xs">{description}</FieldDescription>}
|
|
40
|
+
</div>
|
|
41
|
+
{items?.map((item) => (
|
|
42
|
+
<div key={item.label} className="space-y-1 rounded-lg bg-muted/60 p-2.5">
|
|
43
|
+
<p className="flex items-center gap-1.5 text-xs font-medium [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:shrink-0">
|
|
44
|
+
{item.icon}
|
|
45
|
+
{item.label}
|
|
46
|
+
</p>
|
|
47
|
+
<FieldDescription className="text-xs">{item.help}</FieldDescription>
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ReadabilityField({
|
|
55
|
+
label,
|
|
56
|
+
icon,
|
|
57
|
+
help,
|
|
58
|
+
children,
|
|
59
|
+
}: {
|
|
60
|
+
label: ReactNode;
|
|
61
|
+
icon?: ReactNode;
|
|
62
|
+
help?: ReactNode;
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
}) {
|
|
65
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
66
|
+
return (
|
|
67
|
+
<Field className="gap-1.5">
|
|
68
|
+
<div className="flex items-center gap-1 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:shrink-0">
|
|
69
|
+
{icon}
|
|
70
|
+
<FieldLabel className="flex-1 text-xs text-muted-foreground">{label}</FieldLabel>
|
|
71
|
+
{help && (
|
|
72
|
+
<Popover onOpenChange={setHelpOpen}>
|
|
73
|
+
<PopoverTrigger
|
|
74
|
+
render={
|
|
75
|
+
<IconButton
|
|
76
|
+
variant="ghost"
|
|
77
|
+
size="icon-xs"
|
|
78
|
+
tooltip={false}
|
|
79
|
+
label={typeof label === 'string' ? label : 'Help'}
|
|
80
|
+
>
|
|
81
|
+
<CircleHelp />
|
|
82
|
+
</IconButton>
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
<PopoverContent className="w-56">{help}</PopoverContent>
|
|
86
|
+
</Popover>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
<FieldContent
|
|
90
|
+
className={cn(
|
|
91
|
+
'rounded-lg outline-2 outline-offset-4 outline-dashed outline-transparent transition-[outline-color] duration-200',
|
|
92
|
+
helpOpen && 'outline-primary',
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</FieldContent>
|
|
97
|
+
</Field>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ReadabilitySlider({
|
|
102
|
+
label,
|
|
103
|
+
icon,
|
|
104
|
+
help,
|
|
105
|
+
value,
|
|
106
|
+
onChange,
|
|
107
|
+
min = 0,
|
|
108
|
+
max = 100,
|
|
109
|
+
step = 1,
|
|
110
|
+
format = String,
|
|
111
|
+
disabled,
|
|
112
|
+
live,
|
|
113
|
+
}: {
|
|
114
|
+
label: ReactNode;
|
|
115
|
+
icon?: ReactNode;
|
|
116
|
+
help?: ReactNode;
|
|
117
|
+
value: number;
|
|
118
|
+
onChange: (value: number) => void;
|
|
119
|
+
min?: number;
|
|
120
|
+
max?: number;
|
|
121
|
+
step?: number;
|
|
122
|
+
format?: (value: number) => string;
|
|
123
|
+
disabled?: boolean;
|
|
124
|
+
live?: boolean;
|
|
125
|
+
}) {
|
|
126
|
+
const [draft, setDraft] = useState<{ key: number; value: number }>({ key: value, value });
|
|
127
|
+
const shown = draft.key === value ? draft.value : value;
|
|
128
|
+
return (
|
|
129
|
+
<ReadabilityField label={label} icon={icon} help={help}>
|
|
130
|
+
<div className="flex items-center gap-3 rounded-lg bg-muted/70 px-3 py-2.5">
|
|
131
|
+
<Slider
|
|
132
|
+
min={min}
|
|
133
|
+
max={max}
|
|
134
|
+
step={step}
|
|
135
|
+
value={shown}
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
className="flex-1 [&_[data-slot=slider-track]]:bg-muted-foreground/25"
|
|
138
|
+
onValueChange={(next) => {
|
|
139
|
+
if (live) onChange(next as number);
|
|
140
|
+
else setDraft({ key: value, value: next as number });
|
|
141
|
+
}}
|
|
142
|
+
onValueCommitted={() => {
|
|
143
|
+
if (!live && shown !== value) onChange(shown);
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
<span className="w-9 shrink-0 text-end text-xs tabular-nums text-muted-foreground">
|
|
147
|
+
{format(shown)}
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
</ReadabilityField>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function Segment<T extends string>({
|
|
155
|
+
options,
|
|
156
|
+
value,
|
|
157
|
+
onChange,
|
|
158
|
+
disabled,
|
|
159
|
+
itemContent,
|
|
160
|
+
itemLabel,
|
|
161
|
+
itemHelp,
|
|
162
|
+
itemClassName,
|
|
163
|
+
}: {
|
|
164
|
+
options: readonly T[];
|
|
165
|
+
value: T;
|
|
166
|
+
onChange: (value: T) => void;
|
|
167
|
+
disabled?: boolean;
|
|
168
|
+
itemContent: (id: T) => ReactNode;
|
|
169
|
+
itemLabel: (id: T) => string;
|
|
170
|
+
itemHelp: (id: T) => string;
|
|
171
|
+
itemClassName?: string;
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<ToggleGroup
|
|
175
|
+
value={[value]}
|
|
176
|
+
onValueChange={(values) => {
|
|
177
|
+
const next = values.at(-1) as T | undefined;
|
|
178
|
+
if (next && next !== value) onChange(next);
|
|
179
|
+
}}
|
|
180
|
+
disabled={disabled}
|
|
181
|
+
size="sm"
|
|
182
|
+
spacing={1}
|
|
183
|
+
className="w-full rounded-lg bg-muted/70 p-1"
|
|
184
|
+
>
|
|
185
|
+
{options.map((id) => (
|
|
186
|
+
<Hint key={id} label={itemHelp(id)}>
|
|
187
|
+
<ToggleGroupItem
|
|
188
|
+
value={id}
|
|
189
|
+
aria-label={itemLabel(id)}
|
|
190
|
+
className={cn(
|
|
191
|
+
'flex-1 rounded-md text-muted-foreground hover:text-foreground aria-pressed:bg-background aria-pressed:text-foreground aria-pressed:shadow-sm',
|
|
192
|
+
itemClassName,
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
195
|
+
{itemContent(id)}
|
|
196
|
+
</ToggleGroupItem>
|
|
197
|
+
</Hint>
|
|
198
|
+
))}
|
|
199
|
+
</ToggleGroup>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function WidthField({
|
|
204
|
+
field,
|
|
205
|
+
icon,
|
|
206
|
+
value,
|
|
207
|
+
onChange,
|
|
208
|
+
}: {
|
|
209
|
+
field: 'page_width' | 'content_width';
|
|
210
|
+
icon: ReactNode;
|
|
211
|
+
value: number;
|
|
212
|
+
onChange: (value: number) => void;
|
|
213
|
+
}) {
|
|
214
|
+
const t = useTranslations();
|
|
215
|
+
const w = {
|
|
216
|
+
page_width: {
|
|
217
|
+
label: t('Page width', { note: 'ui.readability.page_width' }),
|
|
218
|
+
help: t('Adjust the maximum width of the page layout to fit different reading needs and screens', { note: 'ui.readability.page_width.help' }),
|
|
219
|
+
slider: t('Page width slider', { note: 'ui.readability.page_width.slider' }),
|
|
220
|
+
sliderHelp: t('A ranged slider to choose the maximum width of the page layout', { note: 'ui.readability.page_width.slider.help' }),
|
|
221
|
+
},
|
|
222
|
+
content_width: {
|
|
223
|
+
label: t('Content width', { note: 'ui.readability.content_width' }),
|
|
224
|
+
help: t('Adjust the maximum width of the text column to fit different reading needs and screens', { note: 'ui.readability.content_width.help' }),
|
|
225
|
+
slider: t('Content width slider', { note: 'ui.readability.content_width.slider' }),
|
|
226
|
+
sliderHelp: t('A ranged slider to choose the maximum width of the content column', { note: 'ui.readability.content_width.slider.help' }),
|
|
227
|
+
},
|
|
228
|
+
}[field];
|
|
229
|
+
return (
|
|
230
|
+
<ReadabilitySlider
|
|
231
|
+
live
|
|
232
|
+
label={w.label}
|
|
233
|
+
icon={icon}
|
|
234
|
+
help={
|
|
235
|
+
<ReadabilityHelp
|
|
236
|
+
title={w.label}
|
|
237
|
+
description={w.help}
|
|
238
|
+
items={[{ icon, label: w.slider, help: w.sliderHelp }]}
|
|
239
|
+
/>
|
|
240
|
+
}
|
|
241
|
+
value={value}
|
|
242
|
+
onChange={onChange}
|
|
243
|
+
min={60}
|
|
244
|
+
max={100}
|
|
245
|
+
step={1}
|
|
246
|
+
format={(v) => `${v}%`}
|
|
247
|
+
/>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|