@terrafirmagreg-team/search 1.2.1 → 1.2.2
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 +5 -6
- package/src/components/dialog.tsx +231 -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.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Section-aware search",
|
|
6
6
|
"license": "LGPL-3.0-only",
|
|
@@ -14,14 +14,13 @@
|
|
|
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.2",
|
|
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",
|
|
26
25
|
"server-only": "^0.0.1"
|
|
27
26
|
},
|
|
@@ -1,231 +1,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 { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
|
|
25
|
-
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
26
|
-
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
27
|
-
import { useSectionCatalog } from '@terrafirmagreg-team/kit/hooks/use-catalog';
|
|
28
|
-
import { useSectionNames } from '@terrafirmagreg-team/kit/hooks/use-section-names';
|
|
29
|
-
import {
|
|
30
|
-
dumpSectionKeys,
|
|
31
|
-
parseSectionLocation,
|
|
32
|
-
} from '@terrafirmagreg-team/sites/location';
|
|
33
|
-
import { searchHitFilter, mergeClients } from '../lib';
|
|
34
|
-
import { getSearchClientSources } from '../lib/sources';
|
|
35
|
-
|
|
36
|
-
const chip =
|
|
37
|
-
'rounded-md border px-2 py-0.5 text-xs font-medium transition-colors cursor-pointer';
|
|
38
|
-
const chipOn = 'bg-accent text-accent-foreground border-transparent';
|
|
39
|
-
const chipOff = 'text-muted-foreground hover:text-foreground';
|
|
40
|
-
const chipMixed = cn(chipOff, 'border-dashed');
|
|
41
|
-
|
|
42
|
-
export function SearchDialog(props: SharedProps & { api: string }) {
|
|
43
|
-
const locale = useResolvedLang();
|
|
44
|
-
const t = useTranslations();
|
|
45
|
-
const sectionNames = useSectionNames();
|
|
46
|
-
const catalog = useSectionCatalog();
|
|
47
|
-
const pathname = usePathname();
|
|
48
|
-
const version = useLocationSearchParams().get('version') ?? undefined;
|
|
49
|
-
const { section, dataset: locationDataset } =
|
|
50
|
-
parseSectionLocation(pathname);
|
|
51
|
-
const dataset = locationDataset;
|
|
52
|
-
const scopeKey = `${section ?? ''}/${locationDataset ?? ''}`;
|
|
53
|
-
|
|
54
|
-
const sections = useMemo(
|
|
55
|
-
() =>
|
|
56
|
-
Object.entries(catalog).filter(
|
|
57
|
-
([, datasets]) => datasets.length > 0,
|
|
58
|
-
),
|
|
59
|
-
[catalog],
|
|
60
|
-
);
|
|
61
|
-
const defaultSel = useMemo((): Set<string> => {
|
|
62
|
-
if (section && dataset) return new Set([`${section}:${dataset}`]);
|
|
63
|
-
if (section)
|
|
64
|
-
return new Set(
|
|
65
|
-
(catalog[section] ?? []).map(({ id }) => `${section}:${id}`),
|
|
66
|
-
);
|
|
67
|
-
return new Set();
|
|
68
|
-
}, [section, dataset, catalog]);
|
|
69
|
-
const [picked, setPicked] = useState<{ key: string; sel: Set<string> }>();
|
|
70
|
-
const sel = picked?.key === scopeKey ? picked.sel : defaultSel;
|
|
71
|
-
const setSel = (value: Set<string>) =>
|
|
72
|
-
setPicked({ key: scopeKey, sel: value });
|
|
73
|
-
const toggle = (ids: readonly string[]) => {
|
|
74
|
-
const next = new Set(sel);
|
|
75
|
-
const allOn = ids.every((id) => next.has(id));
|
|
76
|
-
for (const id of ids) {
|
|
77
|
-
if (allOn) next.delete(id);
|
|
78
|
-
else next.add(id);
|
|
79
|
-
}
|
|
80
|
-
setSel(next);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const client = useMemo((): SearchClient => {
|
|
84
|
-
const from = `${props.api}/${locale}.json`;
|
|
85
|
-
const content = staticClient({ locale, from });
|
|
86
|
-
const selSecs = new Set([...sel].map((entry) => entry.split(':')[0]!));
|
|
87
|
-
const scopedSections =
|
|
88
|
-
sel.size === 0
|
|
89
|
-
? dumpSectionKeys()
|
|
90
|
-
: dumpSectionKeys().filter((sec) => selSecs.has(sec));
|
|
91
|
-
const refs = scopedSections.flatMap((sec) =>
|
|
92
|
-
(catalog[sec] ?? []).map(({ id }) =>
|
|
93
|
-
sec === section && id === dataset && locationDataset
|
|
94
|
-
? { id, version }
|
|
95
|
-
: { id },
|
|
96
|
-
),
|
|
97
|
-
);
|
|
98
|
-
const extra = getSearchClientSources().map((make) =>
|
|
99
|
-
make({ refs, locale, tag: undefined }),
|
|
100
|
-
);
|
|
101
|
-
return mergeClients(content, ...extra);
|
|
102
|
-
}, [locale, sel, section, dataset, locationDataset, version, catalog, props.api]);
|
|
103
|
-
const { search, setSearch, query } = useDocsSearch({ client });
|
|
104
|
-
const showBadges =
|
|
105
|
-
sel.size === 0 ||
|
|
106
|
-
new Set([...sel].map((entry) => entry.split(':')[0]!)).size > 1;
|
|
107
|
-
const items = useMemo(() => {
|
|
108
|
-
const list = query.data !== 'empty' ? (query.data ?? []) : [];
|
|
109
|
-
const inScope = (item: (typeof list)[number]) =>
|
|
110
|
-
sel.size === 0 ||
|
|
111
|
-
!('url' in item) ||
|
|
112
|
-
[...sel].some((entry) =>
|
|
113
|
-
item.url.startsWith(`/${entry.replace(':', '/')}`),
|
|
114
|
-
);
|
|
115
|
-
const hitFilter =
|
|
116
|
-
section && typeof window !== 'undefined'
|
|
117
|
-
? searchHitFilter(
|
|
118
|
-
section,
|
|
119
|
-
new URLSearchParams(window.location.search),
|
|
120
|
-
)
|
|
121
|
-
: undefined;
|
|
122
|
-
return list.filter(
|
|
123
|
-
(item) => inScope(item) && (hitFilter?.(item) ?? true),
|
|
124
|
-
);
|
|
125
|
-
}, [query.data, section, sel]);
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<SearchDialogRoot search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
|
129
|
-
<SearchDialogOverlay />
|
|
130
|
-
<SearchDialogContent>
|
|
131
|
-
<SearchDialogHeader>
|
|
132
|
-
<SearchDialogIcon />
|
|
133
|
-
<SearchDialogInput />
|
|
134
|
-
<SearchDialogClose />
|
|
135
|
-
</SearchDialogHeader>
|
|
136
|
-
{query.isLoading ? (
|
|
137
|
-
<div className="flex flex-col gap-1.5 p-3">
|
|
138
|
-
<Skeleton className="h-9 w-full" />
|
|
139
|
-
<Skeleton className="h-9 w-5/6" />
|
|
140
|
-
<Skeleton className="h-9 w-4/6" />
|
|
141
|
-
</div>
|
|
142
|
-
) : (
|
|
143
|
-
<SearchDialogList
|
|
144
|
-
items={items.length ? items : null}
|
|
145
|
-
Item={(itemProps) => (
|
|
146
|
-
<SearchDialogListItem
|
|
147
|
-
{...itemProps}
|
|
148
|
-
badge={
|
|
149
|
-
showBadges && 'url' in itemProps.item
|
|
150
|
-
? scopeBadge(itemProps.item.url)
|
|
151
|
-
: undefined
|
|
152
|
-
}
|
|
153
|
-
/>
|
|
154
|
-
)}
|
|
155
|
-
/>
|
|
156
|
-
)}
|
|
157
|
-
<SearchDialogFooter>
|
|
158
|
-
<div className="flex flex-wrap items-center gap-1">
|
|
159
|
-
<button
|
|
160
|
-
type="button"
|
|
161
|
-
aria-pressed={sel.size === 0}
|
|
162
|
-
className={cn(chip, sel.size === 0 ? chipOn : chipOff)}
|
|
163
|
-
onClick={() => setSel(new Set())}
|
|
164
|
-
>
|
|
165
|
-
{t('Everywhere', { note: 'ui.search_everywhere' })}
|
|
166
|
-
</button>
|
|
167
|
-
<span aria-hidden className="mx-0.5 h-4 w-px bg-border" />
|
|
168
|
-
{sections.map(([sec, datasets]) => {
|
|
169
|
-
const ids = datasets.map(({ id }) => `${sec}:${id}`);
|
|
170
|
-
if (ids.length === 1) {
|
|
171
|
-
const id = ids[0]!;
|
|
172
|
-
const on = sel.has(id);
|
|
173
|
-
return (
|
|
174
|
-
<button
|
|
175
|
-
key={sec}
|
|
176
|
-
type="button"
|
|
177
|
-
aria-pressed={on}
|
|
178
|
-
className={cn(chip, on ? chipOn : chipOff)}
|
|
179
|
-
onClick={() => toggle(ids)}
|
|
180
|
-
>
|
|
181
|
-
{sectionNames[sec]?.title ?? sec}
|
|
182
|
-
</button>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
const onCount = ids.filter((id) => sel.has(id)).length;
|
|
186
|
-
return [
|
|
187
|
-
<button
|
|
188
|
-
key={sec}
|
|
189
|
-
type="button"
|
|
190
|
-
aria-pressed={onCount > 0}
|
|
191
|
-
className={cn(
|
|
192
|
-
chip,
|
|
193
|
-
onCount === ids.length
|
|
194
|
-
? chipOn
|
|
195
|
-
: onCount > 0
|
|
196
|
-
? chipMixed
|
|
197
|
-
: chipOff,
|
|
198
|
-
)}
|
|
199
|
-
onClick={() => toggle(ids)}
|
|
200
|
-
>
|
|
201
|
-
{sectionNames[sec]?.title ?? sec}
|
|
202
|
-
</button>,
|
|
203
|
-
...datasets.map(({ id, title }) => {
|
|
204
|
-
const entry = `${sec}:${id}`;
|
|
205
|
-
const on = sel.has(entry);
|
|
206
|
-
return (
|
|
207
|
-
<button
|
|
208
|
-
key={entry}
|
|
209
|
-
type="button"
|
|
210
|
-
aria-pressed={on}
|
|
211
|
-
className={cn(chip, on ? chipOn : chipOff)}
|
|
212
|
-
onClick={() => toggle([entry])}
|
|
213
|
-
>
|
|
214
|
-
{title ?? id}
|
|
215
|
-
</button>
|
|
216
|
-
);
|
|
217
|
-
}),
|
|
218
|
-
];
|
|
219
|
-
})}
|
|
220
|
-
</div>
|
|
221
|
-
</SearchDialogFooter>
|
|
222
|
-
</SearchDialogContent>
|
|
223
|
-
</SearchDialogRoot>
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
function scopeBadge(url: string) {
|
|
227
|
-
const sec = url.split('/')[1];
|
|
228
|
-
return sec && sec in catalog ? (sectionNames[sec]?.title ?? sec) : undefined;
|
|
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 { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
|
|
25
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
26
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
27
|
+
import { useSectionCatalog } from '@terrafirmagreg-team/kit/hooks/use-catalog';
|
|
28
|
+
import { useSectionNames } from '@terrafirmagreg-team/kit/hooks/use-section-names';
|
|
29
|
+
import {
|
|
30
|
+
dumpSectionKeys,
|
|
31
|
+
parseSectionLocation,
|
|
32
|
+
} from '@terrafirmagreg-team/sites/location';
|
|
33
|
+
import { searchHitFilter, mergeClients } from '../lib';
|
|
34
|
+
import { getSearchClientSources } from '../lib/sources';
|
|
35
|
+
|
|
36
|
+
const chip =
|
|
37
|
+
'rounded-md border px-2 py-0.5 text-xs font-medium transition-colors cursor-pointer';
|
|
38
|
+
const chipOn = 'bg-accent text-accent-foreground border-transparent';
|
|
39
|
+
const chipOff = 'text-muted-foreground hover:text-foreground';
|
|
40
|
+
const chipMixed = cn(chipOff, 'border-dashed');
|
|
41
|
+
|
|
42
|
+
export function SearchDialog(props: SharedProps & { api: string }) {
|
|
43
|
+
const locale = useResolvedLang();
|
|
44
|
+
const t = useTranslations();
|
|
45
|
+
const sectionNames = useSectionNames();
|
|
46
|
+
const catalog = useSectionCatalog();
|
|
47
|
+
const pathname = usePathname();
|
|
48
|
+
const version = useLocationSearchParams().get('version') ?? undefined;
|
|
49
|
+
const { section, dataset: locationDataset } =
|
|
50
|
+
parseSectionLocation(pathname);
|
|
51
|
+
const dataset = locationDataset;
|
|
52
|
+
const scopeKey = `${section ?? ''}/${locationDataset ?? ''}`;
|
|
53
|
+
|
|
54
|
+
const sections = useMemo(
|
|
55
|
+
() =>
|
|
56
|
+
Object.entries(catalog).filter(
|
|
57
|
+
([, datasets]) => datasets.length > 0,
|
|
58
|
+
),
|
|
59
|
+
[catalog],
|
|
60
|
+
);
|
|
61
|
+
const defaultSel = useMemo((): Set<string> => {
|
|
62
|
+
if (section && dataset) return new Set([`${section}:${dataset}`]);
|
|
63
|
+
if (section)
|
|
64
|
+
return new Set(
|
|
65
|
+
(catalog[section] ?? []).map(({ id }) => `${section}:${id}`),
|
|
66
|
+
);
|
|
67
|
+
return new Set();
|
|
68
|
+
}, [section, dataset, catalog]);
|
|
69
|
+
const [picked, setPicked] = useState<{ key: string; sel: Set<string> }>();
|
|
70
|
+
const sel = picked?.key === scopeKey ? picked.sel : defaultSel;
|
|
71
|
+
const setSel = (value: Set<string>) =>
|
|
72
|
+
setPicked({ key: scopeKey, sel: value });
|
|
73
|
+
const toggle = (ids: readonly string[]) => {
|
|
74
|
+
const next = new Set(sel);
|
|
75
|
+
const allOn = ids.every((id) => next.has(id));
|
|
76
|
+
for (const id of ids) {
|
|
77
|
+
if (allOn) next.delete(id);
|
|
78
|
+
else next.add(id);
|
|
79
|
+
}
|
|
80
|
+
setSel(next);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const client = useMemo((): SearchClient => {
|
|
84
|
+
const from = `${props.api}/${locale}.json`;
|
|
85
|
+
const content = staticClient({ locale, from });
|
|
86
|
+
const selSecs = new Set([...sel].map((entry) => entry.split(':')[0]!));
|
|
87
|
+
const scopedSections =
|
|
88
|
+
sel.size === 0
|
|
89
|
+
? dumpSectionKeys()
|
|
90
|
+
: dumpSectionKeys().filter((sec) => selSecs.has(sec));
|
|
91
|
+
const refs = scopedSections.flatMap((sec) =>
|
|
92
|
+
(catalog[sec] ?? []).map(({ id }) =>
|
|
93
|
+
sec === section && id === dataset && locationDataset
|
|
94
|
+
? { id, version }
|
|
95
|
+
: { id },
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
const extra = getSearchClientSources().map((make) =>
|
|
99
|
+
make({ refs, locale, tag: undefined }),
|
|
100
|
+
);
|
|
101
|
+
return mergeClients(content, ...extra);
|
|
102
|
+
}, [locale, sel, section, dataset, locationDataset, version, catalog, props.api]);
|
|
103
|
+
const { search, setSearch, query } = useDocsSearch({ client });
|
|
104
|
+
const showBadges =
|
|
105
|
+
sel.size === 0 ||
|
|
106
|
+
new Set([...sel].map((entry) => entry.split(':')[0]!)).size > 1;
|
|
107
|
+
const items = useMemo(() => {
|
|
108
|
+
const list = query.data !== 'empty' ? (query.data ?? []) : [];
|
|
109
|
+
const inScope = (item: (typeof list)[number]) =>
|
|
110
|
+
sel.size === 0 ||
|
|
111
|
+
!('url' in item) ||
|
|
112
|
+
[...sel].some((entry) =>
|
|
113
|
+
item.url.startsWith(`/${entry.replace(':', '/')}`),
|
|
114
|
+
);
|
|
115
|
+
const hitFilter =
|
|
116
|
+
section && typeof window !== 'undefined'
|
|
117
|
+
? searchHitFilter(
|
|
118
|
+
section,
|
|
119
|
+
new URLSearchParams(window.location.search),
|
|
120
|
+
)
|
|
121
|
+
: undefined;
|
|
122
|
+
return list.filter(
|
|
123
|
+
(item) => inScope(item) && (hitFilter?.(item) ?? true),
|
|
124
|
+
);
|
|
125
|
+
}, [query.data, section, sel]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<SearchDialogRoot search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
|
129
|
+
<SearchDialogOverlay />
|
|
130
|
+
<SearchDialogContent>
|
|
131
|
+
<SearchDialogHeader>
|
|
132
|
+
<SearchDialogIcon />
|
|
133
|
+
<SearchDialogInput />
|
|
134
|
+
<SearchDialogClose />
|
|
135
|
+
</SearchDialogHeader>
|
|
136
|
+
{query.isLoading ? (
|
|
137
|
+
<div className="flex flex-col gap-1.5 p-3">
|
|
138
|
+
<Skeleton className="h-9 w-full" />
|
|
139
|
+
<Skeleton className="h-9 w-5/6" />
|
|
140
|
+
<Skeleton className="h-9 w-4/6" />
|
|
141
|
+
</div>
|
|
142
|
+
) : (
|
|
143
|
+
<SearchDialogList
|
|
144
|
+
items={items.length ? items : null}
|
|
145
|
+
Item={(itemProps) => (
|
|
146
|
+
<SearchDialogListItem
|
|
147
|
+
{...itemProps}
|
|
148
|
+
badge={
|
|
149
|
+
showBadges && 'url' in itemProps.item
|
|
150
|
+
? scopeBadge(itemProps.item.url)
|
|
151
|
+
: undefined
|
|
152
|
+
}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
<SearchDialogFooter>
|
|
158
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
aria-pressed={sel.size === 0}
|
|
162
|
+
className={cn(chip, sel.size === 0 ? chipOn : chipOff)}
|
|
163
|
+
onClick={() => setSel(new Set())}
|
|
164
|
+
>
|
|
165
|
+
{t('Everywhere', { note: 'ui.search_everywhere' })}
|
|
166
|
+
</button>
|
|
167
|
+
<span aria-hidden className="mx-0.5 h-4 w-px bg-border" />
|
|
168
|
+
{sections.map(([sec, datasets]) => {
|
|
169
|
+
const ids = datasets.map(({ id }) => `${sec}:${id}`);
|
|
170
|
+
if (ids.length === 1) {
|
|
171
|
+
const id = ids[0]!;
|
|
172
|
+
const on = sel.has(id);
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
key={sec}
|
|
176
|
+
type="button"
|
|
177
|
+
aria-pressed={on}
|
|
178
|
+
className={cn(chip, on ? chipOn : chipOff)}
|
|
179
|
+
onClick={() => toggle(ids)}
|
|
180
|
+
>
|
|
181
|
+
{sectionNames[sec]?.title ?? sec}
|
|
182
|
+
</button>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const onCount = ids.filter((id) => sel.has(id)).length;
|
|
186
|
+
return [
|
|
187
|
+
<button
|
|
188
|
+
key={sec}
|
|
189
|
+
type="button"
|
|
190
|
+
aria-pressed={onCount > 0}
|
|
191
|
+
className={cn(
|
|
192
|
+
chip,
|
|
193
|
+
onCount === ids.length
|
|
194
|
+
? chipOn
|
|
195
|
+
: onCount > 0
|
|
196
|
+
? chipMixed
|
|
197
|
+
: chipOff,
|
|
198
|
+
)}
|
|
199
|
+
onClick={() => toggle(ids)}
|
|
200
|
+
>
|
|
201
|
+
{sectionNames[sec]?.title ?? sec}
|
|
202
|
+
</button>,
|
|
203
|
+
...datasets.map(({ id, title }) => {
|
|
204
|
+
const entry = `${sec}:${id}`;
|
|
205
|
+
const on = sel.has(entry);
|
|
206
|
+
return (
|
|
207
|
+
<button
|
|
208
|
+
key={entry}
|
|
209
|
+
type="button"
|
|
210
|
+
aria-pressed={on}
|
|
211
|
+
className={cn(chip, on ? chipOn : chipOff)}
|
|
212
|
+
onClick={() => toggle([entry])}
|
|
213
|
+
>
|
|
214
|
+
{title ?? id}
|
|
215
|
+
</button>
|
|
216
|
+
);
|
|
217
|
+
}),
|
|
218
|
+
];
|
|
219
|
+
})}
|
|
220
|
+
</div>
|
|
221
|
+
</SearchDialogFooter>
|
|
222
|
+
</SearchDialogContent>
|
|
223
|
+
</SearchDialogRoot>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
function scopeBadge(url: string) {
|
|
227
|
+
const sec = url.split('/')[1];
|
|
228
|
+
return sec && sec in catalog ? (sectionNames[sec]?.title ?? sec) : undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
package/src/locales/en_us.json
DELETED
package/src/locales/ru_ru.json
DELETED