@aphrody/koharu-ui 0.0.0-stage → 0.1.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 +37 -2
- package/package.json +70 -4
- package/src/assets/NotoSansCJK-VF.otf +0 -0
- package/src/assets/OFL.txt +92 -0
- package/src/components/ClientOnly.tsx +21 -0
- package/src/components/ShenronExportPanel.tsx +213 -0
- package/src/components/app/AboutDialog.tsx +64 -0
- package/src/components/app/KoharuApp.tsx +58 -0
- package/src/components/app/StartupView.tsx +67 -0
- package/src/components/app/TitleBar.tsx +322 -0
- package/src/components/app/Updater.tsx +143 -0
- package/src/components/app/WindowChrome.tsx +137 -0
- package/src/components/controls/ColorSampling.tsx +70 -0
- package/src/components/controls/ColorWell.tsx +139 -0
- package/src/components/controls/CommitTextarea.tsx +63 -0
- package/src/components/controls/FontPicker.tsx +543 -0
- package/src/components/controls/ModelPicker.tsx +178 -0
- package/src/components/controls/OutputPicker.tsx +170 -0
- package/src/components/editor/ActivityCenter.tsx +234 -0
- package/src/components/editor/AgentPanel.tsx +352 -0
- package/src/components/editor/CanvasCommandBar.tsx +38 -0
- package/src/components/editor/CanvasOverlay.tsx +194 -0
- package/src/components/editor/CanvasWorkspace.tsx +947 -0
- package/src/components/editor/Editor.tsx +56 -0
- package/src/components/editor/InferenceControl.tsx +457 -0
- package/src/components/editor/Inspector.tsx +1073 -0
- package/src/components/editor/PageRail.tsx +581 -0
- package/src/components/editor/ResourceMonitor.tsx +151 -0
- package/src/components/editor/RightSidebar.tsx +50 -0
- package/src/components/editor/SelectionControls.tsx +184 -0
- package/src/components/editor/StatusBar.tsx +41 -0
- package/src/components/editor/ToolBar.tsx +182 -0
- package/src/components/editor/useCanvas.ts +194 -0
- package/src/components/preferences/GenerationPreferences.tsx +118 -0
- package/src/components/preferences/KoharuSettings.tsx +229 -0
- package/src/components/preferences/PipelinePreferences.tsx +173 -0
- package/src/components/preferences/PreferenceFields.tsx +149 -0
- package/src/components/preferences/ProviderPreferences.tsx +173 -0
- package/src/components/preferences/SettingsPage.tsx +336 -0
- package/src/components/preferences/TranslationPreferences.tsx +187 -0
- package/src/components/preferences/TypesettingPreferences.tsx +142 -0
- package/src/components/preferences/models.ts +103 -0
- package/src/components/start/StartView.tsx +277 -0
- package/src/index.ts +59 -0
- package/src/internal/components/accordion.tsx +72 -0
- package/src/internal/components/alert-dialog.tsx +189 -0
- package/src/internal/components/alert.tsx +76 -0
- package/src/internal/components/aspect-ratio.tsx +22 -0
- package/src/internal/components/attachment.tsx +207 -0
- package/src/internal/components/avatar.tsx +109 -0
- package/src/internal/components/badge.tsx +52 -0
- package/src/internal/components/breadcrumb.tsx +125 -0
- package/src/internal/components/bubble.tsx +128 -0
- package/src/internal/components/button-group.tsx +87 -0
- package/src/internal/components/button.tsx +58 -0
- package/src/internal/components/card.tsx +103 -0
- package/src/internal/components/checkbox.tsx +29 -0
- package/src/internal/components/collapsible.tsx +21 -0
- package/src/internal/components/combobox.tsx +298 -0
- package/src/internal/components/context-menu.tsx +272 -0
- package/src/internal/components/dialog.tsx +162 -0
- package/src/internal/components/direction.tsx +6 -0
- package/src/internal/components/drawer.tsx +230 -0
- package/src/internal/components/dropdown-menu.tsx +278 -0
- package/src/internal/components/empty.tsx +104 -0
- package/src/internal/components/field.tsx +238 -0
- package/src/internal/components/hover-card.tsx +52 -0
- package/src/internal/components/input-group.tsx +158 -0
- package/src/internal/components/input.tsx +20 -0
- package/src/internal/components/item.tsx +201 -0
- package/src/internal/components/kbd.tsx +26 -0
- package/src/internal/components/label.tsx +20 -0
- package/src/internal/components/marker.tsx +71 -0
- package/src/internal/components/menubar.tsx +280 -0
- package/src/internal/components/message.tsx +92 -0
- package/src/internal/components/native-select.tsx +61 -0
- package/src/internal/components/navigation-menu.tsx +168 -0
- package/src/internal/components/number-field.tsx +68 -0
- package/src/internal/components/pagination.tsx +130 -0
- package/src/internal/components/popover.tsx +91 -0
- package/src/internal/components/progress.tsx +83 -0
- package/src/internal/components/radio-group.tsx +38 -0
- package/src/internal/components/resizable.tsx +50 -0
- package/src/internal/components/scroll-area.tsx +88 -0
- package/src/internal/components/select.tsx +191 -0
- package/src/internal/components/separator.tsx +25 -0
- package/src/internal/components/sheet.tsx +140 -0
- package/src/internal/components/sidebar.tsx +723 -0
- package/src/internal/components/skeleton.tsx +13 -0
- package/src/internal/components/slider.tsx +56 -0
- package/src/internal/components/spinner.tsx +10 -0
- package/src/internal/components/switch.tsx +32 -0
- package/src/internal/components/table.tsx +116 -0
- package/src/internal/components/tabs.tsx +82 -0
- package/src/internal/components/textarea.tsx +18 -0
- package/src/internal/components/toast.tsx +235 -0
- package/src/internal/components/toggle-group.tsx +89 -0
- package/src/internal/components/toggle.tsx +45 -0
- package/src/internal/components/tooltip.tsx +67 -0
- package/src/internal/hooks/use-mobile.ts +19 -0
- package/src/internal/lib/utils.ts +6 -0
- package/src/internal/styles/globals.css +73 -0
- package/src/lib/backend.ts +85 -0
- package/src/lib/document.ts +46 -0
- package/src/lib/geometry.ts +272 -0
- package/src/lib/i18n.ts +54 -0
- package/src/lib/queries.ts +110 -0
- package/src/lib/store.ts +191 -0
- package/src/lib/translation.ts +35 -0
- package/src/locales/en-US/translation.json +536 -0
- package/src/locales/es-ES/translation.json +536 -0
- package/src/locales/ja-JP/translation.json +536 -0
- package/src/locales/ko-KR/translation.json +536 -0
- package/src/locales/pt-BR/translation.json +536 -0
- package/src/locales/ru-RU/translation.json +536 -0
- package/src/locales/tr-TR/translation.json +536 -0
- package/src/locales/zh-CN/translation.json +536 -0
- package/src/locales/zh-TW/translation.json +536 -0
- package/src/navigation.ts +28 -0
- package/src/providers.tsx +132 -0
- package/src/shenron-pipeline-env.d.ts +14 -0
- package/src/styles.css +246 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
QueryClient,
|
|
5
|
+
type QueryKey,
|
|
6
|
+
queryOptions,
|
|
7
|
+
useIsMutating,
|
|
8
|
+
useMutation,
|
|
9
|
+
useQuery,
|
|
10
|
+
} from '@tanstack/react-query'
|
|
11
|
+
|
|
12
|
+
import { commands, type FontFamily } from '@aphrody/koharu-bridge/protocol'
|
|
13
|
+
|
|
14
|
+
import { call } from './backend'
|
|
15
|
+
|
|
16
|
+
export const projectKey = ['project'] as const
|
|
17
|
+
export const pagesKey = ['pages'] as const
|
|
18
|
+
export const pageKey = ['page'] as const
|
|
19
|
+
export const preparedPageKey = (page: string) => ['prepared-page', page] as const
|
|
20
|
+
export const fontsKey = ['fonts'] as const
|
|
21
|
+
|
|
22
|
+
const projectQuery = queryOptions({
|
|
23
|
+
queryKey: projectKey,
|
|
24
|
+
queryFn: () => call(commands.getProject),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const pagesQuery = queryOptions({
|
|
28
|
+
queryKey: pagesKey,
|
|
29
|
+
queryFn: () => call(commands.getPages),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const pageQuery = queryOptions({
|
|
33
|
+
queryKey: pageKey,
|
|
34
|
+
queryFn: () => call(commands.getPage),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const fontsQuery = queryOptions({
|
|
38
|
+
queryKey: fontsKey,
|
|
39
|
+
queryFn: () => call(commands.getFonts),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const queryClient = new QueryClient({
|
|
43
|
+
defaultOptions: {
|
|
44
|
+
queries: {
|
|
45
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
46
|
+
retry: false,
|
|
47
|
+
refetchOnReconnect: false,
|
|
48
|
+
refetchOnWindowFocus: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export function useProject(enabled = true) {
|
|
54
|
+
return useQuery({ ...projectQuery, enabled })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function usePages(enabled = true) {
|
|
58
|
+
return useQuery({ ...pagesQuery, enabled })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function usePage(enabled = true) {
|
|
62
|
+
return useQuery({ ...pageQuery, enabled })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useFonts(enabled = true) {
|
|
66
|
+
return useQuery({ ...fontsQuery, enabled })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function useFontPreview(font: FontFamily | undefined, enabled = true) {
|
|
70
|
+
return useQuery({
|
|
71
|
+
queryKey: ['font-preview', font?.name],
|
|
72
|
+
queryFn: async () => {
|
|
73
|
+
if (!font) return null
|
|
74
|
+
try {
|
|
75
|
+
return new Uint8Array(await commands.getFontPreview(font.name))
|
|
76
|
+
} catch {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
enabled: enabled && font !== undefined,
|
|
81
|
+
gcTime: 5 * 60 * 1000,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useCommand<Args extends unknown[], Result>(
|
|
86
|
+
key: QueryKey,
|
|
87
|
+
command: (...args: Args) => Promise<Result>,
|
|
88
|
+
label: string,
|
|
89
|
+
onSuccess?: () => Promise<void>,
|
|
90
|
+
) {
|
|
91
|
+
const busy = useIsMutating({ mutationKey: key }) > 0
|
|
92
|
+
const mutation = useMutation({
|
|
93
|
+
mutationKey: key,
|
|
94
|
+
mutationFn: (args: Args) => call(command, ...args),
|
|
95
|
+
meta: { activity: label },
|
|
96
|
+
onSuccess,
|
|
97
|
+
})
|
|
98
|
+
return { run: (...args: Args) => mutation.mutate(args), busy }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function useImportPages() {
|
|
102
|
+
const { run, busy } = useCommand(['import-pages'], commands.import, 'navigator.importing', () =>
|
|
103
|
+
refresh(projectKey, pagesKey, pageKey),
|
|
104
|
+
)
|
|
105
|
+
return { importPages: run, importing: busy }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function refresh(...keys: QueryKey[]): Promise<void> {
|
|
109
|
+
await Promise.all(keys.map((queryKey) => queryClient.invalidateQueries({ queryKey })))
|
|
110
|
+
}
|
package/src/lib/store.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand'
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CanvasState,
|
|
7
|
+
Download,
|
|
8
|
+
EntityId,
|
|
9
|
+
Job,
|
|
10
|
+
Model,
|
|
11
|
+
Frame,
|
|
12
|
+
Preferences,
|
|
13
|
+
Stage,
|
|
14
|
+
StartupState,
|
|
15
|
+
ModelResources,
|
|
16
|
+
} from '@aphrody/koharu-bridge/protocol'
|
|
17
|
+
import { toast } from '../internal/components/toast'
|
|
18
|
+
|
|
19
|
+
export type CanvasTool = 'select' | 'text' | 'draw' | 'eraser' | 'color_picker' | 'remove' | 'pan'
|
|
20
|
+
export const MIN_BRUSH_DIAMETER = 1
|
|
21
|
+
export const MAX_BRUSH_DIAMETER = 128
|
|
22
|
+
|
|
23
|
+
export function isBrushTool(tool: CanvasTool): boolean {
|
|
24
|
+
return tool === 'draw' || tool === 'eraser' || tool === 'remove'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CanvasBrush {
|
|
28
|
+
diameter: number
|
|
29
|
+
color: string
|
|
30
|
+
}
|
|
31
|
+
export type InspectorSection = 'copy' | 'type' | 'layers'
|
|
32
|
+
export type ShortcutAction = CanvasTool | 'fit'
|
|
33
|
+
export type Shortcuts = Record<ShortcutAction, string>
|
|
34
|
+
export type PipelineScope = 'page' | 'selected-pages' | 'project'
|
|
35
|
+
export const pipelineStages: readonly Stage[] = ['detection', 'ocr', 'translation', 'inpainting']
|
|
36
|
+
|
|
37
|
+
interface KoharuStore {
|
|
38
|
+
initialized: boolean
|
|
39
|
+
preferences: Preferences | null
|
|
40
|
+
translationModels: Model[]
|
|
41
|
+
resources: ModelResources | null
|
|
42
|
+
jobs: Record<string, Job>
|
|
43
|
+
downloads: Record<string, Download>
|
|
44
|
+
camera: { zoom: number; translation: [number, number]; fitted: boolean }
|
|
45
|
+
canvasPage: EntityId | null
|
|
46
|
+
canvasRevision: number | null
|
|
47
|
+
canvasGeneration: number
|
|
48
|
+
canvasSize: [number, number]
|
|
49
|
+
fitCanvasRequest: number
|
|
50
|
+
layerFrames: Record<EntityId, Frame>
|
|
51
|
+
selectedLayers: EntityId[]
|
|
52
|
+
selectedPages: EntityId[]
|
|
53
|
+
tool: CanvasTool
|
|
54
|
+
brush: CanvasBrush
|
|
55
|
+
inspector: InspectorSection
|
|
56
|
+
processingScope: PipelineScope
|
|
57
|
+
processingStages: Stage[]
|
|
58
|
+
settingsOpen: boolean
|
|
59
|
+
shortcuts: Shortcuts
|
|
60
|
+
selectPages: (pages: EntityId[]) => void
|
|
61
|
+
showInspector: (section: InspectorSection) => void
|
|
62
|
+
setProcessingScope: (scope: PipelineScope) => void
|
|
63
|
+
setProcessingStages: (stages: Stage[]) => void
|
|
64
|
+
setSettingsOpen: (open: boolean) => void
|
|
65
|
+
selectLayers: (layers: EntityId[]) => void
|
|
66
|
+
setTool: (tool: CanvasTool) => void
|
|
67
|
+
setBrush: (brush: CanvasBrush) => void
|
|
68
|
+
setShortcut: (action: ShortcutAction, key: string) => void
|
|
69
|
+
requestCanvasFit: () => void
|
|
70
|
+
dismissJob: (id: string) => void
|
|
71
|
+
dismissDownload: (id: number) => void
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const defaultShortcuts: Shortcuts = {
|
|
75
|
+
select: 'v',
|
|
76
|
+
text: 't',
|
|
77
|
+
draw: 'b',
|
|
78
|
+
eraser: 'e',
|
|
79
|
+
color_picker: 'i',
|
|
80
|
+
remove: 'j',
|
|
81
|
+
pan: 'h',
|
|
82
|
+
fit: '0',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const useKoharuStore = create<KoharuStore>()((set) => ({
|
|
86
|
+
initialized: false,
|
|
87
|
+
preferences: null,
|
|
88
|
+
translationModels: [],
|
|
89
|
+
resources: null,
|
|
90
|
+
jobs: {},
|
|
91
|
+
downloads: {},
|
|
92
|
+
camera: { zoom: 1, translation: [0, 0], fitted: true },
|
|
93
|
+
canvasPage: null,
|
|
94
|
+
canvasRevision: null,
|
|
95
|
+
canvasGeneration: 0,
|
|
96
|
+
canvasSize: [0, 0],
|
|
97
|
+
fitCanvasRequest: 0,
|
|
98
|
+
layerFrames: {},
|
|
99
|
+
selectedLayers: [],
|
|
100
|
+
selectedPages: [],
|
|
101
|
+
tool: 'select',
|
|
102
|
+
brush: { diameter: 48, color: '#FFFFFF' },
|
|
103
|
+
inspector: 'copy',
|
|
104
|
+
processingScope: 'selected-pages',
|
|
105
|
+
processingStages: [...pipelineStages],
|
|
106
|
+
settingsOpen: false,
|
|
107
|
+
shortcuts: defaultShortcuts,
|
|
108
|
+
selectPages: (selectedPages) => set({ selectedPages: [...new Set(selectedPages)] }),
|
|
109
|
+
showInspector: (inspector) => set({ inspector }),
|
|
110
|
+
setProcessingScope: (processingScope) => set({ processingScope }),
|
|
111
|
+
setProcessingStages: (processingStages) => set({ processingStages }),
|
|
112
|
+
setSettingsOpen: (settingsOpen) => set({ settingsOpen }),
|
|
113
|
+
selectLayers: (selectedLayers) => set({ selectedLayers: [...new Set(selectedLayers)] }),
|
|
114
|
+
setTool: (tool) => set({ tool }),
|
|
115
|
+
setBrush: (brush) => set({ brush }),
|
|
116
|
+
setShortcut: (action, key) =>
|
|
117
|
+
set((state) => ({
|
|
118
|
+
shortcuts: { ...state.shortcuts, [action]: key.toLowerCase().slice(0, 1) },
|
|
119
|
+
})),
|
|
120
|
+
requestCanvasFit: () => set((state) => ({ fitCanvasRequest: state.fitCanvasRequest + 1 })),
|
|
121
|
+
dismissJob: (id) =>
|
|
122
|
+
set((state) => {
|
|
123
|
+
const jobs = { ...state.jobs }
|
|
124
|
+
delete jobs[id]
|
|
125
|
+
return { jobs }
|
|
126
|
+
}),
|
|
127
|
+
dismissDownload: (id) =>
|
|
128
|
+
set((state) => {
|
|
129
|
+
const downloads = { ...state.downloads }
|
|
130
|
+
delete downloads[String(id)]
|
|
131
|
+
return { downloads }
|
|
132
|
+
}),
|
|
133
|
+
}))
|
|
134
|
+
|
|
135
|
+
export function receiveStartupState(state: StartupState): void {
|
|
136
|
+
useKoharuStore.setState({
|
|
137
|
+
initialized: true,
|
|
138
|
+
preferences: state.preferences,
|
|
139
|
+
jobs: byId(state.jobs),
|
|
140
|
+
...canvasSnapshot(state.canvas),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function receiveCanvas(canvas: CanvasState): void {
|
|
145
|
+
useKoharuStore.setState(canvasSnapshot(canvas))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function receiveJob(job: Job): void {
|
|
149
|
+
useKoharuStore.setState((state) => ({ jobs: { ...state.jobs, [job.id]: job } }))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function receiveDownload(download: Download): void {
|
|
153
|
+
useKoharuStore.setState((state) => {
|
|
154
|
+
const downloads = { ...state.downloads }
|
|
155
|
+
if (download.state === 'finished') delete downloads[String(download.id)]
|
|
156
|
+
else downloads[String(download.id)] = download
|
|
157
|
+
return { downloads }
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function receivePreferences(preferences: Preferences): void {
|
|
162
|
+
useKoharuStore.setState({ preferences })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function receiveTranslationModels(translationModels: Model[]): void {
|
|
166
|
+
useKoharuStore.setState({ translationModels })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function receiveResources(resources: ModelResources): void {
|
|
170
|
+
useKoharuStore.setState({ resources })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function receiveError(message: string): void {
|
|
174
|
+
toast.add({ type: 'error', title: 'Could not complete that action', description: message })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function canvasSnapshot(canvas: CanvasState) {
|
|
178
|
+
return {
|
|
179
|
+
canvasPage: canvas.page,
|
|
180
|
+
canvasRevision: canvas.revision,
|
|
181
|
+
canvasGeneration: canvas.generation,
|
|
182
|
+
canvasSize: canvas.size,
|
|
183
|
+
layerFrames: Object.fromEntries(
|
|
184
|
+
canvas.element_frames.map(({ element, frame }) => [element, frame]),
|
|
185
|
+
),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function byId<T extends { id: string | number }>(items: T[]): Record<string, T> {
|
|
190
|
+
return Object.fromEntries(items.map((item) => [String(item.id), item]))
|
|
191
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LanguageChoice,
|
|
3
|
+
Model,
|
|
4
|
+
ModelSelection,
|
|
5
|
+
Provider,
|
|
6
|
+
ProviderPreference,
|
|
7
|
+
} from '@aphrody/koharu-bridge/protocol'
|
|
8
|
+
|
|
9
|
+
export function providerName(entries: ProviderPreference[], provider: Provider): string {
|
|
10
|
+
return entries.find((entry) => entry.config.provider === provider)?.name ?? provider
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function modelKey(model: Model | ModelSelection): string {
|
|
14
|
+
return `${model.provider}:${model.model ?? ''}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function modelSelection(model: Model, quantization: string | null): ModelSelection {
|
|
18
|
+
return {
|
|
19
|
+
provider: model.provider,
|
|
20
|
+
model: model.model,
|
|
21
|
+
quantization,
|
|
22
|
+
vision: model.vision,
|
|
23
|
+
reasoning: model.reasoning,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function orderedLanguageChoices(
|
|
28
|
+
languages: readonly LanguageChoice[],
|
|
29
|
+
): Array<{ tag: string; name: string }> {
|
|
30
|
+
return languages
|
|
31
|
+
.map((language) => ({ tag: language.tag, name: language.name }))
|
|
32
|
+
.toSorted((left, right) =>
|
|
33
|
+
left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' }),
|
|
34
|
+
)
|
|
35
|
+
}
|