@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,45 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
const toggleVariants = cva(
|
|
9
|
+
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "bg-transparent",
|
|
14
|
+
outline: "border border-input bg-transparent hover:bg-muted",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default:
|
|
18
|
+
"h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
19
|
+
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
20
|
+
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
variant: "default",
|
|
25
|
+
size: "default",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function Toggle({
|
|
31
|
+
className,
|
|
32
|
+
variant = "default",
|
|
33
|
+
size = "default",
|
|
34
|
+
...props
|
|
35
|
+
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
|
|
36
|
+
return (
|
|
37
|
+
<TogglePrimitive
|
|
38
|
+
data-slot="toggle"
|
|
39
|
+
className={cn(toggleVariants({ variant, size, className }))}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { Toggle, toggleVariants }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
function TooltipProvider({
|
|
8
|
+
delay = 0,
|
|
9
|
+
...props
|
|
10
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
11
|
+
return (
|
|
12
|
+
<TooltipPrimitive.Provider
|
|
13
|
+
data-slot="tooltip-provider"
|
|
14
|
+
delay={delay}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
21
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
25
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function TooltipContent({
|
|
29
|
+
className,
|
|
30
|
+
side = "top",
|
|
31
|
+
sideOffset = 4,
|
|
32
|
+
align = "center",
|
|
33
|
+
alignOffset = 0,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: TooltipPrimitive.Popup.Props &
|
|
37
|
+
Pick<
|
|
38
|
+
TooltipPrimitive.Positioner.Props,
|
|
39
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
40
|
+
>) {
|
|
41
|
+
return (
|
|
42
|
+
<TooltipPrimitive.Portal>
|
|
43
|
+
<TooltipPrimitive.Positioner
|
|
44
|
+
align={align}
|
|
45
|
+
alignOffset={alignOffset}
|
|
46
|
+
side={side}
|
|
47
|
+
sideOffset={sideOffset}
|
|
48
|
+
className="isolate z-50"
|
|
49
|
+
>
|
|
50
|
+
<TooltipPrimitive.Popup
|
|
51
|
+
data-koharu-ui-layer=""
|
|
52
|
+
data-slot="tooltip-content"
|
|
53
|
+
className={cn(
|
|
54
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
55
|
+
className
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
61
|
+
</TooltipPrimitive.Popup>
|
|
62
|
+
</TooltipPrimitive.Positioner>
|
|
63
|
+
</TooltipPrimitive.Portal>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
2
|
+
@source '../../components/**/*.{ts,tsx}';
|
|
3
|
+
@source '../**/*.{ts,tsx}';
|
|
4
|
+
|
|
5
|
+
/* Translate the Koharu-internal primitive class vocabulary to host M3 roles. */
|
|
6
|
+
@theme inline {
|
|
7
|
+
--radius-sm: var(--md-sys-shape-corner-extra-small);
|
|
8
|
+
--radius-md: var(--md-sys-shape-corner-small);
|
|
9
|
+
--radius-lg: var(--md-sys-shape-corner-medium);
|
|
10
|
+
--radius-xl: var(--md-sys-shape-corner-large);
|
|
11
|
+
--radius-2xl: var(--md-sys-shape-corner-extra-large);
|
|
12
|
+
--radius-3xl: var(--md-sys-shape-corner-extra-large-increased);
|
|
13
|
+
--radius-4xl: var(--md-sys-shape-corner-extra-extra-large);
|
|
14
|
+
|
|
15
|
+
--color-background: var(--md-sys-color-surface);
|
|
16
|
+
--color-foreground: var(--md-sys-color-on-surface);
|
|
17
|
+
--color-card: var(--md-sys-color-surface-container-low);
|
|
18
|
+
--color-card-foreground: var(--md-sys-color-on-surface);
|
|
19
|
+
--color-popover: var(--md-sys-color-surface-container-high);
|
|
20
|
+
--color-popover-foreground: var(--md-sys-color-on-surface);
|
|
21
|
+
--color-primary: var(--md-sys-color-primary);
|
|
22
|
+
--color-primary-foreground: var(--md-sys-color-on-primary);
|
|
23
|
+
--color-secondary: var(--md-sys-color-secondary-container);
|
|
24
|
+
--color-secondary-foreground: var(--md-sys-color-on-secondary-container);
|
|
25
|
+
--color-muted: var(--md-sys-color-surface-container-highest);
|
|
26
|
+
--color-muted-foreground: var(--md-sys-color-on-surface-variant);
|
|
27
|
+
--color-accent: var(--md-sys-color-tertiary-container);
|
|
28
|
+
--color-accent-foreground: var(--md-sys-color-on-tertiary-container);
|
|
29
|
+
--color-destructive: var(--md-sys-color-error);
|
|
30
|
+
--color-border: var(--md-sys-color-outline-variant);
|
|
31
|
+
--color-input: var(--md-sys-color-outline);
|
|
32
|
+
--color-ring: var(--md-sys-color-primary);
|
|
33
|
+
--color-chart-1: var(--md-sys-color-primary);
|
|
34
|
+
--color-chart-2: var(--md-sys-color-secondary);
|
|
35
|
+
--color-chart-3: var(--md-sys-color-tertiary);
|
|
36
|
+
--color-chart-4: var(--md-sys-color-error);
|
|
37
|
+
--color-chart-5: var(--md-sys-color-inverse-primary);
|
|
38
|
+
--color-sidebar: var(--md-sys-color-surface-container);
|
|
39
|
+
--color-sidebar-foreground: var(--md-sys-color-on-surface);
|
|
40
|
+
--color-sidebar-primary: var(--md-sys-color-primary);
|
|
41
|
+
--color-sidebar-primary-foreground: var(--md-sys-color-on-primary);
|
|
42
|
+
--color-sidebar-accent: var(--md-sys-color-secondary-container);
|
|
43
|
+
--color-sidebar-accent-foreground: var(--md-sys-color-on-secondary-container);
|
|
44
|
+
--color-sidebar-border: var(--md-sys-color-outline-variant);
|
|
45
|
+
--color-sidebar-ring: var(--md-sys-color-primary);
|
|
46
|
+
|
|
47
|
+
--shadow-sm: var(--md-sys-elevation-level1);
|
|
48
|
+
--shadow-md: var(--md-sys-elevation-level2);
|
|
49
|
+
--shadow-lg: var(--md-sys-elevation-level3);
|
|
50
|
+
--shadow-xl: var(--md-sys-elevation-level4);
|
|
51
|
+
--shadow-2xl: var(--md-sys-elevation-level5);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@layer base {
|
|
55
|
+
:is([data-koharu-ui], [data-koharu-ui-layer]) *,
|
|
56
|
+
[data-koharu-ui-layer] {
|
|
57
|
+
border-color: var(--md-sys-color-outline-variant);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[data-koharu-ui] {
|
|
61
|
+
color: var(--md-sys-color-on-surface);
|
|
62
|
+
background-color: var(--md-sys-color-surface);
|
|
63
|
+
font-size: 18px;
|
|
64
|
+
line-height: 1.2;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[data-koharu-ui-layer] {
|
|
68
|
+
color: var(--md-sys-color-on-surface);
|
|
69
|
+
background-color: var(--md-sys-color-surface);
|
|
70
|
+
font-size: 18px;
|
|
71
|
+
line-height: 1.2;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { commands } from '@aphrody/koharu-bridge/protocol'
|
|
4
|
+
import type {
|
|
5
|
+
PipelineConfig,
|
|
6
|
+
Preferences,
|
|
7
|
+
ProviderPreferences,
|
|
8
|
+
TypesettingConfig,
|
|
9
|
+
} from '@aphrody/koharu-bridge/protocol'
|
|
10
|
+
|
|
11
|
+
import { receiveError, receivePreferences, receiveTranslationModels } from './store'
|
|
12
|
+
|
|
13
|
+
type Command<Args extends unknown[], Result> = (...args: Args) => Promise<Result>
|
|
14
|
+
|
|
15
|
+
let translationModelsRequest: Promise<void> | null = null
|
|
16
|
+
let translationModelsGeneration = 0
|
|
17
|
+
let preferencesWriteGeneration = 0
|
|
18
|
+
let preferencesWriteQueue: Promise<void> = Promise.resolve()
|
|
19
|
+
|
|
20
|
+
export async function call<Args extends unknown[], Result>(
|
|
21
|
+
command: Command<Args, Result>,
|
|
22
|
+
...args: Args
|
|
23
|
+
): Promise<Result> {
|
|
24
|
+
try {
|
|
25
|
+
return await command(...args)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw report(error)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function dispatch<Args extends unknown[], Result>(
|
|
32
|
+
command: Command<Args, Result>,
|
|
33
|
+
...args: Args
|
|
34
|
+
): void {
|
|
35
|
+
void call(command, ...args).catch(() => undefined)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function refreshPreferences(): Promise<void> {
|
|
39
|
+
const generation = preferencesWriteGeneration
|
|
40
|
+
await preferencesWriteQueue
|
|
41
|
+
const preferences = await call(commands.getPreferences)
|
|
42
|
+
if (generation === preferencesWriteGeneration) receivePreferences(preferences)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function savePreferences(
|
|
46
|
+
pipeline: PipelineConfig,
|
|
47
|
+
providers: ProviderPreferences,
|
|
48
|
+
typesetting: TypesettingConfig,
|
|
49
|
+
): Promise<Preferences> {
|
|
50
|
+
preferencesWriteGeneration += 1
|
|
51
|
+
const pending = preferencesWriteQueue
|
|
52
|
+
.catch(() => undefined)
|
|
53
|
+
.then(() => call(commands.savePreferences, pipeline, providers, typesetting))
|
|
54
|
+
preferencesWriteQueue = pending.then(
|
|
55
|
+
() => undefined,
|
|
56
|
+
() => undefined,
|
|
57
|
+
)
|
|
58
|
+
return pending
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function refreshTranslationModels(force = false): Promise<void> {
|
|
62
|
+
if (!force && translationModelsRequest) return translationModelsRequest
|
|
63
|
+
|
|
64
|
+
const generation = ++translationModelsGeneration
|
|
65
|
+
const request = call(commands.getTranslationModels)
|
|
66
|
+
.then((models) => {
|
|
67
|
+
if (generation === translationModelsGeneration) receiveTranslationModels(models)
|
|
68
|
+
})
|
|
69
|
+
.finally(() => {
|
|
70
|
+
if (translationModelsRequest === request) translationModelsRequest = null
|
|
71
|
+
})
|
|
72
|
+
translationModelsRequest = request
|
|
73
|
+
return request
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function report(error: unknown): Error {
|
|
77
|
+
const message =
|
|
78
|
+
error instanceof Error
|
|
79
|
+
? error.message
|
|
80
|
+
: typeof error === 'string'
|
|
81
|
+
? error
|
|
82
|
+
: 'The native application returned an unknown error.'
|
|
83
|
+
receiveError(message)
|
|
84
|
+
return error instanceof Error ? error : new Error(message)
|
|
85
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Layer } from '@aphrody/koharu-bridge/protocol'
|
|
2
|
+
|
|
3
|
+
export function isTextLayer(layer: Layer): layer is Extract<Layer, { type: 'text' }> {
|
|
4
|
+
return layer.type === 'text'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isGroupLayer(layer: Layer): layer is Extract<Layer, { type: 'group' }> {
|
|
8
|
+
return layer.type === 'group'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isLockedLayer(layer: Layer): boolean {
|
|
12
|
+
return layer.type === 'artwork'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function layerChildren(layers: Layer[], parent: string): Layer[] {
|
|
16
|
+
return layers.filter((layer) => layer.parent === parent)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function expandLayerSelection(layers: Layer[], selected: string[]): string[] {
|
|
20
|
+
const result: string[] = []
|
|
21
|
+
const visit = (id: string) => {
|
|
22
|
+
const layer = layers.find((candidate) => candidate.id === id)
|
|
23
|
+
if (!layer) return
|
|
24
|
+
if (isGroupLayer(layer)) {
|
|
25
|
+
for (const child of layerChildren(layers, id)) visit(child.id)
|
|
26
|
+
} else if (!result.includes(id)) {
|
|
27
|
+
result.push(id)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const id of selected) visit(id)
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function effectiveLayerVisibility(layers: Layer[], layer: Layer) {
|
|
35
|
+
let visible = layer.visibility.visible
|
|
36
|
+
let opacity = layer.visibility.opacity
|
|
37
|
+
let parent = layer.parent
|
|
38
|
+
while (parent) {
|
|
39
|
+
const group = layers.find((candidate) => candidate.id === parent)
|
|
40
|
+
if (!group || !isGroupLayer(group)) break
|
|
41
|
+
visible &&= group.visibility.visible
|
|
42
|
+
opacity *= group.visibility.opacity
|
|
43
|
+
parent = group.parent
|
|
44
|
+
}
|
|
45
|
+
return { visible, opacity }
|
|
46
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { EntityId, Frame, Layer, Point, TransformFrame } from '@aphrody/koharu-bridge/protocol'
|
|
2
|
+
|
|
3
|
+
import { effectiveLayerVisibility, isTextLayer } from './document'
|
|
4
|
+
|
|
5
|
+
const minimumFrameSize = 1e-6
|
|
6
|
+
|
|
7
|
+
export interface Camera {
|
|
8
|
+
zoom: number
|
|
9
|
+
translation: [number, number]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CssFrame {
|
|
13
|
+
left: number
|
|
14
|
+
top: number
|
|
15
|
+
width: number
|
|
16
|
+
height: number
|
|
17
|
+
angle: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ResizeHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'
|
|
21
|
+
|
|
22
|
+
const resizeDirections: Record<ResizeHandle, Point> = {
|
|
23
|
+
nw: { x: -1, y: -1 },
|
|
24
|
+
n: { x: 0, y: -1 },
|
|
25
|
+
ne: { x: 1, y: -1 },
|
|
26
|
+
e: { x: 1, y: 0 },
|
|
27
|
+
se: { x: 1, y: 1 },
|
|
28
|
+
s: { x: 0, y: 1 },
|
|
29
|
+
sw: { x: -1, y: 1 },
|
|
30
|
+
w: { x: -1, y: 0 },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function physicalPoint(clientX: number, clientY: number, bounds: DOMRect): Point {
|
|
34
|
+
const dpr = window.devicePixelRatio
|
|
35
|
+
return { x: (clientX - bounds.x) * dpr, y: (clientY - bounds.y) * dpr }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function pagePoint(
|
|
39
|
+
clientX: number,
|
|
40
|
+
clientY: number,
|
|
41
|
+
bounds: DOMRect,
|
|
42
|
+
camera: Camera,
|
|
43
|
+
): Point {
|
|
44
|
+
const point = physicalPoint(clientX, clientY, bounds)
|
|
45
|
+
return {
|
|
46
|
+
x: (point.x - camera.translation[0]) / camera.zoom,
|
|
47
|
+
y: (point.y - camera.translation[1]) / camera.zoom,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function draftFrame(start: Point, end: Point): Frame {
|
|
52
|
+
return {
|
|
53
|
+
x: Math.min(start.x, end.x),
|
|
54
|
+
y: Math.min(start.y, end.y),
|
|
55
|
+
width: Math.max(1, Math.abs(end.x - start.x)),
|
|
56
|
+
height: Math.max(1, Math.abs(end.y - start.y)),
|
|
57
|
+
angle_degrees: 0,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function selectableLayer(layer: Layer): boolean {
|
|
62
|
+
return layer.type === 'text' || layer.type === 'image'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function layerFrame(layer: Layer): Frame | null {
|
|
66
|
+
const points =
|
|
67
|
+
layer.type === 'text' || layer.type === 'image' || layer.type === 'artwork'
|
|
68
|
+
? layer.geometry?.points
|
|
69
|
+
: null
|
|
70
|
+
if (!points?.length || points.some((point) => !finite(point.x, point.y))) return null
|
|
71
|
+
if (layer.type === 'text' && layer.angle_degrees !== null) {
|
|
72
|
+
if (!finite(layer.angle_degrees)) return null
|
|
73
|
+
const angle = (layer.angle_degrees * Math.PI) / 180
|
|
74
|
+
const cos = Math.cos(angle)
|
|
75
|
+
const sin = Math.sin(angle)
|
|
76
|
+
const xs = points.map((point) => point.x * cos + point.y * sin)
|
|
77
|
+
const ys = points.map((point) => -point.x * sin + point.y * cos)
|
|
78
|
+
const left = Math.min(...xs)
|
|
79
|
+
const top = Math.min(...ys)
|
|
80
|
+
const right = Math.max(...xs)
|
|
81
|
+
const bottom = Math.max(...ys)
|
|
82
|
+
const width = right - left
|
|
83
|
+
const height = bottom - top
|
|
84
|
+
const centerX = (left + right) / 2
|
|
85
|
+
const centerY = (top + bottom) / 2
|
|
86
|
+
return width > minimumFrameSize && height > minimumFrameSize
|
|
87
|
+
? {
|
|
88
|
+
x: centerX * cos - centerY * sin - width / 2,
|
|
89
|
+
y: centerX * sin + centerY * cos - height / 2,
|
|
90
|
+
width,
|
|
91
|
+
height,
|
|
92
|
+
angle_degrees: layer.angle_degrees,
|
|
93
|
+
}
|
|
94
|
+
: null
|
|
95
|
+
}
|
|
96
|
+
if (points.length === 4) {
|
|
97
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = points
|
|
98
|
+
const top: [number, number] = [topRight.x - topLeft.x, topRight.y - topLeft.y]
|
|
99
|
+
const right: [number, number] = [bottomRight.x - topRight.x, bottomRight.y - topRight.y]
|
|
100
|
+
const bottom: [number, number] = [bottomLeft.x - bottomRight.x, bottomLeft.y - bottomRight.y]
|
|
101
|
+
const left: [number, number] = [topLeft.x - bottomLeft.x, topLeft.y - bottomLeft.y]
|
|
102
|
+
const width = Math.hypot(...top)
|
|
103
|
+
const height = Math.hypot(...right)
|
|
104
|
+
if (width > minimumFrameSize && height > minimumFrameSize) {
|
|
105
|
+
const scale = Math.max(width, height, 1)
|
|
106
|
+
const oppositeLengthsMatch =
|
|
107
|
+
Math.abs(Math.hypot(...bottom) - width) <= scale * 1e-6 &&
|
|
108
|
+
Math.abs(Math.hypot(...left) - height) <= scale * 1e-6
|
|
109
|
+
const perpendicular = Math.abs(top[0] * right[0] + top[1] * right[1]) <= width * height * 1e-6
|
|
110
|
+
const diagonalsBisect =
|
|
111
|
+
Math.abs(topLeft.x + bottomRight.x - topRight.x - bottomLeft.x) <= scale * 1e-6 &&
|
|
112
|
+
Math.abs(topLeft.y + bottomRight.y - topRight.y - bottomLeft.y) <= scale * 1e-6
|
|
113
|
+
if (oppositeLengthsMatch && perpendicular && diagonalsBisect) {
|
|
114
|
+
const centerX = points.reduce((sum, point) => sum + point.x, 0) * 0.25
|
|
115
|
+
const centerY = points.reduce((sum, point) => sum + point.y, 0) * 0.25
|
|
116
|
+
return {
|
|
117
|
+
x: centerX - width * 0.5,
|
|
118
|
+
y: centerY - height * 0.5,
|
|
119
|
+
width,
|
|
120
|
+
height,
|
|
121
|
+
angle_degrees: (Math.atan2(top[1], top[0]) * 180) / Math.PI,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const xs = points.map((point) => point.x)
|
|
128
|
+
const ys = points.map((point) => point.y)
|
|
129
|
+
const x = Math.min(...xs)
|
|
130
|
+
const y = Math.min(...ys)
|
|
131
|
+
const width = Math.max(...xs) - x
|
|
132
|
+
const height = Math.max(...ys) - y
|
|
133
|
+
return width > minimumFrameSize && height > minimumFrameSize
|
|
134
|
+
? { x, y, width, height, angle_degrees: 0 }
|
|
135
|
+
: null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function controlFrame(
|
|
139
|
+
layer: Layer,
|
|
140
|
+
frames: Readonly<Record<EntityId, Frame>>,
|
|
141
|
+
): Frame | null {
|
|
142
|
+
const frame = isTextLayer(layer) ? frames[layer.id] : undefined
|
|
143
|
+
return frame && validFrame(frame) ? frame : layerFrame(layer)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function hitTestLayers(
|
|
147
|
+
layers: Layer[],
|
|
148
|
+
point: Point,
|
|
149
|
+
frames: Readonly<Record<EntityId, Frame>>,
|
|
150
|
+
): Layer | null {
|
|
151
|
+
for (let index = layers.length - 1; index >= 0; index -= 1) {
|
|
152
|
+
const layer = layers[index]
|
|
153
|
+
const visibility = effectiveLayerVisibility(layers, layer)
|
|
154
|
+
if (!selectableLayer(layer) || !visibility.visible || visibility.opacity <= 0) continue
|
|
155
|
+
const frame = controlFrame(layer, frames)
|
|
156
|
+
if (frame && frameContains(frame, point)) return layer
|
|
157
|
+
}
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function frameContains(frame: Frame, point: Point): boolean {
|
|
162
|
+
const centerX = frame.x + frame.width * 0.5
|
|
163
|
+
const centerY = frame.y + frame.height * 0.5
|
|
164
|
+
const angle = (-frame.angle_degrees * Math.PI) / 180
|
|
165
|
+
const cos = Math.cos(angle)
|
|
166
|
+
const sin = Math.sin(angle)
|
|
167
|
+
const x = point.x - centerX
|
|
168
|
+
const y = point.y - centerY
|
|
169
|
+
const localX = x * cos - y * sin
|
|
170
|
+
const localY = x * sin + y * cos
|
|
171
|
+
return Math.abs(localX) <= frame.width * 0.5 && Math.abs(localY) <= frame.height * 0.5
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function cssFrame(frame: Frame, camera: Camera): CssFrame {
|
|
175
|
+
const dpr = window.devicePixelRatio
|
|
176
|
+
const scale = camera.zoom / dpr
|
|
177
|
+
return {
|
|
178
|
+
left: (frame.x * camera.zoom + camera.translation[0]) / dpr,
|
|
179
|
+
top: (frame.y * camera.zoom + camera.translation[1]) / dpr,
|
|
180
|
+
width: frame.width * scale,
|
|
181
|
+
height: frame.height * scale,
|
|
182
|
+
angle: frame.angle_degrees,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function resizeFrame(
|
|
187
|
+
frame: Frame,
|
|
188
|
+
handle: ResizeHandle,
|
|
189
|
+
point: Point,
|
|
190
|
+
minimumSize: number,
|
|
191
|
+
): Frame {
|
|
192
|
+
const direction = resizeDirections[handle]
|
|
193
|
+
const angle = (frame.angle_degrees * Math.PI) / 180
|
|
194
|
+
const widthAxis = { x: Math.cos(angle), y: Math.sin(angle) }
|
|
195
|
+
const heightAxis = { x: -widthAxis.y, y: widthAxis.x }
|
|
196
|
+
const center = { x: frame.x + frame.width * 0.5, y: frame.y + frame.height * 0.5 }
|
|
197
|
+
const anchor = {
|
|
198
|
+
x:
|
|
199
|
+
center.x -
|
|
200
|
+
widthAxis.x * direction.x * frame.width * 0.5 -
|
|
201
|
+
heightAxis.x * direction.y * frame.height * 0.5,
|
|
202
|
+
y:
|
|
203
|
+
center.y -
|
|
204
|
+
widthAxis.y * direction.x * frame.width * 0.5 -
|
|
205
|
+
heightAxis.y * direction.y * frame.height * 0.5,
|
|
206
|
+
}
|
|
207
|
+
const delta = { x: point.x - anchor.x, y: point.y - anchor.y }
|
|
208
|
+
const width = direction.x
|
|
209
|
+
? Math.max(minimumSize, direction.x * dot(delta, widthAxis))
|
|
210
|
+
: frame.width
|
|
211
|
+
const height = direction.y
|
|
212
|
+
? Math.max(minimumSize, direction.y * dot(delta, heightAxis))
|
|
213
|
+
: frame.height
|
|
214
|
+
const nextCenter = {
|
|
215
|
+
x:
|
|
216
|
+
anchor.x +
|
|
217
|
+
widthAxis.x * direction.x * width * 0.5 +
|
|
218
|
+
heightAxis.x * direction.y * height * 0.5,
|
|
219
|
+
y:
|
|
220
|
+
anchor.y +
|
|
221
|
+
widthAxis.y * direction.x * width * 0.5 +
|
|
222
|
+
heightAxis.y * direction.y * height * 0.5,
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
...frame,
|
|
226
|
+
x: nextCenter.x - width * 0.5,
|
|
227
|
+
y: nextCenter.y - height * 0.5,
|
|
228
|
+
width,
|
|
229
|
+
height,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function rotateFrame(frame: Frame, start: Point, point: Point): Frame {
|
|
234
|
+
const center = { x: frame.x + frame.width * 0.5, y: frame.y + frame.height * 0.5 }
|
|
235
|
+
const from = { x: start.x - center.x, y: start.y - center.y }
|
|
236
|
+
const to = { x: point.x - center.x, y: point.y - center.y }
|
|
237
|
+
if (
|
|
238
|
+
Math.hypot(from.x, from.y) <= minimumFrameSize ||
|
|
239
|
+
Math.hypot(to.x, to.y) <= minimumFrameSize
|
|
240
|
+
) {
|
|
241
|
+
return frame
|
|
242
|
+
}
|
|
243
|
+
const delta = (Math.atan2(from.x * to.y - from.y * to.x, dot(from, to)) * 180) / Math.PI
|
|
244
|
+
return { ...frame, angle_degrees: normalizeDegrees(frame.angle_degrees + delta) }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function translateFrames(originals: TransformFrame[], delta: Point): TransformFrame[] {
|
|
248
|
+
return originals.map(({ element, frame }) => ({
|
|
249
|
+
element,
|
|
250
|
+
frame: { ...frame, x: frame.x + delta.x, y: frame.y + delta.y },
|
|
251
|
+
}))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function finite(...values: number[]): boolean {
|
|
255
|
+
return values.every(Number.isFinite)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function dot(left: Point, right: Point): number {
|
|
259
|
+
return left.x * right.x + left.y * right.y
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeDegrees(value: number): number {
|
|
263
|
+
return ((((value + 180) % 360) + 360) % 360) - 180
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function validFrame(frame: Frame): boolean {
|
|
267
|
+
return (
|
|
268
|
+
finite(frame.x, frame.y, frame.width, frame.height, frame.angle_degrees) &&
|
|
269
|
+
frame.width > minimumFrameSize &&
|
|
270
|
+
frame.height > minimumFrameSize
|
|
271
|
+
)
|
|
272
|
+
}
|
package/src/lib/i18n.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import i18n, { type Resource } from 'i18next'
|
|
4
|
+
import LanguageDetector from 'i18next-browser-languagedetector'
|
|
5
|
+
import LocalStorageBackend from 'i18next-localstorage-backend'
|
|
6
|
+
import { initReactI18next } from 'react-i18next'
|
|
7
|
+
|
|
8
|
+
import enUS from '@aphrody/koharu-ui/locales/en-US/translation.json'
|
|
9
|
+
import esES from '@aphrody/koharu-ui/locales/es-ES/translation.json'
|
|
10
|
+
import jaJP from '@aphrody/koharu-ui/locales/ja-JP/translation.json'
|
|
11
|
+
import koKR from '@aphrody/koharu-ui/locales/ko-KR/translation.json'
|
|
12
|
+
import ptBR from '@aphrody/koharu-ui/locales/pt-BR/translation.json'
|
|
13
|
+
import ruRU from '@aphrody/koharu-ui/locales/ru-RU/translation.json'
|
|
14
|
+
import trTR from '@aphrody/koharu-ui/locales/tr-TR/translation.json'
|
|
15
|
+
import zhCN from '@aphrody/koharu-ui/locales/zh-CN/translation.json'
|
|
16
|
+
import zhTW from '@aphrody/koharu-ui/locales/zh-TW/translation.json'
|
|
17
|
+
|
|
18
|
+
export const resources = {
|
|
19
|
+
'en-US': { translation: enUS },
|
|
20
|
+
'zh-CN': { translation: zhCN },
|
|
21
|
+
'zh-TW': { translation: zhTW },
|
|
22
|
+
'ja-JP': { translation: jaJP },
|
|
23
|
+
'ru-RU': { translation: ruRU },
|
|
24
|
+
'es-ES': { translation: esES },
|
|
25
|
+
'tr-TR': { translation: trTR },
|
|
26
|
+
'ko-KR': { translation: koKR },
|
|
27
|
+
'pt-BR': { translation: ptBR },
|
|
28
|
+
} satisfies Resource
|
|
29
|
+
|
|
30
|
+
export type SupportedLanguage = keyof typeof resources
|
|
31
|
+
|
|
32
|
+
export const supportedLanguages = Object.keys(resources) as SupportedLanguage[]
|
|
33
|
+
|
|
34
|
+
i18n
|
|
35
|
+
.use(LocalStorageBackend)
|
|
36
|
+
.use(LanguageDetector)
|
|
37
|
+
.use(initReactI18next)
|
|
38
|
+
.init({
|
|
39
|
+
resources,
|
|
40
|
+
supportedLngs: supportedLanguages,
|
|
41
|
+
fallbackLng: 'en-US',
|
|
42
|
+
detection: {
|
|
43
|
+
order: ['localStorage', 'navigator'],
|
|
44
|
+
caches: ['localStorage'],
|
|
45
|
+
},
|
|
46
|
+
interpolation: {
|
|
47
|
+
escapeValue: false, // not needed for react as it escapes by default
|
|
48
|
+
},
|
|
49
|
+
react: {
|
|
50
|
+
useSuspense: false,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export default i18n
|