@terrafirmagreg-team/search 1.2.1 → 1.2.3
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 +6 -6
- package/src/components/dialog.tsx +290 -231
- package/src/locales/en_us.json +0 -3
- package/src/locales/ru_ru.json +0 -3
- package/src/locales.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrafirmagreg-team/search",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Section-aware search",
|
|
6
6
|
"license": "LGPL-3.0-only",
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"./src/components/*.tsx",
|
|
15
15
|
"./src/components/*/index.tsx",
|
|
16
16
|
"./src/components/*/index.ts"
|
|
17
|
-
]
|
|
18
|
-
"./locales": "./src/locales.ts"
|
|
17
|
+
]
|
|
19
18
|
},
|
|
20
19
|
"dependencies": {
|
|
21
|
-
"@terrafirmagreg-team/kit": "1.2.
|
|
20
|
+
"@terrafirmagreg-team/kit": "1.2.3",
|
|
22
21
|
"@terrafirmagreg-team/sites": "1.2.1",
|
|
23
|
-
"@terrafirmagreg-team/translate": "1.2.
|
|
24
|
-
"@terrafirmagreg-team/ui": "1.2.
|
|
22
|
+
"@terrafirmagreg-team/translate": "1.2.3",
|
|
23
|
+
"@terrafirmagreg-team/ui": "1.2.2",
|
|
25
24
|
"fumadocs-core": "16.17.8",
|
|
25
|
+
"lucide-react": "^0.577.0",
|
|
26
26
|
"server-only": "^0.0.1"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
@@ -1,231 +1,290 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import {
|
|
3
|
-
SearchDialog as SearchDialogRoot,
|
|
4
|
-
SearchDialogClose,
|
|
5
|
-
SearchDialogContent,
|
|
6
|
-
SearchDialogFooter,
|
|
7
|
-
SearchDialogHeader,
|
|
8
|
-
SearchDialogIcon,
|
|
9
|
-
SearchDialogInput,
|
|
10
|
-
SearchDialogList,
|
|
11
|
-
SearchDialogOverlay,
|
|
12
|
-
SearchDialogListItem,
|
|
13
|
-
type SharedProps,
|
|
14
|
-
} from '@terrafirmagreg-team/ui/components/blocks/search/dialog';
|
|
15
|
-
import {
|
|
16
|
-
useDocsSearch,
|
|
17
|
-
type SearchClient,
|
|
18
|
-
} from 'fumadocs-core/search/client';
|
|
19
|
-
import { staticClient } from 'fumadocs-core/search/client/orama-static';
|
|
20
|
-
import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
|
|
21
|
-
import { usePathname } from 'next/navigation';
|
|
22
|
-
import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
|
|
23
|
-
import { useMemo, useState } from 'react';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from '@terrafirmagreg-team/
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
</
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
SearchDialog as SearchDialogRoot,
|
|
4
|
+
SearchDialogClose,
|
|
5
|
+
SearchDialogContent,
|
|
6
|
+
SearchDialogFooter,
|
|
7
|
+
SearchDialogHeader,
|
|
8
|
+
SearchDialogIcon,
|
|
9
|
+
SearchDialogInput,
|
|
10
|
+
SearchDialogList,
|
|
11
|
+
SearchDialogOverlay,
|
|
12
|
+
SearchDialogListItem,
|
|
13
|
+
type SharedProps,
|
|
14
|
+
} from '@terrafirmagreg-team/ui/components/blocks/search/dialog';
|
|
15
|
+
import {
|
|
16
|
+
useDocsSearch,
|
|
17
|
+
type SearchClient,
|
|
18
|
+
} from 'fumadocs-core/search/client';
|
|
19
|
+
import { staticClient } from 'fumadocs-core/search/client/orama-static';
|
|
20
|
+
import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
|
|
21
|
+
import { usePathname } from 'next/navigation';
|
|
22
|
+
import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
|
|
23
|
+
import { useMemo, useState } from 'react';
|
|
24
|
+
import { ChevronDown } from 'lucide-react';
|
|
25
|
+
import {
|
|
26
|
+
DropdownMenu,
|
|
27
|
+
DropdownMenuCheckboxItem,
|
|
28
|
+
DropdownMenuContent,
|
|
29
|
+
DropdownMenuItem,
|
|
30
|
+
DropdownMenuSeparator,
|
|
31
|
+
DropdownMenuTrigger,
|
|
32
|
+
} from '@terrafirmagreg-team/ui/components/ui/dropdown-menu';
|
|
33
|
+
import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
|
|
34
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
35
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
36
|
+
import { useSectionCatalog } from '@terrafirmagreg-team/kit/hooks/use-catalog';
|
|
37
|
+
import { useSectionNames } from '@terrafirmagreg-team/kit/hooks/use-section-names';
|
|
38
|
+
import { parseSectionLocation } from '@terrafirmagreg-team/sites/location';
|
|
39
|
+
import { searchHitFilter, mergeClients } from '../lib';
|
|
40
|
+
import { getSearchClientSources } from '../lib/sources';
|
|
41
|
+
|
|
42
|
+
const chip =
|
|
43
|
+
'rounded-md border px-2 py-0.5 text-xs font-medium transition-colors cursor-pointer';
|
|
44
|
+
const chipOn = 'bg-accent text-accent-foreground border-transparent';
|
|
45
|
+
const chipOff = 'text-muted-foreground hover:text-foreground';
|
|
46
|
+
|
|
47
|
+
type ScopeSel = { secs: Set<string>; dss: Set<string> };
|
|
48
|
+
const EMPTY_SEL: ScopeSel = { secs: new Set(), dss: new Set() };
|
|
49
|
+
|
|
50
|
+
function toggled(set: Set<string>, id: string): Set<string> {
|
|
51
|
+
const next = new Set(set);
|
|
52
|
+
if (next.has(id)) next.delete(id);
|
|
53
|
+
else next.add(id);
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ScopeDropdown({
|
|
58
|
+
label,
|
|
59
|
+
allLabel,
|
|
60
|
+
items,
|
|
61
|
+
picked,
|
|
62
|
+
onToggle,
|
|
63
|
+
onAll,
|
|
64
|
+
}: {
|
|
65
|
+
label: string;
|
|
66
|
+
allLabel: string;
|
|
67
|
+
items: { id: string; title: string }[];
|
|
68
|
+
picked: Set<string>;
|
|
69
|
+
onToggle: (id: string) => void;
|
|
70
|
+
onAll: () => void;
|
|
71
|
+
}) {
|
|
72
|
+
const active = picked.size > 0;
|
|
73
|
+
const text =
|
|
74
|
+
picked.size === 0
|
|
75
|
+
? label
|
|
76
|
+
: picked.size === 1
|
|
77
|
+
? (items.find((item) => picked.has(item.id))?.title ??
|
|
78
|
+
[...picked][0])
|
|
79
|
+
: `${label}: ${picked.size}`;
|
|
80
|
+
return (
|
|
81
|
+
<DropdownMenu modal={false}>
|
|
82
|
+
<DropdownMenuTrigger
|
|
83
|
+
aria-pressed={active}
|
|
84
|
+
className={cn(
|
|
85
|
+
chip,
|
|
86
|
+
'inline-flex items-center gap-1',
|
|
87
|
+
active ? chipOn : chipOff,
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{text}
|
|
91
|
+
<ChevronDown className="size-3" />
|
|
92
|
+
</DropdownMenuTrigger>
|
|
93
|
+
<DropdownMenuContent side="top" align="start" className="max-h-64 min-w-40">
|
|
94
|
+
<DropdownMenuItem
|
|
95
|
+
onClick={onAll}
|
|
96
|
+
className="gap-2 px-2.5 py-1.5 text-xs font-medium"
|
|
97
|
+
>
|
|
98
|
+
{allLabel}
|
|
99
|
+
</DropdownMenuItem>
|
|
100
|
+
<DropdownMenuSeparator />
|
|
101
|
+
{items.map(({ id, title }) => (
|
|
102
|
+
<DropdownMenuCheckboxItem
|
|
103
|
+
key={id}
|
|
104
|
+
checked={!active || picked.has(id)}
|
|
105
|
+
onCheckedChange={() => onToggle(id)}
|
|
106
|
+
closeOnClick={false}
|
|
107
|
+
className="text-xs"
|
|
108
|
+
>
|
|
109
|
+
{title}
|
|
110
|
+
</DropdownMenuCheckboxItem>
|
|
111
|
+
))}
|
|
112
|
+
</DropdownMenuContent>
|
|
113
|
+
</DropdownMenu>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function SearchDialog(props: SharedProps & { api: string }) {
|
|
118
|
+
const locale = useResolvedLang();
|
|
119
|
+
const t = useTranslations();
|
|
120
|
+
const sectionNames = useSectionNames();
|
|
121
|
+
const catalog = useSectionCatalog();
|
|
122
|
+
const pathname = usePathname();
|
|
123
|
+
const version = useLocationSearchParams().get('version') ?? undefined;
|
|
124
|
+
const { section, dataset: locationDataset } =
|
|
125
|
+
parseSectionLocation(pathname);
|
|
126
|
+
const dataset = locationDataset;
|
|
127
|
+
const scopeKey = `${section ?? ''}/${locationDataset ?? ''}`;
|
|
128
|
+
|
|
129
|
+
const sections = useMemo(
|
|
130
|
+
() =>
|
|
131
|
+
Object.entries(catalog).filter(
|
|
132
|
+
([, datasets]) => datasets.length > 0,
|
|
133
|
+
),
|
|
134
|
+
[catalog],
|
|
135
|
+
);
|
|
136
|
+
const defaultSel = useMemo((): ScopeSel => {
|
|
137
|
+
if (section && dataset)
|
|
138
|
+
return { secs: new Set([section]), dss: new Set([dataset]) };
|
|
139
|
+
if (section) return { secs: new Set([section]), dss: new Set() };
|
|
140
|
+
return { secs: new Set(), dss: new Set() };
|
|
141
|
+
}, [section, dataset]);
|
|
142
|
+
const [picked, setPicked] = useState<{ key: string; sel: ScopeSel }>();
|
|
143
|
+
const sel = picked?.key === scopeKey ? picked.sel : defaultSel;
|
|
144
|
+
const setSel = (value: ScopeSel) =>
|
|
145
|
+
setPicked({ key: scopeKey, sel: value });
|
|
146
|
+
const everywhere = sel.secs.size === 0 && sel.dss.size === 0;
|
|
147
|
+
const scopePairs = useMemo(() => {
|
|
148
|
+
const out = new Set<string>();
|
|
149
|
+
for (const [sec, datasets] of sections) {
|
|
150
|
+
if (sel.secs.size && !sel.secs.has(sec)) continue;
|
|
151
|
+
for (const { id } of datasets) {
|
|
152
|
+
if (sel.dss.size && !sel.dss.has(id)) continue;
|
|
153
|
+
out.add(`${sec}:${id}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}, [sections, sel]);
|
|
158
|
+
const datasetOptions = useMemo(() => {
|
|
159
|
+
const map = new Map<string, string | undefined>();
|
|
160
|
+
for (const [sec, datasets] of sections) {
|
|
161
|
+
if (sel.secs.size && !sel.secs.has(sec)) continue;
|
|
162
|
+
for (const { id, title } of datasets) {
|
|
163
|
+
if (!map.has(id)) map.set(id, title);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return [...map.entries()].map(([id, title]) => ({ id, title }));
|
|
167
|
+
}, [sections, sel.secs]);
|
|
168
|
+
|
|
169
|
+
const client = useMemo((): SearchClient => {
|
|
170
|
+
const from = `${props.api}/${locale}.json`;
|
|
171
|
+
const content = staticClient({ locale, from });
|
|
172
|
+
const refsById = new Map<string, { id: string; version?: string }>();
|
|
173
|
+
for (const entry of scopePairs) {
|
|
174
|
+
const [sec, id] = entry.split(':');
|
|
175
|
+
if (!refsById.has(id)) {
|
|
176
|
+
refsById.set(
|
|
177
|
+
id,
|
|
178
|
+
sec === section && id === dataset && locationDataset
|
|
179
|
+
? { id, version }
|
|
180
|
+
: { id },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const extra = getSearchClientSources().map((make) =>
|
|
185
|
+
make({ refs: [...refsById.values()], locale, tag: undefined }),
|
|
186
|
+
);
|
|
187
|
+
return mergeClients(content, ...extra);
|
|
188
|
+
}, [locale, scopePairs, section, dataset, locationDataset, version, props.api]);
|
|
189
|
+
const { search, setSearch, query } = useDocsSearch({ client });
|
|
190
|
+
const showBadges =
|
|
191
|
+
everywhere ||
|
|
192
|
+
new Set([...scopePairs].map((entry) => entry.split(':')[0]!)).size > 1;
|
|
193
|
+
const items = useMemo(() => {
|
|
194
|
+
const list = query.data !== 'empty' ? (query.data ?? []) : [];
|
|
195
|
+
const inScope = (item: (typeof list)[number]) =>
|
|
196
|
+
everywhere ||
|
|
197
|
+
!('url' in item) ||
|
|
198
|
+
[...scopePairs].some((entry) =>
|
|
199
|
+
item.url.startsWith(`/${entry.replace(':', '/')}`),
|
|
200
|
+
);
|
|
201
|
+
const hitFilter =
|
|
202
|
+
section && typeof window !== 'undefined'
|
|
203
|
+
? searchHitFilter(
|
|
204
|
+
section,
|
|
205
|
+
new URLSearchParams(window.location.search),
|
|
206
|
+
)
|
|
207
|
+
: undefined;
|
|
208
|
+
return list.filter(
|
|
209
|
+
(item) => inScope(item) && (hitFilter?.(item) ?? true),
|
|
210
|
+
);
|
|
211
|
+
}, [query.data, section, everywhere, scopePairs]);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<SearchDialogRoot search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
|
215
|
+
<SearchDialogOverlay />
|
|
216
|
+
<SearchDialogContent>
|
|
217
|
+
<SearchDialogHeader>
|
|
218
|
+
<SearchDialogIcon />
|
|
219
|
+
<SearchDialogInput />
|
|
220
|
+
<SearchDialogClose />
|
|
221
|
+
</SearchDialogHeader>
|
|
222
|
+
{query.isLoading ? (
|
|
223
|
+
<div className="flex flex-col gap-1.5 p-3">
|
|
224
|
+
<Skeleton className="h-9 w-full" />
|
|
225
|
+
<Skeleton className="h-9 w-5/6" />
|
|
226
|
+
<Skeleton className="h-9 w-4/6" />
|
|
227
|
+
</div>
|
|
228
|
+
) : (
|
|
229
|
+
<SearchDialogList
|
|
230
|
+
items={items.length ? items : null}
|
|
231
|
+
Item={(itemProps) => (
|
|
232
|
+
<SearchDialogListItem
|
|
233
|
+
{...itemProps}
|
|
234
|
+
badge={
|
|
235
|
+
showBadges && 'url' in itemProps.item
|
|
236
|
+
? scopeBadge(itemProps.item.url)
|
|
237
|
+
: undefined
|
|
238
|
+
}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
/>
|
|
242
|
+
)}
|
|
243
|
+
<SearchDialogFooter>
|
|
244
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
245
|
+
<ScopeDropdown
|
|
246
|
+
label={t('Sections', { note: 'ui.search_sections' })}
|
|
247
|
+
allLabel={t('All', { note: 'ui.search_all' })}
|
|
248
|
+
items={sections.map(([sec]) => ({
|
|
249
|
+
id: sec,
|
|
250
|
+
title: sectionNames[sec]?.title ?? sec,
|
|
251
|
+
}))}
|
|
252
|
+
picked={sel.secs}
|
|
253
|
+
onToggle={(id) =>
|
|
254
|
+
setSel({ ...sel, secs: toggled(sel.secs, id) })
|
|
255
|
+
}
|
|
256
|
+
onAll={() => setSel({ ...sel, secs: new Set() })}
|
|
257
|
+
/>
|
|
258
|
+
<ScopeDropdown
|
|
259
|
+
label={t('Datasets', { note: 'ui.search_datasets' })}
|
|
260
|
+
allLabel={t('All', { note: 'ui.search_all' })}
|
|
261
|
+
items={datasetOptions.map(({ id, title }) => ({
|
|
262
|
+
id,
|
|
263
|
+
title: title ?? id,
|
|
264
|
+
}))}
|
|
265
|
+
picked={sel.dss}
|
|
266
|
+
onToggle={(id) =>
|
|
267
|
+
setSel({ ...sel, dss: toggled(sel.dss, id) })
|
|
268
|
+
}
|
|
269
|
+
onAll={() => setSel({ ...sel, dss: new Set() })}
|
|
270
|
+
/>
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
aria-pressed={everywhere}
|
|
274
|
+
className={cn(chip, everywhere ? chipOn : chipOff)}
|
|
275
|
+
onClick={() => setSel(EMPTY_SEL)}
|
|
276
|
+
>
|
|
277
|
+
{t('Everywhere', { note: 'ui.search_everywhere' })}
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</SearchDialogFooter>
|
|
281
|
+
</SearchDialogContent>
|
|
282
|
+
</SearchDialogRoot>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
function scopeBadge(url: string) {
|
|
286
|
+
const sec = url.split('/')[1];
|
|
287
|
+
return sec && sec in catalog ? (sectionNames[sec]?.title ?? sec) : undefined;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
package/src/locales/en_us.json
DELETED
package/src/locales/ru_ru.json
DELETED